Linux网络学习【深入理解多路转接select、poll的代码改写】


🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统与网络知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》 《MySQL数据库学习》 《Python知识内容》 《cpolar知识学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在学习五种IO模型与非阻塞IO之后,一个实际问题随之而来:当服务器需要同时处理多个客户端连接时,如何避免等待某一个连接的数据,而耽误其他连接的处理? 将套接字设置为非阻塞,可以避免读写调用因数据未就绪而持续等待,但不断遍历连接、反复尝试读取,也可能造成空转.select 与 poll 提供了另一种思路:统一等待多个描述符的就绪事件,再由程序处理相应连接.不过,多路转接并不会替应用程序完成数据收发,"就绪通知"也不等于"读写完成" .从单连接阻塞服务器改写为多连接服务器,关键并不是简单地替换几个接口,而是重新组织程序的事件循环:如何维护关注的描述符?如何区分新连接与已有连接的数据到达?连接关闭后如何移除监听并释放资源?这些问题,决定了我们能否把模型原理转化为正确运行的代码.本文将以 "单连接程序 → select 改写 → poll 改写 → 多客户端验证" 为主线,首先理解统一等待与就绪检查的工作方式,再分别分析 select 的描述符集合、集合重建,以及 poll 的描述符数组、关注事件与返回事件.在此基础上,逐步完成连接接收、读写处理、断开清理与超时判断,并结合非阻塞读写讨论部分收发及异常处理.通过对同一服务器程序的逐步改写,理解两种接口的差异与共同思路,为后续学习事件驱动和更高效的多路复用机制打下基础.

目录

一、I/O多路转接之select

1.1初识select

1.什么是 select?

select 是 Linux 提供的一种I/O多路复用(I/OMultiplexing)系统调用.

它允许一个进程(或线程)同时监视多个文件描述符(fd),当其中一个或多个文件描述符满足 I/O 就绪条件时,select 就会返回,通知程序进行相应的读写操作.

简单来说:select 的核心作用就是让一个线程能够同时等待多个文件描述符的 I/O 事件.

需要注意,select 本身并不负责读取或者发送数据,它只负责检测文件描述符是否就绪,真正的数据读写仍然需要通过 read()、recv()、write()、send() 等函数完成.

2.为什么需要 select?

在传统的 TCP 网络编程中,我们经常使用 accept() 接收客户端连接,使用 recv() 接收客户端发送的数据.

例如:

c 复制代码
int sockfd = accept(listenfd, NULL, NULL);

char buffer[1024];

recv(sockfd, buffer, sizeof(buffer), 0);

默认情况下,socket 通常采用阻塞模式.

如果客户端一直不发送数据,那么 recv() 就可能一直阻塞,导致当前线程无法继续执行后面的代码.

假设服务器同时连接了三个客户端:

复制代码
客户端 A:没有发送数据
客户端 B:发送了数据
客户端 C:没有发送数据

如果服务器正在调用 recv() 等待客户端 A 的数据,那么即使客户端B已经发送数据,服务器也无法及时处理 B 的请求.

这就是传统阻塞 I/O 在单线程处理多个客户端时面临的问题.

为了解决这个问题,我们可以使用 select.

3.select 如何解决这个问题?

使用 select 后,服务器不需要依次阻塞等待每个客户端,而是可以同时监视多个客户端的 socket.

例如:

复制代码
                服务器
                  |
                select
                  |
        +---------+---------+
        |         |         |
     客户端 A  客户端 B  客户端 C
        |         |         |
      无数据    有数据     无数据
                  |
             select 返回
                  |
          服务器处理 B 的数据

具体工作过程:

  1. 服务器将多个客户端的 socket 添加到文件描述符集合.
  2. 调用 select(),等待这些 socket 的 I/O 事件就绪.
  3. 如果没有任何 socket 就绪,服务器可以进入阻塞等待状态.
  4. 当某个 socket 就绪时,select() 返回.
  5. 服务器检查哪些 socket 已经就绪.
  6. 对就绪的 socket 执行相应的读写操作.
  7. 处理完成后,再次调用 select(),继续等待新的事件.

通过这种方式,一个线程就可以管理多个客户端连接.

4.select 与多线程有什么区别?

传统的多线程服务器通常会为每个客户端分配一个线程.

复制代码
客户端 A ---- 线程 A
客户端 B ---- 线程 B
客户端 C ---- 线程 C

而使用 select:

复制代码
客户端 A ----+
客户端 B ----+---- 一个线程(select)
客户端 C ----+

两者的主要区别:

对比项 多线程 select
处理方式 多个线程处理客户端 一个线程监视多个客户端
线程数量 通常需要多个线程 可以只使用一个线程
线程切换 可能产生较多切换开销 不需要为每个连接切换线程
并行处理 多个线程可能并行执行 单线程事件处理通常是串行的
适用场景 需要并行处理任务 多连接 I/O 就绪管理

需要强调:select 可以让一个线程管理多个连接,但并不意味着多个客户端的业务代码会真正并行执行.

如果一个客户端的处理过程长时间阻塞,其他客户端仍然可能受到影响.

5.select 属于什么 I/O 模型?

Linux 网络编程中,常见的 I/O 模型包括:

  1. 阻塞 I/O
  2. 非阻塞 I/O
  3. I/O 多路复用
  4. 信号驱动 I/O
  5. 异步 I/O

select 属于其中的 I/O 多路复用模型,同时也属于同步 I/O.

为什么说它是同步 I/O?

因为 select 只是负责等待 I/O 就绪,真正的数据读取仍然需要程序主动调用 recv() 或 read() 完成.

例如:

c 复制代码
select(...);  // 等待 I/O 就绪

recv(...);    // 主动读取数据

所以:

  • select():检测 I/O 是否就绪.
  • recv():真正执行数据读取.

1.2select函数原型

1.select 函数介绍

select 是 Linux 提供的 I/O 多路复用系统调用,能够同时监视多个文件描述符的读、写以及特定异常条件.

当某些文件描述符满足 I/O 就绪条件时,select 就会返回,通知程序执行相应的I/O操作.

2.函数原型

c 复制代码
#include <sys/select.h>

int select(
    int nfds,
    fd_set *readfds,
    fd_set *writefds,
    fd_set *exceptfds,
    struct timeval *timeout
);

1.nfds

nfds 表示需要监视的最大文件描述符值加1.

例如:

c 复制代码
// 假设需要监视 fd 3、5、8
int nfds = 8 + 1;

注意:nfds 不是文件描述符的数量,而是最大文件描述符值加1.

2.readfds

readfds 是需要监视读就绪事件的文件描述符集合.

例如:

c 复制代码
fd_set readfds;

FD_ZERO(&readfds);
FD_SET(3, &readfds);
FD_SET(5, &readfds);

这里表示需要监视 fd 3 和 fd 5 的读就绪状态.

3.writefds

writefds 用来监视文件描述符的写就绪状态.

c 复制代码
fd_set writefds;

FD_ZERO(&writefds);
FD_SET(5, &writefds);

当对应的 socket 满足写就绪条件时,select 就可以报告它可写.

4.exceptfds

