
🔥承渊政道: 个人主页
❄️个人专栏: 《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 的数据
具体工作过程:
- 服务器将多个客户端的 socket 添加到文件描述符集合.
- 调用
select(),等待这些 socket 的 I/O 事件就绪. - 如果没有任何 socket 就绪,服务器可以进入阻塞等待状态.
- 当某个 socket 就绪时,
select()返回. - 服务器检查哪些 socket 已经就绪.
- 对就绪的 socket 执行相应的读写操作.
- 处理完成后,再次调用
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 模型包括:
- 阻塞 I/O
- 非阻塞 I/O
- I/O 多路复用
- 信号驱动 I/O
- 异步 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 → 再次监听.
具体步骤:
- 创建文件描述符集合.
- 将需要监视的 fd 添加到集合中.
- 调用
select(). - 内核检查对应fd 是否满足就绪条件.
- 如果没有事件就绪,可以阻塞等待.
- 当存在就绪事件时,
select返回. - 程序通过
FD_ISSET()检查就绪的 fd. - 调用
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 主要可以检测三种就绪条件:
- 读就绪
- 写就绪
- 特定异常条件
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 存在三个主要问题:
- 文件描述符编号受到固定限制.
- 每次调用都需要重新设置和传递集合.
- 返回后需要遍历检查就绪的 fd.
poll 使用数组管理文件描述符,解决了 fd_set 固定容量的问题.
epoll 使用事件注册和就绪通知机制,减少每轮完整提交及遍历监视集合的开销.
因此,在Linux高并发网络服务器中,通常更倾向于使用 epoll.
1.7select使⽤示例:检测标准输⼊输出
1.标准输入和标准输出
在Linux 中,一个进程通常拥有三个标准文件描述符.
| 文件描述符 | 宏 | 作用 |
|---|---|---|
| 0 | STDIN_FILENO | 标准输入 |
| 1 | STDOUT_FILENO | 标准输出 |
| 2 | STDERR_FILENO | 标准错误 |
标准输入通常用于接收键盘输入.
标准输出通常用于向终端输出数据.
标准错误通常用于输出错误信息.
本节使用 select 检测标准输入和标准输出的 I/O 就绪状态.
2.实现目标
程序需要完成以下功能:
- 使用
select监视标准输入是否可读. - 如果标准输入可读,调用
read()读取数据. - 存在待输出数据时,监视标准输出是否可写.
- 如果标准输出可写,调用
write()输出数据. - 重复上述过程.
注意:标准输出通常长期处于可写状态,因此不能在没有待输出数据时一直监视写事件,否则可能造成频繁的无意义唤醒.
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
服务器返回:书
服务器需要实现以下功能:
- 使用 TCP 协议进行网络通信.
- 使用
select实现 I/O 多路复用. - 支持多个客户端同时连接.
- 使用一个线程管理多个客户端.
- 接收客户端发送的英文单词.
- 根据单词查询字典并返回结果.
- 客户端断开连接后,及时删除对应的文件描述符.
本示例规定:
- 每个 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;
};
}






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