欢迎光临
我们一直在努力

关于select服务器(IO多路复用)的学习

学习自C++从0实现百万并发Reactor服务器_实战课程_慕课网

先给出完整代码,这个程序的核心功能是:

  • 在指定的端口上监听客户端的连接。
  • 可以同时处理多个客户端的连接。
  • 当任何一个客户端发来消息时,服务器会把收到的消息原封不动地再发回给那个客户端。
  • 它使用 select 模型,使得单个线程就能管理所有的连接,而不会因为等待某个客户端而阻塞。
  • 并体会sellect()的设计缺陷:

  • 1024限制: fd_set 的大小通常被硬编码为1024,限制了并发连接数。
  • 需要拷贝readfds
  • 性能瓶颈: 每次都要进行线性扫描 (for 循环) 和 fd_set 的内核/用户空间拷贝,当连接数上千时,性能下降明显。
  • // 初始化服务端的监听端口。
    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 循环仍然会扫描到那个无效的值,造成少量性能浪费。从后往前找是找到新的最大值的最快方法。
    赞(0)
    未经允许不得转载:171主机测评 » 关于select服务器(IO多路复用)的学习
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址