exceptfds 用于监视特定异常条件,例如 TCP 带外数据相关的紧急条件.

它不是用于检测所有普通 socket 错误的通用集合.

5.timeout

timeout 用于控制 select 的等待时间.

c 复制代码
struct timeval {
    time_t tv_sec;       // 秒
    suseconds_t tv_usec; // 微秒
};

有三种常见情况:

(1)永久等待

c 复制代码
select(nfds, &readfds,
       NULL, NULL, NULL);

只要没有事件就绪、信号中断或错误,就会一直等待.

(2)等待指定时间

c 复制代码
struct timeval timeout = {5, 0};

select(nfds, &readfds,
       NULL, NULL, &timeout);

最多等待约 5 秒.

(3)立即返回

c 复制代码
struct timeval timeout = {0, 0};

select(nfds, &readfds,
       NULL, NULL, &timeout);

不阻塞,立即检查就绪状态.

注意:Linux 上 select 可能修改 timeout,因此在循环中应重新初始化.

3.fd_set 文件描述符集合

fd_set 是用于保存文件描述符的集合类型.

在 Linux 中,可以将它理解为位图.

例如:

text 复制代码
fd:     0 1 2 3 4 5 6 7
状态:   0 0 0 1 0 1 0 0

这里表示 fd 3 和 fd 5 在集合中.

四个常用操作宏:

宏 功能
FD_ZERO() 清空集合
FD_SET() 添加文件描述符
FD_CLR() 删除文件描述符
FD_ISSET() 判断文件描述符是否在集合中

示例:

c 复制代码
fd_set fds;

FD_ZERO(&fds);

FD_SET(3, &fds);
FD_SET(5, &fds);

if (FD_ISSET(3, &fds)) {
    printf("fd 3 在集合中\n");
}

FD_CLR(3, &fds);

4.select 返回值

返回值 含义
大于 0 有 I/O 事件就绪
等于 0 等待超时
等于 -1 调用失败,检查 errno

例如:

c 复制代码
int ret = select(nfds, &readfds,
                 NULL, NULL, NULL);

if (ret > 0) {
    printf("有事件就绪\n");
} else if (ret == 0) {
    printf("等待超时\n");
} else {
    perror("select");
}

需要注意,返回值表示结果集合中就绪位的总数,同一文件描述符如果在多个集合中就绪,可以被分别计数.


1.3理解select执⾏过程

1.select 的基本执行流程

select 的工作流程可以概括为:

准备集合 → 调用 select → 内核检测 → 返回就绪结果 → 处理 I/O → 再次监听.

具体步骤:

  1. 创建文件描述符集合.
  2. 将需要监视的 fd 添加到集合中.
  3. 调用 select().
  4. 内核检查对应fd 是否满足就绪条件.
  5. 如果没有事件就绪,可以阻塞等待.
  6. 当存在就绪事件时,select 返回.
  7. 程序通过 FD_ISSET() 检查就绪的 fd.
  8. 调用 read()、recv() 等执行实际 I/O 操作.

执行流程:

text 复制代码
        用户程序
            |
            v
     创建 fd_set 集合
            |
            v
      添加需要监视的 fd
            |
            v
       调用 select()
            |
            v
       内核检查 fd
            |
            v
       是否有 fd 就绪?
          /      \
         否       是
         |        |
       等待     返回结果
         |        |
         +--------+
                  |
                  v
          FD_ISSET 检查
                  |
                  v
           recv/read 等
                  |
                  v
            下一轮循环

2.select 执行前后的变化

假设程序需要监视三个 fd:

text 复制代码
fd 3
fd 4
fd 5

首先创建集合:

c 复制代码
fd_set readfds;

FD_ZERO(&readfds);

FD_SET(3, &readfds);
FD_SET(4, &readfds);
FD_SET(5, &readfds);

调用 select 之前:

text 复制代码
fd:       0 1 2 3 4 5
readfds:  0 0 0 1 1 1

然后调用:

c 复制代码
int ret = select(6, &readfds,
                 NULL, NULL, NULL);

假设只有 fd 4 读就绪.

调用返回后:

text 复制代码
fd:       0 1 2 3 4 5
readfds:  0 0 0 0 1 0

可以发现,select 修改了传入的集合.

原本的集合代表需要监视的 fd.

返回后的集合代表已经就绪的 fd.

3.如何判断哪个 fd 就绪?

使用 FD_ISSET().

c 复制代码
if (FD_ISSET(3, &readfds)) {
    // fd 3 读就绪
}

if (FD_ISSET(4, &readfds)) {
    // fd 4 读就绪
}

if (FD_ISSET(5, &readfds)) {
    // fd 5 读就绪
}

如果只有 fd 4 就绪,那么只有第二个条件成立.

需要注意:FD_ISSET() 只是检查集合中的某一位,不负责真正读取数据.

4.为什么需要重新设置集合?

因为 select 会修改传入的 fd_set.

假设第一次调用之前:

text 复制代码
监视 fd:3、4、5

第一次调用之后:

text 复制代码
就绪 fd:4

如果第二次不重新设置,那么 fd 3 和 fd 5 就不在后续的监视集合中了.

所以每次调用前都需要重新准备集合.

5.正确的 select 循环

实际开发中,可以保存两份集合:

c 复制代码
fd_set master;
fd_set ready;

其中:

  • master:保存所有需要监视的 fd.
  • ready:保存本轮 select 的结果.

示例:

c 复制代码
fd_set master;
fd_set ready;

FD_ZERO(&master);

FD_SET(3, &master);
FD_SET(4, &master);
FD_SET(5, &master);

while (1) {
    // 复制原始集合
    ready = master;

    int ret = select(6, &ready,
                     NULL, NULL, NULL);

    if (ret < 0) {
        perror("select");
        break;
    }

    if (FD_ISSET(3, &ready)) {
        // 处理 fd 3
    }

    if (FD_ISSET(4, &ready)) {
        // 处理 fd 4
    }

    if (FD_ISSET(5, &ready)) {
        // 处理 fd 5
    }
}

上述示例假设 fd 3、4、5 都是有效且保持打开的描述符,仅展示集合管理流程.

在真正的服务器中,如果有客户端连接,就将客户端 fd 添加到 master.

如果客户端断开连接,就从 master 中删除该 fd.

6.select 在内核中的执行过程

从用户态和内核态的角度看:

第一步:用户态准备 fd 集合

应用程序设置需要监视的文件描述符.

第二步:进入内核态

用户程序调用 select,内核获得对应的监视请求.

第三步:检查 fd 的就绪状态

内核检查文件描述符是否满足读、写或特定异常条件.

第四步:等待 I/O 事件

如果没有 fd 就绪且允许等待,线程可能进入休眠状态.

第五步:返回就绪结果

当出现就绪事件、超时或信号中断等情况时,调用返回.

第六步:用户态处理 I/O

应用程序根据返回值与集合判断哪些 fd 就绪,随后执行具体操作.

注意:这是逻辑执行流程.内核实际实现还涉及等待队列、设备驱动和进程调度等机制.


1.4socket就绪条件

1.什么是 socket 就绪?

