Unix网络编程中,selectpoll的IO复用原理是怎样的?

2026-05-25 05:191阅读0评论SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计3608个文字,预计阅读时间需要15分钟。

Unix网络编程中,select/poll的IO复用原理是怎样的?

IO复用select/poll系列文章导航:《Unix网络编程》笔记:描述进程需要一种预先告知内核的能力,以便内核一旦发现进程指定的I/O条件就立即通知该进程。这种能力使得内核能及时响应进程的I/O请求。

IO复用之select/poll

系列文章导航:《Unix 网络编程》笔记

概述

进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个 I/O 条件准备就绪,他就通知进程。这个能力被称为 I/O 复用。

典型应用场景有:

  • 客户处理多个描述符(如之前的应用那样)
  • 客户同时处理多个套接字,不过这种情况比较少见
  • 服务器既要处理监听套接字,又要处理已经连接的套接字
  • 服务器既要处理 TCP,又要处理 UDP
  • 服务器要处理多个服务或多个协议
  • 许多重要的应用程序也需要这种技术
IO模型

Unix 下的 5 种 I/O 模型:

  1. 阻塞式 I/O
  2. 非阻塞式 I/O
  3. I/O 复用(select 和 poll)
  4. 信号驱动式 I/O (SIGIO)
  5. 异步 I/O (aio_)

输入操作的阶段

  1. 等待数据准备好
  2. 从内核向进程复制数据

不同的 I/O 模型就是这两个阶段的行为的不同

阻塞式 I/O

阻塞 I/O 就是两个阶段都等待。

可以看出,这种方式比较缓慢,进程在等待期间不能做其他的事情。

非阻塞式 I/O

非阻塞 I/O 在所查询的数据没有就绪时不会阻塞(上述阶段一),而是直接返回一个错误

用户进程需要不断地进行调用,询问是否准备就绪,一旦就绪才开始处理。

当一个应用进程像这样对一个非阻塞描述符循环调用查询操作时,我们称之为轮询(pollling)。

这种做法往往耗费大量 CPU 时间,反而不太好用。但是在特定的场合下也能发挥其作用。

I/O 复用

I/O 复用会等待在上述两个阶段。但是,第一个阶段的等待是等待在如 select 这些方法上的,而不是 recvfrom,而 select 可以同时等待多个套接字,所以说即使准备就绪 socket 的排在后面,也可以“插队”得到响应。

这种方式也类似于多线程的阻塞式 I/O 模式。

而根据具体的细节,I/O 复用又可以分为:

  • select
  • poll
  • epoll
信号驱动式 I/O

与内核建立 SIGIO 的信号关联并设置回调,当内核有 FD 准备就绪时,发送 SIGIO 信号通知用户,期间用户应用可以执行其他业务,无需阻塞等待。

这种方式在阶段一不会发生阻塞,而且和非阻塞式 I/O 相比也不会发生轮询现象。

缺点是:

  • 当有大量 IO 操作时,信号较多,SIGIO 处理函数不能及时处理可能导致信号队列溢出
  • 内核空间与用户空间的频繁信号交互性能比较低
异步 I/O

告知内核启动某个操作,并让内核完成整个操作后通知我们。(像是在给下属布置任务)

这种方式在上述两个阶段都是非阻塞的,其缺点有:

  • 由于调用方只需调用而无需等待,而具体工作可能很消耗资源,所以多线程高并发环境下可能会耗尽系统资源导致系统崩溃
  • 为了限制并发,需要对上述情况进行处理,从而导致回调函数会变得复杂
问题:消息阻塞

在上一章中已经描述了这个问题发生的原因和造成的原因。

这里侧重问题的解决。

select 函数

等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。

#include <sys/select.h> #include <sys/time.h> int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout); 参数和返回值

这里按照原书的倒序解释各个参数:

timeout :最长等待时间

struct timeval { long tv_sec; long tv_usec; }

分别代表秒和微秒:

  • 如果结构体的两个参数均设为 0,则不等待
  • 如果结构体为空,则一直等待

readset、writeset、exceptset

