news 2026/10/10 11:10:39

Linux Socket 编程实战:从 TCP/epoll 到高并发优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux Socket 编程实战:从 TCP/epoll 到高并发优化

简介:面向 Linux 网络编程入门与进阶的读者,这份 Word 文档以图文方式系统梳理 Socket 编程的核心知识:网络中进程如何通信、Socket 的本质与设计理念,以及 socket、bind、listen、connect、accept、read、write、close 等基础接口的用法。文档重点剖析了 TCP 三次握手建立连接和四次挥手释放连接的完整流程,对 SYN、ACK、FIN 等标志位与状态迁移细节亦有说明,并附有客户端与服务端的可运行示例,便于对照实验验证。资源压缩包共 1 个 docx 文件,大小仅 77KB,内容按“原理—接口—连接管理—实践”递进展开,结构紧凑却不失系统性;既有理论铺垫又有代码落地,可以作为课堂讲义,也能在求职面试前快速翻阅查漏补缺。已有 241 人学习下载,适合想快速建立 Linux 网络编程整体认知、厘清 TCP 生命周期并动手完成一次简单通信实验的开发者,是性价比很高的入门资料。

1. 从最小可运行代码理解 Linux Socket 编程:它藏在每一台服务器背后

很多刚入门 Linux 后端的人以为“网络编程”就是发 HTTP 请求、接 JSON 响应,直到第一次被面试官问“TCP 三次握手在代码里是哪一行触发的”,或者自己写的服务一上线就被几千个连接打穿,才意识到问题出在 Socket 这一层。Linux Socket 编程本质上是用户态进程和内核网络协议栈之间的接口,你调用的每个socket()、bind()、listen()都是实打实的系统调用,直接决定了服务的并发能力、延迟表现和稳定性。这篇文章适合两类人:一类是做嵌入式 Linux 开发、需要在板子上写通信模块的工程师;另一类是写后端服务、想知道连接为什么断、端口为什么被占、并发一上来就卡死的应用开发者。我会从最小可运行的 TCP 代码开始,一路讲到 epoll、UDP、参数调优和排错思路,每一段代码都能直接编译运行,不用额外装任何框架。

2. 先跑通一个 TCP 回显服务:socket/bind/listen/accept 四步走

2.1 为什么用 C 而不是 Python:看清系统调用的底层逻辑

我在实际项目里见过不少工程师用 Python 写 Socket 服务,开发速度确实快,但遇到“连接数一高就 CPU 飙到 100%”或者“进程崩溃后端口 30 秒内起不来”这类问题,Python 的封装把细节藏得太深,你很难定位到具体是哪个系统调用出的问题。而 Linux 面经里几乎必考的socket、bind、listen、accept这四个函数,在 C 里调用一次就能看懂全貌:每次调用都是陷入内核,操作的是同一个文件描述符表,错误码通过errno直接反映到用户态。

我一般会建议初学者先用 C 跑通一个最小服务器,再回去看 Python 的socket模块文档,你会瞬间理解它为什么那样设计。C 的写法确实啰嗦——结构体初始化要memset、地址结构要区分 IPv4 和 IPv6、返回值要逐行检查——但正是这些“啰嗦”把网络编程里最容易出错的边界条件暴露在了明面上。等你把下面的代码编译运行过一次,再去查man 7 ip、man 7 tcp等手册,效率会高得多。

2.2 最小 TCP 服务器:从 socket() 到 accept() 的完整代码

下面这段代码是一个能跑的 TCP 回显服务器:客户端发送任意字节,服务器原样返回。我特意去掉了错误处理和日志,只保留主流程,目的是让你先把骨架看清楚。

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #define PORT 8888 #define BACKLOG 16 int main() { int listen_fd, conn_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len = sizeof(client_addr); char buf[1024]; ssize_t n; // 1. 创建 IPv4 TCP socket listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd < 0) { perror("socket"); exit(1); } // 2. 绑定地址,注意结构体必须先清零 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind"); exit(1); } // 3. 进入监听状态 if (listen(listen_fd, BACKLOG) < 0) { perror("listen"); exit(1); } printf("Echo server listening on port %d\n", PORT); // 4. 接受连接并回显 while (1) { conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len); if (conn_fd < 0) { perror("accept"); continue; } // 单连接处理:读完客户端关闭为止 while ((n = read(conn_fd, buf, sizeof(buf))) > 0) { write(conn_fd, buf, n); } close(conn_fd); } close(listen_fd); return 0; }