socket 就绪是指某个 socket 当前满足进行相应 I/O 操作的条件.

例如:

  • 接收缓冲区中存在可以读取的数据.
  • 发送缓冲区有可用空间.
  • 监听 socket 有新的连接等待接受.
  • 对端已经关闭连接的发送方向.

select 主要可以检测三种就绪条件:

  1. 读就绪
  2. 写就绪
  3. 特定异常条件

2.读就绪条件

当 socket 满足以下情况之一时,通常会表现为读就绪.

1.接收缓冲区中存在数据

假设客户端发送:

text 复制代码
hello

当数据到达服务器的 socket 接收缓冲区时,该 socket 就可能变成读就绪状态.

服务器可以执行:

c 复制代码
recv(sockfd, buffer, sizeof(buffer), 0);

读取数据.

2.对端正常关闭发送方向

例如,客户端关闭TCP连接.

服务器在读取完接收缓冲区中的剩余数据之后,recv() 通常返回0.

c 复制代码
ssize_t n = recv(sockfd, buffer,
                 sizeof(buffer), 0);

if (n == 0) {
    printf("客户端关闭发送方向\n");
}

此时 socket 仍然可能被 select 判断为读就绪.

因为读取操作可以立即得到 EOF,而不需要继续等待.

因此,读就绪不代表一定能够读取到普通业务数据.

3.监听 socket 有新连接

当一个监听 socket 的已完成连接队列中存在等待接受的连接时,它可以表现为读就绪.

例如:

c 复制代码
if (FD_ISSET(listenfd, &readfds)) {
    int clientfd = accept(listenfd,
                          NULL, NULL);
}

这里:

  • listenfd:负责监听连接.
  • clientfd:负责与客户端通信.

因此,监听socket也需要加入读集合.

4.socket 出现某些错误

当 socket 出现某些错误时,也可能表现为读就绪.

此时调用 recv() 可能返回-1.

因此需要处理以下情况:

c 复制代码
ssize_t n = recv(sockfd, buffer,
                 sizeof(buffer), 0);

if (n > 0) {
    // 成功读取数据
} else if (n == 0) {
    // 对端关闭发送方向
} else {
    // 读取失败,检查 errno
}

3.写就绪条件

写就绪通常表示 socket 当前具备不阻塞地发送一定数据的条件.

例如:

c 复制代码
fd_set writefds;

FD_ZERO(&writefds);
FD_SET(sockfd, &writefds);

int ret = select(sockfd + 1,
                 NULL, &writefds,
                 NULL, NULL);

当:

c 复制代码
FD_ISSET(sockfd, &writefds)

返回非零时,就表明该 socket 被报告为写就绪.

但是需要注意:

写就绪不代表任意大小的数据都可以一次发送完成。

例如:

c 复制代码
send(sockfd, buffer, 1024 * 1024, 0);

即使 socket 已经写就绪,仍然可能只发送一部分数据.

因此,实际网络编程通常需要处理部分发送问题,并配合非阻塞 socket 使用.

4.为什么不能一直监听写事件?

TCP socket 在正常情况下经常处于可写状态.

如果程序始终监听某个 socket 的写事件:

c 复制代码
FD_SET(sockfd, &writefds);

即使没有数据需要发送,select 也可能不断立即返回.

这会使程序产生大量无意义的循环,增加CPU开销.

因此,一般采用以下策略:

  • 存在待发送数据:监听写事件.
  • 数据全部发送完成:取消写事件监听.

5.异常条件就绪

exceptfds 用来监视特定异常条件,例如TCP带外数据相关的紧急条件.

c 复制代码
fd_set exceptfds;

FD_ZERO(&exceptfds);
FD_SET(sockfd, &exceptfds);

需要注意,exceptfds 不是用于检测所有网络错误的集合.

在普通TCP服务器中,主要关注读就绪和写就绪即可.


1.5select的特点

1.select 属于同步 I/O

select 是一种同步 I/O 多路复用机制.

它只负责等待文件描述符满足 I/O 就绪条件.

当文件描述符就绪后,程序仍然需要主动调用:

c 复制代码
read();
recv();
write();
send();

完成数据传输.

因此,select 并不是异步 I/O.

2.一个线程可以监视多个 fd

传统的阻塞 I/O 模型中,如果程序正在等待某个socket的数据,就可能无法及时处理其他 socket.

而 select 可以让一个线程同时等待多个文件描述符.

例如:

text 复制代码
                  select
                     |
          +----------+----------+
          |          |          |
         fd3        fd4        fd5
          |          |          |
        无数据     有数据      无数据
                     |
                  处理 fd4

因此,不需要为每个客户端都创建单独的线程.

但需要注意,单线程中的业务处理通常仍然是串行执行的.

3.支持阻塞等待

当没有文件描述符就绪时,select 可以使当前线程进入休眠等待状态.

c 复制代码
select(nfds, &readfds,
       NULL, NULL, NULL);

这样可以避免程序不断轮询文件描述符状态而浪费 CPU 资源.

4.支持超时机制

可以通过 timeout 指定最长等待时间.

c 复制代码
struct timeval timeout = {3, 0};

select(nfds, &readfds,
       NULL, NULL, &timeout);

表示最多等待约 3 秒.

超时机制适用于需要定期执行其他任务的程序.

5.select 是水平触发机制

select 使用水平触发(Level Triggered,LT)式的就绪检测.

水平触发的核心思想:

只要文件描述符仍然满足就绪条件,下一次调用select时仍然可以检测到它.

例如:

客户端向服务器发送 100 字节数据.

服务器只读取了 20 字节:

c 复制代码
recv(sockfd, buffer, 20, 0);

如果接收缓冲区中还有 80 字节未读取,那么重新将该 socket 加入读集合并调用 select 后,仍然可以检测到它的读就绪状态.

直到就绪条件不再满足.

因此,select 不要求程序在一次通知中就读完所有数据.

6.select 具有较好的可移植性

select 在多种类 Unix 系统中都得到支持.

而 epoll 是 Linux 特有的 I/O 多路复用接口.

因此,select 在跨平台开发方面具有一定优势.

但是不同操作系统在文件描述符限制、超时处理等方面可能存在差异.


1.6select缺点

虽然 select 可以实现 I/O 多路复用,但是在监视大量文件描述符时存在一些明显不足.

1.文件描述符编号存在限制

Linux 常用的 glibc 实现中:

c 复制代码
FD_SETSIZE = 1024

因此,fd_set 通常只能安全表示编号为 0~1023 的文件描述符.

例如:

c 复制代码
FD_SET(5, &readfds);

可以正常使用.

但是:

c 复制代码
FD_SET(1024, &readfds);

在这种实现中会产生未定义行为.

注意:限制的是文件描述符编号,并不是客户端数量.

因此,不能简单地认为服务器一定能管理 1024 个客户端连接.

2.每次调用都需要重新准备集合

因为 select 会修改传入的 fd_set,所以每次调用之前都需要重新准备集合.

例如:

c 复制代码
while (1) {
    FD_ZERO(&readfds);

    FD_SET(fd1, &readfds);
    FD_SET(fd2, &readfds);

    select(maxfd + 1, &readfds,
           NULL, NULL, NULL);
}