设置具体关心的描述符集合,通常是一个整数数组,不同的位标识不同的描述符。

void FD_ZERO(fd_set *fdset); /* clear all bits in fdset */ void FD_SET(int fd, fd_set *fdset); /* turn on the bit for fd in fdset */ void FD_CLR(int fd, fd_set *fdset); /* turn off the bit for fd in fdset */ int FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset ? */

例如:

fd_set rset; FD_ZERO(&rset); /* initialize the set: all bits off */ FD_SET(1, &rset); /* turn on bit for fd 1 */ FD_SET(4, &rset); /* turn on bit for fd 4 */ FD_SET(5, &rset); /* turn on bit for fd 5 */

如果把这三个参数都设置为空,相当于获得了一个比 sleep 更为精准的定时器。

maxfdp1

(max file descriptor plus 1)

最大描述符增加 1,这样我们就不用遍历完所有的位了

返回信息

  • 有事件触发,返回事件的数量
  • 定时器到时,返回 0
  • 出错,如打断,返回 -1
就绪条件

套接字准备好读和写的条件:

参考:socket - 描述符就绪条件

套接字异常的条件:

如果一个套接字存在带外数据或仍处于带外标记,那么它有异常要处理(24章中讲解)

最大描述符数

一般不会使用太多的描述符,在一些特殊应用场景下确实需要担心这个问题

限制数量:

  • 早先的系统会限制最大描述符的数量,现在的 Unix 版本往往没有这个限制
  • select 一般会定义一个 FD_SETSIZE 来限制最大描述符的数量,如果想要修改这个值,可能要重新编译内核
  • 有些厂家正在扩展该值,以支持更大的连接数

如果强制修改造成的问题:

  • 描述符数量增大时可能存在扩展性问题

其他方法:

  • 改用 poll 代替 select,可以避免描述符有限的问题
str_cli 改进

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); // fileno 把标准IO文件指针转换成描述符 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) 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)); } } } 问题:半关闭和缓冲 批量输入

在前面的 echo 例子中,交互式地发送消息是很合适的,但是在该例子中,管道容量的利用率并不高,管道上始终只有少量的消息在流动。

假如我们需要传输一个比较大的消息,那么用批量方式可以大大提高管道的利用效率

我们可以利用 Unix 的重定向实现,将文件内容重定向到套接字中,如下图所示:

但是批量输入中有一个问题:对于文件末尾的 EOF,strcli 函数会结束自身执行,并返回到 main 函数中。main 函数也执行完毕,程序随之关闭。

在这种输入方式下,标准输入中的 EOF 并不意味着我们同时也完成了从套接字的读取,可能还有消息还在去或回来的路上。

我们需要的是一种关闭 TCP 连接一半的方法,也就是说,我们想给服务器发送一个 FIN,告诉它我们已经完成了数据的发送,但是仍然保持套接字描述符的打开以便继续读取服务器发来的响应。这将由 shutdown 函数来完成。

缓冲机制
  • 我们的客户端代码 str_cli 会在有数据时调用 fgets 读取,并读入 stdio 的缓冲区中
  • 然而 fgets 只返回其中第一行,其余输入行仍在 stdio 缓冲区中
  • 之后 write 把这一行输入写给服务器
  • 但是尽管缓冲区中还有数据,select 却不会被触发了:因为 select 监听的是我们指定的某一个描述符,而对 stdio 中的各种函数自带的缓冲区毫无察觉
  • 可以把 fgets 之类的函数换成系统调用 read 和 write,从而避免这种问题

Select 和 stdio 一起用所产生的问题

shutdown

之前用的 close 函数的缺陷:

  • close 只是把相关的引用计数 -1,而不能起到立刻关闭的效果,而 shutdown 则是直接关闭(四次挥手)
  • close 终止两个方向的数据传送,shutdown 在这方面的控制更灵活一些

#include <sys/socket.h> int shutdown(int sockfd, int howto);

howto 的可选值:

值 含义 说明 SHUT_RD 关闭读 丢弃套接字接收缓冲区数据,之后接收的消息都被确认,然后丢弃 SHUT_WR 关闭写 留在发送缓冲区的数据被发送,后跟 TCP 的正常连接终止序列 SHUT_RDWR 读写都关闭 等同于上面两个一起的作用 str_cli 改进

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 is readable */ 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)) { /* input is readable */ if ((n = Read(fileno(fp), buf, MAXLINE)) == 0) { stdineof = 1; Shutdown(sockfd, SHUT_WR); /* send FIN */ FD_CLR(fileno(fp), &rset); continue; } Writen(sockfd, buf, n); } } }

  • 我们用 Read 和 Write 代替了 Fgets 和 Fputs
  • 用 Shutdown 代替了直接终止
改进:select 代替多进程

多进程会占用大量的系统资源,影响系统的吞吐量,或许我们可以改用 Select 来同时为多个客户提供服务。

改进代码

int main(int argc, char** argv) { int i, maxi, maxfd, listenfd, connfd, sockfd; int nready, client[FD_SETSIZE]; 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); printf("new client: %s, port %d\n", Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL), ntohs(cliaddr.sin_port)); 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) { /*4connection 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 */ } } } } 拒绝服务攻击

在上述模型中,如果有恶意用户不断建立新的链接,但是不发送有用的信息,则服务会被一直阻塞,耗费服务器的资源。

当一个服务器在处理多个客户时,它绝对不能阻塞于只与单个客户相关的某个函数调用。否则,可能会遭受 DoS 型攻击。

可能的解决方法包括:

  1. 使用非阻塞 IO
  2. 让每个客户由单独的控制线程提供服务
  3. 对 IO 操作设置一个超时
pselect

#include <sys/socket.h> #include <signal.h> #include <time.h> int pselect( int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timespec * timeout, const sigset_t *sigmask );

select 对比的参数解释:

Unix网络编程中,select/poll的IO复用原理是怎样的?

  • 使用 timespec 代替 timeval ,最高精确到纳秒

    struct timespec { time_t tv_sec; long tv_nsec; }

  • pselect 增加了第六个参数:一个指向信号掩码的指针。该

poll 函数介绍

事件如下:

poll 程序修改

略,参见原文

本文共计3608个文字,预计阅读时间需要15分钟。

Unix网络编程中,select/poll的IO复用原理是怎样的?

IO复用select/poll系列文章导航:《Unix网络编程》笔记:描述进程需要一种预先告知内核的能力,以便内核一旦发现进程指定的I/O条件就立即通知该进程。这种能力使得内核能及时响应进程的I/O请求。

IO复用之select/poll

系列文章导航:《Unix 网络编程》笔记

概述

进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个 I/O 条件准备就绪,他就通知进程。这个能力被称为 I/O 复用。

典型应用场景有:

  • 客户处理多个描述符(如之前的应用那样)
  • 客户同时处理多个套接字,不过这种情况比较少见
  • 服务器既要处理监听套接字,又要处理已经连接的套接字
  • 服务器既要处理 TCP,又要处理 UDP
  • 服务器要处理多个服务或多个协议
  • 许多重要的应用程序也需要这种技术
IO模型

Unix 下的 5 种 I/O 模型:

  1. 阻塞式 I/O
  2. 非阻塞式 I/O
  3. I/O 复用(select 和 poll)
  4. 信号驱动式 I/O (SIGIO)
  5. 异步 I/O (aio_)

输入操作的阶段

  1. 等待数据准备好
  2. 从内核向进程复制数据

不同的 I/O 模型就是这两个阶段的行为的不同

阻塞式 I/O

阻塞 I/O 就是两个阶段都等待。

可以看出,这种方式比较缓慢,进程在等待期间不能做其他的事情。

非阻塞式 I/O

非阻塞 I/O 在所查询的数据没有就绪时不会阻塞(上述阶段一),而是直接返回一个错误

用户进程需要不断地进行调用,询问是否准备就绪,一旦就绪才开始处理。

当一个应用进程像这样对一个非阻塞描述符循环调用查询操作时,我们称之为轮询(pollling)。

这种做法往往耗费大量 CPU 时间,反而不太好用。但是在特定的场合下也能发挥其作用。