编译命令是gcc -o echo_server echo_server.c,运行后另开终端用telnet 127.0.0.1 8888即可测试。这段代码里有几个参数值得细说:

  • BACKLOG是内核维护的连接队列长度,表示“已完成三次握手、但还没被accept()取走”的连接上限。设成 16 是保守值,生产环境一般设 128 或更高,但也不是越大越好,因为每个等待中的连接都占用内核内存。
  • htons和htonl的作用是把本机字节序转成网络字节序。x86 架构是小端存储,而 TCP/IP 协议规定多字节数值用大端传输,漏掉这一步会得到完全错误的端口号或 IP 地址。
  • INADDR_ANY表示绑定所有网卡地址。如果只需要本机回环测试,可以改成htonl(INADDR_LOOPBACK)。

这段代码的局限很明显:accept之后进入阻塞式read,同一时刻只能处理一个连接,第二个客户端连进来之后只能干等。下一章我专门讲这个问题怎么解决。

2.3 配套客户端与参数说明:为什么必须关注每个返回值

没有客户端的服务器是不完整的。实际调试时我经常用telnet和nc做连通性探测,但有限场景下需要控制发送内容、观察半关闭行为,所以还是准备一段最小客户端代码:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 8888 int main() { int sock_fd; struct sockaddr_in server_addr; char *msg = "hello from client"; char buf[1024]; sock_fd = socket(AF_INET, SOCK_STREAM, 0); // 客户端不需要 bind,内核会自动分配临时端口 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); if (connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("connect"); exit(1); } write(sock_fd, msg, strlen(msg)); ssize_t n = read(sock_fd, buf, sizeof(buf) - 1); if (n > 0) { buf[n] = '\0'; printf("received: %s\n", buf); } close(sock_fd); return 0; }

inet_pton是把点分十进制的 IP 字符串转成二进制网络字节序,比老的inet_addr更安全,因为它能识别非法地址并返回 0。connect是一个阻塞调用,它内部替客户端完成了三次握手的第三阶段:服务器端收到 SYN 后回复 SYN+ACK,客户端在内核里自动应答 ACK,connect返回的那一刻连接已经建立。

对初学者来说,最容易忽略的是read的返回值判断。如果n == 0,表示对端正常关闭连接;如果n < 0,要看errno区分是EINTR(被信号打断,可以重试)还是ECONNRESET(对端发了 RST)。很多线上事故的排查起点,就是一个返回值没判断好导致无限循环读垃圾数据。

3. 并发上来之前先解决 IO 模型:从阻塞到 epoll

3.1 阻塞模型为什么在真实服务器上翻车:进程与线程的消耗

上一章的服务器只能串行处理连接:read阻塞期间,内核收到了新连接的握手请求,只能让它在BACKLOG队列里排队。如果同事的监控脚本每 5 秒来连一次,这个服务看着没问题;一旦有几十个客户端同时上报数据,队列溢出后新连接直接被内核拒绝,表现就是客户端connect超时或返回ECONNREFUSED。

常见的补救方案是“来一个连接就fork一个进程”。这确实能解决并发阻塞的问题,但代价也实打实:每次fork要复制页表、创建新的 task_struct,内存开销随连接数线性增长;两个连接的上下文切换开销也远高于单进程内的事件循环。嵌入式 Linux 项目里内存本来就紧张,更扛不住这种模型。所以多进程方案只适合连接数很少、每次处理时间很长的场景,比如与外部设备的长周期采集通信。

我一般会建议从 select 开始理解事件驱动,再切到 epoll。select 的代码量不大,概念上也好接受:把所有需要关注的 fd 放进一个集合,内核帮你“盯着”,一旦有 fd 可读可写就返回。但它有两个硬伤,理解了这两个硬伤,你就自然明白 epoll 存在的理由。

3.2 select 服务器如何支撑 1024 个 fd:代码与 fd_set 的局限

下面是一个用 select 改写的多连接服务器骨架,它解决了“单连接阻塞”的问题:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <sys/select.h> #include <netinet/in.h> #define PORT 8888 int main() { int listen_fd, conn_fd, max_fd, nready, i; struct sockaddr_in server_addr, client_addr; socklen_t client_len; fd_set read_set, all_set; int client_fds[FD_SETSIZE]; char buf[1024]; listen_fd = socket(AF_INET, SOCK_STREAM, 0); memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); listen(listen_fd, 128); // 初始化客户端 fd 表,-1 表示空位 for (i = 0; i < FD_SETSIZE; i++) { client_fds[i] = -1; } FD_ZERO(&all_set); FD_SET(listen_fd, &all_set); max_fd = listen_fd; while (1) { read_set = all_set; // select 会修改集合,每次必须重新拷贝 nready = select(max_fd + 1, &read_set, NULL, NULL, NULL); if (nready < 0) { perror("select"); continue; } // 新连接到达 if (FD_ISSET(listen_fd, &read_set)) { client_len = sizeof(client_addr); conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len); for (i = 0; i < FD_SETSIZE; i++) { if (client_fds[i] == -1) { client_fds[i] = conn_fd; break; } } FD_SET(conn_fd, &all_set); if (conn_fd > max_fd) max_fd = conn_fd; nready--; } // 遍历所有客户端 fd 看是否有数据 for (i = 0; i < FD_SETSIZE && nready > 0; i++) { if (client_fds[i] < 0) continue; if (FD_ISSET(client_fds[i], &read_set)) { ssize_t n = read(client_fds[i], buf, sizeof(buf)); if (n <= 0) { close(client_fds[i]); FD_CLR(client_fds[i], &all_set); client_fds[i] = -1; } else { write(client_fds[i], buf, n); } nready--; } } } close(listen_fd); return 0; }

select 的调用方式有个容易踩的坑:每次调用都会修改read_set,所以必须先维护一份all_set,每次循环开始前重新赋值。max_fd + 1是 select 需要扫描的文件描述符上限,不传对的话内核会漏掉高编号 fd 的事件。

FD_SETSIZE 在 Linux 上默认是 1024,也就是这个方案最多同时管理 1024 个 fd。而且每次调用 select,内核都要线性扫描整个集合判断哪些 fd 就绪,连接数一多,O(n) 的扫描成本就成了性能瓶颈。1024 对小 demo 够用,但真实服务器上客户端连接数量级一上来,就得换 epoll。

3.3 从 select 到 epoll:LT/ET 的差异与实战设置

epoll 和 select 的核心区别是“注册回调”而非“轮询扫描”。你用epoll_ctl把 fd 注册进内核的事件表,内核只在 fd 真正就绪时才通知你,没有就绪事件时epoll_wait直接睡眠,CPU 占用几乎为零。

以下是一个 epoll 版 TCP 回显服务器的核心逻辑:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <sys/epoll.h> #include <netinet/in.h> #include <errno.h> #define PORT 8888 #define MAX_EVENTS 64 int main() { int listen_fd, epoll_fd, conn_fd, nfds, i; struct sockaddr_in server_addr, client_addr; socklen_t client_len; struct epoll_event ev, events[MAX_EVENTS]; char buf[1024]; listen_fd = socket(AF_INET, SOCK_STREAM, 0); memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); listen(listen_fd, 128); epoll_fd = epoll_create1(0); ev.events = EPOLLIN; ev.data.fd = listen_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev); while (1) { nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); for (i = 0; i < nfds; i++) { if (events[i].data.fd == listen_fd) { // 接受所有新连接,逐个注册到 epoll client_len = sizeof(client_addr); conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len); ev.events = EPOLLIN | EPOLLRDHUP; // EPOLLRDHUP 用于感知对端关闭 ev.data.fd = conn_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev); } else { ssize_t n = read(events[i].data.fd, buf, sizeof(buf)); if (n <= 0) { // n == 0 对端关闭,n < 0 且 EINTR 则重试,其他错误则关闭 close(events[i].data.fd); } else { write(events[i].data.fd, buf, n); } } } } }

这段代码默认是 LT(水平触发)模式:只要 fd 的缓冲区里还有数据没读完,epoll_wait就会反复通知你。LT 的好处是代码逻辑简单,读不完下次还能继续;缺点是如果每次只读一点,同一个事件会被反复唤醒,浪费 CPU。如果改成ev.events = EPOLLIN | EPOLLET,就是 ET(边缘触发)模式,fd 从无数据变为有数据时只通知一次,你必须一次性把数据读完,否则剩下的数据会一直留在缓冲区里。

ET 模式对代码要求高,但你一旦读不完整就丢数据。我自己的习惯是:能跑通业务先上 LT,性能测试确实有瓶颈再换 ET。ET 模式下必须配合非阻塞 socket 和循环read直到返回EAGAIN,否则read阻塞在最后一个字节上,整个事件循环就停了。

4. UDP 与连接杂项:协议选择、setsockopt 参数与半关闭细节

4.1 UDP 还是 TCP:不只是“快”的问题

很多处理物联网设备上报的场景,开发者第一反应是“数据量小,用 UDP 快”。但 UDP 的“快”是有代价的:没有握手、没有重传、没有拥塞控制,数据报一旦在中间链路丢失,应用层不会收到任何通知。如果业务允许丢少量数据且每条报文都自带完整上下文,UDP 确实是好选择,比如实时音视频、游戏位置同步;但如果是交易、控制指令这类不能丢的载荷,老老实实用 TCP,或者自己在 UDP 之上实现确认重传机制——后者的工程量往往超出预期,涉及报文序号、超时重传、去重、流控,本质上是把 TCP 重新造一遍。

我在嵌入式 Linux 项目里见过最多的问题不是选了 TCP 还是 UDP,而是选了 UDP 之后不做 MTU 控制。默认情况下 UDP 报文超过 MTU 会触发 IP 分片,分片丢失后整个数据报无法重组,表现为“手机连不上设备、偶发丢包”。常见的做法是把应用层报文限制在 1400 字节以内,从根上避开分片。

4.2 SO_REUSEADDR、SO_KEEPALIVE、TCP_NODELAY 三个必调参数

setsockopt是 Socket 编程里最容易被忽略但回报率最高的函数。第一个必设参数是SO_REUSEADDR,它解决的是服务器重启时端口被 TIME_WAIT 占用的问题。TCP 连接关闭后,主动关闭方会进入 TIME_WAIT 状态,维持 2MSL 时间(Linux 上默认 60 秒左右),期间端口不能重新绑定。开发时重启一次服务等一分钟,体验极差;生产上频繁重启也会造成空窗期。加上下面这行代码,服务器就能在 TIME_WAIT 残留期间重新绑定同一端口:

int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

第二个是SO_KEEPALIVE,它让内核定期发送探测包检测对端是否存活。默认参数很不适合应用层使用——空闲 2 小时才开始探测,探测失败 9 分钟才能断开。实际项目中我一般会关掉内核 keepalive,自己在应用层做心跳超时判断,比如 30 秒没收到数据就主动关闭连接并清理资源。真想用内核探测,可以用TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT调整参数,但每台机器的sysctl配置可能不同,运维成本不低。

第三个是TCP_NODELAY,针对小报文场景。TCP 默认启用 Nagle 算法:当发送缓冲区里还有未确认的数据时,后续小报文会被延迟合并。如果业务是“客户端请求一条、服务器响应一条”的交互模式,这个延迟会造成明显的 40ms 级 RT 抖动。禁用 Nagle 后小报文立即发出,但会增加网络中的小包数量,要按场景权衡:

int flag = 1; setsockopt(conn_fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));

