学习自C++从0实现百万并发Reactor服务器_实战课程_慕课网
先给出完整代码,这个程序的核心功能是:
并体会sellect()的设计缺陷:
// 初始化服务端的监听端口。
int initserver(int port);
int main(int argc,char *argv[])
{
if (argc != 2) { printf("usage: ./tcpselect port\\n"); return -1; }
// 初始化服务端用于监听的socket。
int listensock = initserver(atoi(argv[1]));
printf("listensock=%d\\n",listensock);
if (listensock < 0) { printf("initserver() failed.\\n"); return -1; }
// 读事件:1)已连接队列中有已经准备好的socket(有新的客户端连上来了);
// 2)接收缓存中有数据可以读(对端发送的报文已到达);
// 3)tcp连接已断开(对端调用close()函数关闭了连接)。
// 写事件:发送缓冲区没有满,可以写入数据(可以向对端发送报文)。
fd_set readfds; // 需要监视读事件的socket的集合,大小为16字节(1024位)的bitmap。
FD_ZERO(&readfds); // 初始化readfds,把bitmap的每一位都置为0。
FD_SET(listensock,&readfds); // 把服务端用于监听的socket加入readfds。
int maxfd=listensock; // readfds中socket的最大值。
while (true) // 事件循环。
{
// 用于表示超时时间的结构体。
struct timeval timeout;
timeout.tv_sec=10; // 秒
timeout.tv_usec=0; // 微秒。
fd_set tmpfds=readfds; // 在select()函数中,会修改bitmap,所以,要把readfds复制一份给tmpfds,再把tmpfds传给select()。
// 调用select() 等待事件的发生(监视哪些socket发生了事件)。
int infds=select(maxfd+1,&tmpfds,NULL,NULL,0);
// 如果infds<0,表示调用select()失败。
if (infds<0)
{
perror("select() failed"); break;
}
// 如果infds==0,表示select()超时。
if (infds==0)
{
printf("select() timeout.\\n"); continue;
}
// 如果infds>0,表示有事件发生,infds存放了已发生事件的个数。
for (int eventfd=0;eventfd<=maxfd;eventfd++)
{
if (FD_ISSET(eventfd,&tmpfds)==0) continue; // 如果eventfd在bitmap中的标志为0,表示它没有事件,continue
// 如果发生事件的是listensock,表示已连接队列中有已经准备好的socket(有新的客户端连上来了)。
if (eventfd==listensock)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
if (clientsock < 0) { perror("accept() failed"); continue; }
printf ("accept client(socket=%d) ok.\\n",clientsock);
FD_SET(clientsock,&readfds); // 把bitmap中新连上来的客户端的标志位置为1。
if (maxfd<clientsock) maxfd=clientsock; // 更新maxfd的值。
}
else
{
// 如果是客户端连接的socke有事件,表示接收缓存中有数据可以读(对端发送的报文已到达),或者有客户端已断开连接。
char buffer[1024]; // 存放从接收缓冲区中读取的数据。
memset(buffer,0,sizeof(buffer));
if (recv(eventfd,buffer,sizeof(buffer),0)<=0)
{
// 如果客户端的连接已断开。
printf("client(eventfd=%d) disconnected.\\n",eventfd);
close(eventfd); // 关闭客户端的socket
FD_CLR(eventfd,&readfds); // 把bitmap中已关闭客户端的标志位清空。
if (eventfd == maxfd) // 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
{
for (int ii=maxfd;ii>0;ii–) // 从后面往前找。
{
if (FD_ISSET(ii,&readfds))
{
maxfd = ii; break;
}
}
}
}
else
{
// 如果客户端有报文发过来。
printf("recv(eventfd=%d):%s\\n",eventfd,buffer);
// 把接收到的报文内容原封不动的发回去。
send(eventfd,buffer,strlen(buffer),0);
}
}
}
}
return 0;
}
对于select(),需要把要监听的socket传给它。可以监控以下事件。
读事件:1)已连接队列中有已经准备好的socket(有新的客户端连上来了);
2)接收缓存中有数据可以读(对端发送的报文已到达);
3)tcp连接已断开(对端调用close()函数关闭了连接)。
写事件:发送缓冲区没有满,可以写入数据(可以向对端发送报文)。
下面来一步步拆解代码:
1.初始化阶段
// 初始化服务端用于监听的socket。
int listensock = initserver(atoi(argv[1]));
printf("listensock=%d\\n", listensock);
// …
fd_set readfds; // 定义一个“主”fd集合,我们称之为“主监控列表”
FD_ZERO(&readfds); // 清空这个列表
FD_SET(listensock, &readfds); // 把监听socket这第一个需要关心的对象,加入到列表中
int maxfd = listensock; // 当前监控的所有fd中的最大值,目前只有listensock
- readfds 就像一个总的花名册,记录了所有我们需要关心的socket(包括监听socket和所有已连接的客户端socket)。
- maxfd 是一个重要的优化。它告诉 select 函数只需要检查到哪个号码的socket即可,这样不必检查到 FD_SETSIZE (1024)。
2.核心事件循环 while (true)
2.1为什么要复制一份传进select()?
fd_set tmpfds = readfds;
当select返回后,拿到的tmpfds已经被内核“污染”了。它不再是最初的“监视全集”,而变成了“已就绪的子集”。
- select 函数有一个非常重要的特性:它会修改你传入的 fd_set 集合!
- 当 select 返回时,你传入的 tmpfds 会被内核改写,里面只剩下那些真正发生了事件的socket。其他未发生事件的socket都会被从 tmpfds 中移除。
- 而我们的 readfds (主监控列表) 必须保持完整,记录着所有需要监控的socket。所以,每次循环都必须把 readfds 复制一份给 tmpfds,然后把这个“临时工” tmpfds 交给 select 去“蹂躏”。
-
select函数在设计时,为了追求极致的简洁,让fd_set这个参数身兼二职。
1. 作为“输入”: 在调用select之前,你通过FD_ZERO和FD_SET来构建一个fd_set(比如代码中的readfds)。这个fd_set的位图(bitmap)上,标记了所有你希望内核去监视的socket。
2. 作为“输出”: 当select函数阻塞结束并返回时,内核会直接修改你传进去的那份fd_set!它的修改规则是:
- 对于那些没有发生事件的socket,内核会将其在fd_set中对应的位(bit)从1清零。
- 对于那些已经发生事件的socket,内核会保留其对应的位为1。
2.2select的调用
int infds = select(maxfd + 1, &tmpfds, NULL, NULL, 0);
- maxfd + 1: 内核需要检查的范围是 [0, maxfd]。
- &tmpfds: 传入我们关心的读事件集合(的副本)。
- NULL, NULL: 我们在这个程序里不关心写事件和异常事件。
- 0 (作为最后一个参数): 这里的 0 是一个空指针 (void *)0,等同于 NULL。它告诉 select 无限期阻塞,直到有事件发生才返回。如果想设置超时,应该传递 &timeout 结构体的地址。
3.事件处理阶段
for (int eventfd = 0; eventfd <= maxfd; eventfd++)
线性扫描:
- select 只告诉你“有 infds 个事件发生了”,但没告诉你是哪些socket。
- 所以,我们别无选择,只能从0开始,一直遍历到 maxfd,用 FD_ISSET 挨个去问:“是不是你?是不是你发生了事件?”
- 这就是 select 的主要性能瓶颈:O(n) 的轮询开销。即使只有1个socket活跃,也需要遍历所有 maxfd 个socket。
if (FD_ISSET(eventfd, &tmpfds) == 0) continue;
- FD_ISSET 用于检查 eventfd 是否在被 select 修改后的 tmpfds 集合中。如果不在,说明它没事件,跳过。
4.事件分发:新连接 vs. 客户端消息
程序通过判断eventfd是不是listensock来区分两种不同的读事件。
情况1:新连接
// … accept() …
int clientsock = accept(listensock, …);
printf("accept client(socket=%d) ok.\\n", clientsock);
FD_SET(clientsock, &readfds); // 【关键】把新来的客户端加入“主监控列表”
if (maxfd < clientsock) maxfd = clientsock; // 【关键】更新maxfd
- 逻辑: 监听socket发生读事件,意味着“已连接队列”中有新的客户端在等待处理。
- 操作: 调用 accept() 取出新连接,得到 clientsock。然后,这个新的 clientsock 就成了我们下一个需要关心读事件的对象,所以必须用 FD_SET 将它加入到 readfds(主列表)中,以便在下一次 select 循环时能被监控。同时,可能需要更新 maxfd 的值。
情况2:已连接的客户端事件 (else 分支)
char buffer[1024];
if (recv(eventfd, buffer, sizeof(buffer), 0) <= 0)
- 如何判断是有新数据可以读取还是连接断开了?
- 客户端socket的读事件有两种可能:有新数据,或者连接断开。
- recv() 函数的返回值是判断的唯一标准:
- > 0: 成功收到数据,返回值是数据长度。
- == 0: 这是一个明确的信号,表示对端(客户端)调用了 close(),发送了FIN包,连接已正常关闭。
- < 0: 发生了错误,或者连接被异常重置。
- 所以 recv() <= 0 是处理连接断开的通用写法。
// 连接断开的处理
close(eventfd); // 关闭socket,释放资源
FD_CLR(eventfd, &readfds); // 【关键】从“主监控列表”中移除
if (eventfd == maxfd) // 【关键】如果关闭的是当前最大的fd,需要重新计算maxfd
{
for (int ii = maxfd; ii > 0; ii–) {
if (FD_ISSET(ii, &readfds)) {
maxfd = ii; break;
}
}
}
- 逻辑: 客户端走了,我们就不再需要监控它了。
- 操作: close() 关闭连接,FD_CLR 将其从 readfds(主列表)中移除。
- maxfd的维护: 这是一个非常细致的优化。如果不重新计算 maxfd,即使最大的fd已经关闭,select 和 for 循环仍然会扫描到那个无效的值,造成少量性能浪费。从后往前找是找到新的最大值的最快方法。




