网络编程笔记(三)-I/O复用:select和poll函数
网络编程笔记(三)-I/O复用:select和poll函数
参考《UNIX网络编程》第 6 章,《TCP/IP 网络编程》 第 7、12 章。
I/O 模型
I/O 复用的场合:
-
当客户处理多个描述符时(一般是交互式输入和网络套接字),必须使用I/O复用。
-
当客户同时处理多个套接字时,这种情况很少出现。
-
如果一个 TCP 服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用 I/O 复用。
-
如果一个服务器既要处理 TCP,又要处理 UDP,一般就要使用 I/O 复用。
-
如果一个服务器要处理多个服务或多个协议,一般就要使用 I/O 复用。
UNIX 下可用的 5 种 I/O 模型:
- 阻塞式 I/O;
- 非阻塞式 I/O;
- I/O 复用(select 和 poll);
- 信号驱动式 I/O(SIGIO);
- 异步 I/O(POSIX 的 aio_系列函数)。
输入操作的 2 个阶段:
- 等待数据准备好(一般是等待数据从网络中到达,到达时被数据被复制到内核中的缓冲区);
- 从内核向进程复制数据(把数据从内核缓冲区复制到应用进程缓冲区)。
前四种为同步 I/O,因为真正的 I/O 操作将阻塞进程。
select 函数
定义和功能
#include <sys/select.h>
#include <sys/time.h>
/*
参数:
maxfd:监视对象文件描述符数量(最大文件描述符+1)
readset:是否存在待读取数据的文件描述符
writeset:是否可传输无阻塞数据的文件描述符
exceptset:是否发生异常的文件描述符
timeout:超时信息 (如果select发生了阻塞,那么就通过设置timeout防止这种情况)
*/
// 返回值:若有就绪描述符则返回其数目,超时返回 0,失败返回 -1
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset,
const struct timeval * timeout)
- 可读:是否存在套接字接收数据。
- 可写:无需阻塞传输数据的套接字有哪些。
- 异常:哪些套接字发生异常。
select 函数调用过程:
- 设置文件描述符,指定监视范围,设置超时。
- 调用 select 函数。
- 查看调用结果。
fd_set:设置文件描述符
fd_set 数据类型的操作以位为单位进行。注册和更改值得操作由下列宏完成。
void FD_ZERO(fd_set *fdset); // 将fd_set变量得所有位初始化为0
void FD_SET(int fd, fd_set *fdset); // 注册:将文件描述符fd置1
void FD_CLR(int fd, fd_set *fdset); // 清除:将文件描述符fd置0
void FD_ISSET(int fd, fd_set *fdset); // 判断文件描述符fd是否被设置为1
maxfd 和中间 3 个描述符集参数
maxfd:指定待测试的描述符个数(待测试的最大描述符 +1,+1 是因为从 0 开始),描述符 0, 1, 2, ... 一直到 maxfd - 1 都将被测试。
如果我们对 readset、writeset、exceptset 中的某一个条件不感兴趣,就可以把它设为空指针。
描述符集参数是值-结果参数。原来为 1 的所有位均变为 0,但发生变化的文件描述符对应位除外——值仍为 1 的文件描述符发生了变化。select 返回后使用 FD_ISSET 宏来测试 fd_set 数据类型。因此,每次重新调用 select 函数时,都得再次把所有描述符集内所关心的位置为 1。
struct timeval:超时时间
struct timeval{
long tv_sec; // sec
long tv_usec; // microsec
};
这个参数有三种选择:
- 永远等待下去:仅在有一个描述符准备好 I/O 时才返回。为此要将该参数设置为空。
- 等待一段固定时间(等待时间不超过这个固定时间)。为此要设置秒数和微秒数。
- 根本不等待:检查描述符后立即返回,这称为轮询(polling)。为此要设置秒数和微秒数均为 0。
描述符就绪条件
对于每一列满足任何一行,该列的表头就准备好了。
str_cli 的两次修改
初始的 str_cli 如下:
#include "unp.h"
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);
}
}
第一次修改
初始的 str_cli 问题是:当套接字发生某些事情时,客户可能阻塞与 fgets 调用。新版本改为阻塞于 select 调用——或是等待标准输入可读,或是等待套接字可读。
- 如果对端 TCP 发送数据,那么该套接字变为可读,并且 read 返回一个大于 0 的值(即读入数据的字节数)
- 如果对端 TCP 发送一个 FIN(对端进程终止),那么该套接字变为可读,并且read 返回 0(EOF)
- 如果对端 TCP 发送一个 RST(对端主机崩溃并重新启动),那么该套接字变为可读,并且 read 返回 -1,errno 含有确切的错误码。
#include "../lib/unp.h"
void str_cli(FILE *fp, int sockfd) {
int maxfdp1;
fd_set rset; // readset: 读描述符集
char sendline[MAXLINE], recvline[MAXLINE];
FD_ZERO(&rset);
for (;;) {
FD_SET(fileno(fp), &rset); // 打开标准IO文件指针fp
FD_SET(sockfd, &rset); // 打开套接字描述符
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
if (Readline(sockfd, recvline, MAXLINE) == 0) // EOF
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
if (Fgets(sendline, MAXLINE, fp) == NULL) return; /* all done */
Writen(sockfd, sendline, strlen(sendline));
}
}
}
这个版本的 str_cli 存在两个问题:
- 批量输入时,标准输入的 EOF 并不意味着同时也完成了从套接字的读入(套接字上还有数据在传送)。
- 混合使用 stdio 和 select 导致错误——select 并不知道 stdio 使用了缓冲区。fgets 返回一行写给服务器,随后 select 被调用以等待新的工作,而不管 stdio 缓冲区还有额外的输入待消费。
shutdown 函数
#include <sys/socket.h>
// 返回值:成功返回0,出错返回-1
int shutdown(int sockfd, int howto)
close 和 shutdown 对比:
- close 把描述符的引用计数减 1,仅在该计数变为 0 时才关闭套接字。 shutdown 可以不管引用计数,直接激发 TCP 的正常连接终止序列(四次握手)。
- close 终止读和写两个方向的数据传输。shutdown 可以告诉对方自己已经完成了数据发送,即使对端仍有数据要发送。shutdown 可以只关闭其中一个流,而非同时断开两个流。
shutdown 的行为依赖于 howto 参数:
- SHUT_RD:断开输入流,关闭连接的读这一半——套接字中不再有数据可接收,进程不能再对这样的套接字调用任何读函数。可以把第二个参数指定为 SHUT_RD 防止环回复制。
关闭SO_USELOOPBACK套接字选项也能防止回环 - SHUT_WR:断开输出流:关闭连接的写这一半(半关闭,half-close)进程不能再对这样的套接字调用任何写函数。
- SHUT_RDWR:同时断开 I/O 流,连接的读半部和写半部都关闭,等效于调用两次 shutdown。
第二次修改
- 使用 select:服务器关闭它那一段的连接就会通知我们。
- 使用 shutdown:废弃了以文本行为中心的代码,改而针对缓冲区操作。
#include "../lib/unp.h"
void str_cli(FILE *fp, int sockfd) {
int maxfdp1, stdineof; // stdineof是一个初始化为0的新标志,只要该标志为0,每次在主循环我们总是select标准输入的可读性。
fd_set rset;
char buf[MAXLINE];
int n;
stdineof = 0;
FD_ZERO(&rset);
for (;;) {
if (stdineof == 0) FD_SET(fileno(fp), &rset); // select标准输入的可读性
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
if ((n = Read(sockfd, buf, MAXLINE)) == 0) {
if (stdineof == 1) // 在套接字读到EOF时,如果我们已在标准输入上遇到 EOF,那就是正常的终止
return; /* normal termination */
else // 如果没有在标准输入上遇到 EOF,那就是服务器进程已经过早终止。
err_quit("str_cli: server terminated prematurely");
}
Write(fileno(stdout), buf, n);
}
if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
if ((n = Read(fileno(fp), buf, MAXLINE)) == 0) {
stdineof = 1; // 标准输入上碰到EOF,将新标志置为1
Shutdown(sockfd, SHUT_WR); /* send FIN */
FD_CLR(fileno(fp), &rset);
continue;
}
Writen(sockfd, buf, n); // read和write:对缓冲区而不是文本行进行操作。
}
}
}
TCP 回射服务器程序(修订版)
服务器使用 select 的版本,避免了为每个客户创建一个新进程。
《TCP/IP网络编程》P203-P205
#include <arpa/inet.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>
#define BUF_SIZE 100
void error_handling(char *message);
int main(int argc, char *argv[]) {
int serv_sock, clnt_sock;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
struct sockaddr_in clnt_adr;
socklen_t adr_sz;
fd_set reads, cpy_reads;
struct timeval timeout;
int fd_max, str_len, fd_num, i;
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1) {
error_handling("socket error");
}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) {
error_handling("bind() error");
}
if (listen(serv_sock, 5) == -1) {
error_handling("listen() error");
}
FD_ZERO(&reads);
FD_SET(serv_sock, &reads); // 初始时,监听套接字是描述集中唯一的非0项
fd_max = serv_sock; // 这里不加1,下面select时就要加1,因为从0到fd_max-1
while (1) {
cpy_reads = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1) break;
if (fd_num == 0) continue;
for (i = 0; i < fd_max + 1; i++) {
if (FD_ISSET(i, &cpy_reads)) {
if (i == serv_sock) { // connection request
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads); // 注册与客户端连接的套接字文件描述符
if (fd_max < clnt_sock) {
fd_max = clnt_sock;
}
printf("connected client : %d \n", clnt_sock);
} else { // read message
str_len = read(i, buf, BUF_SIZE);
if (str_len == 0) { // close
FD_CLR(i, &reads);
close(i);
printf("closed client %d \n", i);
} else {
write(i, buf, str_len); // echo
}
}
}
}
}
close(serv_sock);
return 0;
}
void error_handling(char *message) {
fputs(message, stderr);
fputs("\n", stderr);
exit(1);
}
《UNIX 网络编程》P138 - P142
/* include fig01 */
#include "../lib/unp.h"
int main(int argc, char **argv) {
int i, maxi, maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE]; // client数组,记录客户套接字
ssize_t n;
fd_set rset, allset;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
maxfd = listenfd; /* initialize */
maxi = -1; /* index into client[] array */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* -1 indicates available entry */
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
/* end fig01 */
/* include fig02 */
for (;;) {
rset = allset; /* structure assignment */
nready = Select(maxfd + 1, &rset, NULL, NULL, NULL);
if (FD_ISSET(listenfd, &rset)) { /* new client connection */
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *)&cliaddr, &clilen);
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) {
client[i] = connfd; /* save descriptor */
break;
}
if (i == FD_SETSIZE) err_quit("too many clients");
FD_SET(connfd, &allset); /* add new descriptor to set */
if (connfd > maxfd) maxfd = connfd; /* for select */
if (i > maxi) maxi = i; /* max index in client[] array */
if (--nready <= 0) continue; /* no more readable descriptors */
}
for (i = 0; i <= maxi; i++) { /* check all clients for data */
if ((sockfd = client[i]) < 0) continue;
if (FD_ISSET(sockfd, &rset)) {
if ((n = Read(sockfd, buf, MAXLINE)) == 0) {
/*connection closed by client */
Close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else
Writen(sockfd, buf, n);
if (--nready <= 0) break; /* no more readable descriptors */
}
}
}
}
/* end fig02 */
pselect 函数
#include <sys/select.h>
#include<signal.h>
#include<time.h>
// 返回值:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
int pselect(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *execptset,
const struct timespec *timeout, const sigset_t *sigmask);
-
pselect 使用 timespec 结构,而不使用 timeval 结构.
struct timespec { time_t tv_sec; // seconds long tv_nsec; // nanoseconds,指定纳秒而不是微秒 };
-
pselect 函数增加了第六个参数——一个指向信号掩码的指针。该参数允许先禁止递交某些信号,再测试由这些当前被禁止的信号处理函数设置全局变量,然后调用 pselect,告诉它重新设置信号掩码。
poll 函数
poll 提供的功能与 select 类似,不过在处理流设备时,它能提供额外信息。
#include <poll.h>
// 返回值:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
/*
参数:
fd: 第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。
nfds: 指定结构结构数组中的元素个数。
timeout: 指定poll函数在返回前等待多长时间。
*/
int poll(struct pollfd *fd, nfds_t nfds, int timeout);
要测试的条件由 events 指定,poll 函数在相应的 revents 成员中返回该描述符的状态。
struct pollfd {
int fd; // 被监视的文件描述符
short events; // 请求的事件
short revents; // 实际发生的事件
};
events 和 revents 的一些常值:第一部分处理输入,第二部分处理输出,第三部分处理错误(只能在 revents 中设定)。poll 识别三类数据:普通(normal),优先级带(priority band),高优先级(high priority)。
timeout 值:
- INFTIM:永远等待;
- 0:立即返回,不阻塞进程;
- >0:等待指定数目的毫秒数。
使用 poll 函数改写 TCP 回射服务器程序:《UNIX 网络编程》P146-P148
总结
- 5 种 I/O 模型,这里只讨论 I/O 复用模型。
- I/O 复用最常用的函数是 select。批量输入时,即使用户输入结束,仍然有可能有数据残留在服务器和客户间的管道上(2个方向),需要使用 shutdown 函数以利用其半关闭特性。
- 混合使用 stdio 和 read/write 有危险,需要针对缓冲区而不是文本行进行操作。
- poll 函数功能类似 select,但它能为流设备提供额外信息