4.3 非阻塞 connect 与 accept 后的错误处理细节

connect默认是阻塞的:如果目标 IP 不可达,可能要等几十秒才超时返回。在事件驱动模型里,这种阻塞是不能接受的。常见的做法是把 socket 设为非阻塞再调connect,此时connect立即返回EINPROGRESS,之后用select或epoll监听这个 fd 的EPOLLOUT事件来判断连接是否成功。

int flags = fcntl(sock_fd, F_GETFL, 0); fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK); int ret = connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); if (ret < 0 && errno != EINPROGRESS) { perror("connect nonblock"); exit(1); } // 之后在 epoll 里注册 EPOLLOUT,触发时用 getsockopt 检查 SO_ERROR

accept同样有非阻塞处理的问题:在高并发下,accept可能返回EMFILE(进程 fd 数耗尽),此时如果忽略错误直接 continue,会导致新连接无人处理;正确做法是保留一个空闲 fd,EMFILE时先close它,accept拿到新连接后立即关闭新连接,再重新打开那个保留 fd。这是典型的 Linux 高并发血泪坑,不少线上事故都源于这里。

5. 常见故障排查:现象 → 原因 → 解决

5.1 服务器重启后端口被占:TIME_WAIT 与 SO_REUSEADDR 的混淆