如果需要监视大量 fd,就需要不断重新设置集合.

3.用户态和内核态之间的数据传递

每次调用 select,都需要将监视集合提供给内核,并由内核返回就绪结果.

监视范围越大,集合检查以及相关数据传递的开销通常也越大.

这也是 select 在大规模连接场景下存在性能局限的原因之一.

4.需要遍历文件描述符

select 返回之后,程序通常需要依次检查文件描述符.

例如:

c 复制代码
for (int fd = 0; fd <= maxfd; fd++) {
    if (FD_ISSET(fd, &readfds)) {
        // 处理就绪的 fd
    }
}

假设需要监视1000个fd,但是只有一个 fd 就绪.

程序仍然可能需要遍历整个范围,才能找到这个就绪的 fd.

因此,这种就绪检查一般具有 O(n) 级别的扫描开销.

5.文件描述符稀疏时也存在额外检查

假设:

text 复制代码
fd 3
fd 100
fd 800
fd 1000

程序只需要监视这四个 fd.

但是如果使用:

c 复制代码
for (int fd = 0; fd <= 1000; fd++) {
    if (FD_ISSET(fd, &readfds)) {
        // 处理
    }
}

仍然需要检查大量没有使用的编号.

因此,select 的普通遍历方式在 fd 编号稀疏时也不够理想.

6.select、poll、epoll 对比

对比项 select poll epoll
fd 管理 fd_set pollfd 数组 内核事件注册
固定 fd_set 限制 有 无 无
每次传入监听集合 需要 需要 不必完整重传
获取就绪事件 遍历检查 遍历检查 返回就绪事件列表
平台支持 较广泛 较广泛 Linux
适合场景 少量连接 中小规模连接 大量并发连接

需要注意,epoll 并不是所有操作都具有 O(1) 时间复杂度,它的实际性能还受到就绪事件数量等因素影响.

7.为什么后来出现 poll 和 epoll?

select 存在三个主要问题:

  1. 文件描述符编号受到固定限制.
  2. 每次调用都需要重新设置和传递集合.
  3. 返回后需要遍历检查就绪的 fd.

poll 使用数组管理文件描述符,解决了 fd_set 固定容量的问题.

epoll 使用事件注册和就绪通知机制,减少每轮完整提交及遍历监视集合的开销.

因此,在Linux高并发网络服务器中,通常更倾向于使用 epoll.


1.7select使⽤示例:检测标准输⼊输出

1.标准输入和标准输出

在Linux 中,一个进程通常拥有三个标准文件描述符.

文件描述符 宏 作用
0 STDIN_FILENO 标准输入
1 STDOUT_FILENO 标准输出
2 STDERR_FILENO 标准错误

标准输入通常用于接收键盘输入.

标准输出通常用于向终端输出数据.

标准错误通常用于输出错误信息.

本节使用 select 检测标准输入和标准输出的 I/O 就绪状态.

2.实现目标

程序需要完成以下功能:

  1. 使用 select 监视标准输入是否可读.
  2. 如果标准输入可读,调用 read() 读取数据.
  3. 存在待输出数据时,监视标准输出是否可写.
  4. 如果标准输出可写,调用 write() 输出数据.
  5. 重复上述过程.

注意:标准输出通常长期处于可写状态,因此不能在没有待输出数据时一直监视写事件,否则可能造成频繁的无意义唤醒.

3.完整代码

文件名:select_stdio.c

c 复制代码
#include <errno.h>
#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>

int main(void) {
    char buf[1024];

    size_t pending = 0;
    size_t sent = 0;

    int input_eof = 0;

    fprintf(stderr, "请输入内容,Ctrl+D 结束:\n");

    while (!input_eof || sent < pending) {
        fd_set readfds, writefds;

        FD_ZERO(&readfds);
        FD_ZERO(&writefds);

        // 没有待输出数据时,监视标准输入
        if (!input_eof && sent == pending) {
            FD_SET(STDIN_FILENO, &readfds);
        }

        // 有待输出数据时,监视标准输出
        if (sent < pending) {
            FD_SET(STDOUT_FILENO, &writefds);
        }

        int maxfd = STDIN_FILENO > STDOUT_FILENO
                        ? STDIN_FILENO
                        : STDOUT_FILENO;

        int ret = select(maxfd + 1,
                         &readfds, &writefds,
                         NULL, NULL);

        if (ret < 0) {
            if (errno == EINTR)
                continue;

            perror("select");
            return 1;
        }

        // 标准输入读就绪
        if (FD_ISSET(STDIN_FILENO, &readfds)) {
            ssize_t n = read(STDIN_FILENO,
                             buf, sizeof(buf));

            if (n == 0) {
                input_eof = 1;
            } else if (n < 0) {
                if (errno == EINTR)
                    continue;

                perror("read");
                return 1;
            } else {
                pending = (size_t)n;
                sent = 0;
            }
        }

        // 标准输出写就绪
        if (FD_ISSET(STDOUT_FILENO, &writefds)) {
            ssize_t n = write(STDOUT_FILENO,
                              buf + sent,
                              pending - sent);

            if (n < 0) {
                if (errno == EINTR)
                    continue;

                perror("write");
                return 1;
            }

            if (n == 0) {
                fprintf(stderr, "write 返回 0\n");
                return 1;
            }

            sent += (size_t)n;

            if (sent == pending) {
                sent = 0;
                pending = 0;
            }
        }
    }

    return 0;
}

4.编译和运行

编译:

bash 复制代码
gcc select_stdio.c -o select_stdio

运行:

bash 复制代码
./select_stdio

输入:

text 复制代码
hello

按下回车,程序读取数据后,再输出:

text 复制代码
hello

也可以使用管道测试:

bash 复制代码
printf "hello\n" | ./select_stdio

5.代码分析

1.创建文件描述符集合

c 复制代码
fd_set readfds, writefds;

FD_ZERO(&readfds);
FD_ZERO(&writefds);

创建两个集合:

  • readfds:监视标准输入读事件.
  • writefds:监视标准输出写事件.

2.添加标准输入

c 复制代码
FD_SET(STDIN_FILENO, &readfds);

STDIN_FILENO 的值通常是 0.

表示将标准输入加入读事件监视集合.

完整代码中,只有没有待输出数据且没有到达 EOF 时才会添加.

3.调用 select

c 复制代码
select(maxfd + 1,
       &readfds, &writefds,
       NULL, NULL);

等待需要监视的输入或输出事件就绪.

4.检查标准输入

c 复制代码
if (FD_ISSET(STDIN_FILENO, &readfds)) {
    // 标准输入读就绪
}

如果标准输入读就绪,就通过 read() 读取数据.

5.检查标准输出

c 复制代码
if (FD_ISSET(STDOUT_FILENO, &writefds)) {
    // 标准输出写就绪
}

如果标准输出写就绪,就通过 write() 输出缓冲区中的数据.

6.为什么要使用 sent?

write() 不一定一次写完全部数据.

因此程序通过 sent 记录已经写出的字节数.

例如:

c 复制代码
sent += (size_t)n;

如果仍然有数据未输出,就继续等待标准输出写就绪.

直到全部输出完成.


1.8select使⽤示例:使⽤select实现字典服务器

1.项目需求

本节使用 select 实现一个 TCP 字典服务器.

客户端向服务器发送英文单词,服务器返回对应的中文含义.

例如:

text 复制代码
客户端发送:apple
服务器返回:苹果

客户端发送:book
服务器返回:书

服务器需要实现以下功能:

  1. 使用 TCP 协议进行网络通信.
  2. 使用 select 实现 I/O 多路复用.
  3. 支持多个客户端同时连接.
  4. 使用一个线程管理多个客户端.
  5. 接收客户端发送的英文单词.
  6. 根据单词查询字典并返回结果.
  7. 客户端断开连接后,及时删除对应的文件描述符.

本示例规定:

  • 每个 TCP 连接处理一次单词查询.
  • 客户端发送以换行符结束的单词.
  • 服务器收到完整单词后返回翻译结果.
  • 响应发送完成后,服务器关闭该客户端连接.

2.字典服务器的基本原理

服务器需要维护两类 socket.

1.监听 socket

监听 socket 主要负责接收新的客户端连接.

c 复制代码
int listenfd;

当 listenfd 读就绪时,说明可能存在等待接受的客户端连接.

此时调用:

c 复制代码
int clientfd = accept(listenfd, NULL, NULL);

获取与客户端通信的 socket.

2.通信 socket

通信 socket 主要负责与客户端进行数据交互.

c 复制代码
int clientfd;

当客户端发送数据时,通信socket可能变成读就绪状态.

服务器通过:

c 复制代码
recv(clientfd, buffer, sizeof(buffer), 0);

接收客户端发送的数据.

当服务器有数据需要返回时,可以监视对应 socket 的写就绪状态,然后执行 send().

3.select 如何管理多个客户端?

服务器将监听 socket 和各客户端 socket 加入对应的文件描述符集合.

然后通过 select() 等待 I/O 事件.

text 复制代码
                  服务器
                    |
                 select
                    |
       +------------+------------+
       |            |            |
    listenfd      clientA      clientB
       |            |            |
     新连接       读就绪       读就绪
       |            |            |
    accept()      recv()       recv()
       |            |            |
    注册 fd       查询字典      查询字典
                    |            |
                  send()       send()

整个服务器可以由一个线程运行,不需要为每个客户端创建一个独立的线程.

3.实现字典查询功能

首先定义一个简单的字典查询函数.

c 复制代码
const char *lookup(const char *word) {
    if (strcmp(word, "apple") == 0)
        return "苹果\n";

    if (strcmp(word, "book") == 0)
        return "书\n";

    if (strcmp(word, "hello") == 0)
        return "你好\n";

    if (strcmp(word, "select") == 0)
        return "I/O 多路复用\n";

    return "未找到该单词\n";
}

这里使用 strcmp() 比较字符串.

如果找到对应单词,就返回中文含义,否则返回未找到.

4.完整服务器代码

文件名:select_dict_server.c

c 复制代码
#define _POSIX_C_SOURCE 200809L

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <unistd.h>

#define PORT 8888
#define WORD_SIZE 128
#define REPLY_SIZE 128

// 保存客户端状态
typedef struct {
    int active;
    int replying;

    char word[WORD_SIZE];
    size_t word_len;

    char reply[REPLY_SIZE];
    size_t reply_len;
    size_t sent;
} Client;

// 客户端状态数组
static Client clients[FD_SETSIZE];

// 设置非阻塞模式
static int set_nonblock(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);

    if (flags < 0)
        return -1;

    return fcntl(fd, F_SETFL,
                 flags | O_NONBLOCK);
}

// 字典查询
static const char *lookup(const char *word) {
    if (strcmp(word, "apple") == 0)
        return "苹果\n";

    if (strcmp(word, "book") == 0)
        return "书\n";

    if (strcmp(word, "hello") == 0)
        return "你好\n";

    if (strcmp(word, "select") == 0)
        return "I/O 多路复用\n";

    return "未找到该单词\n";
}

// 关闭客户端
static void close_client(int fd) {
    close(fd);

    memset(&clients[fd], 0,
           sizeof(clients[fd]));

    printf("客户端断开:fd=%d\n", fd);
}

// 准备响应内容
static void set_reply(int fd, const char *msg) {
    Client *c = &clients[fd];

    snprintf(c->reply, sizeof(c->reply),
             "%s", msg);

    c->reply_len = strlen(c->reply);
    c->sent = 0;
    c->replying = 1;
}

