第五章 无堵塞套接字和单进程轮询服务器
·5.1 无堵塞套接字
堵塞套接字在等待输入/输出时会进入睡眠,不能继续其他的操作。在并发服务器模式下这一缺点并不明显,但在一些复杂应用中可能需要在单进程中为多个连接服务,这时堵塞套接字会大大降低效率。另外,进程可能一直被堵塞。比如服务器端崩溃,而客户端并不知道,此时客户端进程将一直堵塞。
无堵塞套接字会对读、写、建立连接、接收连接过程产生影响。总的来说就是不等待所有资源到齐,而立即操作并返回,这点在和处理无堵塞套接字上有一些区别。如果本已堵塞而由于使用无堵塞套接字,那么errno将返回EWOULDBLOCK,通过下面语句可以判断:
ret = accept(...);
if (ret < 0 & errno != EWOULDBLOCK) //如果错误返回并且错误原因不是无堵塞
无堵塞套接字的两种实现:
int (flags = fcntl(sock_fd, F_GETFL, 0) < 0) error_proc();
flags |= O_NONBLOCK;
if (fcntl(sock_fd, F_SETFL, flags) < 0) error_proc(); //这种方法是POSIX标准定义方式
int b_on = 1; //在ioctl函数中使用FIONBIO命令
ioctl(sock_fd, FIONBIO,&b_on);
·5.2 单进程轮询服务器模式
make_null(serv_slot, maxlen); //serv_slot[]是连接套接字描述符数组,本进程为其提供服务
listen(listen_fd, MAXSIZE); //建立倾听套接字
do{
//从完全倾听队列中接收一个连接套接字描述符
conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, sizeof(cli_addr));
if (conn_fd < 0 && errno != EWOULDBLOCK)
error_proc(); //错误处理
else if (conn_fd >= 0) //接收到新的连接套接字描述符
create_new_connect(conn_fd, serv_slot, &maxlen); //建立新连接
for (i = 0; i < maxlen; ++i) //从0到maxlen都是有效连接,进程轮流为其服务
serve_for(serv_slot[i]); //本进程为第i个连接服务
} while (continue);
使用单进程轮询服务器模式仍然无法避免客户端的某些意外(比如非正常断线)或恶意行为造成失效,而且如果客户数量增加,服务器端的相应时延也会加大。因此我们仍然使用并发服务器模式来提供并行的服务,因为一个服务器子进程失效不会影响到其他进程的工作。
第六章 带外数据与多路复用、信号驱动的输入/输出模型
·6.1 多路复用的输入/输出模型
多路复用的概念:进程不是主动询问套接字情况,而是希望对监视的套接字向系统登记,而后采用被动的态度等待。当监视的套接字上发生了事件,进程去检查发生的状况然后做相应的处理。在这种工作方式下,进程是在已经知道在套接字上发生了事件才去检测,在没有发生事件的时候进入睡眠状态。
头文件:<sys/time.h> <unistd.h> [<signal.h>(pselect使用)]
主要函数:
int select (int maxfd, fd_set *rdset, fd_set *wrset, fdset *exset, struct timeval *timeout);
[maxfd是需要监视的最大文件描述符值+1,即系统监视从0到maxfd-1的文件描述符;
rdset, wrset, exset是对应需要检测的可读、可写和异常文件描述符集合;
timeout内没有发生事件,函数返回0]
int pselect(int maxfd, fd_set *rdset, fd_set *wrset, fdset *exset, struct timespec *timeout,const sigset_t sigmask); //POSIX中对select函数的增强,参数sigmask是执行后对堵塞信号恢复
文件描述符集合:
FD_ZERO(fd_set *fdset); //清空初始化
FD_SET(int fd, fd_set *fdset); //增加
FD_CLR(int fd, fd_set *fdset); //删除
FD_ISSET(int fd, fd_set *fdset); //判断包含
·套接字的读、写和异常就绪条件
读就绪:倾听套接字完全连接队列建立新连接;连接套接字的读缓冲区超过读下限、读管道关闭和套接字异常。
写就绪:连接套接字的写缓冲区空闲小于某下限、写管道被关闭和套接字异常。
异常就绪:套接字上到达外带数据。异常就绪连带触发读、写就绪。
上面列出部分常用就绪条件,具体参考帮助手册。
基本用法:
FD_ZERO(&r_set); //初始化
FD_SET(listen_fd, &r_set); //加入可读文件描述符集合
ret = select(listen_fd+1, &r_set, NULL, NULL, NULL); //对倾听套接字进行就绪判断
·6.2 信号驱动的输入/输出模型
信号驱动通常用于接收紧急数据。进程先向系统登记,然后系统检测到数据到达后会向接收者发生SIGIO信号,然后接收者在信号处理器中接收数据。这种方式通常用在接收紧急的控制数据场合。
数据接收者设置:
#include <fcntl.h>
int fcntl(int fd, int cmd,...);
//使用命令F_SETOWN,第三个参数如果是正整数表示进程号,负整数表示进程组接收
·6.3 系统I/O模型的总结
本书讲述了“堵塞方式、非堵塞方式、多路复用和信号驱动”四种I/O模型。
1 堵塞方式:
广泛使用在并发服务器上,当套接字不满足操作条件立即堵塞等待资源。
2 非堵塞方式:
广泛使用在单进程轮询服务器上,浪费较大CPU资源使用场合较少。
3 多路复用方式:
广泛使用在单进程进行多客户端服务上,比非堵塞方式在轮询中节约CPU时间。
4 信号驱动方式:
广泛使用在接收紧急数据场合。
·6.4 带外数据的接收和发送
带外数据就是指在正常数据流信道之外传输的数据,通常用在对远端进程的同步和控制。它和信号驱动方式几乎相同,但是发送的是SIGURG信号,不是SIGIO。
头文件:<sys/types.h>, <sys/socket.h>
主要函数:
int send(int sockfd, void *buf, int len, int flags); //使用MSG_OOB控制选项发送带外数据
int recv(int sockfd, void *buf, int len, int flags); //使用MSG_OOB控制选项接收带外数据
带外数据一次只允许发送一个字节,如send(sock_fd, “bc”, 2, MSG_OOB),TCP只认为最后一个是带外数据,之前都是普通数据。在特殊情况下,带外数据包优先被接收方接受。
接收方在缺省情况下(使用ioctl函数可以改变)使用一个字节的外带数据缓冲区接收外带数据,并且外带数据段和普通套接字数据段字符集不同,可以区分外带数据和普通数据。
如果接收方收到多个带外数据段,TCP会和先前一次收到的数据段中数据做比较,如果其值相同则认为它们是同个带外数据段。由于发送方可能发送多个外带数据段,接收外带数据是必须做容错处理。
接收方同一时刻只允许有一个字节的外带数据,先到者如果没有被及时处理,那么任何后来的外带数据段都将覆盖它。带外数据被覆盖后成为普通数据。
收到外带数据将触发异常就绪。直到读指针大于带外数据标示(紧急)指针后解除异常。
服务器端接收外带数据可以使用:
1 多路复用方式,要点:
检测异常就绪和读就绪套接字先后顺序不同,结果也不同。
2 异步信号驱动方式,要点:
设计SIGURG信号处理器,处理前后注意屏蔽/堵塞信号。
3 检测带外数据标记方式,要点:
//套接字设置成SO_OOBINLINE,即外带数据看成普通数据存放,on=1
setsockopt(conn_fd,SOL_SOCKET,SO_OOBINLINE,&on,sizeof(on));
ioctrl(conn_fd, SIOCATMARK,&n_data); //检测读指针是否和带外数据标示指针重合
if (n_data == 1) //带外数据到达
注意:被覆盖的带外数据将保留继续保留在读缓冲区里,而后当成普通数据读入。如果使用过的带外数据没有及时得从缓冲区里删除,该带外数据可能会被当场普通数据读入,如sleep()系统调用可能导致这类需要紧急处理的过程产生诡异的行为!