第一章 引言和网络编程基础知识
1.1 分别简述OSI参考模型和TCP/IP模型,并阐述他们之间的对应关系
1.2 简述数据传输的三种方式及其优缺点
优点 | 缺点 | |
---|---|---|
电路交换 | 双方可以随时通信,实时性强. 双方通信时按发送顺序传送数据,不存在失序问题. | 电路交换的平均连接建立时间较长. 信道利用低. |
报文交换 | 不需要为通信双方预先建立一条专用的通信线路。 通信双方不是固定占有一条通信线路,提高了通信线路的利用率. | 经历存储、转发这一过程,从而引起转发时延. 要求网络中每个结点有较大的缓冲区 |
分组交换 | 加速了数据在网络中的传输 简化了存储管理 减少了出错机率和重发数据量 | 每个分组都要加上源、目的地址和分组编号等信息,使传送的信息量大 可能出现失序、丢失或重复分组 |
1.3 简述C/S,B/S模型并分析他们的优缺点
C/S架构
C/S 架构是一种典型的两层架构,其全称是Client/Server,即客户端、服务器端架构,其客户端包含一个或多个在用户的电脑上运行的程序,而服务器端有两种,一种是数据库服务器端,客户端通过数据库连接访问服务器端的数据;另一种是Socket服务器端,服务器端的程序通过Socket与客户端的程序通信。
C/S 架构也可以看做是胖客户端架构。因为客户端需要实现绝大多数的业务逻辑和界面展示。
C/S优点和缺点
优点:
- C/S架构的界面和操作可以很丰富。
- 安全性能可以很容易保证,实现多层认证也不难。
- 由于只有一层交互,因此响应速度较快。
缺点:
- 用户群固定。由于程序需要安装才可使用,因此不适合面向一些不可知的用户。
- 维护成本高,发生一次升级,则所有客户端的程序都需要改变。
B/S架构
B/S架构的全称为Browser/Server,即浏览器/服务器结构。主要事务逻辑在服务器端实现,B/S架构的系统无须特别安装,只有Web浏览器即可。因此也被称为瘦客户端。
必须强调的是C/S和B/S并没有本质的区别:B/S是基于特定通信协议(HTTP)的C/S架构,也就是说B/S包含在C/S中,是特殊的C/S架构。
B/S优点和缺点
优点:
- 客户端无需安装,有Web浏览器即可。
- B/S架构可以直接放在广域网上,通过一定的权限控制实现多客户访问的目的,交互性较强。
- B/S架构无需升级多个客户端,升级服务器即可。
缺点:
- 在跨浏览器上,B/S架构不尽如人意。
- 表现要达到C/S程序的程度需要花费不少精力。
- 在速度和安全性上需要花费巨大的设计成本,这是B/S架构的最大问题。
- 客户端服务器端的交互是请求-响应模式,通常需要刷新页面,这并不是客户乐意看到的。
1.4 应用程序在什么情况下建议使用UDP
UDP: 无连接交互
- 没有可靠保证
- 依赖下层系统保证
- 程序中应该有相应保障措施
TCP: 面向连接的交互
- 提供传输可靠性
- 程序要求简单
应用程序只在以下情况使用UDP:
- 应用程序指明必须使用UDP;
- 应用程序协议要依靠硬件进行广播或组播
- 应用协议在可靠的环境中运行,不需要额外的可靠性处理。
1.5 请阐述无状态服务器和有状态服务器概念及其特点,并介绍其优缺点。
服务器所维护的与客户交互的信息称为状态信息。不保存任何状态信息的服务器称为无状态服务器,反之称为有状态服务器。
有状态服务器
有状态服务器在服务器中保存少量信息,可减少客户端与服务器端交换报文的大小,保存了客户之前有过的请求,允许服务器快速的相应请求。
特点
- 保存客户请求的数据(状态)
- 服务端容易对客户状态进行管理
- 服务端并不要求每次客户请求都携带额外的状态数据
优点
- 由于服务器可以区分各个客户,并保留每个客户以前的请求信息,报文中不必包含所有字段信息。
缺点
- 通常情况下报文丢失、重复或交付失序,或者客户端程序崩溃都会使服务器的状态信息不正确,此时就可能产生不正确的响应。
无状态服务器
无状态服务器的动机是协议的不可靠性。客户请求报文必须指定操作类型、文件名、传输数据在文件中的位置和传输字节数、要写入的文件数据等。
特点:
- 并不保存客户请求的数据(状态)
- 客户在请求时需要携带额外的状态数据
- 无状态服务器更加健壮,重启服务器不会丢失状态信息,这使得维护和扩容更加简单
优点
无状态服务器则不会因为报文丢失,失序等问题导致状态信息出错,出现问题。
缺点
每次都要携带额外的状态信息,产生额外的数据。
第二章 客户服务器软件中的并发处理
2.1 并发、并行的概念及其区别
并发:
当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间
段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发(Concurrent)。
并行:
当系统有一个以上CPU时,
当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
区别:
并行是指两个或者多个事件在同一时刻发生;
并发是指两个或多个事件在同一时间间隔内发生。是指在一段时间内宏观上有多个程序在同时运行,在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。
2.2 进程、线程的概念及其联系和区别
进程的概念
进程是表示资源分配的基本单位。它是一个执行某一个特定程序的实体,它拥有独立的地址空间、执行堆栈、文件描述符等。
线程的概念
有时被称为轻量级进程,线程是进程中执行运算的最小单位,亦即执行处理机调度的基本单位。
进程和线程的联系
一个进程至少拥有一个线程——主线程,也可以拥有多个线程;一个线程必须有一个父进程。
多个进程可以并发执行;一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。
进程与线程的区别:
调度:线程作为CPU调度和分配的基本单位,进程作为拥有资源(内存资源)的基本单位
并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
2.3 阻塞、非阻塞、同步和异步的概念
同步/异步调用:
同步:
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。
异步:
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
阻塞/非阻塞:
阻塞:
是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
阻塞调用和同步调用实际上是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
非阻塞:
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。(一般采用轮询方式,没好会返回没完成,然后继续轮询,好了就返回完成)
2.4 介绍linux fork实现的原理
fork相当于复制了一个进程的执行版本。以当前进程作为父进程创建出一个新的子进程,并且将父进程的所有资源拷贝给子进程,这样子进程作为父进程的一个副本存在。父子进程几乎时完全相同的,但也有不同的如父子进程pid不同。
fork后,父子进程具有相同的数据空间、代码空间、堆栈、所有的文件描述字;但相互之间互不影响。
fork函数有三个返回值
- 该进程为父进程时,返回子进程的pid
- 该进程为子进程时,返回0
- fork执行失败,返回-1
那么fork函数为什么是一次调用,却返回了两次呢?
当程序执行到下面的语句: pid=fork();
由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。
2.5 exec函数的使用
系统调用execl执行另一个程序。调用execl并不创建新进程,所以前后的进程ID并未改变,execl只是用另一个新程序替换了当前进程的正文、数据、堆栈;
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
path 是要执行的二进制文件或脚本的完整路径。
arg是要传给程序的完整参数列表,包括arg[0],一般是执行程序的名字。
最后一个参数可为NULL
exec函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数。fork是分身术,exec变身术。
2.6 Linux 下五种I/O模型
-
阻塞I/O
进程会一直阻塞,直到数据拷贝完成
-
非阻塞I/O
非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是阻塞的;
-
I/O复用(select 和poll)
可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。
-
信号驱动I/O
允许套接口进行信号驱动I/O,并使用一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
-
异步I/O
数据拷贝的时候进程无需阻塞
区别和比较
同步IO引起进程阻塞,直至IO操作完成。IO复用是先通过select调用阻塞。异步IO不会引起进程阻塞。
同步IO和异步IO的区别就在于:数据拷贝的时候进程是否阻塞!
阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回!
第三章
3.1 什么是套接字?
套接字是一个主机本地网络应用程序所创建的, 为操作系统所控制的接口 (“门”) .
应用进程通过这个接口,使用传输层提供的服务, 跨网络发送(或接收)消息.
Client/server模式的通信接口——套接字接口.
主动和被动套接字
创建方式相同,使用方式不同
等待传入连接的套接字——被动,如服务器套接字
发起连接的套接字——主动,如客户套接字
指明端点地址:创建时不指定,使用时指明,允许协议族自由的选择地址表示方式
TCP/IP需要指明协议端口号和IP地址
-
TCP/IP协议族和地址族的对应:
TCP/IP协议族:PF_INET
对应的TCP/IP的地址族:AF_INET
3.2 socket调用的参数含义
PF_INET: TCP/IP的协议族
AF_INET: TCP/IP的地址族
struct sockaddr 是通用的地址结构
struct socketaddr_in 是IP专用的地址结构
SSOCK_DGRAM: 数据报服务,UDP协议
SSOCK_STREAM: 流服务,TCP协议
3.4 socket调用的相关函数
通用函数
socket函数
int Socket( int domain, int type, int protocol)
功能:创建一个新的套接字,返回套接字描述符
参数说明:
domain:域类型,指明使用的协议栈,
AF_INET:IPv4协议,
AF_INET6:IPv6协议,
AF_LOCAL:Unix域协议,
AF_ROUTE:路由套接口,
AF_KEY:密钥套接口
type: 指明需要的服务类型, AF_INET地址族下如
SOCK_DGRAM: 数据报服务,UDP协议
SOCK_STREAM: 流服务,TCP协议
SOCKET_RAW:提供传输层以下的协议,例如接收和发送ICMP报文
protocol:一般都取0(由系统根据服务类型选择默认的协议)
IPPROTO_TCP、
IPPROTO_UDP、
IPPROTO_ICMP
请创建一个用于TCP通信的套接字。
举例:
s=socket(AF_INET,SOCK_STREAM,0)
send函数
int send(int sockfd, const void * data, int data_len, unsigned int flags)
功能:
在TCP连接上发送数据,返回成功传送数据的长度,出错时返回-1。
send会将外发数据复制到OS内核中,也可以使用send发送面向连接的UDP报文。
参数说明:
sockfd:套接字描述符
data:指向要发送数据的指针
data_len:数据长度
flags:通常为0,设置为 MSG_DONTWAIT为非阻塞
记住如果send()函数的返回值小于len的话,则你需要再次发送剩下的数据。
802.3,MTU为1492B,如果包小于1K,那么send()一般都会一次发送光的。
举例(p50):
send(s,req,strlen(req),0);另外可尝试send的阻塞效果
recv函数
int recv(int sockfd, void *buf, int buf_len,unsigned int flags);
功能:
从TCP接收数据,返回实际接收的数据长度,出错时返回-1。
服务器使用其接收客户请求,客户使用它接受服务器的应答。如果没有数据,将阻塞。
如果TCP收到的数据大于(/小于)缓存的大小,只抽出能够填满缓存的足够数据(/抽出所有数据并返回它实际接收的字节数)。
也可以使用recv接收面向连接的UDP的报文,若缓存不能装下整个报文,填满缓存后剩下的数据将被丢弃。
参数说明:
Sockfd:套接字描述符
Buf:指向内存块的指针
Buf_len:内存块大小,以字节为单位
flags:一般为0(MSG_WAITALL接收到指定长度数据时才返回),设置为 MSG_DONTWAIT为非阻塞
举例:
recv(sockfd,buf,8192,0)
sendto函数(UDP)
int sendto(int sockfd, const void * data, int data_len, unsigned int flags, struct sockaddr *remaddr,sock_len remaddr_len)
功能:基于UDP发送数据报,返回实际发送的数据长度,出错时返回-1
参数说明:
sockfd:套接字描述符
data:指向要发送数据的指针
data_len:数据长度
flags:通常为0,设置为 MSG_DONTWAIT为非阻塞
remaddr:远端地址:IP地址和端口号
remaddr_len :地址长度
举例:
sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&address, sizeof(address));
recvfrom函数(UDP)
int recvfrom(int sockfd, void *buf, int buf_len,unsigned int flags,struct sockaddr *from,sock_len *fromlen);
功能:从UDP接收数据,返回实际接收的字节数,失败时返回-1
参数说明:
Sockfd:套接字描述符
buf:指向内存块的指针
buf_len:内存块大小,以字节为单位
flags:一般为0
from:远端的地址,IP地址和端口号
fromlen:远端地址长度
举例:
recvfrom(sockfd,buf,8192,0,(struct sockaddr *)&address, &sizeof(address));
close函数
close(int sockfd);
功能:
撤销套接字.
如果只有一个进程使用,立即终止连接并撤销该套接字,如果多个进程共享该套接字,将引用数减一,如果引用数降到零,则关闭连接并撤销套接字。
参数说明:
Sockfd:套接字描述符
举例:
close(socket_descriptor)
服务端常用函数
bind函数
int bind(int sockfd,struct sockaddr * my_addr,int addrlen)
功能:为套接字指明一个本地端点地址
TCP/IP协议使用sockaddr_in结构,包含IP地址和端口号,服务器使用它来指明熟知的端口号,然后等待连接。
参数说明:
Sockfd: 套接字描述符,指明创建连接的套接字
my_addr: 本地地址,IP地址和端口号
addrlen: 地址长度
为什么TCP服务端需要调用bind函数而客户端通常不需要呢?
客户端使用socket服务时,操作系统随之指定一个不会产生冲突的端口给客户端。在很多场景下, 我们要在一个pc上开启多个客户端进程, 如果指定固定端口, 必然会造成端口冲突, 影响通信!
举例:
struct sockaddr_in server_addr; /*服务器地址结构*/ /*设置服务器地址*/ bzero(&server_addr, sizeof(server_addr)); /*清零*/ server_addr.sin_family = AF_INET; /*协议族*/ server_addr.sin_addr.s_addr = htonl(INADDR_ANY); /*本地地址*/ server_addr.sin_port = htons(PORT); /*服务器端口*/ /*绑定地址结构到套接字描述符*/ err = bind(ss, (struct sockaddr*)&server_addr, sizeof(server_addr));
accept函数
int accept(int sockfd, struct sockaddr *addr, int *addrlen);
功能:获取传入连接请求,返回新的连接的套接字描述符。
为每个新的连接请求创建了一个新的套接字,服务器只对新的连接使用该套接字,原来的监听套接字接收其他的连接请求。新的连接上传输数据使用新的套接字,使用完毕,服务器将关闭这个套接字。
参数说明:
Sockfd: 套接字描述符,指明正在监听的套接字
addr: 提出连接请求的主机地址
addrlen: 地址长度
举例:
new_sockfd = accept(sockfd, (struct sockaddr *)&address, sizeof(address));
listen函数
#include <sys/socket.h>
int listen(int sockfd, int backlog);
/**
* 监听socket
*
* @param sockfd socket文件描述符
* @param backlog 提示内核监听队列的最大长度
* @return 函数执行成功返回0,失败返回-1
*/
客户端常用函数
connect函数
int connect(int sockfd,struct sockaddr *server_addr,int sockaddr_len)
功能: 同远程服务器建立主动连接,成功时返回0,若连接失败返回-1。
参数说明:
Sockfd: 套接字描述符,指明创建连接的套接字
Server_addr: 指明远程端点:IP地址和端口号
sockaddr_len: 地址长度
举例(P49):
/*连接服务器*/ connect(s, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
3.5 大端字节序、小端字节序的概念,理解其转换的原理。
大于一个字节的变量类型的表示方法有两种:
小端字节序(Little Endian,LE):在表示变量的内存地址的起始地址存放低字节,高字节顺序存放;
大端字节序(Big Endian,BE):在表示变量的内存地址的起始地址存放高字节,低字节顺序存放。
网络字节顺序:最高位字节在前(大端字节序)
主机字节序:一般和主机制造商的规定有关,不同PC字节序也不同
主机字节序和网络字节序的转换
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); /*主机字节序到网络字节序的长整型转换*/
uint32_t ntohl(uint32_t netlong); /*网络字节序到主机字节序的长整型转换*/
uint16_t htons(uint16_t hostshort); /*主机字节序到网络字节序的短整型转换*/
uint16_t ntohs(uint16_t netshort); /*网络字节序到主机字节序的短整型转换*/
3.6 不精确指明协议软件接口的优缺点
TCP/IP和应用程序之间的接口应该是不精确指明的:
- 不规定接口的细节
- 只建议需要的功能集
- 允许系统设计者选择有关API的具体实现细节
优点:
- 提供了灵活性和容错能力
- 便于各种OS实现TCP/IP
- 接口可以是过程的,也可以是消息的
缺点:
- 不同的OS中的接口细节不同
- 厂商增加与现有API不同的新接口时,编程更困难,移植性差
- 程序员需要重新学习接口知识
3.7 拓展Linux I/O 用于TCP/IP
扩展文件描述符: 可以用于网络通信
扩展read和write: 可以用于网络标识符
额外功能的处理,增加新系统调用:
- 指明本地和远端的端口,远程IP地址
- 使用TCP还是UDP
- 启动传输还是等待传入连接
- 可以接收多少传入连接
第4章 TCP网络编程基础
4.1 TCP客户-服务器模型实现的流程
服务端:
- 套接字初始化
socket()
- 套接字与端口的绑定
bind()
- 设置服务器的侦听连接连接
listen()
- 接受客户端连接请求
accept()
- 接收和发送数据
read()、write()
并进行数据处理 - 处理完毕的套接字关闭
close()
客户端:
- 套接字初始化
socket()
- 连接服务器
connect()
- 读写网络数据
read()、write()
并进行数据处理 - 最后套接字关闭
close()
过程。
4.2 用户层和内核层交互过程
用户层和内核层交互过程
- 向内核传入数据的交互过程,向内核传入数据的函数有send()、bind()等
- 内核传出数据的交互过程,从内核得到数据的函数有accept()、recv()等。
4.3 简介信号处理的实现和其函数原型
信号是发生某件事情时的一个通知。信号将事件发送给相关的进程,相关进程可以对信号进行捕捉并处理。信号的捕捉由系统自动完成,信号处理函数的注册通过函数signal()完成。
signal(int signum, sighandler_t handler);
参数signum指出要设置处理方法的信号:
SIGINT:用于终止进程运行向当前活动的进程发送这个信号。通常是由Ctrl+C终止进程造成的,与Ctrl+C一 致,kill命令默认发送SIGINT信号。
SIGPIPE:正在写入套接字的时候,当读取端已经关闭时,可以得到一个SIGPIPE信号。
第二个参数handler是一个处理函数。
void sig_int(int sign)
{
printf("Catch a SIGINT signal\n");
/*释放资源*/
}
signal(SIGINT, sig_int);
第5章 基于UDP协议的接收和发送
5.1 UDP编程框架
服务器端:
- 建立套接字文件描述符,使用函数
socket()
,生成套接字文件描述符。 - 设置服务器地址和侦听端口,初始化要绑定的网络地址结构。
- 绑定侦听端口,使用
bind()
函数,将套接字文件描述符和一个地址类型变量进行绑定。 - 接收客户端的数据,使用
recvfrom()
函数接收客户端的网络数据。 - 向客户端发送数据,使用
sendto()
函数向服务器主机发送数据。 - 关闭套接字,使用
close()
函数释放资源。
客户端:
- 建立套接字文件描述符,
socket()
; - 设置服务器地址和端口,
struct sockaddr_in
; - 向服务器发送数据,
sendto()
; - 接收服务器的数据,
recvfrom()
; - 关闭套接字,
close()
。
bind()
int s;
s = socket(AF_INET, SOCK_DGRAM, 0);
5.2 UDP协议程序设计中的几个问题和解决办法
-
UDP报文丢失数据
对策:客户端和服务端会对超时的数据进行重发。
-
UDP数据发送中的乱序
主要是由于路由的和路由的存储转发的顺序不同造成的。路由器的存储转不同发可能造成数据顺序的更改。
对策:可以采用发送端在数据段中加入数据报序号的方法
-
UDP协议中的connect()函数
connect()函数在TCP协议中会发生三次握手,建立一个持续的连接,一般不用于UDP。在UDP协议中使用connect()函数的作用仅仅表示确定了另一方的地址,并没有其他的含义。
-
UDP缺乏流量控制
UDP协议没有TCP协议所具有的滑动窗口概念,接收数据的时候直接将数据放到缓冲区中。当缓冲区满的时候,后面到来的数据会覆盖之前的数据造成数据的丢失。
对策:增大接收数据缓冲区 或 接收方接收单独处理
第六讲 客户端软件设计中的算法和问题
6.1 客户标识服务器位置的几种方式
-
在编译程序时,将服务器的域名或者IP地址说明为常量
执行快,但是服务器移动后不便
-
要求用户在启动程序时指定服务器
使用机器名,不必重新编译客户程序
-
从稳定的存储设备中获得关于服务器的信息
如果文件不存在,客户软件就不能执行
-
使用某个单独的协议来找到服务器(如广播或组播)
只能在本地小环境下应用
6.2 为什么TCP调用recv接收数据时要进行多次接收?
TCP不保持记录的边界,面向流的概念,多次接收。
原因:大块数据被分片封装发送或由于接收方接收缓冲小而数据被发方分次发送
/**
* 客户发送请求,等待响应
* 发送请求:send;
* 等待响应:recv;
*/
send(s, req, strlen(req), 0);
while ((n = recv (s, bptr, buflen, 0)) > 0)
{
bptr +=n;
buflen -=n;
}
6.3 gethostbyname、 getservbyname 和getprotobyname 的功能
gethostbyname:
主机域名到二进制的转换
接受一个机器域名字符串,返回一个hostent结构,内含一个二进制表示的主机IP地址
getservbyname:
两个参数指明期望的服务和协议。返回servent类型的结构指针;
注意按网络字节序返回协议端口号;
getprotobyname:
由协议名返回协议号;
返回一个protoent类型结构的地址
6.4 TCP连接到服务器
TCP客服服务器中客户端connect接口完成的四项任务
- 对指明的套接字进行检测:有效,还没有连接
- 将第二个参数给出的端点地址填入套接字中
- 为此套接字选择一个本地端点地址
- 发起一个TCP连接,并返回一个值
6.5 简述TCP面向连接的客户端算法和UDP无连接客户端算法
TCP 面向连接的算法:
- 分配套接字
- 找到期望与之通信的服务器IP地址和协议端口号
- 指明此连接需要在本地机器中的、任意的、未使用的协议端口,并允许TCP选择一个这样的端口
- 将这个套接字连接到服务器
- 使用应用级协议与服务器通信
- 关闭连接
UDP无连接客户端算法
- 分配套接字
- 找到期望与之通信的服务器IP地址和协议端口号
- 指明这种通信需要本地机器中的、任意的、未使用的协议端口,并允许UDP选择一个这样的端口
- 指明报文所要发往的服务器
- 使用应用级协议与服务器通信
- 关闭连接
第七讲 服务器软件设计的算法和问题
本章与第八第九章内容相似,故结合第八第九章一起复习。
7.1 无连接和面向连接的服务器访问
使用TCP的服务器是面向连接的服务器
使用UDP的服务器是无连接的服务器
选择要考虑TCP和UDP的语义特点
TCP的语义
- 点到点通信
- 建立可靠连接
- 可靠交付
- 具有流控的传输
- 双工传输
- 流模式
UDP的语义
- 多对多通信
- 不可靠服务
- 缺乏流控制
- 报文模式
面向连接服务的优点:
-
易于编程
自动处理分组丢失,分组失序问题
自动验证数据差错,处理连接状态
面向连接服务的缺点:
- 对每个连接都有一个单独的套接字,耗费更多的资源
- 在空闲的连接上不发送任何分组
- 始终运行的服务器会因为客户的崩溃,导致无用套接字的过多而耗尽资源,终止运行
无连接服务的优点:没有资源耗尽问题
无连接服务的缺陷:需要自己完成可靠通信问题
- 必要时,需要一种自适应重传的复杂技术,需要程序员具有相当的专业知识
- 对于不可靠通信的场合,尽量使用tcp
特殊情况
是否需要组播或者广播是考虑选择何种传输方式的一个因素
支持组播或者广播的服务器必须是无连接的,今后会不断增加这样的应用。
7.2 无状态和有状态的服务器应用
状态信息:服务器维护的,关于它和客户正进行的交互状态信息
无状态服务器:没有保留任何状态信息
状态服务器:维护状态信息的服务器
状态问题源于对确保可靠性的要求,特别对无连接传输
传输协议不能保证可靠,应用协议的设计必须保证可靠
优化服务器
- 在服务器加入大的文件缓存和信息索引可以改善服务器性能
- 需要程序员极其小心:检查文件名、文件偏移等,重复、失序问题
- 如果客户出了故障重新启动,将会重新获得一个不同的端口号,先前的表项将会失去作用,最终会耗尽服务器资源
- 服务器可以选择删除LRU(最近最少使用)但是如果客户经常崩溃,可能让服务器删除一个合法的客户条目
- 优化无状态服务器的时候,程序员必须及其小心
- 如果客户经常崩溃或者重启,或者网络使报文重复或者迟延,管理少量状态信息也会消耗资源
7.3 一些概念&循环服务器是否够用
观测响应时间
客户发送请求到服务器响应之间的全部时延。
请求处理时间
服务器处理单个孤立的请求所花费的时间。
循环服务器观测响应时间为N/2+1的推导
假设服务器处理一个任务的时间为t,队列第一个等待的时间是t ,第二个等待的时间为:2t ,这样第N个等待的时间为Nt ,所以等待的总时间变为1t+2t+…+Nt=(1+N)Nt/2.所以平均等待时间为(1+N)Nt/2/N=
(1+N)t/2=(N/2+1/2)t,因此约等于(N/2+1 )个服务器处理时间.
判断循环服务器是否够用
如果一个服务器设计处理K个客户,每个客户每秒发送R个请求,服务器请求处理时间必须小于每请求1/KR秒。否则请求队列将溢出。这时设计者必须考虑并发实现
7.4 服务器的四种基本类型及其特点
基本类型 | 特点 |
---|---|
循环的 无连接 | 对每个请求的处理少,通常为无状态的,简单服务,计算少 |
循环的 面向连接 | 要求可靠传输的,对每个请求处理少的服务 |
并发的 无连接 | 不常见,为每个请求创建一个新线程或进程 |
并发的 面向连接 | 最一般的。可靠传输,并发处理多个请求。 |
7.5 四种基本类型服务器的算法
结合8,9章的进程结构模型图,一起复习。
循环面向连接服务器算法
- 创建套接字并将其绑定到它所提供服务的熟知端口上;
- 将该端口设置为被动模式,使其准备为服务器所用;
- 从该套接字上接收下一个连接请求,获得该连接的新的套接字;
- 在新套接字上重复地读取来自客户的数据,构造响应,按照应用协议向客户发回响应;
- 当某个特定客户完成交互时,关闭连接,并返回步骤3以接受新的连接。
进程结构
使用一个单执行线程
使用两个套接字
- 一个套接字处理请求
- 另外一个套接字处理和客户的通信(临时的)
循环无连接服务器算法
- 创建套接字并将其绑定到所提供服务的熟知端口上;
- 重复读取来自客户的请求,构造响应,按照应用协议向客户发回响应。
进程结构:只需要一个执行进程
并发无连接服务器算法
- 主1. 创建套接字并将其绑定到所提供服务的熟知地址上。让该套接字保持为未连接的
- 主2. 反复调用recvfrom接收来自客户的下一个请求,创建一个新的从线程来处理响应
- 从1. 从来自主线程的特定请求以及到该套接字的访问开始
- 从2. 根据应用协议构造应答,并用sendto将该应答发回给客户
- 从3. 退出(即:从线程处理完一个请求后就终止)
并发面向连接的服务器算法
- 主1. 创建套接字并将其绑定到所提供服务的熟知地址上。让该套接字保持为无连接的
- 主2. 将该端口设置为被动模式
- 主3. 反复调用accept以便接收来自客户的下一个连接请求,并创建新的从线程或者进程来处理响应
- 从1. 由主线程传递来的连接请求开始
- 从2. 用该连接与客户进行交互;读取请求并发回响应
- 从3. 关闭连接并退出
并发面向连接单线程进程服务器进程结构
- 服务器包括一个主进程,以及零个或者多个从进程,每个进程一个线程。与循环面向连接的服务器(右侧)进程的区别!
- 主服务器使用accept阻塞调用,节约CPU资源,连接到来的时候,accept马上返回。
单线程服务器的线程结构
单线程、并发服务器的线程和套接字结构
- 一个执行线程管理所有的套接字
7.6 单线程实现表面并发、面向连接的服务器算法
可以通过共享内存的线程达到期望的并发,但当:
- 出现在服务器的请求没有超过服务器的处理能力可以获得表面上的并发。
- 单线程的服务器使用select系统调用进行I/O复用
- 创建套接字并将其绑定到这个服务的熟知端口上,将该套接字加到一个表中,该表中的项是可以进行I/O的描述符。
- 使用select在已经有的套接字上等待I/O
- 如果最初的套接字准备就绪,使用accept获得下一个连接,并将这个新的套接字加入到表中,该表中的项是可以进行I/O的描述符。
- 如果最初的套接字以外的套接字就绪,就使用recv或read获得下一个请求,构造响应,用send或者write将响应发回给客户
- 继续按照以上的步骤2进行处理
客户的问题或者使用了阻塞系统调用的情况对于循环服务器以及使用单线程实现的并发服务中死锁都可能发生
7.7 Windows和linux下socket编程区别
- 头文件
windows下winsock.h或winsock2.h linux下netinet/in.h、<sys/socket.h> - 初始化
windows下需要用WSAStartup启动Ws2_32.lib,并且要用#pragma comment(lib,"Ws2_32")来告知编译器链接该lib。linux下不需要 - 关闭socket
windows下closesocket(...) linux下close(...) - 获取错误码
windows下getlasterror()/WSAGetLastError() linux下,未能成功执行的socket操作会返回-1;如果包含了errno.h,就会设置errno变量 - 多线程/多进程
windows下包含process.h,使用_beginthread和_endthread,进程创建CreateProcess() linux下包含pthread.h,使用pthread_create和pthread_exit,进程创建fork() - 用IP定义一个地址(sockaddr_in的结构的区别)
名称相同,都是struct sockaddr、struct sockaddr_in,这两者通常转换使用;在Windows下面名称都是大写,而在Linux下为小写
windows下addr_var.sin_addr.S_un.S_addr linux下addr_var.sin_addr.s_addr 而且Winsock里最后那个32bit的S_addr也有几个以联合(Union)的形式与它共享内存空间的成员变量(便于以其他方式赋值),而Linux的Socket没有这个联合,就是一个32bit的s_addr。
第8章 循环服务器
8.1 一些服务的模型选择
TIME服务器选用什么模型最合适?
TIME服务几乎不需要什么计算,可以利用循环实现
Time服务需要实时性,因此选择无连接方式
因此循环无连接服务器模型最为合适
循环的面向连接的服务器每处理一个连接循环一次
连接达到以前在accept阻塞
建立新的连接以后创建新套接字处理
处理完毕,关闭,返回accept阻塞
DAYTIME服务
不需要客户的请求信息,检测到连接就响应
发送完响应,服务器主动关闭连接
每个连接只发送一个响应
DAYTIME服务用循环面向连接的服务模型比较好
第9章 并发服务器
9.1 并发服务器类型有几种?
- 并发的面向连接的服务器
- 并发无连接服务器
- 单线程表面并发服务器
9.2 线程的优缺点
优点
-
更高的效率:上下文切换的额外开销减少
- 上下文切换:线程切换需要执行的指令
- 同一进程中的两个线程比不同进程中的两个线程切换要快
- 进程内的线程切换不用改变虚拟存储器的映射
-
共享存储:
- 并发服务器中的多个副本需要相互通信或者访问共享的数据
- 利用线程容易构造监控系统
缺点
由于线程间共享存储和进程状态,一个线程的动作可能对同一个进程内的其他线程产生影响。
- 两个线程如果同一时刻访问同一个变量,会产生相互干扰
- 调用一个静态的数据项的库函数不是线程安全(thread safe)的,覆盖将会导致错误
- 缺乏健壮性,一个线程出错,服务器将会终止整个进程
并发服务器可以在一个进程中用若干线程实现
优点是:
- 开销少
- 共享存储器
- 可以监控
缺点是:
- 增加了编程的复杂性
- 必须使用同步机制(信号量和条件变量)协调线程对全局变量和一些库程序的访问
- 必须弄清一些可能影响整个进程的系统函数
9.3 单线程服务器的线程结构
单线程并发服务器必须完成原来主线程和从线程双方的职责
- 维护一组套接字
- 组中主套接字绑定到接受连接的熟知端口上
- 其它从套接字对应一个连接
- 服务器把这组套接字描述符作为一个参数传递给select,并等待任何一个套接字的活动,select返回一个屏蔽位,指明哪个套接字就绪,服务器再决定如何处理。
- 使用描述符来区别主套接字和从套接字的操作
- 主套接字描述符准备就绪,使用原来主线程的操作accept
- 从套接字的描述符就绪,使用原来从线程的操作read
9.4 select
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
- int maxfdp是一个整数值,是指描述符集合中所有文件描述符的范围,即所有文件描述符的最大值加1;
- fd_set *readfds是指向fd_set结构的指针,如果有一个文件可读,select就会返回一个大于0的值,表示有文件可读
- fd_set *writefds,如果有一个文件可写,select就会返回一个大于0的值
- fd_set *errorfds同上面两个参数的意图,用来监视文件错误异常。
- struct timeval* timeout是select的超时时间,
Select函数有三种执行结果:
- 永远等待下去:仅在有一个或一个以上描述字准备好i/o才返回,为此,我们将timeout设置为空指针。
- 等待固定时间:在有一个描述字准备好时返回,但不超过由timeout参数指定的秒数和微秒数。
- 根本不等待,检查描述字后立即返回,这称为轮询。这种情况下,timeout必须指向结构timeval,且定时器的值必须为0。
Select函数的返回值如下:
- 如果在指定超时值到达之前有一个或多个描述字满足条件,则函数返回值大于零;
- 如果超时时间到时,没有描述字满足条件,函数返回值为0;
第十讲 复杂服务器设计
10.1 复杂服务器
多协议服务器(TCP、UDP)
一个服务器的服务可以同时在TCP和UDP传输协议之上来提供。多服务服务器( DayTIME、TIME、Echo )
利用同一台服务器提供多种服务多协议多服务服务器(DayTIME、TIME、Echo 、TCP、UDP)
利用同一台服务器提供多种服务,而且可以通过不同的协议进行传输。
10.2 复杂服务器的进程结构
循环多协议服务器进程结构
在任何时候,一个循环的多协议服务器至少打开3个套接字。最初,服务器打开一个UDP和一个TCP套接字。当一个请求到达UDP套接字,服务器会计算出相应,通过UDP套接字返回给客户端。当一个TCP请求到达TCP套接字时,服务器调用accept获得这个新的连接。accept为这个连接创建第三个套接字,服务器使用这个套接字与客户端通信。一旦交互结束,服务器将关闭第三个套接字,并等待另外两个套接字激活。
单线程并发多协议服务器进程结构
单线程并发多协议服务器是有多个用于一个TCP链连接的套接字
循环无连接、多服务服务器设计
打开一组套接字,每一个套接字与一个熟知服务相对应。服务器使用select系统调用的等待任一套接字的数据报的到达。
循环面向连接多服务器设计
先为每一种服务创建一个套接字,并将该套接字绑定在熟知服务端口上,使用select等待任一套接字上传入连接请求。只要有一个套接字就绪,服务器就调用accept获得这个新连接。accept为这个传入连接创建新的套接字。服务器使用这个新的套接字与客户交互,之后便将关闭。除了主套接字外,服务器在任何时候最多只有一个打开的附加套接字。
并发、面向连接、多服务服务器设计
当一个连接请求到达时,服务器就调用一个进程,接受并直接处理这个新的连接,或者,他也可以创建一个新的从进程来处理这个新连接。实际上,一个多服务器程序可以设计成循环的处理某些服务,而对其他一些服务则并发处理。
单线程表面并发面向连接多服务器设计
当这个多服务服务器开始执行时,它先为每个服务创建一个套接字,并将该套接字绑定在熟知服务端口上,,使用select等待任一套接字上传入连接请求。只要有一个套接字就绪,服务器就调用accept获得这个新连接。accept为这个传入连接创建新的套接字。服务器使用这个新的套接字与客户交互,之后便将关闭。除了主套接字外,服务器在任何时候最多只有一个打开的附加套接字。
从多服务服务器调用单独的程序
主服务器使用fork创建一个新进程来处理每个链接。然而,与以前的设计不同,从进程以调用execve的方式用一个新的程序替代原来的代码,这个新的程序将处理所有的客户端通信。从概念上来看,使用execve就把处理各个服务同设立在连接的主服务器代码分离开了。
第十一讲 服务器并发性的统一、高效管理
11.1 为什么需要在服务器中进行从进程/线程的预分配
- 当创建进程/线程时间较长的时候,也能维持高吞吐量。
- 主服务器在开始执行时就创建N个从进程/线程
- 将所接受的新的请求分配给这N个从进程/线程中的一个处理
- 进程/线程并不退出
11.2 结合图示,介绍无连接服务器预分配的进程结构
算法:多个从线程同时绑定在一个socket上调用recvfrom获得发送方的地址和其发送的数据报,并调用sendto应答。一个数据报到达的时候系统只唤醒一个从进程。
无连接服务器并发等级取决于到达的请求数
如果某操作系统在调用recvfrom时,会同时激活所有的从进程,请问应该使用什么技术手段加以解决,并简要描述。
互斥,调用recvfrom前申请互斥,pthread_mutex_lock
调用结束pthread_mutex_unlock
11.3 结合图示,介绍有连接服务器预分配的进程结构
所有的从进程继承了对熟知端口套接字的访问。当各个从进程调用accept返回时,它接受新套接字以用于这个连接。虽然主进程创建了对应熟知端口的套接字,但是它并不使用这个套接字进行其他操作。图中虚线表明主进程使用该套接字的方式与从进程不同。
面向连接服务器的并发等级与活跃的连接数有关
预分配方案怎样用于不能进程并发调用accept的系统中?
使用一个共享的互斥量mutex或文件锁定,以便保证任何时候只有一个从线程能够调用accept。