int main(void) {
    // 1. 创建监听 socket
    int listenfd = socket(AF_INET,
                          SOCK_STREAM, 0);

    if (listenfd < 0) {
        perror("socket");
        return 1;
    }

    // 设置非阻塞
    if (listenfd >= FD_SETSIZE ||
        set_nonblock(listenfd) < 0) {
        fprintf(stderr, "监听 socket 初始化失败\n");
        close(listenfd);
        return 1;
    }

    // 2. 设置地址复用
    int opt = 1;

    if (setsockopt(listenfd,
                   SOL_SOCKET,
                   SO_REUSEADDR,
                   &opt,
                   sizeof(opt)) < 0) {
        perror("setsockopt");
        close(listenfd);
        return 1;
    }

    // 3. 设置服务器地址
    struct sockaddr_in addr = {0};

    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);

    addr.sin_addr.s_addr =
        htonl(INADDR_LOOPBACK);

    // 4. 绑定地址
    if (bind(listenfd,
             (struct sockaddr *)&addr,
             sizeof(addr)) < 0) {
        perror("bind");
        close(listenfd);
        return 1;
    }

    // 5. 开始监听
    if (listen(listenfd, 32) < 0) {
        perror("listen");
        close(listenfd);
        return 1;
    }

    printf("服务器监听 127.0.0.1:%d\n", PORT);
    fflush(stdout);

    // 6. 进入事件循环
    while (1) {
        fd_set readfds;
        fd_set writefds;

        FD_ZERO(&readfds);
        FD_ZERO(&writefds);

        // 监听新的客户端连接
        FD_SET(listenfd, &readfds);

        int maxfd = listenfd;

        // 7. 将客户端添加到集合
        for (int fd = 0;
             fd < FD_SETSIZE; ++fd) {

            if (!clients[fd].active)
                continue;

            if (clients[fd].replying) {
                // 等待发送响应
                FD_SET(fd, &writefds);
            } else {
                // 等待接收请求
                FD_SET(fd, &readfds);
            }

            if (fd > maxfd)
                maxfd = fd;
        }

        // 8. 调用 select
        int ret = select(maxfd + 1,
                         &readfds,
                         &writefds,
                         NULL,
                         NULL);

        if (ret < 0) {
            if (errno == EINTR)
                continue;

            perror("select");
            break;
        }

        // 9. 处理新连接
        if (FD_ISSET(listenfd, &readfds)) {
            while (1) {
                int fd = accept(listenfd,
                                NULL, NULL);

                if (fd < 0) {
                    if (errno == EINTR)
                        continue;

                    if (errno != EAGAIN &&
                        errno != EWOULDBLOCK) {
                        perror("accept");
                    }

                    break;
                }

                // 检查 fd 的有效范围
                if (fd >= FD_SETSIZE ||
                    set_nonblock(fd) < 0) {

                    fprintf(stderr,
                            "客户端 fd 不可用\n");

                    close(fd);
                    continue;
                }

                // 初始化客户端状态
                memset(&clients[fd], 0,
                       sizeof(clients[fd]));

                clients[fd].active = 1;

                printf("新客户端:fd=%d\n", fd);
            }
        }

        // 10. 遍历客户端
        for (int fd = 0;
             fd < FD_SETSIZE; ++fd) {

            Client *c = &clients[fd];

            if (!c->active)
                continue;

            // 客户端读就绪
            if (!c->replying &&
                FD_ISSET(fd, &readfds)) {

                char buf[256];

                ssize_t n = recv(fd, buf,
                                 sizeof(buf), 0);

                if (n == 0) {
                    close_client(fd);
                } else if (n < 0) {
                    if (errno != EINTR &&
                        errno != EAGAIN &&
                        errno != EWOULDBLOCK) {

                        close_client(fd);
                    }
                } else {
                    // 处理收到的字节
                    for (ssize_t i = 0;
                         i < n; ++i) {

                        // 检测完整单词
                        if (buf[i] == '\n') {
                            if (c->word_len > 0 &&
                                c->word[c->word_len - 1]
                                    == '\r') {
                                --c->word_len;
                            }

                            c->word[c->word_len] = '\0';

                            set_reply(fd,
                                      lookup(c->word));

                            break;
                        }

                        // 避免缓冲区溢出
                        if (c->word_len >=
                            WORD_SIZE - 1) {

                            set_reply(fd,
                                      "单词过长\n");

                            break;
                        }

                        c->word[c->word_len++] =
                            buf[i];
                    }
                }
            }

            // 客户端写就绪
            if (c->active &&
                c->replying &&
                FD_ISSET(fd, &writefds)) {

                ssize_t n = send(
                    fd,
                    c->reply + c->sent,
                    c->reply_len - c->sent,
                    MSG_NOSIGNAL
                );

                if (n > 0) {
                    c->sent += (size_t)n;

                    // 响应全部发送完毕
                    if (c->sent == c->reply_len) {
                        close_client(fd);
                    }
                } else if (
                    n == 0 ||
                    (errno != EINTR &&
                     errno != EAGAIN &&
                     errno != EWOULDBLOCK)
                ) {
                    close_client(fd);
                }
            }
        }
    }

    close(listenfd);

    return 0;
}

5.编译和运行

1.编译服务器

bash 复制代码
gcc select_dict_server.c -o dict_server

2.启动服务器

bash 复制代码
./dict_server

服务器输出:

text 复制代码
服务器监听 127.0.0.1:8888

3.启动客户端

新开一个终端:

bash 复制代码
nc 127.0.0.1 8888

输入:

text 复制代码
apple

服务器返回:

text 复制代码
苹果

响应完成后,服务器会关闭本次客户端连接.

再次建立连接:

bash 复制代码
nc 127.0.0.1 8888

输入:

text 复制代码
book

服务器返回:

text 复制代码
书

可以同时打开多个终端,建立多个客户端连接,观察服务器如何处理不同客户端的数据.

6.核心代码分析

1.创建监听 socket

c 复制代码
int listenfd = socket(AF_INET,
                      SOCK_STREAM, 0);

这里创建一个 TCP socket.

其中:

  • AF_INET:使用 IPv4.
  • SOCK_STREAM:使用面向字节流的 socket,通常对应TCP.
  • 0:使用默认协议.

2.绑定 IP 和端口

c 复制代码
struct sockaddr_in addr = {0};

addr.sin_family = AF_INET;
addr.sin_port = htons(8888);

addr.sin_addr.s_addr =
    htonl(INADDR_LOOPBACK);

这里设置:

  • IP 地址:127.0.0.1
  • 端口:8888

其中 htons() 用于将端口转换为网络字节序.

3.将 listenfd 加入读集合

c 复制代码
FD_SET(listenfd, &readfds);

监听 socket 读就绪表示可能有新的客户端连接等待接受.

因此,当:

c 复制代码
FD_ISSET(listenfd, &readfds)

返回非零时,可以尝试执行 accept().

4.将客户端 fd 加入集合

当客户端连接成功后,服务器会获得一个通信 socket.

例如:

c 复制代码
int fd = accept(listenfd, NULL, NULL);

如果客户端当前没有等待发送的响应数据,就监听其读事件:

c 复制代码
FD_SET(fd, &readfds);

当客户端发送数据后,对应 fd 就可能变成读就绪状态.

5.为什么需要重新构建集合?

因为 select 会修改传入的 fd_set.

所以服务器每一轮都执行:

c 复制代码
FD_ZERO(&readfds);
FD_ZERO(&writefds);

然后根据当前活跃客户端重新添加文件描述符.

这样可以确保服务器始终监视正确的 socket.

6.为什么要使用非阻塞模式?

c 复制代码
set_nonblock(fd);

将 socket 设置为非阻塞,可以避免某个客户端的读写操作长时间阻塞整个事件循环.

如果暂时无法完成读写,相关函数可能返回 -1,并设置:

c 复制代码
errno == EAGAIN

或:

c 复制代码
errno == EWOULDBLOCK

此时不应该直接认为连接发生了严重错误,而应该等待后续 I/O 就绪事件.

7.为什么需要处理 TCP 拆包问题?

TCP 是字节流协议,并不保留应用层消息边界.

例如,客户端发送:

text 复制代码
apple\n

服务器可能一次收到:

text 复制代码
apple\n

也可能分两次收到:

text 复制代码
app

以及:

text 复制代码
le\n

因此,不能直接认为每次 recv() 都会收到一个完整单词.

本示例使用换行符 \n 作为消息结束标志.

程序会不断拼接收到的数据,直到发现换行符后,才进行字典查询.

8.为什么需要处理部分发送?

调用:

c 复制代码
send(fd, buffer, length, 0);

不保证一次就能发送全部数据.

所以程序需要记录已经发送的字节数:

c 复制代码
c->sent += (size_t)n;

如果还有数据没有发送,就继续监视该 socket 的写事件.

直到整个响应发送完毕.

9.客户端断开后为什么需要清理?

如果客户端断开连接,服务器需要及时关闭对应 socket,并清理客户端状态.

本示例使用:

c 复制代码
close_client(fd);

完成:

  • 关闭文件描述符.
  • 清空客户端状态.
  • 使后续事件循环不再监视该客户端.

否则,服务器可能继续监视已经失效的文件描述符.

7.为什么一个线程能够处理多个客户端?

关键就在于:

c 复制代码
select(maxfd + 1,
       &readfds,
       &writefds,
       NULL,
       NULL);

假设当前存在:

text 复制代码
listenfd = 3

客户端 A = fd 4
客户端 B = fd 5
客户端 C = fd 6

当客户端 B 发送数据时:

text 复制代码
fd 5 读就绪

服务器处理 fd 5。

当客户端 A 发送数据时:

text 复制代码
fd 4 读就绪

服务器处理 fd 4.

所有这些操作都可以放在同一个事件循环中完成.

因此:

select 使一个线程能够等待多个客户端的 I/O 事件,并在事件就绪时进行相应处理.

但一个线程处理这些客户端的业务代码时,通常仍然是串行执行的.


二、poll代码改写

cpp 复制代码
//Log.hpp
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>

enum
{
    Debug = 0,
    Info,
    Warning,
    Error,
    Fatal
};

enum
{
    Screen = 10,
    OneFile,
    ClassFile
};

std::string LevelToString(int level)
{
    switch (level)
    {
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warning:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    default:
        return "Unknown";
    }
}

const int defaultstyle = Screen;
const std::string default_filename = "log.";
const std::string logdir = "log";

class Log
{
public:
    Log() : style(defaultstyle), filename(default_filename)
    {
        mkdir(logdir.c_str(), 0775);
    }
    void Enable(int sty) //
    {
        style = sty;
    }
    std::string TimeStampExLocalTime()
    {
        time_t currtime = time(nullptr);
        struct tm *curr = localtime(&currtime);
        char time_buffer[128];
        snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
                 curr->tm_year + 1900, curr->tm_mon + 1, curr->tm_mday,
                 curr->tm_hour, curr->tm_min, curr->tm_sec);
        return time_buffer;
    }
    void WriteLogToOneFile(const std::string &logname, const std::string &message)
    {
        umask(0);
        int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
        if(fd < 0) return;
        write(fd, message.c_str(), message.size());
        close(fd);
        // std::ofstream out(logname);
        // if (!out.is_open())
        //     return;
        // out.write(message.c_str(), message.size());
        // out.close();
    }
    void WriteLogToClassFile(const std::string &levelstr, const std::string &message)
    {
        std::string logname = logdir;
        logname += "/";
        logname += filename;
        logname += levelstr;
        WriteLogToOneFile(logname, message);
    }

    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    void WriteLog(const std::string &levelstr, const std::string &message)
    {
        pthread_mutex_lock(&lock);
        switch (style)
        {
        case Screen:
            std::cout << message;
            break;
        case OneFile:
            WriteLogToClassFile("all", message);
            break;
        case ClassFile:
            WriteLogToClassFile(levelstr, message);
            break;
        default:
            break;
        }
        pthread_mutex_unlock(&lock);
    }
    void LogMessage(int level, const char *format, ...) // 类C的一个日志接口
    {
        char leftbuffer[1024];
        std::string levelstr = LevelToString(level);
        std::string currtime = TimeStampExLocalTime();
        std::string idstr = std::to_string(getpid());

        char rightbuffer[1024];
        va_list args; // char *, void *
        va_start(args, format);
        // args 指向了可变参数部分
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, args);
        va_end(args); // args = nullptr;
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s] ",
                 levelstr.c_str(), currtime.c_str(), idstr.c_str());

        std::string loginfo = leftbuffer;
        loginfo += rightbuffer;
        WriteLog(levelstr, loginfo);
    }
    // void operator()(int level, const char *format, ...)
    // {
    //     LogMessage(int level, const char *format, ...)
    // }
    ~Log() {}

private:
    int style;
    std::string filename;
};

Log lg;

class Conf
{
public:
    Conf()
    {
        lg.Enable(Screen);
    }
    ~Conf()
    {}
};

Conf conf;
cpp 复制代码
//Main.cc
#include <iostream>
#include <memory>
#include "PollServer.hpp"

// ./select_server 8080
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cout << "Usage: " << argv[0] << " port\n" << std::endl;
        return 1;
    }
    uint16_t localport = std::stoi(argv[1]);
    std::unique_ptr<PollServer> svr = std::make_unique<PollServer>(localport);

    svr->InitServer();
    svr->Loop();

    return 0;
}
cpp 复制代码
//Makefile
poll_server:Main.cc
	g++ -o $@ $^ -std=c++14 -lpthread
.PHONY:clean
clean:
	rm -f poll_server
cpp 复制代码
//PollServer.hpp
#pragma once

#include <iostream>
#include <string>
#include <poll.h>
#include <memory>
#include "Log.hpp"
#include "Socket.hpp"

using namespace Net_Work;

const static int gdefaultport = 8888;
const static int gbacklog = 8;
const static int gnum = 1024;
const static int gdefaultfd = -1;
const static int gdefultevent = 0;

class PollServer
{
public:
    PollServer(int port = gdefaultport) : _port(port), _listensock(new TcpSocket()), _isrunning(false), _timeout(-1)
    {
    }
    void HandlerEvents()
    {
        for (int i = 0; i < gnum; i++)
        {
            if (_events[i].fd == gdefaultfd)
                continue;

            int fd = _events[i].fd;
            short revents = _events[i].revents;
            if (revents & POLLIN)
            {
                if (fd == _listensock->GetSockFd())
                {
                    // 新连接到来
                    std::string clientip;
                    uint16_t clientport;
                    int sockfd = _listensock->AcceptConnection(&clientip, &clientport);
                    if (sockfd < 0)
                        continue;
                    lg.LogMessage(Debug, "get a new client, %s:%d\n", clientip.c_str(), clientport);
                    int j = 0;
                    for (; j < gnum; j++)
                    {
                        if (_events[j].fd == gdefaultfd)
                            break;
                    }
                    if (j < gnum)
                    {
                        // 给poll添加新的sockfd
                        _events[j].fd = sockfd;
                        _events[j].events = POLLIN;
                        _events[j].revents = gdefultevent;
                        lg.LogMessage(Debug, "add a new client, fd is : %d\n", sockfd);
                        PrintDebug();
                    }
                    else
                    {
                        // 扩容或者close
                    }
                }
                else
                {
                    char buffer[1024];
                    // fd上面的读事件就绪了
                    ssize_t n = recv(fd, buffer, sizeof(buffer) - 1, 0);
                    if (n > 0)
                    {
                        buffer[n] = 0;
                        lg.LogMessage(Debug, "client info# %s\n", buffer);

                        std::string echo_message = "[server echo]# ";
                        echo_message += buffer;
                        // 写回去 --- epoll会说
                        send(fd, echo_message.c_str(), echo_message.size(), 0);
                    }
                    else if (n == 0)
                    {
                        ::close(fd);
                        _events[i].fd = gdefaultfd;
                        _events[i].events = _events[i].revents = gdefultevent;
                        lg.LogMessage(Info, "client quit..., remove fd: %d\n", fd);
                        PrintDebug();
                    }
                    else
                    {
                        ::close(fd);
                        _events[i].fd = gdefaultfd;
                        _events[i].events = _events[i].revents = gdefultevent;
                        lg.LogMessage(Info, "recv error..., remove fd: %d\n", fd);
                        PrintDebug();
                    }
                }
            }
            if (revents & POLLOUT)
            {
                // TODO
            }
        }
    }
    void InitServer()
    {
        _listensock->BuildListenSocketMethod(_port, gbacklog);
        for (int i = 0; i < gnum; i++)
        {
            _events[i].fd = gdefaultfd;
            _events[i].events = gdefultevent;
            _events[i].revents = gdefultevent;
        }
        _events[0].fd = _listensock->GetSockFd();
        _events[0].events = POLLIN; // 表示对读事件关心(listensock上有新连接,就是读事件就绪)
        lg.LogMessage(Debug, "add listen socket, fd is : %d\n", _listensock->GetSockFd());
    }
    void Loop()
    {
        _isrunning = true;
        while (_isrunning)
        {
            int n = poll(_events, gnum, _timeout);
            switch (n)
            {
            case 0:
                lg.LogMessage(Info, "poll timeout...\n");
                break;
            case -1:
                lg.LogMessage(Error, "poll error!!!\n");
                break;
            default:
                // 正常的就绪的fd
                lg.LogMessage(Info, "get a read event!\n");
                HandlerEvents();
                break;
            }
        }
        _isrunning = false;
    }
    void Stop()
    {
        _isrunning = false;
    }

