嵌入式从0到精通——Linux C|TCP 并发服务器实现方案详解
2026/9/8 15:41:50 网站建设 项目流程

摘要:在 Linux 网络编程中,TCP 服务端默认是单循环阻塞服务器,同一时间只能服务一个客户端。本文讲解 5 种主流 TCP 并发模型:多进程、多线程、select、poll、epoll,分析各自优缺点,附带核心示例代码,适合 Linux 网络编程入门学习。

前言

在学习 Linux 网络 socket 编程的时候,最简单的 TCP 服务器是单循环服务器:socket→bind→listen→accept→recv/send→close。这种服务器有致命缺陷:同一时刻只能处理一个客户端连接。当一个客户端连接上来之后,服务端阻塞在recv,其他客户端无法接入,这在实际项目完全不可用。

为了解决同时服务多个客户端的需求,就需要实现TCP 并发服务器。常见实现分为两大类:

  1. 多任务模型:多进程、多线程(创建子进程 / 子线程处理客户端业务,主进程继续等待新连接)
  2. IO 多路复用模型:select、poll、epoll(单进程,监听多个文件描述符 IO 事件,无频繁创建销毁进程线程开销)

学习前提:掌握 TCP 三次握手、四次挥手、socket 基础 API,理解文件描述符。

一、多进程版 TCP 并发服务器

原理

  1. 主进程调用accept()阻塞等待新客户端连接;
  2. 一旦获取到通信套接字connfd,调用fork()创建子进程;
  3. 子进程负责和该客户端收发数据,业务处理完毕关闭套接字,子进程退出;
  4. 父进程关闭通信套接字(父子进程文件描述符拷贝,父不需要使用 connfd),继续循环调用accept等待下一个客户端。

注意:子进程退出会产生僵尸进程,父进程需要回收子进程资源;可以使用SIGCHLD信号捕获,在信号处理函数内部调用waitpid(WNOHANG)非阻塞回收僵尸子进程。

核心伪代码