I/O 复用

I/O 复用会等待在上述两个阶段。但是,第一个阶段的等待是等待在如 select 这些方法上的,而不是 recvfrom,而 select 可以同时等待多个套接字,所以说即使准备就绪 socket 的排在后面,也可以“插队”得到响应。

这种方式也类似于多线程的阻塞式 I/O 模式。

而根据具体的细节,I/O 复用又可以分为:

  • select
  • poll
  • epoll
信号驱动式 I/O

与内核建立 SIGIO 的信号关联并设置回调,当内核有 FD 准备就绪时,发送 SIGIO 信号通知用户,期间用户应用可以执行其他业务,无需阻塞等待。

这种方式在阶段一不会发生阻塞,而且和非阻塞式 I/O 相比也不会发生轮询现象。

缺点是:

  • 当有大量 IO 操作时,信号较多,SIGIO 处理函数不能及时处理可能导致信号队列溢出
  • 内核空间与用户空间的频繁信号交互性能比较低
异步 I/O

告知内核启动某个操作,并让内核完成整个操作后通知我们。(像是在给下属布置任务)

这种方式在上述两个阶段都是非阻塞的,其缺点有:

  • 由于调用方只需调用而无需等待,而具体工作可能很消耗资源,所以多线程高并发环境下可能会耗尽系统资源导致系统崩溃
  • 为了限制并发,需要对上述情况进行处理,从而导致回调函数会变得复杂
问题:消息阻塞

在上一章中已经描述了这个问题发生的原因和造成的原因。

这里侧重问题的解决。

select 函数

等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。

#include <sys/select.h> #include <sys/time.h> int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout); 参数和返回值

这里按照原书的倒序解释各个参数:

timeout :最长等待时间

struct timeval { long tv_sec; long tv_usec; }

分别代表秒和微秒:

  • 如果结构体的两个参数均设为 0,则不等待
  • 如果结构体为空,则一直等待

readset、writeset、exceptset

设置具体关心的描述符集合,通常是一个整数数组,不同的位标识不同的描述符。

void FD_ZERO(fd_set *fdset); /* clear all bits in fdset */ void FD_SET(int fd, fd_set *fdset); /* turn on the bit for fd in fdset */ void FD_CLR(int fd, fd_set *fdset); /* turn off the bit for fd in fdset */ int FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset ? */

例如:

fd_set rset; FD_ZERO(&rset); /* initialize the set: all bits off */ FD_SET(1, &rset); /* turn on bit for fd 1 */ FD_SET(4, &rset); /* turn on bit for fd 4 */ FD_SET(5, &rset); /* turn on bit for fd 5 */

如果把这三个参数都设置为空,相当于获得了一个比 sleep 更为精准的定时器。

maxfdp1

(max file descriptor plus 1)

最大描述符增加 1,这样我们就不用遍历完所有的位了

返回信息

  • 有事件触发,返回事件的数量
  • 定时器到时,返回 0
  • 出错,如打断,返回 -1
就绪条件

套接字准备好读和写的条件:

参考:socket - 描述符就绪条件

套接字异常的条件:

如果一个套接字存在带外数据或仍处于带外标记,那么它有异常要处理(24章中讲解)

最大描述符数

一般不会使用太多的描述符,在一些特殊应用场景下确实需要担心这个问题

限制数量:

  • 早先的系统会限制最大描述符的数量,现在的 Unix 版本往往没有这个限制
  • select 一般会定义一个 FD_SETSIZE 来限制最大描述符的数量,如果想要修改这个值,可能要重新编译内核
  • 有些厂家正在扩展该值,以支持更大的连接数

如果强制修改造成的问题:

  • 描述符数量增大时可能存在扩展性问题

其他方法:

  • 改用 poll 代替 select,可以避免描述符有限的问题
str_cli 改进

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); // fileno 把标准IO文件指针转换成描述符 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) 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)); } } } 问题:半关闭和缓冲 批量输入

在前面的 echo 例子中,交互式地发送消息是很合适的,但是在该例子中,管道容量的利用率并不高,管道上始终只有少量的消息在流动。