    void PrintDebug()
    {
        std::cout << "current poll rfds list is : ";
        for (int i = 0; i < gnum; i++)
        {
            if (_events[i].fd == gdefaultfd)
                continue;
            else
                std::cout << _events[i].fd << " ";
        }
        std::cout << std::endl;
    }
    ~PollServer()
    {
    }

private:
    std::unique_ptr<Socket> _listensock;
    int _port;
    int _isrunning;
    struct pollfd _events[gnum];
    int _timeout;
};
cpp 复制代码
//Socket.hpp
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define Convert(addrptr) ((struct sockaddr *)addrptr)

namespace Net_Work
{
    const static int defaultsockfd = -1;
    const int backlog = 5;

    enum
    {
        SocketError = 1,
        BindError,
        ListenError,
    };

    // 封装一个基类,Socket接口类
    // 设计模式:模版方法类
    class Socket
    {
    public:
        virtual ~Socket() {}
        virtual void CreateSocketOrDie() = 0;
        virtual void BindSocketOrDie(uint16_t port) = 0;
        virtual void ListenSocketOrDie(int backlog) = 0;
        virtual int AcceptConnection(std::string *peerip, uint16_t *peerport) = 0;
        virtual bool ConnectServer(std::string &serverip, uint16_t serverport) = 0;
        virtual int GetSockFd() = 0;
        virtual void SetSockFd(int sockfd) = 0;
        virtual void CloseSocket() = 0;
        virtual bool Recv(std::string *buffer, int size) = 0;
        virtual void Send(std::string &send_str) = 0;
        virtual void ReUseAddr() = 0;
        // TODO
    public:
        void BuildListenSocketMethod(uint16_t port, int backlog)
        {
            CreateSocketOrDie();
            ReUseAddr();
            BindSocketOrDie(port);
            ListenSocketOrDie(backlog);
        }
        bool BuildConnectSocketMethod(std::string &serverip, uint16_t serverport)
        {
            CreateSocketOrDie();
            return ConnectServer(serverip, serverport);
        }
        void BuildNormalSocketMethod(int sockfd)
        {
            SetSockFd(sockfd);
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket(int sockfd = defaultsockfd) : _sockfd(sockfd)
        {
        }
        ~TcpSocket()
        {
        }
        void CreateSocketOrDie() override
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
                exit(SocketError);
        }
        void BindSocketOrDie(uint16_t port) override
        {
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_addr.s_addr = INADDR_ANY;
            local.sin_port = htons(port);

            int n = ::bind(_sockfd, Convert(&local), sizeof(local));
            if (n < 0)
                exit(BindError);
        }
        void ListenSocketOrDie(int backlog) override
        {
            int n = ::listen(_sockfd, backlog);
            if (n < 0)
                exit(ListenError);
        }
        int AcceptConnection(std::string *peerip, uint16_t *peerport) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int newsockfd = ::accept(_sockfd, Convert(&peer), &len);
            if (newsockfd < 0)
                return -1;
            *peerport = ntohs(peer.sin_port);
            *peerip = inet_ntoa(peer.sin_addr);
            return newsockfd;
        }
        bool ConnectServer(std::string &serverip, uint16_t serverport) override
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(serverip.c_str());
            server.sin_port = htons(serverport);

            int n = ::connect(_sockfd, Convert(&server), sizeof(server));
            if (n == 0)
                return true;
            else
                return false;
        }
        int GetSockFd() override
        {
            return _sockfd;
        }
        void SetSockFd(int sockfd) override
        {
            _sockfd = sockfd;
        }
        void CloseSocket() override
        {
            if (_sockfd > defaultsockfd)
                ::close(_sockfd);
        }
        bool Recv(std::string *buffer, int size) override
        {
            char inbuffer[size];
            ssize_t n = recv(_sockfd, inbuffer, size-1, 0);
            if(n > 0)
            {
                inbuffer[n] = 0;
                *buffer += inbuffer; // 故意拼接的
                return true;
            }
            else if(n == 0) return false;
            else return false;
        }
        void Send(std::string &send_str) override
        {
            // 多路转接我们在统一说
            send(_sockfd, send_str.c_str(), send_str.size(), 0);
        }
        void ReUseAddr() override
        {
            int opt = 1;
            setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        }
    private:
        int _sockfd;
    };

}

🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容


每日心灵鸡汤: 走自己的路,超越昨天!

沿着别人的轨迹,不一定能走到自己想去的地方,只有走到自己的路上,踩下去的每一脚才会有力量.一个人真正的成长,不是与他人攀比,而是超越昨天的自己.忙碌着、充实着,心里才能充满希望;期待着、盼望着,生活才能更加精彩.别人有别人的优秀和光环,我们也有自己的独一无二.发现缺点,接受不足,才能让每一个今天优于过去的昨天;尽己所能,扬长避短,就是对自己最好的成全.

相关推荐
雪落漂泊28 分钟前
Linux进程(四)概念:命令行参数和环境变量篇
linux·运维·服务器
qq_377572771 小时前
linux x86 arm loongarch
linux
Dragon~Snow1 小时前
Centos Stream10 安装 MySQL-9.7.2-linux-glibc2.28-x86_64.tar
linux·mysql·centos
say_fall1 小时前
【Linux系统编程】Linux进程信号:从“外卖到了“到内核里的一张位图
linux·运维·服务器·车载系统·智能硬件
坤坤子吖2 小时前
Linux网络编程:UDP进阶,从字典服务器到简单聊天室
linux·服务器·网络·c++·笔记·学习
Rabitebla2 小时前
【Linux学习笔记】进程篇(九):进程的创建与退出——fork、写时拷贝与退出码
linux·c++·笔记·学习
李松桃2 小时前
基于IP的服务配置
linux·nginx-核心模块配置
FungLeo2 小时前
成为全栈·基础补充·Linux 服务器入门
linux·运维·服务器·ssh·部署·成为全栈
爱和冰阔落2 小时前
【Linux网络】TCP 服务器怎么写?从 listen、accept 到 read/write,手写完整 Echo Server
linux·运维·服务器·网络