现象:kill 掉旧的服务器进程后立刻重启,报bind: Address already in use,等 60 秒左右又能正常启动。

原因:服务器作为主动关闭方(或客户端先断开,服务器随后关闭 socket),连接会进入 TIME_WAIT 状态,内核保留该五元组,防止延迟的重传报文串到新连接上。TIME_WAIT 期间同一端口不允许重新绑定。

解决:在bind()之前设置SO_REUSEADDR。注意它只对处于 TIME_WAIT 的端口有效,如果端口被 ESTABLISHED 状态或另一个进程占用,设置这个参数也没用。SO_REUSEPORT是另一回事,允许两个进程同时绑定同一端口做负载分流,但需要每个 socket 都设置,且内核版本要支持。

5.2 客户端发完数据后服务器没响应:TCP 半关闭与状态机理解不透

现象:客户端send成功,但服务器那边的read一直不返回;客户端close之后服务器才收到数据。

原因:TCP 是全双工的,close会关闭发送和接收两个方向。如果客户端shutdown(fd, SHUT_WR)只关闭发送方向,服务器仍能继续写回数据,但很多初学者在客户端写代码时直接close,顺手把接收方向也关了;更隐蔽的情况是 Nagle 算法和延迟 ACK 相互作用,小数据报文被滞留在发送缓冲区,只有收到对端 ACK 才会清空,而对端的延迟 ACK 又在等更多数据,形成 40ms 级别的死锁。

