文章目录
推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接。
业务拆解
我的上一篇 文章 里头介绍了两个 网络 I/O 无法复用的案例,其中一个可以实现多路连接,而另一个不可以。我们通过使用 while 无限循环 配合接收函数 accept 以实现多路网络 I/O,但是由于没有使用好 recv 和 send 函数,导致了它不能 “复用”。
在本篇文章之中,我们要实现三个不同的多路复用网络 I/O 的服务器。这三个案例是要吸取这篇 文章 的教训,要改进 recv 和 send 函数的使用方法,以实现 多路 + 可复用 的网络服务器。
我们还是使用 NetAssist 软件来检测代码的效果。

准备工作
我们先要准备这些头文件
#include <errno.h> // 这是全局变量 errno,用于健壮的读取功能
#include <stdio.h>
#include <sys/socket.h> // 创建和管理套接字。绑定地址、监听连接和接受连接。发送和接收数据。设置和获取套接字选项。 socket()、connect()、sendto()、recvfrom()、accept()
#include <netinet/in.h> // 提供了结构体 sockaddr_in
#include <string.h>
#include <unistd.h> // close 函数,关闭套接字
#include <pthread.h> // 一线程一请求的连接模式
#include <sys/select.h> // select 事件触发机制
#include <poll.h> // POLL 事件触发机制
#include <sys/epoll.h> // EPOLL 事件高并发机制
以及占用端口建立服务器,用以监听来访 IP 的网络 I/O 套接字的函数 init_server。
int init_server(unsigned short port) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(port); // 0-1023,
// 设置端口复用
// 当服务器主动关闭 TCP 连接时,会进入 TIME_WAIT 状态(通常持续 2MSL,约 1-4 分钟)。在此期间,操作系统会保留该端口绑定记录,防止延迟到达的数据包干扰新连接。
// 问题:服务器崩溃或重启后尝试重新绑定端口时,会因 TIME_WAIT 状态导致 bind() 失败(错误:Address already in use)
// 以下处理措施:能避免再次启用服务器程序时,系统的宕机
int reuse = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
// 如果未设置 SO_REUSEADDR,导致端口被占用后无法立即重用(TIME_WAIT 状态)
printf("setsockopt failed: %s\n", strerror(errno));
return -1;
}
// setsockopt 是一个用于设置套接字选项的系统调用函数。
// 第二项参数 level:指定选项所在的协议级别。常见的值包括:SOL_SOCKET:表示套接字级别的选项。IPPROTO_TCP:表示 TCP 协议级别的选项。IPPROTO_IP:表示 IP 协议级别的选项。IPPROTO_IPV6:表示 IPv6 协议级别的选项。
// 第三项参数 optname:指定要设置的选项名称。不同的协议级别有不同的选项名称。例如:在 SOL_SOCKET 级别,常见的选项包括 SO_REUSEADDR、SO_KEEPALIVE、SO_LINGER 等。在 IPPROTO_TCP 级别,常见的选项包括 TCP_NODELAY 等。
// 第四项参数 optval:指向包含选项值的内存区域。选项值的类型和大小取决于 optname
// 第五项参数 optlen:指定 optval 的长度(以字节为单位)。
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
printf("bind failed: %s\n", strerror(errno)); // strerror 函数定义在 <string.h> 中,而 errno 是 <errno.h> 的全局变量
return -1;
}
// 一次监听 10 个来访 IP:PORT
// printf("listen finshed: %d\n", sockfd); // 3
if (listen(sockfd, 10) < 0) {
// 将套接字设置为被动模式:套接字从主动连接模式(用于客户端)转换为被动监听模式(用于服务器)。我们可以把这个 socket 想象成公司的前台小姐。
// 5:是监听队列的最大长度,表示系统可以为该套接字排队的最大未完成连接数。当新的连接请求到达时,如果队列已满,新的连接请求将被拒绝。
// 返回值:成功,返回 0。失败,返回 -1,并设置 errno 以指示错误原因。
printf("listen finshed: %d\n", sockfd);
return -1;
}
return sockfd;
}
案例一:一 I/O 一线程的多线程版本
该案例下,一 I/O 一线程的服务器模式可以直观成以下流程图
该模式下的网络 I/O 是由程序创建任务线程所调用的回调函数来负责的。
案例一的 C 代码
负责 I/O 任务的回调函数如下。
// 这是一个针对于线程的回调函数
void *client_thread(void *arg) {
int clientfd = *(int *)arg;
while (1) {
char buffer[1024] = {0}; // 不断地重置缓冲区
int count = recv(clientfd, buffer, 1024, 0); // 读取内容,“0” 是套接字 clientfd 对应网络 I/O 文件的默认权限模式
if (count == 0) { // disconnect
printf("client disconnect: %d\n", clientfd);
close(clientfd); // 关闭 I/O
break;
}
// parser
printf("RECV: %s\n", buffer);
count = send(clientfd, buffer, count, 0); // 对于服务器来说是先读内容后发回信;对于客户端来说
printf("SEND: %s\n", buffer);
}
}
该回调函数又内置了一个 while 无限循环 以达成 “可复用” 网络 I/O 的服务器。为了理解这一句话我们需要理解 recv 到底是一个什么样的函数。
如果套接字(socket)被设置为阻塞模式(默认模式),recv 函数会阻塞调用线程,直到满足以下条件之一:
- 接收到数据:当有数据到达时,recv 函数会返回接收到的数据长度。
- 发生错误:如果发生错误(如连接中断、套接字关闭等),recv 函数会返回一个负值,并设置相应的错误码。
- 到达文件结束符(EOF):如果对端关闭了连接,recv 函数会返回0,表示没有更多数据可读。
也就是说本案例的套接字 clientfd 自建立以来,就一直处于阻塞模式下,只要网络连接所对应的套接字还在以及客户端不发消息,该任务线程便会一直阻塞在recv函数处,客户端发一下消息,该任务线程就动一动。是 while 无限循环 + 阻塞模式的套接字 + send/recv 才实现了本案例的网络 I/O 的复用。
服务器可以实现如下,while 无限循环 实现 “多路连接的关键”,加上前面的回调函数性质可不就是多路复用网络服务器。
int main() {
int sockfd = init_server(PORT);
struct sockaddr_in clientaddr; // 申请地址的内存空间
socklen_t len = sizeof(clientaddr);
while (1) {
printf("accept\n");
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
if (clientfd < 0) {
printf("accept failed\n");
} else {
printf("accept finshed: %d\n", clientfd);
}
pthread_t thid;
pthread_create(&thid, NULL, client_thread, &clientfd); // 每一个网络 I/O 的多次交流都在不同的线程栈上进行
}
printf("exit\n");
return 0;
}
案例一的代码运行效果
代码编译(关于 Linux 多线程编程的使用案例,读者可以参考我的这篇 文章)。
qiming@qiming:~/share/CTASK/TCP_test$ gcc -o networkio networkio.c -lpthread
程序执行,一开始没有来访 IP 的时候如下
qiming@qiming:~/share/CTASK/TCP_test$ ./networkio
accept
连接该服务器

