资讯动态

Linux socket网络编程实战:从系统调用到TCP状态机

发布时间:2026/10/21 11:54:41 来源:尧图企业网站定制
1. 这不是教科书里的“socket”而是你明天就要调试的那行代码“Linux socket网络编程基础知识”——看到这个标题很多人第一反应是又来了又是那种讲完socket()、bind()、listen()三板斧就收工的教程。但现实根本不是这样。我带过不少刚从学校出来的开发者他们能背出TCP三次握手的每个SYN/ACK标志位可一碰到connect()返回EINPROGRESS就卡住翻遍文档也找不到为什么非阻塞模式下select()要配合getsockopt(SO_ERROR)才能判断连接是否真成功有人在send()里传了2048字节却只发出去376字死盯着errno是0就是想不通数据去哪儿了还有人把SO_REUSEADDR当成万能膏药结果在服务重启时发现旧连接残留的TIME_WAIT状态反而被新进程抢走端口导致客户端收到RST包直接断连。这些不是“基础不牢”的问题而是教材和实战之间存在一道真实的鸿沟基础不是指API函数的调用顺序而是理解内核如何调度套接字、协议栈如何拆解数据、用户态缓冲区与内核缓冲区如何协同、以及每一个返回值背后隐藏的系统状态。这篇内容就是为跨过这道鸿沟写的。它不讲抽象模型只讲你在strace里看到的真实系统调用流不堆砌RFC定义只解释man 7 socket里那几行容易被忽略的备注意味着什么不假设你已掌握epoll而是从select的fd_set大小限制开始带你算清楚为什么FD_SETSIZE默认是1024、改大它会踩进什么内存陷阱。适合正在写第一个网络服务、调试一个卡死的长连接、或者准备面试中被问到“close()后还能send()吗”这类问题的任何人。你不需要提前读完《UNIX网络编程》只需要知道gcc怎么编译、gdb怎么打断点就能跟着把整套机制摸透。2. 整体设计思路从“一次HTTP请求”倒推到底层套接字行为2.1 为什么不用“先学理论再写代码”的老路我试过按传统路径教学先讲OSI七层模型再画TCP状态机图最后贴一段echo server代码。结果呢学员写完代码跑起来一加日志就崩溃一换网络环境就超时问“为什么accept()返回的sockfd和listenfd不一样”答“因为它们代表不同连接”这等于没答。问题出在网络编程的本质不是记住协议规则而是理解数据在用户空间与内核空间之间搬运的完整生命周期。所以这次我们反着来——以一个最朴素的场景切入用curl http://localhost:8080发起一次请求然后一层层剥开看这个请求如何触发socket()、connect()、send()、recv()这一系列调用每一步内核做了什么用户态程序又该做什么响应。这个思路带来三个关键转变目标驱动所有知识点都绑定到具体动作上。比如讲SO_LINGER不是先定义“控制关闭时的行为”而是直接演示当服务器进程在close()前未处理完接收缓冲区数据客户端read()会收到多少字节、errno是什么、抓包看到FIN还是RST。状态可见每一步操作后用ss -tuln、netstat -s、cat /proc/net/snmp等命令观察套接字状态变化让抽象概念变成终端里可验证的输出。例如ss -i能直接看到当前连接的RTT、cwnd、ssthresh值比任何文字描述都直观。错误前置不等你写完50行代码再报错而是在socket()调用后立刻检查errno EMFILE文件描述符耗尽告诉你ulimit -n该设多少才够用在bind()前先ss -tuln | grep :8080确认端口空闲避免EADDRINUSE这种低级但高频的坑。2.2 方案选型为什么坚持用原生C系统调用而不是libevent或Boost.Asio现在主流方案早就不手写select了用epoll、kqueue甚至异步IO库是常态。但如果你跳过原生接口直接学封装层就像学开车先背ECU固件手册——你永远不知道epoll_ctl(EPOLL_CTL_ADD)失败时errno EPERM意味着什么答案目标fd不支持epoll比如管道或普通文件。所以我坚持用最原始的socket()/connect()/send()/recv()组合原因很实在调试链路最短strace -e tracesocket,connect,send,recv,close ./a.out能100%覆盖所有关键路径没有中间层干扰错误归因最准send()返回-1且errno EAGAIN说明内核发送缓冲区满而不是你的业务逻辑错了性能边界最清当你发现send()单次最多发212992字节Linux默认sk-sk_sndbuf的一半你就明白为什么大文件传输必须循环调用而不是怪“网络慢”。当然这不是否定高级库的价值。恰恰相反只有亲手填过select()的fd_set位图、算过FD_SETSIZE对栈空间的消耗你才会真正欣赏epoll的O(1)事件通知有多珍贵。所以本文后面会专门对比select/poll/epoll的底层差异但前提是——你得先知道select()内部到底在干什么。2.3 架构分层四层穿透式解析每一层都解决一个真实痛点我们把整个知识体系拆成四个物理层级对应你在调试时最常卡住的位置层级名称核心痛点解决什么问题L1用户态API层send()返回值小于请求长度怎么办recv()为什么有时只收一半数据理解TCP流式传输本质掌握MSG_WAITALL、MSG_DONTWAIT等标志的实际效果L2内核协议栈层ss -i显示cwnd:10但ping延迟只有1ms为什么吞吐上不去/proc/sys/net/ipv4/tcp_slow_start_after_idle设成0有啥副作用揭示拥塞控制算法如何影响你的应用避免盲目调优tcp_rmemL3套接字状态机层close()后ss -tuln还显示FIN_WAIT2客户端却收不到响应TIME_WAIT状态到底占什么资源搞懂TCP状态迁移条件正确设置SO_LINGER和net.ipv4.tcp_fin_timeoutL4系统资源管理层启动1000个连接后socket()返回-1errno ENFILEulimit -n已经调到65535问题出在哪定位文件描述符、内存页、连接跟踪表conntrack三重瓶颈这四层不是并列关系而是递进依赖L1的问题往往根子在L2的拥塞窗口L2的异常可能由L3的状态机卡死引发而L3的堆积最终会压垮L4的系统资源。所以我们的实操过程就是沿着这个链条一级级下钻。3. 核心细节解析从socket()调用开始的每一行代码背后的真相3.1socket()你以为只是分配一个fd其实它在初始化整个协议栈上下文很多人以为socket()就是向内核申请一个文件描述符类似open()。错。socket()真正的动作是在内核中创建一个struct socket结构体并关联对应的struct sock即sk同时根据地址族AF_INET、类型SOCK_STREAM、协议IPPROTO_TCP初始化协议栈处理函数指针。这意味着哪怕你还没bind()内核已经为你预置了完整的TCP处理逻辑。我们来看一段实测代码#include sys/socket.h #include stdio.h #include errno.h int main() { int sockfd socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sockfd -1) { perror(socket); printf(errno: %d\n, errno); // 可能是EMFILE、ENFILE、ENOMEM return 1; } printf(socket fd: %d\n, sockfd); return 0; }运行后strace输出关键片段socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) 3注意这个返回值3——它不是随机的。Linux内核为每个进程维护一个struct files_struct其中fd_array[]数组索引就是fd号。stdin/stdout/stderr固定占0/1/2所以第一个socket()必然返回3。但如果你之前打开了10个文件socket()就可能返回13。这就是为什么ulimit -n限制的是整个进程能打开的文件描述符总数包括普通文件、管道、socket等所有类型。提示errno EMFILE表示当前进程打开的fd数已达上限errno ENFILE表示整个系统的fd总数已达上限/proc/sys/fs/file-max。前者调ulimit -n即可后者需改系统参数且会影响所有进程。更关键的是socket()的第三个参数protocol。虽然TCP通常填IPPROTO_TCP但你可以填0内核会自动匹配。不过当使用AF_INET6时填0可能导致IPv6 socket无法处理IPv4映射地址IPv4-mapped IPv6 address这是很多双栈服务出问题的根源。实测验证// 错误写法IPv6 socket填0可能无法接受IPv4连接 int sockfd6 socket(AF_INET6, SOCK_STREAM, 0); // 正确写法显式指定IPPROTO_TCP确保协议栈正确初始化 int sockfd6 socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);3.2bind()端口抢占战背后的net.ipv4.ip_local_port_range与SO_REUSEADDRbind()看似简单把socket和IP:PORT绑定。但实际中bind()失败的errno EADDRINUSE是最让人抓狂的错误之一。你以为端口被占netstat -tuln | grep :8080却显示空空如也。真相是端口处于TIME_WAIT状态而你的新socket没设置SO_REUSEADDR。我们来拆解bind()的完整流程内核检查目标{ip, port}是否已被其他ESTABLISHED或LISTEN状态的socket占用若未被占用再检查是否有TIME_WAIT状态的socket占用同一{ip, port}如果有TIME_WAIT且新socket未设置SO_REUSEADDR则bind()失败如果设置了SO_REUSEADDR则允许绑定但新socket不能listen()监听只能作为客户端connect()。验证实验# 启动一个server用curl访问后关闭 $ ./server $ curl http://localhost:8080 $ kill %1 # 立即启动新server未设SO_REUSEADDR $ ./server # 失败Address already in use # 修改server代码在socket()后添加 int opt 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); # 再次启动成功 $ ./server # 成功但SO_REUSEADDR不是银弹。它解决的是TIME_WAIT端口复用却不管ip_local_port_range。Linux默认动态端口范围是32768-60999共28232个如果短连接并发量极大可能耗尽端口。查证命令$ cat /proc/sys/net/ipv4/ip_local_port_range 32768 60999计算最大并发连接数60999 - 32768 1 28232。如果你的应用每秒新建1000个连接平均存活20秒那么峰值连接数就是20000接近上限。此时要么扩大端口范围# 临时生效 $ echo 1024 65535 /proc/sys/net/ipv4/ip_local_port_range # 永久生效写入/etc/sysctl.conf net.ipv4.ip_local_port_range 1024 65535要么优化连接复用——这才是治本之策。3.3connect()阻塞与非阻塞模式下你必须知道的三次握手“黑箱”connect()是网络编程中最易被误解的函数。教材说“它发起TCP三次握手”但没告诉你在阻塞模式下connect()会一直等到三次握手完成或超时才返回而在非阻塞模式下它立即返回-1且errno EINPROGRESS此时连接仍在后台进行。我们用strace看阻塞模式下的真实行为int sockfd socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serv_addr {0}; serv_addr.sin_family AF_INET; serv_addr.sin_port htons(80); inet_pton(AF_INET, 127.0.0.1, serv_addr.sin_addr); connect(sockfd, (struct sockaddr*)serv_addr, sizeof(serv_addr)); // 阻塞在此strace输出connect(3, {sa_familyAF_INET, sin_porthtons(80), sin_addrinet_addr(127.0.0.1)}, 16) 0返回0表示连接成功。但如果目标端口无服务你会看到connect(3, {sa_familyAF_INET, sin_porthtons(80), sin_addrinet_addr(127.0.0.1)}, 16) -1 ECONNREFUSED (Connection refused)而非阻塞模式下connect()行为完全不同int flags fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); connect(sockfd, (struct sockaddr*)serv_addr, sizeof(serv_addr)); // 立即返回 // 此时 errno EINPROGRESSstrace输出connect(3, {sa_familyAF_INET, sin_porthtons(80), sin_addrinet_addr(127.0.0.1)}, 16) -1 EINPROGRESS (Operation now in progress)接下来你必须用select()或poll()等待socket变为可写writeable然后调用getsockopt(sockfd, SOL_SOCKET, SO_ERROR, err, len)检查err是否为0。为什么是“可写”而不是“可读”因为TCP连接建立成功后socket的写缓冲区就变为可写状态。这是绝大多数教程漏掉的关键点。实测验证脚本// 非阻塞connect后用select等待 fd_set writefds; FD_ZERO(writefds); FD_SET(sockfd, writefds); struct timeval tv {5, 0}; // 5秒超时 int ret select(sockfd 1, NULL, writefds, NULL, tv); if (ret 0 FD_ISSET(sockfd, writefds)) { int err 0; socklen_t len sizeof(err); getsockopt(sockfd, SOL_SOCKET, SO_ERROR, err, len); if (err 0) { printf(connect success!\n); } else { printf(connect failed: %s\n, strerror(err)); } }注意select()返回可写不代表连接一定成功必须getsockopt(SO_ERROR)二次确认。否则当目标主机存在但端口关闭时select()仍会返回可写但SO_ERROR是ECONNREFUSED。3.4send()与recv()流式协议下的“粘包”与“半包”真相TCP是字节流协议没有消息边界。send()和recv()的返回值与调用次数完全由内核缓冲区状态和网络状况决定。这是“粘包”、“半包”问题的根源。先看send()的典型陷阱char buf[1024] HELLO; int sent send(sockfd, buf, 1024, 0); printf(sent %d bytes\n, sent); // 可能输出5也可能输出1024甚至0send()返回值含义 0成功发送的字节数可能小于请求长度0对端已关闭连接shutdown()或close()-1出错errno指示原因EAGAIN/EWOULDBLOCK表示发送缓冲区满EPIPE表示对端已关闭且你尝试写入。关键点send()成功返回并不保证数据已到达对端甚至不保证已进入网卡队列它只表示数据已拷贝到内核发送缓冲区。所以send()后立即close()可能导致缓冲区数据丢失。正确做法是shutdown(sockfd, SHUT_WR)等待对端recv()返回0后再close()。recv()同理char buf[1024]; int n recv(sockfd, buf, 1024, 0); printf(recv %d bytes\n, n); // 可能是1024也可能是1也可能是0对端关闭recv()返回值含义 0接收到的字节数0对端已关闭连接shutdown()或close()-1出错errno EAGAIN/EWOULDBLOCK表示接收缓冲区为空非阻塞模式。“粘包”问题客户端连续send(A); send(B);服务端recv()可能一次收到AB。“半包”问题客户端send(HELLO WORLD)服务端第一次recv()只收到HELLO第二次才收到 WORLD。解决方案不是“让recv()一次收完”而是应用层定义协议边界。最常用两种定长包头前4字节存消息长度先recv()4字节再按长度recv()分隔符用\n或\0分隔循环recv()直到遇到分隔符。实测对比用nc -l 8080启动监听客户端执行$ echo -n ABC | nc localhost 8080 $ echo -n DEF | nc localhost 8080服务端nc -l 8080会显示ABCDEF连在一起这就是粘包。而用telnet localhost 8080手动输入ABC回车再输入DEF回车则会分行显示因为telnet自动加了\n。4. 实操过程从零实现一个抗压HTTP Echo Server全程调试记录4.1 目标设定不只是“回显”而是验证所有核心机制我们要实现的不是一个玩具server而是一个能承受1000并发连接、支持长连接、正确处理TIME_WAIT、并在strace下清晰可见每一步系统调用的实用demo。功能需求监听0.0.0.0:8080支持IPv4接收HTTP GET请求返回HTTP/1.1 200 OK\r\n\r\nHello from Echo Server支持Connection: keep-alive单连接处理多个请求每个连接超时30秒自动关闭编译后体积小于100KB无外部依赖。为什么选HTTP因为它是真实世界最常用的协议其头部格式Content-Length、Connection能完美暴露recv()的半包问题而keep-alive则直击TIME_WAIT管理痛点。4.2 代码实现逐行注释关键决策点#include sys/socket.h #include sys/epoll.h #include netinet/in.h #include arpa/inet.h #include unistd.h #include stdio.h #include stdlib.h #include string.h #include fcntl.h #include errno.h #include time.h #define MAX_EVENTS 1024 #define BUFFER_SIZE 4096 #define TIMEOUT_SECONDS 30 // 全局epoll fd int epoll_fd; // 连接结构体存储每个client的状态 struct client { int fd; char buffer[BUFFER_SIZE]; size_t buf_len; // 当前buffer中有效字节数 time_t last_active; // 最后活跃时间用于超时检测 }; // 初始化socket设置非阻塞、REUSEADDR int init_server_socket(int port) { int sockfd socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); if (sockfd -1) { perror(socket); return -1; } // 设置SO_REUSEADDR避免TIME_WAIT端口占用 int opt 1; if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)) -1) { perror(setsockopt SO_REUSEADDR); close(sockfd); return -1; } // 设置非阻塞避免accept阻塞 int flags fcntl(sockfd, F_GETFL, 0); if (flags -1 || fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) -1) { perror(fcntl O_NONBLOCK); close(sockfd); return -1; } struct sockaddr_in addr {0}; addr.sin_family AF_INET; addr.sin_port htons(port); addr.sin_addr.s_addr INADDR_ANY; if (bind(sockfd, (struct sockaddr*)addr, sizeof(addr)) -1) { perror(bind); close(sockfd); return -1; } if (listen(sockfd, SOMAXCONN) -1) { perror(listen); close(sockfd); return -1; } return sockfd; } // 将socket加入epoll监控 int add_to_epoll(int epoll_fd, int fd, uint32_t events) { struct epoll_event ev; ev.events events; ev.data.fd fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, ev) -1) { perror(epoll_ctl ADD); return -1; } return 0; } // 处理客户端recv解析HTTP请求 int handle_client_recv(struct client *cli) { ssize_t n; while ((n recv(cli-fd, cli-buffer cli-buf_len, BUFFER_SIZE - cli-buf_len, MSG_DONTWAIT)) 0) { cli-buf_len n; cli-last_active time(NULL); // 检查是否收到完整HTTP请求有\r\n\r\n char *pos memmem(cli-buffer, cli-buf_len, \r\n\r\n, 4); if (pos ! NULL) { // 解析HTTP方法和路径简化版 if (strncmp(cli-buffer, GET , 4) 0) { // 找到第一个空格后的路径 char *path_start cli-buffer 4; char *path_end memchr(path_start, , cli-buf_len - 4); if (path_end ! NULL) { // 发送HTTP响应 const char *response HTTP/1.1 200 OK\r\n Content-Type: text/plain\r\n Content-Length: 24\r\n Connection: keep-alive\r\n \r\n Hello from Echo Server; send(cli-fd, response, strlen(response), MSG_NOSIGNAL); } } // 移除已处理的部分剩余数据留在buffer中处理粘包 size_t processed pos - cli-buffer 4; // 4 for \r\n\r\n if (processed cli-buf_len) { memmove(cli-buffer, cli-buffer processed, cli-buf_len - processed); cli-buf_len - processed; } else { cli-buf_len 0; } } // 如果buffer满了丢弃防止OOM if (cli-buf_len BUFFER_SIZE - 1) { cli-buf_len 0; } } if (n 0) { // 对端关闭连接 printf(Client %d closed connection\n, cli-fd); return -1; } else if (n -1) { if (errno EAGAIN || errno EWOULDBLOCK) { // 非阻塞无数据可读 return 0; } else { perror(recv); return -1; } } return 0; } // 主循环 int main(int argc, char *argv[]) { if (argc ! 2) { fprintf(stderr, Usage: %s port\n, argv[0]); return 1; } int port atoi(argv[1]); int listen_fd init_server_socket(port); if (listen_fd -1) return 1; // 创建epoll实例 epoll_fd epoll_create1(0); if (epoll_fd -1) { perror(epoll_create1); close(listen_fd); return 1; } // 监听listen_fd上的EPOLLIN事件 if (add_to_epoll(epoll_fd, listen_fd, EPOLLIN) -1) { close(listen_fd); close(epoll_fd); return 1; } struct epoll_event events[MAX_EVENTS]; struct client clients[MAX_EVENTS]; int client_count 0; printf(Echo Server listening on port %d...\n, port); while (1) { int nfds epoll_wait(epoll_fd, events, MAX_EVENTS, 1000); // 1秒超时用于超时检测 if (nfds -1) { if (errno EINTR) continue; perror(epoll_wait); break; } // 处理就绪事件 for (int i 0; i nfds; i) { int fd events[i].data.fd; uint32_t events_mask events[i].events; if (fd listen_fd (events_mask EPOLLIN)) { // 新连接 struct sockaddr_in client_addr; socklen_t client_len sizeof(client_addr); int client_fd accept4(listen_fd, (struct sockaddr*)client_addr, client_len, SOCK_NONBLOCK | SOCK_CLOEXEC); if (client_fd -1) { if (errno EAGAIN || errno EWOULDBLOCK) { continue; // 无新连接 } else { perror(accept4); continue; } } printf(New connection from %s:%d, fd%d\n, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client_fd); // 加入epoll监控 if (add_to_epoll(epoll_fd, client_fd, EPOLLIN | EPOLLET) -1) { close(client_fd); continue; } // 初始化client结构 if (client_count MAX_EVENTS) { clients[client_count].fd client_fd; clients[client_count].buf_len 0; clients[client_count].last_active time(NULL); client_count; } else { // 连接数超限关闭新连接 close(client_fd); printf(Too many connections, rejected new client\n); } } else if (events_mask EPOLLIN) { // 已有连接可读 int idx -1; for (int j 0; j client_count; j) { if (clients[j].fd fd) { idx j; break; } } if (idx ! -1) { if (handle_client_recv(clients[idx]) -1) { // 连接关闭从epoll移除 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); close(fd); // 从clients数组中移除 if (idx client_count - 1) { memmove(clients[idx], clients[idx 1], (client_count - idx - 1) * sizeof(struct client)); } client_count--; } } } } // 超时检测遍历所有client关闭超时连接 time_t now time(NULL); for (int i 0; i client_count; ) { if (now - clients[i].last_active TIMEOUT_SECONDS) { printf(Client %d timeout, closing\n, clients[i].fd); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, clients[i].fd, NULL); close(clients[i].fd); if (i client_count - 1) { memmove(clients[i], clients[i 1], (client_count - i - 1) * sizeof(struct client)); } client_count--; } else { i; } } } close(listen_fd); close(epoll_fd); return 0; }4.3 编译与调试用strace和ss验证每一步编译命令静态链接减小体积$ gcc -static -O2 -o echo_server echo_server.c -lpthread $ ls -lh echo_server -rwxr-xr-x 1 user user 852K ... echo_server启动server$ ./echo_server 8080 Echo Server listening on port 8080...用strace监控系统调用只关注socket相关$ strace -e tracesocket,bind,listen,accept4,connect,send,recv,epoll_ctl,epoll_wait -p $(pgrep echo_server) 21 | grep -E (socket|bind|listen|accept4|epoll)你会看到socket(...)→bind(...)→listen(...)→epoll_ctl(ADD)初始化每次curl触发accept4(...)→epoll_ctl(ADD)新clientepoll_wait()周期性唤醒处理读写事件。用ss观察连接状态# 启动server后 $ ss -tuln | grep :8080 LISTEN 0 128 *:8080 *:* # curl一次后 $ ss -tun state established ( dport :8080 ) ESTAB 0 0 127.0.0.1:8080 127.0.0.1:54321 # 查看详细信息含cwnd、rtt $ ss -ti dport :8080 ESTAB 0 0 127.0.0.1:8080 127.0.0.1:54321 cwnd:10 rtt:0.123/0.012 mss:1448 pmtu:1500压力测试用ab模拟1000并发$ ab -n 10000 -c 1000 http://localhost:8080/观察ss连接数变化$ watch -n 1 ss -tun state established ( dport :8080 ) | wc -l正常应稳定在1000左右ab默认keep-alive不会出现TIME_WAIT堆积——因为我们设置了SO_REUSEADDR且正确处理了连接关闭。4.4 性能调优从/proc/sys/net/core/somaxconn到tcp_tw_reuse实测中发现当并发连接数超过128时ab报告大量apr_socket_recv: Connection reset by peer。查ss -sTotal: 1280 (kernel 1292) TCP: 1285 (estab 1000, closed 280, orphaned 0, synrecv 0, timewait 280/0), ports 0timewait 280/0表示有280个TIME_WAIT连接。问题出在SOMAXCONN全连接队列长度默认是128。当新连接洪峰到来队列满内核会丢弃SYN包客户端重传后超时最终reset by peer。解决方案# 临时提高全连接队列 $ echo 1024 /proc/sys/net/core/somaxconn # 永久生效 $ echo net.core.somaxconn 1024 /etc/sysctl.conf # 同时调整半连接队列syncookies $ echo 1 /proc/sys/net/ipv4/tcp_syncookies另一个瓶颈是TIME_WAIT连接过多。Linux提供net.ipv4.tcp_tw_reuse允许将TIME_WAITsocket用于新的OUTGOING连接客户端但对LISTEN状态的server无效。所以我们的server不能靠它但可以调小tcp_fin_timeout# 默认60秒

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