解决:需要双向交互的应用,客户端在发送完请求数据后不要急着close,而是把close放到收到响应之后;服务器侧用shutdown(conn_fd, SHUT_WR)告知客户端“响应发完了”,等客户端主动关闭连接后再释放资源。发送小报文时按场景评估TCP_NODELAY是否要禁用 Nagle。

5.3 客户端莫名收到 connection reset by peer:RST 意味着协议栈级的“拒绝”

现象:客户端read返回 -1,errno是ECONNRESET,日志里看到Connection reset by peer。

原因:RST 报文说明对端收到数据后,发现这个连接已经不存在或不合法,直接拒绝而不是正常关闭。最常见的触发点是:服务器进程崩溃退出时,内核会给所有未关闭的连接发送 RST;另一个常见场景是服务器端缓冲区还有未读数据就调了close,内核会丢弃接收缓冲区数据并回 RST。

解决:先查服务器进程是否崩溃(dmesg 或 coredump)。排除崩溃因素后,检查服务器代码是否在read返回 0 之前就关闭了 fd,比如处理完业务后不管接收缓冲区是否有残留数据就直接close。规范做法是shutdown(fd, SHUT_RD)先停止接收,再shutdown(fd, SHUT_WR)发送 FIN,最后等对端关闭再close。

5.4 粘包半包是玄学?不,是应用协议边界没定义

现象:客户端一次send了 200 字节,服务器端第一次read只收到了 120 字节,第二次read收到 80 字节加下一批数据的前几十字节;数据在应用层接错了,解析出来的业务字段全部错位。

原因:TCP 是字节流协议,没有“消息”边界。read返回多少取决于内核缓冲区状态、网卡接收时机、调度延迟,发送端的两次send可能被合并成一次到达,也可能被拆开。这不是 bug,是 TCP 的固有特性。

解决:必须在应用层协议里定义边界。三个常见方案:固定长度报文(适合字段定长的场景);长度前缀 + 内容体(先读 4 字节头,把内容长度解出来,再循环读到足够长度);分隔符(文本协议用\n或\r\n)。我一般用长度前缀方案,代码逻辑最稳。服务器端读数据时要维护每个连接独立的缓冲区,把收到的字节追加到缓冲尾部,然后循环检查有没有完整报文,有则从缓冲区头部取出并解析,剩余数据保留给下次读取。缓冲区长度固定时还要处理“一条报文超过缓冲区长度”的情况,常见做法是动态扩容或直接按最大报文长度分配。

6. 用 strace 验证 Socket 调用链路与实测性能边界