我们发现,该发送 按钮是可以重复按下的,可实现信息的多次发送,命令行中便会出现以下现象(我总共摁了5次)
qiming@qiming:~/share/CTASK/TCP_test$ ./networkio
accept
accept finshed: 4
accept
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
也是可以实现多路复用的,

命令行出现了以下现象
qiming@qiming:~/share/CTASK/TCP_test$ ./networkio
accept
accept finshed: 4
accept
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
accept finshed: 5
accept
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
RECV: I'm qiming,the apprentice of 0voice school.
SEND: I'm qiming,the apprentice of 0voice school.
只是该版本的多路复用网络服务器有一个致命的缺点:一个任务线程负责服务一个 IP 的 I/O 请求,一个线程对计算机资源的占用是极大的,8M,这导致了服务器的性能变差,多路连接的数量天花板很低,可实现连接数相对较少。
案例二:SELECT 版本
select 是一种用于实现多路复用(I/O 多路复用)的系统调用,它允许程序同时监视多个文件描述符(包括套接字),以确定哪些文件描述符已经准备好进行读、写或异常操作。在服务器中编程,select 可以用来同时处理多个客户端连接,提高服务器的并发处理能力。
select 多路复用 I/O 会使用到结构体类型 fd_set 。fd_set 是一个用于表示文件描述符集合的数据结构,通常用于 select 系统调用中。它是一个位掩码数组,每个位对应一个文件描述符,
typedef struct {
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(unsigned long))];
} fd_set;
__FD_SETSIZE:
__FD_SETSIZE是一个宏定义,表示 fd_set 能够处理的最大文件描述符数量。在大多数系统中,默认值是 1024。例如,如果 __FD_SETSIZE 是 1024,那么 fd_set 可以处理从 0 到 1023 的文件描述符。
fds_bits:
- fds_bits 是一个数组,每个元素是一个 unsigned long 类型的值,用于存储位掩码。
- 每个 unsigned long 类型的值通常有 32 或 64 位(取决于平台),因此可以表示 32 或 64 个文件描述符。
- 数组的大小是 FD_SETSIZE / (8 * sizeof(unsigned long)),确保整个数组可以表示 FD_SETSIZE 个文件描述符。
这是 fd_set 套接字集合,而且是映射集,第 n 个二进制位表示对应的第 n 个文件描述符,取值 0 或 1 表示该文件 I/O 是否就绪。
select 多路复用 I/O 除了 fd_set 结构体外,还有 FD_ZERO、FD_SET、FD_CLR 和 FD_ISSET 宏操作方法,具体的程序流程图如下。
虽然这个 SELECT 版本的服务器对计算机资源的占用没有前一个版本的多,但由于该 I/O 机制是靠遍历全部 I/O 文件来执行的,时间复杂度是 O(n),效率相对较低。另外,套接字的模式是默认的阻塞模式,不利于服务器的性能提升。
案例二的 C 代码
和一 I/O 一线程的模式一样的是,服务器的监听套接字 sockfd 和其他的网络 I/O 套接字都是 “阻塞模式”,accept 、recv 和 send 函数都是在接收不到信号的时候会阻塞进程,因而需要条件判断的宏 FD_ISSET,以绕过阻塞。实际上是以逻辑判断绕过阻塞。
select 函数允许程序同时监视多个文件描述符(包括套接字),以确定哪些文件描述符已经准备好进行读、写或异常操作。select 函数的原型如下,
#include <sys/select.h>
#include <sys/time.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- nfds:
- 指定要监视的最大文件描述符加1。例如,如果最大的文件描述符是 10,那么 nfds 应该设置为 11。
- 这个参数确保 select 只检查从 0 到 nfds-1 的文件描述符。
- readfds:
- 指向 fd_set 类型的指针,表示需要监视的文件描述符集合,这些文件描述符准备好进行读操作时,select 会返回。
- 如果不需要监视读操作,可以设置为 NULL。
- writefds:
- 指向 fd_set 类型的指针,表示需要监视的文件描述符集合,这些文件描述符准备好进行写操作时,select 会返回。
- 如果不需要监视写操作,可以设置为 NULL。
- exceptfds:
- 指向 fd_set 类型的指针,表示需要监视的文件描述符集合,这些文件描述符准备好进行异常操作时,select 会返回。
- 如果不需要监视异常操作,可以设置为 NULL。
- timeout:
- 指向 struct timeval 类型的指针,表示 select 调用的超时时间。
- 如果设置为 NULL,select 将阻塞直到至少有一个文件描述符准备好。
- 如果设置为 {0, 0},select 将立即返回,不会阻塞。
- 返回值
- 返回值 > 0:表示有文件描述符准备好,返回值是准备好的文件描述符的数量。
- 返回值 == 0:表示在超时时间内没有文件描述符准备好。
- 返回值 < 0:表示发生错误,通常是因为无效的文件描述符或超时参数。
本案例中的 select 函数是非阻塞的,尽管所有套接字都是阻塞的。而且 rset 是会被传出参数的,操作系统内核会去主动修改套接字集合的。只要套接字所代表的 I/O 文件内还有数据,那对应集合中的映射值依旧为 1,表示此文件还有 I/O 事件没完成。
另外,select 机制水平触发 (level_triggle)的,本次读取还没进行完,就下次继续读,直至该套接字在 fd_set 中的映射显示 0。就像水位警示器一样,只要潮汐危机一刻未解除,就一直发出信号警报。
int main() {
int sockfd = init_server(PORT);
struct sockaddr_in clientaddr; // 申请地址的内存空间
socklen_t len = sizeof(clientaddr);
fd_set rfds, rset; // 这是套接字集合,rfds 是总体文件描述符集合,rset 将会是 rfds 的复制品
FD_ZERO(&rfds); // 置零
FD_SET(sockfd, &rfds); // 先把用于监听的 sockfd 加入 rfds 之中
int maxfd = sockfd; // 一开始没有接收 I/O 的时候,最大的 fd 就是 sockfd
while (1) {
rset = rfds; // 把 rfds 复制过来,保护数据 rfds
struct timeval tv; // timeval 是一个 <sys/time.h> 结构体,用于表示时间值,通常用于指定时间间隔或时间戳。
tv.tv_sec = 0;
tv.tv_usec = 5000; // 5 毫秒= 5000 微秒
int nready = select(maxfd+1, &rset, NULL, NULL, &tv); // 返回就绪集的数量
// select 函数处理的总集合是 0 ~ maxfd
// 所关心的是集合 rset 内的读事件就绪情况
// select 函数的另外三个参数分别是,所关注的可写事件集合、错误事件集合、延时时间 5 毫秒
if (FD_ISSET(sockfd, &rset)) { // accept
// 宏操作 FD_ISSET 检查就绪集 rset 中是否有监控套接字 sockfd
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); // 从监控名单中抽取一位来访 IP:port 的 I/O 注册成一个新的客户 fd
if (clientfd < 0) {
printf("accept failed\n");
} else {
printf("accept finshed: %d\n", clientfd);
}
FD_SET(clientfd, &rfds); // 将套接字映射集合 rfds 的第 clientfd 位置为 1;这等价于说在套接字集合 rfds 上注册 clientfd
if (clientfd > maxfd) maxfd = clientfd; // 新的文件描述符的加入,意味着原来的文件描述符的范围变大了
}
// recv
// 这里就是我们复制 rfds 的原因,前面 rfds 已经因为注册新 clientfd 的原因,得到了更新。但任务还没完成,还要接着处理,故而使用 rset。
int i = 0;
for (i = sockfd+1; i <= maxfd;i ++) { // i fd
if (FD_ISSET(i, &rset)) { // 检测在集合 rset 的第 i 个文件描述符位是否为 1,表示这个 I/O 文件就是我们要处理的读写任务
char buffer[1024] = {0}; // 缓冲区重置为 0
int count = recv(i, buffer, 1024, 0); // 读取内容,
if (count == 0) { // disconnect // 连接失败的情况
printf("client disconnect: %d\n", i);
close(i); // 关闭套接字 i 所对应的 I/O 文件,并且使得该文件所绑定的远程客户端 IP:port 可以重新被 sockfd 所 listen 监听到
FD_CLR(i, &rfds); // 移除文件描述符,准确说是把 rfds 的第 i 个套接字置为 0
continue;
}
// 该条件判断其实体现了 select 多路复用的模式是 Level-triggle 水平触发
// 原因是每次检测都会检查所有的文件描述符是否可读,如果可读那就继续读,这就是所谓的水平触发
// 如果发现读取完了,那就把这个连接关闭,下次该远程客户端可以被 listen 监听到
printf("RECV: %s\n", buffer);
count = send(i, buffer, count, 0); // 服务器是先读取后发送
printf("SEND: %d\n", count);
}
}
}
printf("exit\n");
return 0;
}
案例二的代码运行效果
代码编译
qiming@qiming:~/share/CTASK/TCP_test$ gcc -o networkio networkio.c
执行程序,一开始没有来访 IP,程序并不是处于挂起状态,而是处于 while 无限循环之中。
qiming@qiming:~/share/CTASK/TCP_test$ ./networkio
连接服务器

