I/O 复用
IO multiplexing 多路复用, 也称事件驱动 IO(event driven IO)。通常, 单个进程无法实现同时处理多个 IO, 而基于 select/epoll 方式, 可以轮询所负责的 socket, 一旦任意一个或多个 socket 就绪(可读/可写/异常), 才继续执行, 使得进程不会阻塞在套接字上, 二十 select/epoll 函数上.
阻塞式
以阻塞式 TCP回射 的客户端 str_cli 程序来说:
1 2 3 4 5 6 7 8 9 10 11 12 |
void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL) { Writen(sockfd, sendline, strlen(sendline)); if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout); } } |
程序先阻塞与用户向服务器发送(writen)数据, 等待用户输入, 而后阻塞与从 socket 获取(readline)数据. 用户一直不输入, 则一直阻塞, 即便服务端已有数据需要客户端处理.
select 复用式
select 函数结构如下:
1 2 |
int select(int maxfdp1, fdset *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout); |
select 可以在预设的最大监听数下, 监听若干个套接字的读, 写或异常状况的一个或多个, 并可以设定阻塞时间.
select 参数意思分别为最大监听的套接字个数加 1, 是否可读, 可写, 异常, 阻塞时间. 可以通过可读可写异常参数定义套接字就绪条件, 根据需要可以选择性制空, 时间置空则一直等待, 知道有套接字就绪位置.
fd_set 为一个套接字相关的结构体, 并赋予了四个方法
FD_ZERO 初始化
FD_SET 打开描述符
FD_CLR 关闭描述符
FD_ISSET 判断描述符是否就绪
以下为 select 改写的 str_cli 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include "unp.h" void str_cli(FILE *fp, int sockfd) { int maxfdp1; fd_set rset; char sendline[MAXLINE], recvline[MAXLINE]; FD_ZERO(&rset); for ( ; ; ) { FD_SET(fileno(fp), &rset); FD_SET(sockfd, &rset); maxfdp1 = max(fileno(fp), sockfd) + 1; Select(maxfdp1, &rset, NULL, NULL, NULL); if (FD_ISSET(sockfd, &rset)) { /* socket 可读 */ if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout); } if (FD_ISSET(fileno(fp), &rset)) { /* 标准输入可读(用户开始输入数据) */ if (Fgets(sendline, MAXLINE, fp) == NULL) return; /* 结束关闭 */ Writen(sockfd, sendline, strlen(sendline)); } } } |
其中, 先计算最大需要监听的描述符个数 maxfdp1, 而后调用 select() 函数, 这里之间听套接字是否可读, 如果没有就绪的套接字, 则一直阻塞下去.
一旦有套接字可读, 则 select() 停止阻塞, 程序继续执行, 通过 FD_ISSET 函数判断套接字是否就绪, 是则执行.
以上, 便规避了直接阻塞与 I/O 上, 而是先由 select 托管, 直到有套接字可用, 再执行下去.
另外, 本程序还有一个错误,
1 2 3 4 5 6 |
if (FD_ISSET(fileno(fp), &rset)) { /* 标准输入可读(用户开始输入数据) */ if (Fgets(sendline, MAXLINE, fp) == NULL) return; /* 结束关闭 */ Writen(sockfd, sendline, strlen(sendline)); } |
当客户端获取完了所有用户数据的时候, return 关闭, 但是此时只能保证客户端数据全部发送给服务端了, 但是不能保证服务端处理完成, 更不能保证所有数据都接收完毕, 故此时应当使用更优雅的关闭方式. 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#include "unp.h" void str_cli(FILE *fp, int sockfd) { int maxfdp1, stdineof; fd_set rset; char buf[MAXLINE]; int n; // 标识符, 标志是否处理客户输入 stdineof = 0; FD_ZERO(&rset); for ( ; ; ) { if (stdineof == 0) // 当且仅当标识符为默认状态下, 才服务客户端输入的数据 FD_SET(fileno(fp), &rset); FD_SET(sockfd, &rset); maxfdp1 = max(fileno(fp), sockfd) + 1; Select(maxfdp1, &rset, NULL, NULL, NULL); if (FD_ISSET(sockfd, &rset)) { /* socket 可读 */ if ( (n = Read(sockfd, buf, MAXLINE)) == 0) { if (stdineof == 1) return; /* normal termination */ else err_quit("str_cli: server terminated prematurely"); } Write(fileno(stdout), buf, n); } if (FD_ISSET(fileno(fp), &rset)) { /* 标准输入可读(用户开始输入数据) */ // 处理完用户的所有数据, 将 stdineof 标志符激活, 此时不再处理客户数据 if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) { stdineof = 1; Shutdown(sockfd, SHUT_WR); /* 发送 FIN 标志, 终止对套接字的写操作 */ FD_CLR(fileno(fp), &rset); continue; } Writen(sockfd, buf, n); } } } |
shutdown(int socketfd, int howto) 函数可以选择行的关闭读或写.
鸣谢 Unix 网络编程