写完了代码并不代表能上线,我习惯在交付前用strace把整个 Socket 调用链拉出来看一遍。strace -f -e trace=network,read,write,close ./echo_server会输出每个系统调用的参数和返回值,比如:

socket(AF_INET, SOCK_STREAM, 0) = 3 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 bind(3, {sa_family=AF_INET, sin_port=htons(8888), sin_addr=inet_addr("0.0.0.0")}, 16) = 0 listen(3, 128) = 0 epoll_create1(0) = 4 epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN, {u32=3...}}) = 0

对照这些输出,你可以快速确认BACKLOG是否生效、连接是否进入监听队列、accept是否按预期返回。如果看到accept返回EMFILE,说明进程 fd 配额撞顶了,优先查ulimit -n。压测方面我通常用wrk或ab测 HTTP 层,用自写的多线程客户端测原生 Socket 层。一句话经验是:单机测试时先看吞吐量和延迟的曲线是线性还是断崖,断崖处大概率对应某个系统参数的上限,比如net.core.somaxconn限制了 listen 队列长度,fs.file-max限制了全系统 fd 总量。想深入调优时,依次检查sysctl net.ipv4和net.core下的参数,每次只改一个,并记录压测前后数据对比。

一个我踩过多次的教训是:不要在服务器代码里为了“高并发”盲目加线程池。事件驱动模型如果逻辑处理好,单进程 epoll 在普通物理机上撑住数万个长连接并不罕见;线程池只有在处理 CPU 密集任务或第三方阻塞调用时才值得引入。如果epoll_wait的超时时间设了 -1,主循环里任何阻塞操作都会串行拖累所有连接,阻塞调用一律丢到独立线程。这个习惯我一直保留着,每次写服务器代码都先问一遍“事件循环里有没有阻塞调用”,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 11:10:37

基于Spark的地铁客流分析系统:架构、实践与避坑指南

简介&#xff1a;面向计算机专业毕业设计的完整项目&#xff0c;基于Spark的地铁大数据客流分析系统&#xff0c;以城市地铁客流数据为分析对象&#xff0c;覆盖数据采集、清洗、存储、分析、可视化与客流预测等环节&#xff0c;适合大数据方向学生用于课程设计、毕设参考或技术…

作者头像 李华
网站建设 2026/10/10 11:09:50

2024移动应用开发赛项02卷拆解:八个任务背后的真实行业需求

简介&#xff1a;2024年全国职业院校技能大赛移动应用开发赛题全面解析&#xff0c;是一份面向职业院校参赛选手、指导教师及移动应用开发学习者的PDF文档。资源围绕移动应用设计与开发赛项&#xff0c;系统拆解了产品原型设计、移动应用开发、应用部署测试三大模块&#xff0c…

作者头像 李华
网站建设 2026/10/10 11:09:37

C++模板编译期调试指南:从static_assert到concepts

写C模板最崩溃的一刻&#xff0c;不是逻辑想不出来&#xff0c;而是明明在编译器里报了一屏又一屏的错误&#xff0c;却找不到自己写的哪一行出了问题。模板编译期调试就是这么反人类——你没法在运行时打断点&#xff0c;只能跟编译器在编译这一层互相拉扯。但这么多年写泛型代…

作者头像 李华
网站建设 2026/10/10 11:07:32

FTP客户端选型实战指南:SFTP/FTPS断点续传与跨平台工具对比

1. 这不是“软件列表”&#xff0c;而是一份FTP客户端选型实战手记你是不是也经历过&#xff1a;凌晨两点&#xff0c;服务器日志报错&#xff0c;急需把修复后的配置文件传上去&#xff0c;结果发现用的客户端连SFTP都不支持&#xff1b;或者团队协作时&#xff0c;设计师传来…

作者头像 李华
网站建设 2026/10/10 11:06:54

AI重塑软件工程:从开发到运维的范式变革

1. 开发方式的第一层变化&#xff1a;AI 从“自动补全”变成“设计讨论伙伴”先从一个我观察到的细节说起。最近一年我参与过的几个项目里&#xff0c;开发工具链迭代很快&#xff0c;但是最有趣的信号反而不是代码写得有多快&#xff0c;而是团队讨论问题的方式变了。以前评审…

作者头像 李华