命令行出现的现象是
qiming@qiming:~/share/CTASK/TCP_test$ ./networkio
accept finshed: 4
RECV: I'm qiming,the apprentice of 0voice school.
SEND: 45
RECV: I'm qiming,the apprentice of 0voice school.
SEND: 45
RECV: I'm qiming,the apprentice of 0voice school.
SEND: 45
我这里也发送了三次信息。它当然也是可以实现多路复用的,

命令行的现象(在新的连接上,我发了两条消息,故而总共5条消息)
qiming@qiming:~/share/CTASK/TCP_test$ ./networkio
accept finshed: 4
RECV: I'm qiming,the apprentice of 0voice school.
SEND: 45
RECV: I'm qiming,the apprentice of 0voice school.
SEND: 45
RECV: I'm qiming,the apprentice of 0voice school.
SEND: 45
accept finshed: 5
RECV: I'm qiming,the apprentice of 0voice school.
SEND: 45
RECV: I'm qiming,the apprentice of 0voice school.
SEND: 45
虽然这个 SELECT 版本的服务器对计算机资源的占用没有前一个版本的多,但由于该 I/O 机制是靠遍历全部 I/O 文件来执行的,时间复杂度是 O(n),效率相对较低。另外,套接字的模式是默认的阻塞模式,不利于服务器的性能提升。
案例三:POLL 版本
POLL机制是一种基于事件触发的 I/O 多路复用机制,它可以同时监视多个文件描述符,当其中任意一个文件描述符就绪时,就会通知程序进行相应的读写操作。该事件触发机制围绕着一个特殊的结构体类型 struct pollfd
struct pollfd {
int fd; 文件描述符
short events; 请求监控的事件 (输入)
short revents; 实际发生的事件 (输出)
};
其中 events 是应用程序告诉内核"我关心该套接字的什么事件",revents 是内核告诉应用程序"该套接字发生了什么事件",内核通过此字段通知应用程序具体发生的事件类型,标识文件描述符的当前就绪状态。
- POLL机制的实现原理
- 用户将想要监听的
socket文件绑定struct pollfd对象,并注册监听事件至struct pollfd对象events成员,监听多个socket文件使用struct pollfd数组。 - 用户通过
struct pollfd数组注册poll事件至poll_list链表,poll_list链表单个元素可以存储固定数量的struct pollfd对象。 - poll系统调用采用轮询方式获取
socket事件信息,一次 poll 调用需完成整个poll_list链表轮询工作,轮询socket的过程中会创建socket等待队列项,并加入socket等待队列(用于socket唤醒进程)。如果检测到socket处于就绪状态,将socket事件保存在struct pollfd对象的revents成员。 - poll系统调用完成一次轮询后,如果检测到有
socket处于就绪状态,则将poll_list链表所有的struct pollfd通过copy_to_user拷贝至用户struct pollfd数组。如果未检测到有socket处于就绪状态,根据超时时间确定是否返回或者阻塞进程。
- 用户将想要监听的
具体可见以下的流程图
案例三的 C 代码
函数 init_server 的定义与前文一样。该服务器的代码如下。
int main() {
int sockfd = init_server(PORT);
struct sockaddr_in clientaddr; // 申请地址的内存空间
socklen_t len = sizeof(clientaddr);
struct pollfd fds[1024] = {0}; // 我们的查询最多只关注 1024 个事件
fds[sockfd].fd = sockfd; // 注册监听套接字 sockfd 入关心的套接字集合
fds[sockfd].events = POLLIN; // 我们关注的是 sockfd 的读事件信息
int maxfd = sockfd; // 一开始,套接字序列最大也就监听套接字 sockfd
while (1) {
int nready = poll(fds, maxfd+1, -1);
// poll 函数用于同时监视多个文件描述符的状态变化(如可读、可写、异常等)
// fds 表示事件总集,maxfd + 1 表示当前所监控的套接字的实际数量,-1 表示表示无限阻塞:直到至少一个文件描述符就绪或捕获到信号才返回。
// 高效监控多个描述符:通过一次系统调用监控 fds 数组中所有描述符,比顺序检查每个描述符更高效(指代 select 机制)
// 事件通知机制:当某个描述符就绪(如数据到达、可写、断开连接等),内核会唤醒进程并返回就绪描述符数量
// 内核监控所有 fds 数组中指定的文件描述符,
// 网络 I/O 是自动发生的,是客户端决定的
// poll 函数的内核操作:
// 1. 直接操作 fds 数组(无需全量拷贝),内核会根据我们所关心的内容进行检查
// 2. 仅遍历数组中的有效描述符
// 3. 将就绪事件写入 revents(操作系统写的)
if (fds[sockfd].revents & POLLIN) { // sockfd 所发生的事件是否是一个 “读事件”
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); // 接收信号
if (clientfd < 0) {
printf("accept failed\n");
} else {
printf("accept finshed: %d\n", clientfd);
}
//FD_SET(clientfd, &rfds); //
fds[clientfd].fd = clientfd; // 注册 clientfd
fds[clientfd].events = POLLIN; // 我们关心 clientfd 的读事件
if (clientfd > maxfd) maxfd = clientfd; // 扩大关注范围
}
int i = 0;
for (i = sockfd+1; i <= maxfd;i ++) { // i fd
if (fds[i].revents & POLLIN) { // 这是二进制运算的 “按位与” 运算,clientfd 所发生的事件是否是一个 “读事件”
char buffer[1024] = {0}; // 内存重置
int count = recv(i, buffer, 1024, 0); // 读取内存
if (count == 0) { // disconnect
printf("client disconnect: %d\n", i);
close(i); // 关闭该网络 I/O
fds[i].fd = -1;
fds[i].events = 0; // 重置事件
continue;
}
// 该条件判断其实体现了 POLL 多路复用的模式是 Level-triggle 水平触发
// 原因是每次检测都会检查所有的文件描述符是否可读,如果可读那就继续读,这就是所谓的水平触发
// 如果发现读取完了,那就把这个连接关闭,下次该远程客户端可以被 listen 监听到
printf("RECV: %s\n", buffer);
count = send(i, buffer, count, 0);
printf("SEND: %d\n", count);
}
}
}
printf("exit\n");
return 0;
}
注意到函数 poll 是连接可复用的关键,
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- struct pollfd *fds
- 这是一个指向 struct pollfd 类型数组的指针。
- nfds_t nfds
- 指定 fds 数组中包含的 struct pollfd 结构的数量。
- int timeout
- 指定 poll 函数的超时时间,单位为毫秒。
- 如果 timeout 为 -1,poll 将无限期阻塞,直到有文件描述符就绪。
- 如果 timeout 为 0,poll 将立即返回,不会阻塞。
- 如果 timeout 为正数,poll 将阻塞指定的毫秒数,如果在超时时间内没有文件描述符就绪,则返回。
- 返回值
- 成功:返回就绪的文件描述符的数量。
- 失败:返回 -1,并设置 errno。
- 超时:返回 0。
对套接字的事件检查是靠位运算执行的。
案例三的代码运行效果
代码编译
qiming@qiming:~/share/CTASK/TCP_test$ gcc -o networkio networkio.c
程序执行,一开始没有外来连接时,程序挂起
qiming@qiming:~/share/CTASK/TCP_test$ ./networkio
读者可以根据前面的演示方法,进行尝试。
总结
| 服务器模式 | 监听套接字模式 | 使连接可复用的方法 | 时间复杂度 | 计算机资源占用 |
|---|---|---|---|---|
| 一 I/O 一请求 | 阻塞 | 创造任务线程,无限循环执行阻塞的 recv 和 send 函数 | O(1) | 极大 |
| SELECT | 阻塞 | 无限循环的执行 select 函数(每次执行都需要调用操作系统内核遍历所有套接字对应的 I/O 文件),使用条件判断绕开阻塞的 recv 和 send 函数 | O(N) | 相对较少 |
| POLL | 阻塞 | 无限循环的执行 poll 函数(每次执行都需要调用操作系统内核遍历所有套接字对应的 I/O 文件),使用条件判断绕开阻塞的 recv 和 send 函数 | O(N) | 相对较少 |
poll 机制相对于 select 机制具有以下优势:
- 没有文件描述符数量限制,可以处理任意数量的文件描述符。
- 更灵活的事件类型,支持多种事件类型。
- 更好的可扩展性,适用于高并发场景。
- 更简单的使用方式,初始化和操作更加直观。
那么我们如何再次改进服务器的性能呢?使其查询的时间复杂度更低,而且对计算机的资源占用更低?这就是 EPOLL,我们还将介绍其百万并发。

被折叠的 条评论
为什么被折叠?



