尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Linux 网络编程:TCP 通信基础、并发服务器与 IO 多路复用详解

Linux 网络编程:TCP 通信基础、并发服务器与 IO 多路复用详解 1. TCP 通信程序基础流程TCP传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。编写 TCP 通信程序需要分别实现服务端和客户端。1.1 TCP 服务端流程TCP 服务端的主要职责是监听客户端连接请求并为每个客户端创建独立的通信通道。基本流程如下创建套接字使用socket()函数创建监听套接字绑定地址使用bind()函数将套接字与本地 IP 地址和端口绑定开始监听使用listen()函数将套接字设置为监听状态获取新连接使用accept()函数接受客户端连接请求返回新的通信套接字收发数据通过新获取的通信套接字与客户端进行数据收发recv()/send()关闭套接字通信结束后关闭套接字注意TCP 服务端会为每一个客户端创建一个新的连接套接字newfd与其通信。1.2 TCP 客户端流程TCP 客户端主动发起连接请求与服务端建立通信。基本流程如下创建套接字使用socket()函数创建通信套接字绑定地址可选步骤通常由系统自动分配连接服务器使用connect()函数向服务端发起连接请求收发数据连接建立后与服务端进行数据交换关闭套接字通信结束后关闭套接字2. TCP 单执行流服务端的问题在 socket API 中accept()获取新连接、recv()接收数据、send()发送数据默认都是阻塞接口条件不满足就一直等待。阻塞接口导致单执行流无法同时和多个客户端持续通信要么只能和一个客户端持续通信accept 后进入收发循环要么只能和一个客户端通信一次accept 后收发一次数据3. 解决方案并发服务器并发服务器目标同时和多个客户端通信核心思路为每一条客户端新连接创建独立执行流进行通信3.1 两种基础解决方案将所有接口设置为非阻塞使用fcntl()设置文件描述符为非阻塞模式将每一个阻塞操作放到单独的执行流中推荐3.2 多执行流方案多进程稳定性高、健壮性更强但资源开销大多线程资源开销更低线程间通信更方便3.3 TCP 并发服务端通用流程创建套接字绑定地址开始监听循环获取新连接为新连接创建独立执行流执行流内部循环接收数据 → 发送数据连接断开则退出循环关闭套接字3.4 多进程注意事项使用信号处理SIGCHLD避免产生僵尸进程父进程创建子进程之后关闭新连接 fd父进程不再使用3.5 多线程注意事项创建线程后调用pthread_detach()分离线程无需主线程 join 等待回收主线程拿到 newfd 不能 close线程共享进程文件描述符表主线程关闭会导致工作线程无法使用 socket3.6 多执行流方案的缺陷不适合海量客户端场景每一条客户端连接都要创建进程/线程执行流数量过多CPU 上下文切换开销巨大4. IO 模型基础概念4.1 几组关键名词区分4.1.1 阻塞 / 非阻塞描述接口特性调用函数后是否立即返回阻塞条件不满足一直等待不返回非阻塞条件不满足直接报错立即返回4.1.2 同步 / 异步描述任务完成方式操作由谁完成同步任务操作由调用者自己完成异步任务操作由其他主体完成注意IO 操作分为两大阶段①等待 IO 就绪②拷贝数据4.2 四种经典 IO 模型4.2.1 阻塞 IO流程发起 IO 调用 → 内核等待 IO 就绪 → 拷贝数据 → 返回优点逻辑简单编码容易缺点阻塞等待浪费 CPU 资源4.2.2 非阻塞 IO流程发起 IO 调用若未就绪直接返回错误用户循环重试轮询优点CPU 利用率相比阻塞 IO 更高缺点需要循环轮询代码逻辑复杂4.2.3 信号驱动 IO流程预先注册 IO 就绪信号IO 就绪内核发送信号通知进程进程收到信号后发起 IO 拷贝优点相比非阻塞轮询更加实时CPU 利用率更高缺点流程更加复杂4.2.4 异步 IO流程发起 IO 调用立即返回内核全权负责【等待就绪 数据拷贝】全部完成后通知进程优点资源利用率最高缺点控制逻辑复杂度最高5. 多路复用/多路转接 IO 模型5.1 核心功能同时监控大量文件描述符检测哪些 fd 发生 IO 就绪事件可读、可写、异常。5.2 解决原有单执行流服务器痛点原始单执行流问题没有新连接时accept()阻塞、没有数据时recv()阻塞程序卡在一处无法处理其他客户端。多路复用思想单执行流内统一监控所有 fd只对已经就绪的描述符执行 IO 操作不会被未就绪的阻塞接口卡住单进程单线程即可实现并发服务器。6. select 模型基础示例#include stdio.h #include fcntl.h #include unistd.h #include string.h #include stdlib.h #include sys/select.h int main() { // 1. 定义事件集合并对集合进行初始化 fd_set rfds; FD_ZERO(rfds); // 清空集合 // 2. 将描述符添加到集合中 FD_SET(0, rfds); int maxfd 0; // 最大的描述符 // 4. 开始监控 while(1) { // 3. 定义监控超时时间 struct timeval tv; tv.tv_sec 3; // 设置3s的监控超时时间 tv.tv_usec 0; // select(maxfd1, rfds, wfds, efds, timeout) fd_set tmp_rfds rfds; // 每次创建临时集合进行监控 // 因为select调用返回时会修改集合会移除没有就绪的描述符 int ret select(maxfd 1, tmp_rfds, NULL, NULL, tv); if (ret 0) { perror(select error); continue; } if (ret 0) { printf(监控超时没有描述符就绪\n); continue; } // 5. 通过判断描述符是否在集合中确定是否描述符就绪了事件 for (int i 0; i maxfd; i) { if (FD_ISSET(i, tmp_rfds)) { // 6. 若就绪了事件根据事件进行处理 char buffer[1024] {0}; read(i, buffer, 1023); printf(获取可读数据: %s\n, buffer); } } } return 0; }7. select 模型优缺点总结7.1 select 模型的优点监控超时阻塞时间可以精确到微秒select 使用struct timeval结构体可以精确设置秒和微秒级别的超时时间。跨平台兼容性好select 不仅仅在 Linux 下可以使用在 Windows、macOS 以及其他 Unix-like 系统上也得到广泛支持。7.2 select 模型的缺点监控描述符数量有限select 所能监控的描述符数量受限于__FD_SETSIZE宏默认值为 1024。操作稍显麻烦每次监控都需要重置超时时间和监控集合增加了编程复杂度。监控性能偏低内核进行监控时会涉及多次描述符集合的遍历效率不高。需要程序员再次遍历select 监控完成后程序员需要遍历所有描述符通过FD_ISSET宏判断哪些描述符就绪了事件。7.3 select 使用场景适用于监控描述符数量不多甚至只有一个仅用于进行超时控制的场景或者需要支持跨平台的场景。8. poll 模型8.1 poll 模型功能poll 模型也是针对大量描述符进行监控的多路复用技术与 select 类似但接口设计有所不同。8.2 头文件与函数原型#include poll.h int poll(struct pollfd *fds, nfds_t nfds, int timeout);8.3 参数说明fdspollfd 结构体数组struct pollfd { int fd; // 要监控的描述符 short events; // 该描述符需要监控的事件 short revents; // 监控返回时设置的实际就绪事件 };事件宏POLLIN-可读POLLOUT-可写异常事件POLLHUP 挂断POLLERR 出错POLLNVAL 无效请求。nfdsfd 数组的有效元素数量timeout监控阻塞超时时间毫秒-1阻塞直到有就绪0非阻塞0指定的阻塞时间返回值0 出错0 超时0 实际就绪的描述符数量。8.4 poll 操作流程定义描述符事件结构体数组数组的大小根据实际场景确定将需要监控的描述符以及所要监控的事件添加到数组中发起 poll 监控调用监控原理与 select 并无差别将数组数据拷贝到内核对所有的有效元素进行遍历判断是否有就绪有就绪则直接返回并且设置对应元素的 revents 成员没有就绪的就阻塞进程当有描述符就绪的时候唤醒进程阻塞再次遍历数组重置 revents 成员设置为实际就绪的事件标志位调用返回后遍历数组根据数组中 revents 确定实际就绪的事件进行 IO 操作8.5 poll 优缺点总结8.5.1 优点相较于 select 在操作上有所简化不需要定义多个事件集合也不需要每次重置集合所能监控的描述符数量是没有限制的8.5.2 缺点poll 的监控实现原理与流程和 select 并无差别因为监控时存在多次遍历因此性能相对较低每次监控完毕同样需要遍历数组找出就绪的描述符与事件才能进行操作操作上麻烦效率上也低可能存在大量空轮询8.6 poll 使用场景与 select 雷同描述符数量较少或者只有一个进行超时管理的场景。8.7 额外知识点SIGPIPE 信号处理网络通信程序编写时都要先忽略 SIGPIPE 信号。当 TCP 对端关闭了连接或者关闭了读操作继续 send 写入数据就会触发 SIGPIPE 异常导致程序异常退出。signal(SIGPIPE, SIG_IGN);注意TCP 连接断开后继续 recv 就会返回 0 不再阻塞继续 send 就会触发 SIGPIPE 异常。9. epoll 模型9.1 epoll 操作流程与原理创建 epoll 实例通过epoll_create在内核中创建 eventpoll 结构struct eventpoll { spinlock_t lock; struct mutex mtx; wait_queue_head_t wq; wait_queue_head_t poll_wait; struct list_head rdlist; // 就绪链表 struct rb_root rbr; // 红黑树根节点 // ... };9.2 epoll 核心 API9.2.1 epoll_create / epoll_create1int epoll_create(int size); int epoll_create1(int flags);功能在内核创建eventpoll对象并返回文件描述符作为 epoll 操作句柄。epoll_create的size早期版本用于提示最大监控 fd 数量Linux 2.6 之后仅作为提示传大于 0 数值即可。epoll_create1常用标志EPOLL_CLOEXEC执行 exec 创建新程序时自动关闭 epoll 句柄防止句柄泄露。9.2.2 epoll_ctlint epoll_ctl(int epfd, int op, int fd, struct epoll_event* ev);功能向内核 eventpoll 结构添加 / 修改 / 删除 fd 监控。参数epfdepoll_create返回句柄op操作类型EPOLL_CTL_ADD新增 fd 监控EPOLL_CTL_MOD修改已有 fd 监控事件EPOLL_CTL_DEL移除 fd 监控fd目标待监控文件描述符ev事件结构体struct epoll_event { uint32_t events; // 监控事件EPOLLIN / EPOLLOUT __u64 data; // 联合体 union { int fd; void *ptr; } data; };使用示范struct epoll_event ev; ev.events EPOLLIN; ev.data.fd listen_fd;9.2.3 epoll_waitint epoll_wait(int epfd, struct epoll_event* evs, int maxevents, int timeout);功能阻塞等待获取就绪 fd 对应的事件结构。参数epfdepoll 句柄evs用户定义的 epoll_event 数组内核把就绪事件填入该数组maxevents数组最大长度防止越界timeout超时时间毫秒-1永久阻塞0立即返回非阻塞0指定阻塞毫秒返回值0出错≥0返回就绪 fd 数量。9.3 epoll 操作流程与内核原理epoll_create在内核生成eventpoll结构体。定义struct epoll_event设置监控事件与 fd。epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev)添加监控。内核核心机制为 fd 注册回调函数fd 就绪时自动将 epoll_event 拷贝加入rdlist就绪双向链表。epoll_wait()获取就绪事件内核只查看就绪链表rdlist链表为空 → 进程阻塞有 fd 就绪触发回调数据放入就绪链表唤醒进程将就绪链表内事件批量拷贝到用户数组 evsepoll_wait返回遍历 evs 数组处理就绪 fd。9.4 epoll 两种触发模式select/poll 只支持水平触发 LTepoll 支持 LT 默认、ET 边沿触发。9.4.1 水平触发 LTEPOLLLT默认模式只要缓冲区存在未读完数据每次epoll_wait都会持续上报事件。可读事件接收缓冲区有数据就持续触发可写事件发送缓冲区存在空闲空间持续触发。9.4.2 边沿触发 ETEPOLLET仅新数据到达瞬间触发一次事件。特点数据到达只会通知一次缓冲区残留数据不会再次触发事件如果本次事件没有读完缓冲区全部数据残留数据不会再次唤醒epoll_wait规范写法必须一次性循环读完缓冲区全部数据ET 模式必须搭配非阻塞 IOMSG_DONTWAIT防止最后一次 recv 阻塞卡死程序。典型代码范式while(1) { ret recv(fd, buftotal, 1024, MSG_DONTWAIT); if(ret 0) { if(errno EAGAIN) break; // 缓冲区无数据 } total ret; }9.5 epoll 优缺点优点性能最优采用事件回调机制内核维护红黑树保存所有 fd 就绪链表仅保存就绪 fdepoll_wait只会返回就绪 fd无全体 fd 遍历不存在大量空轮询性能不会随着监控 fd 数量上涨急剧衰减缺点Linux 专有跨平台兼容性差fd 数量极少场景下内核维护红黑树、就绪链表会带来微小额外开销适用场景大量文件描述符同时监控但同一时刻只有少量 fd 活跃的高并发网络程序。9.6 epoll 惊群问题现象父进程创建 epoll随后 fork 多个子进程所有子进程复制 epoll 句柄。当 fd 就绪所有阻塞在epoll_wait的子进程同时被唤醒造成大量无效唤醒、CPU 飙升。解决方案EPOLLEXCLUSIVEepoll_ctl添加监控时设置排他唤醒内核保证就绪事件只会唤醒其中一个进程使用SO_REUSEPORT端口复用多个进程各自创建 listen 套接字监听同一端口用户态加锁多进程竞争锁只有抢到锁的进程处理就绪事件。9.7 select / poll / epoll 横向对比总结特性selectpollepollfd 上限默认 1024无限制无限制内核机制每次全量拷贝 fd 集合、轮询遍历每次拷贝 pollfd 数组、轮询遍历红黑树管理 fd 就绪链表回调触发方式仅水平触发 LT仅水平触发 LTLT默认/ ET 边沿触发性能表现fd 越多性能下降越快fd 越多性能下降越快大量 fd 场景性能稳定跨平台好一般Linux 独有使用开销每次调用重建 fd 集合简化于 select初始化稍重高并发收益巨大
返回列表