假如我们需要传输一个比较大的消息,那么用批量方式可以大大提高管道的利用效率

我们可以利用 Unix 的重定向实现,将文件内容重定向到套接字中,如下图所示:

但是批量输入中有一个问题:对于文件末尾的 EOF,strcli 函数会结束自身执行,并返回到 main 函数中。main 函数也执行完毕,程序随之关闭。

在这种输入方式下,标准输入中的 EOF 并不意味着我们同时也完成了从套接字的读取,可能还有消息还在去或回来的路上。

我们需要的是一种关闭 TCP 连接一半的方法,也就是说,我们想给服务器发送一个 FIN,告诉它我们已经完成了数据的发送,但是仍然保持套接字描述符的打开以便继续读取服务器发来的响应。这将由 shutdown 函数来完成。

缓冲机制
  • 我们的客户端代码 str_cli 会在有数据时调用 fgets 读取,并读入 stdio 的缓冲区中
  • 然而 fgets 只返回其中第一行,其余输入行仍在 stdio 缓冲区中
  • 之后 write 把这一行输入写给服务器
  • 但是尽管缓冲区中还有数据,select 却不会被触发了:因为 select 监听的是我们指定的某一个描述符,而对 stdio 中的各种函数自带的缓冲区毫无察觉
  • 可以把 fgets 之类的函数换成系统调用 read 和 write,从而避免这种问题

Select 和 stdio 一起用所产生的问题

shutdown

之前用的 close 函数的缺陷:

  • close 只是把相关的引用计数 -1,而不能起到立刻关闭的效果,而 shutdown 则是直接关闭(四次挥手)
  • close 终止两个方向的数据传送,shutdown 在这方面的控制更灵活一些

#include <sys/socket.h> int shutdown(int sockfd, int howto);

howto 的可选值:

值 含义 说明 SHUT_RD 关闭读 丢弃套接字接收缓冲区数据,之后接收的消息都被确认,然后丢弃 SHUT_WR 关闭写 留在发送缓冲区的数据被发送,后跟 TCP 的正常连接终止序列 SHUT_RDWR 读写都关闭 等同于上面两个一起的作用 str_cli 改进

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 is readable */ 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)) { /* input is readable */ if ((n = Read(fileno(fp), buf, MAXLINE)) == 0) { stdineof = 1; Shutdown(sockfd, SHUT_WR); /* send FIN */ FD_CLR(fileno(fp), &rset); continue; } Writen(sockfd, buf, n); } } }

  • 我们用 Read 和 Write 代替了 Fgets 和 Fputs
  • 用 Shutdown 代替了直接终止
改进:select 代替多进程

多进程会占用大量的系统资源,影响系统的吞吐量,或许我们可以改用 Select 来同时为多个客户提供服务。

改进代码

int main(int argc, char** argv) { int i, maxi, maxfd, listenfd, connfd, sockfd; int nready, client[FD_SETSIZE]; 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); printf("new client: %s, port %d\n", Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL), ntohs(cliaddr.sin_port)); 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) { /*4connection 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 */ } } } } 拒绝服务攻击

在上述模型中,如果有恶意用户不断建立新的链接,但是不发送有用的信息,则服务会被一直阻塞,耗费服务器的资源。

当一个服务器在处理多个客户时,它绝对不能阻塞于只与单个客户相关的某个函数调用。否则,可能会遭受 DoS 型攻击。

可能的解决方法包括:

  1. 使用非阻塞 IO
  2. 让每个客户由单独的控制线程提供服务
  3. 对 IO 操作设置一个超时
pselect

#include <sys/socket.h> #include <signal.h> #include <time.h> int pselect( int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timespec * timeout, const sigset_t *sigmask );

select 对比的参数解释:

Unix网络编程中,select/poll的IO复用原理是怎样的?

  • 使用 timespec 代替 timeval ,最高精确到纳秒

    struct timespec { time_t tv_sec; long tv_nsec; }

  • pselect 增加了第六个参数:一个指向信号掩码的指针。该

poll 函数介绍

事件如下:

poll 程序修改

略,参见原文