int main(int argc, char *argv[]) { int sockfd = socket(AF_INET, SOCK_STREAM, 0); bind(sockfd, ...); listen(sockfd, 10); while(1) { int connfd = accept(sockfd, NULL, NULL); pid_t pid = fork(); if(pid > 0) { //父进程:不需要通信套接字,关闭,继续等待新连接 close(connfd); } else if(pid == 0) { //子进程:处理客户端业务 close(sockfd); //子进程不需要监听套接字 char buf[1024]={0}; recv(connfd, buf, sizeof(buf), 0); send(connfd, buf, strlen(buf),0); close(connfd); exit(0); //子进程结束 } else { perror("fork"); close(connfd); } } close(sockfd); return 0; }

优缺点

✅优点:

  1. 进程之间资源完全隔离,一个客户端异常崩溃,不会影响其他客户端和主服务;
  2. 逻辑简单,代码容易写。

❌缺点:

  1. fork()开销大,每来一个客户端就要创建一个完整进程;进程数量受系统限制,不适合高并发上万连接场景;
  2. 需要处理僵尸进程回收,增加信号处理代码;
  3. 进程间通信复杂,如果多个客户端之间要交换数据,需要使用 IPC(管道、共享内存、消息队列)。

二、多线程版 TCP 并发服务器

原理

进程开销大,线程是轻量级进程,共享进程堆、全局数据区,只拥有独立栈。

  1. 主线程accept获取新连接connfd
  2. 调用pthread_create创建子线程,把通信套接字传入线程函数;
  3. 子线程专门处理这个客户端读写业务;
  4. 线程设置为分离属性pthread_detach,线程结束后操作系统自动回收资源,不需要主线程调用pthread_join阻塞等待;
  5. 主线程继续循环等待下一个客户端。

注意:传参坑点:不要直接把局部变量connfd地址传给线程,多个线程会竞争读取同一个变量,出现值错乱;可以动态 malloc 申请堆内存存放 fd,线程内部用完 free 释放。

核心伪代码

void *client_task(void *arg) { int connfd = *(int *)arg; free(arg); char buf[1024]={0}; recv(connfd, buf, sizeof(buf),0); send(connfd, buf, strlen(buf),0); close(connfd); return NULL; } int main(int argc, char *argv[]) { int sockfd = socket(AF_INET, SOCK_STREAM, 0); bind(sockfd,...); listen(sockfd,10); while(1) { int *p_fd = malloc(sizeof(int)); *p_fd = accept(sockfd, NULL, NULL); pthread_t tid; pthread_create(&tid, NULL, client_task, p_fd); pthread_detach(tid); //设置分离属性,自动回收线程资源 } close(sockfd); return 0; }

编译需要增加链接库:gcc server.c -o server -pthread

✅优点:

  1. 线程创建销毁开销远小于进程;
  2. 线程共享进程全局资源,线程间数据交互简单,直接读写全局变量(需要互斥锁保护临界资源)。

❌缺点:

  1. 一个线程发生段错误崩溃,整个进程全部崩溃,所有客户端全部断开;
  2. 大量线程会消耗内存(每个线程默认 8M 栈),线程数量上限受限;
  3. 多线程访问共享资源必须加互斥锁,锁处理不当会产生死锁。

三、IO 多路复用 select 模型

IO 多路复用:单进程,同时监听多个文件描述符是否可读 / 可写事件,当某个 fd 事件就绪,才去执行读写操作。避免阻塞在某一个 fd 上。

select 核心特点

  1. 使用位图fd_set存储要监听的文件描述符,最大监听 1024 个 fd
  2. 用户态的 fd 集合每次调用 select 都会拷贝到内核;
  3. 内核返回整个位图集合,应用程序必须循环遍历所有 fd 判断哪个事件就绪;
  4. 只支持水平触发。

核心工作流程

  1. FD_ZERO清空文件描述符集合;
  2. FD_SET把监听套接字sockfd加入集合;
  3. 调用select()阻塞等待 IO 事件;
  4. 如果sockfd就绪:代表有新客户端连接,调用accept拿到connfd,把 connfd 加入监听集合;
  5. 如果普通connfd就绪:代表客户端发来数据,执行recv/send;客户端断开则调用 close,并且从 fd 集合清除该 fd。

核心伪代码

fd_set rdfds; int maxfd = sockfd; while(1) { FD_ZERO(&rdfds); FD_SET(sockfd, &rdfds); //遍历保存所有已经存在的connfd,加入rdfds //...... select(maxfd+1, &rdfds, NULL, NULL, NULL); if(FD_ISSET(sockfd, &rdfds)) { //有新客户端连接 int connfd = accept(sockfd,NULL,NULL); FD_SET(connfd, &rdfds); if(connfd>maxfd) maxfd = connfd; } //遍历判断各个connfd是否就绪 for(int i=sockfd+1; i<=maxfd;i++) { if(FD_ISSET(i,&rdfds)) { //读写业务逻辑 int ret = recv(i, buf, sizeof(buf),0); if(ret <=0) { close(i); FD_CLR(i,&rdfds); } } } }

✅优点:单进程,没有进程 / 线程创建销毁开销,相比多进程多线程,支持更多并发连接。 ❌缺点:最大 1024 个 fd 上限;每次调用 select 都拷贝集合;返回之后必须遍历全部 fd,效率随着连接数上升下降。

四、poll 模型

poll 解决 select 位图 1024 的数量限制,使用结构体数组保存需要监听的 fd 事件。

  • 无文件描述符数量硬编码上限;
  • 依然需要把数组拷贝到内核,返回后需要遍历数组查找就绪事件;
  • 只支持水平触发。

在高连接场景性能依然有瓶颈,工业开发中优先 epoll。

五、epoll 模型(Linux 高性能 IO 多路复用)

epoll 是 Linux 独有的 IO 多路复用,高性能网络编程主流方案。

三大核心函数

  1. epoll_create(size):创建 epoll 对象,返回 epoll 文件描述符;
  2. epoll_ctl(epfd, op, fd, event):向内核添加、修改、删除要监听的 fd 和事件;
  3. epoll_wait()阻塞等待事件,直接返回已经就绪的事件数组,不需要遍历全部 fd。

epoll 特点

  1. 使用内核红黑树保存监听 fd,没有 1024 上限;
  2. 用户态和内核只拷贝就绪事件,不需要拷贝全部 fd 集合;
  3. 直接返回就绪的 fd 集合,程序只处理就绪的 fd;
  4. 支持水平触发 LT边沿触发 ET两种模式;边沿触发效率更高。

✅优点:海量并发连接场景,性能不会随着连接数量增加明显下降;Nginx 底层就是 epoll。 ❌缺点:Linux 平台专有 API,跨平台性差。

六、五种并发模型对比总结

模型资源开销最大连接稳定性跨平台适用场景
多进程很高少,几百高,进程隔离跨平台客户端数量少,安全性优先
多线程中等几千低,线程崩溃整个进程挂跨平台中等并发,业务逻辑简单
select最大 1024跨平台连接数小于 1024,简单小程序
poll无硬上限跨平台中等并发,兼容多系统
epoll很低十万百万级别仅 Linux高并发网络服务器 (Nginx)

七、拓展:TCP 粘包问题

只要是 TCP 流式套接字,就会存在粘包现象:多条业务数据在内核缓冲区粘连到一起,应用层读取无法区分数据包边界。

解决粘包三种方案:

  1. 固定结构体收发:直接发送固定大小结构体;
  2. 特殊分隔符:数据包末尾加换行符\n等标记;
  3. 自定义帧协议:帧头 + 数据长度 + 业务数据 + 校验 + 帧尾(工业项目最常用)。

下一篇博客我们讲解 SQLite3 数据库 C 语言 API 实战,实现词典程序。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询