资讯动态

Linux网络编程实战:从socket到epoll的高并发服务器开发指南

发布时间:2026/10/2 6:10:39 来源:尧图企业网站定制
1. 网络编程到底在学什么很多朋友一提到“Linux网络编程”就头皮发麻觉得这是大佬才配碰的东西。其实换个角度想你天天用的微信、刷的网页、连的数据库底层全是同一套玩意儿——socket。我做了这么多年后端带过不少新人发现大家卡住的点根本不在语法而在“不知道代码跑起来以后操作系统到底干了什么”。这一篇我打算用执行顺序来讲透Linux网络编程的完整链路从socket的创建到bind、listen、accept再到收发数据的read/write最后把阻塞、并发、IO多路复用这些绕不开的坎儿一次说清。每段代码都配了“为什么这么写”的说明不是我抄书是这些年真在金拱门一样的生产环境里踩坑踩出来的经验。如果你是刚开始学Linux网络编程或者写过一点socket但总是“能跑不知道为什么能跑”这篇应该能帮你把整张地图拼完整。内容偏实操所有代码都基于Linux环境gcc直接编译就能跑。涉及多线程和IO多路复用的地方我会标注重点因为这些才是真正决定你服务器能不能扛住压力的分水岭。2. 从socket到连接建立核心链路拆解2.1 为什么一切从socket开始socket在中文里常被翻译成“套接字”听着很玄其实它就是一个文件描述符。Linux里有个核心哲学一切皆文件。网络连接也被抽象成文件你往这个“文件”里写数据内核帮你通过网卡发出去别人发来的数据内核放到这个文件的缓冲区里你read就能读出来。这个设计的好处是你可以用操作普通文件的read/write接口去操作网络连接上层API统一了底层逻辑各干各的。我见过不少新手纠结“socket到底在哪一层”其实不用纠结你只要记住socket是应用层和内核网络协议栈之间的门把手。应用程序创建socket其实是在内核里申请一个网络端点内核返回给你一个整数fd以后所有操作都靠这个整数来指代这次网络通信。拿生活中打电话来类比socket就是你的电话机bind是给自己的电话机绑定一个号码IP和端口listen是让电话机进入待机状态accept是听到铃声后拿起听筒。这样一想整个流程就顺了。2.2 服务端第一步socket与bind的细节服务端要提供网络服务第一步一定是创建socket然后绑定地址。直接看代码#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h int main() { int listen_fd socket(AF_INET, SOCK_STREAM, 0); if (listen_fd 0) { perror(socket); exit(EXIT_FAILURE); } struct sockaddr_in server_addr; memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_addr.s_addr htonl(INADDR_ANY); server_addr.sin_port htons(8080); int opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); if (bind(listen_fd, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { perror(bind); close(listen_fd); exit(EXIT_FAILURE); } printf(socket created and bound to port 8080\n); return 0; }这段代码里有几个地方值得细说。首先是socket(AF_INET, SOCK_STREAM, 0)AF_INET表示IPv4地址族SOCK_STREAM表示TCP流式套接字最后一个0表示让内核自动选择协议TCP。你要是写UDP把SOCK_STREAM换成SOCK_DGRAM就行。然后是htons和htonl这两个函数专门处理字节序。网络字节序统一是大端的你的机器可能是小端所以端口号和IP地址在填入结构体之前必须转换。不转会出现什么情况你绑定的端口明明是8080结果实际监听的是0x1F90反过来变成32840连都连不上。这块出错不报明显异常排查起来最坑。SO_REUSEADDR这个选项是经验之谈。如果没有它服务端程序崩溃重启后内核可能还认为端口被占用你会看到“Address already in use”。开发调试时加了它可以秒级重启服务不用等TIME_WAIT状态结束。生产环境我也建议保留配合平滑重启非常有用。bind的作用是把这个socket和本机的IP、端口关联起来。INADDR_ANY表示监听本机所有网卡地址也就是说无论客户端连你哪个IP只要端口对都能到达这个socket。如果只想让特定IP访问这里可以填具体地址比如inet_addr(192.168.1.100)。2.3 listen和accept连接队列怎么工作bind完成之后socket还只是一个“号码已分配但未接通”的电话机要让别人能打进来必须调用listen。listen的本质是在内核里为该socket建立两个队列半连接队列SYN队列和全连接队列accept队列。半连接队列存放的是已完成TCP三次握手第一步收到SYN、但还没完成握手的连接全连接队列存放的是三次握手已完成、等待应用层accept取走的连接。内核自动帮你维护你不需要操作队列本身但理解这个机制对后面排查“连接堆积”问题特别有帮助。继续写代码if (listen(listen_fd, 128) 0) { perror(listen); close(listen_fd); exit(EXIT_FAILURE); } printf(listening on port 8080...\n); while (1) { struct sockaddr_in client_addr; socklen_t client_len sizeof(client_addr); int conn_fd accept(listen_fd, (struct sockaddr *)client_addr, client_len); if (conn_fd 0) { perror(accept); continue; } char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, client_addr.sin_addr, client_ip, INET_ADDRSTRLEN); printf(accept connection from %s:%d\n, client_ip, ntohs(client_addr.sin_port)); close(conn_fd); }listen的第二个参数是backlog表示全连接队列的最大长度。传统上认为这是“等待accept的连接数上限”但实际上Linux 2.2以后它代表全连接队列的长度半连接队列的长度由内核参数tcp_max_syn_backlog控制。你填128意思是内核最多帮你缓存128个已完成握手但还没被accept的连接。accept是个阻塞调用如果没有新连接到达这个函数会一直卡住直到有客户端连入。它返回的是一个“新socket”的fd这个fd专门用于和当前这个客户端通信。监听socket要留在原地继续accept下一个连接这里一对多、一对一的角色要分清listen_fd是永远监听的conn_fd才是真正干活的。我在很多教学代码里看到有人用单线程循环accept一个处理一个处理完再accept。这种模型只适合演示真上线会被性能吊打。原因很简单如果某个客户端连上之后半天不发送数据服务端就会一直阻塞在读操作上后面的客户端全部排队等待整个服务等于瘫痪。这就引出下一节的内容如何处理多个连接。3. 收发数据read、write与缓冲区机制3.1 阻塞与非阻塞理解socket的脾气默认创建的socket是阻塞模式。阻塞的意思是当你调用read去读数据如果内核缓冲区里没有数据这个线程就睡在那儿直到有数据到达或者对端关闭连接才返回。同理write如果发送缓冲区满了也会阻塞等待腾出空间。这种模式对单客户端场景非常友好代码简单、逻辑清晰。但多客户端场景下一个阻塞read就可能让整个进程卡死因为CPU不知道哪个客户端会先发数据。这不是代码写得不对而是模型选错了。为什么阻塞模式让新手又爱又恨爱在于简单不爱在于坑多。解决办法有几种把socket设置成非阻塞模式配合poll/epoll使用或者用多线程/多进程每个连接一个线程去阻塞读写再或者用IO多路复用让一个线程管理成千上万个连接。从实践经验看初学者应该先吃透阻塞模式因为非阻塞epoll只是在阻塞模式的基础上增加了一层事件通知机制底层数据读写逻辑并没有变。先把read/write搞明白再谈高并发节奏才不会乱。3.2 为什么read返回值必须判断服务端accept到客户端连接后就要进入数据收发环节。看一段经典的echo服务代码char buf[1024]; while (1) { memset(buf, 0, sizeof(buf)); ssize_t n read(conn_fd, buf, sizeof(buf) - 1); if (n 0) { perror(read); break; } else if (n 0) { printf(client closed connection\n); break; } printf(received: %s\n, buf); write(conn_fd, buf, n); }read返回值是这里最关键的判断依据。返回值大于0读到了n个字节这就是正常数据返回值等于0对端关闭了连接你这边继续read只会永远返回0必须关闭socket返回值小于0出错但是要分情况如果errno是EINTR被信号打断可以继续读如果是EAGAIN或EWOULDBLOCK非阻塞模式下无数据也是正常情况不能当错误处理。新手最容易犯的错误是只判断n 0不判断n 0结果客户端断开后服务端陷入死循环CPU飙升还找不到原因。其实加一个n 0的break就能解决的事能省一天排查时间。write也有门道。你以为write返回n就代表n个字节全部发送成功其实不然。在阻塞模式下write返回等于请求发送的字节数一般就是全部发出去了。但在非阻塞模式下write可能只发送一部分数据就返回返回的数值小于你传入的长度剩下的字节需要你放进应用层缓冲区等socket可写时继续发送。生产环境经常要配合一个应用层的发送队列来做这件事不能直接把大块数据硬塞给write。3.3 粘包问题TCP是流不是消息很多从UDP转过来的人会踩同一个坑第一次send一个“hello”第二次send一个“world”结果对端一次recv收到了“helloworld”。这是因为TCP是字节流协议它只保证字节的顺序不保证消息的边界。两次send的数据可能被内核合并成一个数据段发出去对端自然就读到一起了。解决粘包没有银弹常规做法有三种固定长度每个消息固定是4字节或8字节不足补位简单但浪费空间。分隔符消息之间加特殊分隔符比如HTTP的\r\n\r\n实现简单但要处理数据中恰好出现分隔符的情况。长度前缀每个消息头用固定字节数声明消息体长度比如前4字节存长度后面跟消息体服务端先读4字节再读对应长度的数据。生产环境最常用的是长度前缀。拿JSON协议举例一般会设计成“4字节长度 JSON字符串”。接收端先读满4字节转成int就知道后面要读多少然后循环读直到读够再解析JSON。这里要注意一个细节read不一定一次就能读够你要求的长度必须循环读直到累计长度达到消息头声明的字节数。很多人在这块写了个单次read就以为收完整了数据一多就出现半包问题。4. 走进多并发从多进程到epoll4.1 多进程模型与多线程模型的取舍早期的网络服务器喜欢用多进程模型一个连接fork一个子进程。父进程负责accept把连接fd传给子进程子进程阻塞读写处理完退出。这种模型的优点是完全隔离一个子进程崩了不影响别人缺点是开销大进程切换成本高而且进程数量一多系统资源就被吃光。后来流行多线程模型一个连接创建一个线程。线程比进程轻量共享地址空间通信方便但线程不安全问题也随之而来比如两个线程同时对同一个fd调用close可能把别人正在用的连接关掉。线程池是对多线程模型的一种优化提前创建一批线程任务队列里来了连接就分给空闲线程处理避免了频繁创建销毁线程的开销。不过无论是多进程还是多线程本质上还是“一连接一线程”的思路连接数上万以后线程上下文切换的开销会成为瓶颈。你的机器可能只有几十个核但连接有十万个线程切换就会消耗大量CPU在调度上真正干活的CPU时间比例会很低。C10K问题就是这么来的——如何用单线程管理一万个并发连接。4.2 IO多路复用select的缺陷与poll的改进IO多路复用解决的核心问题是让一个线程同时监视多个fd哪个fd有数据可读就去处理哪个。这样一来无论连接有多少线程数都能保持稳定。select是最早的方案使用方式是在一个fd集合上做轮询内核告诉你哪些fd可读可写。但它有三个明显的槽点第一fd集合大小有限制FD_SETSIZE通常是1024超过这个范围就没法监视第二每次调用select都要从用户态把整个fd集合拷贝到内核态连接多了开销很大第三返回后你需要遍历整个集合逐个判断是哪个fd就绪了这个复杂度是O(n)的连接数上万时纯遍历就慢得离谱。poll的改进是去掉了1024的限制改用链表结构管理fd但仍然逃不掉“每次全量拷贝全量遍历”的宿命。所以在poll和select时代上万并发基本是极限。4.3 epollLinux高并发的真正答案epoll是Linux特有的IO多路复用方案也是目前高性能网络库的基石Nginx、Redis、Netty底层都是这一套思路。epoll的三个关键接口int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);epoll_create在内核创建一个事件表返回一个fd。epoll_ctl负责往这个事件表里添加、修改、删除你要监视的fdop分别对应EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL。epoll_wait是真正的等待函数传入一个事件数组内核把就绪的事件填进去并返回就绪个数你只需要遍历这个返回的小数组即可复杂度是O(k)k是就绪的事件数而不是总连接数。epoll的高明之处在于它把fd集合留在内核空间维护不需要每次调用都从用户态拷贝全量数据同时采用回调机制当fd有数据到达时内核主动把该fd放入就绪链表epoll_wait只需要从就绪链表里取数据不用重新扫描全量集合。这就是epoll在大规模连接下依然能保持高性能的根本原因。再来看看epoll在服务端怎么做#define MAX_EVENTS 1024 int main() { int listen_fd socket(AF_INET, SOCK_STREAM, 0); // ... bind和listen的代码同前 ... int epfd epoll_create(1); struct epoll_event ev, events[MAX_EVENTS]; ev.events EPOLLIN; ev.data.fd listen_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, ev); while (1) { int nready epoll_wait(epfd, events, MAX_EVENTS, -1); for (int i 0; i nready; i) { int fd events[i].data.fd; if (fd listen_fd) { // 有新的客户端连接 struct sockaddr_in client_addr; socklen_t client_len sizeof(client_addr); int conn_fd accept(listen_fd, (struct sockaddr *)client_addr, client_len); ev.events EPOLLIN; ev.data.fd conn_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, ev); printf(new connection: %d\n, conn_fd); } else { // 已有连接有数据到达 char buf[1024]; ssize_t n read(fd, buf, sizeof(buf)); if (n 0) { close(fd); epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); } else { write(fd, buf, n); } } } } }这段代码里事件类型EPOLLIN表示可读事件。监听socket关注EPOLLIN因为新连接到达时监听socket会变为可读连接socket也关注EPOLLIN因为客户端发数据时连接socket会变为可读。epoll_wait返回后先判断是哪个fd就绪如果是监听fd就accept把新连接加入事件表如果是连接fd就read并处理数据。这套模型有几个必须注意的点监听socket和连接socket的事件不要混淆。连接socket不应该注册EPOLLIN之外的那些莫名其妙事件否则内核会不停通知你CPU白白消耗。accept返回的conn_fd默认是阻塞模式但在epoll托管下逻辑上其实不需要阻塞因为内核已经告诉你“这个fd有数据可读”。不过为了万无一失通常会把conn_fd设置成非阻塞防止极端情况下read时数据被别的线程读完导致阻塞卡住。客户端断开时epoll会返回EPOLLIN或EPOLLRDHUP事件你需要通过read(fd, buf, sizeof(buf))的返回值来判断读到0说明对端关闭这时要调用close(fd)并epoll_ctl(EPOLL_CTL_DEL)把fd从事件表移除。不彻底移除会导致少量fd泄漏长期跑下来fd耗尽服务端无法accept新连接。如果以后要处理读写两个方向的高并发还会用到EPOLLOUT事件socket发送缓冲区可写和水平触发/边沿触发的区别但那是进阶话题。先掌握服务端读方向的epoll流程你就能写出支撑几千连接的小型服务器了。4.4 水平触发与边沿触发一个决定性能的选项epoll默认是水平触发模式LT意思是只要fd上有数据没读完epoll_wait就会一直返回这个fd。处理起来很舒服哪怕你一次只读了一个字节剩下的数据下次还会通知你。不容易漏数据代码安全。边沿触发模式ET则只在状态变化时通知一次。fd缓冲区内从无数据变成有数据epoll_wait会通知你一次之后不管数据有没有读完它都不会再通知。所以ET模式下你必须一次性把数据读完直到read返回EAGAIN否则剩下的数据就滞留在缓冲区里可能影响下一次正常通信。看起来ET更高效省去了重复通知的麻烦但代价是编程复杂度大幅上升。你必须处理“读不够”的情况配合非阻塞socket循环读还要处理对端一次发来很多数据、多次读才读完的场景。新手很容易在这里丢数据。我的建议是刚开始学习严格用LT。等你的服务真正出现“大量空闲连接频繁唤醒”的性能问题时你再考虑ET。说实话99%的业务场景LT都够用Nginx为了极致性能用ET咱们写业务服务的习惯了LT就行。另外如果你的代码跑起来后客户端频繁断连但你确认逻辑没写错可以检查一下是不是把EPOLLET加上了却没用非阻塞socket循环读。这种“半吊子ET”是经典事故源之一。真要上ET记住一个准则非阻塞读写 循环直到EAGAIN。缺一不可。5. 进阶惊群问题与TCP粘包再思考5.1 accept惊群多线程下的隐藏炸弹当你从单线程epoll进化到多线程epoll时会撞上“惊群”问题。什么意思假设你有4个线程都阻塞在epoll_wait上等待同一个listen_fd这时来一个新连接内核会把所有等待线程全部唤醒。但最终只有一个线程能成功accept其他三个线程accept时可能会返回EAGAIN或者竞争到空连接。被无谓唤醒的线程白白浪费了CPU时间这就是惊群thundering herd。Linux 2.6以前这个问题在accept层面就存在。后来内核给accept加了互斥但epoll_wait层面仍然存在。到了Linux 4.5引入了EPOLLEXCLUSIVE事件标志可以保证唤醒等待队列中的一个线程而不是全部。使用方式是在监听socket注册事件时加上这个标志ev.events EPOLLIN | EPOLLEXCLUSIVE;多线程下这个参数几乎是必需品。否则同样的服务单线程100% CPU能扛住的并发多线程下CPU飙到200%还不一定扛得住因为大量线程在做无用功。还有一个小细节多线程epoll的架构往往是主线程负责处理新连接worker线程负责处理已建连的数据读写。监听socket只被主线程epoll监视连接socket才被分发到worker线程管辖的epoll中。这对减少惊群也有一定作用。5.2 为什么UDP不需要accept三次握手TCP有连接状态、可靠传输、流量控制所以编程模型复杂。UDP就不一样它是无连接的只管把数据报发出去不管对方收没收到。体现在代码上UDP服务端只需要socket、bind、recvfrom、sendto不需要listen和accept。所以UDP服务端的核心循环可以写成int sock_fd socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in server_addr; // bind ... while (1) { struct sockaddr_in client_addr; socklen_t len sizeof(client_addr); char buf[1024]; recvfrom(sock_fd, buf, sizeof(buf), 0, (struct sockaddr *)client_addr, len); sendto(sock_fd, buf, strlen(buf), 0, (struct sockaddr *)client_addr, len); }每次recvfrom不仅拿到数据还能拿到发送方的地址sendto时把这个地址回填即可。UDP没有粘包问题因为每个recvfrom对应一个完整数据报内核已经帮你按包分了。但也因为无连接丢包、乱序都得应用层自己负责。比如视频通话场景丢几帧可以忍延迟高不行所以用UDP文件传输场景一个字节都不能丢肯定用TCP。5.3 发送缓冲区和接收缓冲区内核帮你干的那些事很多人不理解为什么调用send后数据不会立刻出现在对方的recv里。这中间要经过本机内核的发送缓冲区、网络传输、对端内核的接收缓冲区最后才是应用程序的read。send成功只代表数据进入了本机的发送缓冲区不代表对方已经收到。这个特性在排查问题时特别重要。如果对方应用层迟迟没读到数据先别急着怀疑网络丢包可能是发送缓冲区把数据暂存了也可能对端接收缓冲区满了导致TCP窗口缩小本机发送窗口跟着变小。用ss -ant或netstat -an查看连接状态能看到Recv-Q和Send-Q这两个字段它们分别是对端未读数据量和本机发送缓冲区的积压量。如果Recv-Q持续增长说明对方应用层读取速度跟不上如果Send-Q持续增长说明对方接收窗口打不开甚至可能对方进程已经卡死。修改socket缓冲区大小的接口是setsockopt的SO_RCVBUF和SO_SNDBUF大小通常要设成4KB的整数倍。但这个操作务必谨慎随便调大可能白白占用大量内存毕竟每个连接都有各自的缓冲区一万个连接乘以128KB就是1.28GB的内存开销。6. 断线与异常处理写过线上服务才懂的事6.1 半包与粘包不只是粘包前面提到粘包其实对应还有一个问题叫半包。服务端声明一条消息长度100字节但客户端分两次发送第一次40字节第二次60字节。服务端第一次read只读到40字节如果此时就按完整消息解析必然失败。正确做法是维护一个应用层缓冲区每次read到的数据先append到缓冲区然后检查缓冲区里是否够一个完整消息根据消息头长度判断。够长就取出完整的消息处理剩下不足一个消息的部分继续留在缓冲区等下一波数据。这就是很多网络库里“接收缓冲”的来源也是所有自定义TCP协议必须具备的基础能力。我见过有团队直接用recv返回的数据长度来判断一条消息的边界结果线上隔三差五出解析错误。后来每个人都在自己的业务代码里写了半包处理但每个人写的都长得不一样维护成本极高。当时我建议统一封装一个读缓冲类把“分包”问题收敛到一层解决才算彻底根治。6.2 被动断开与主动断开close和shutdown的区别socket关闭有两条路close和shutdown。close是把fd引用计数的引用减一只有当引用计数减到0时才真正关闭连接。如果同一个fd被fork被子进程继承父进程调用close并不会立刻断连因为子进程还握着这个fd。这就是为什么有时候服务端close了客户端还觉得连接是通的——可能还有别的进程/线程持有该fd。shutdown则不同它直接切断连接的方向。shutdown(fd, SHUT_RD)表示关闭读方向之后read返回0shutdown(fd, SHUT_WR)表示关闭写方向之后write返回SIGPIPE信号错误SHUT_RDWR就是两个方向都关。shutdown不会释放fd你还得额外调用close才能释放资源。什么时候必须用shutdown而不是close比如你写了一个HTTP服务端给客户端发完响应后需要半关闭写方向SHUT_WR让对方知道“数据发完了你可以开始读完整响应了”但你还想继续读客户端可能发来的请求。这种情况close会把整条连接干掉message边界就丢了。6.3 SIGPIPE一个让程序悄悄崩溃的信号这是网络编程新手最容易栽的坑。当服务端向一个已关闭的socket写入数据时内核会发送SIGPIPE信号给进程。如果不处理进程默认会被终止而且是安静地终止连日志都没有。典型的场景客户端断开连接但服务端还没感知到继续往这个fd上write第一两次可能正常返回第三次可能就触发SIGPIPE整个服务进程就没了。你排查半天网络问题结果发现进程没了。解决方式有两种。第一在信号层面忽略它signal(SIGPIPE, SIG_IGN);第二在send/write时加上MSG_NOSIGNAL标志send(fd, buf, len, MSG_NOSIGNAL);这样write或send就会返回-1errno变成EPIPE你可以正常处理错误而不是进程直接蒸发。我个人习惯是两种都做全局忽略SIGPIPE然后每个send都带MSG_NOSIGNAL双保险。日志里宁可看到EPIPE报错也不能让进程在毫无预警的情况下消失。7. 常用排查命令与工具清单7.1 先看连接状态ss和netstat怎么用服务器出了问题第一件事不是改代码而是确认网络连接状态。Linux下最常用的命令是ssnetstat在部分发行版里已经不再默认安装。ss -ant这个命令列出所有TCP连接状态。重点看几个字段Local Address本地地址和端口、Peer Address对端地址和端口、State连接状态、Recv-Q和Send-Q收发缓冲区积压量。如果看到大量连接处于SYN_SENT状态说明客户端发出的SYN包没有得到服务端响应可能是服务端端口没监听。如果看到大量TIME_WAIT这是正常现象表示主动关闭连接的一方正在等待2MSL时间防止旧连接的数据包残留。TIME_WAIT本身不是问题但量大到几万时需要检查是不是连接频繁建立关闭可以通过复用TIME_WAIT连接tcp_tw_reuse缓解但这不是银弹过度依赖反而掩盖了连接管理不合理的问题。7.2 抓包工具tcpdump与nc组合拳有时候代码层面看不出来问题就需要抓包来看真实网络行为。tcpdump是Linux下最强大的抓包工具之一常用命令tcpdump -i eth0 tcp port 8080 -nn -XX-i eth0指定网卡tcp port 8080过滤端口-nn不解析域名和端口为服务名-XX同时输出十六进制和ASCII格式能直接看到数据内容。如果服务跑在回环地址上网卡要换成lo。抓包后重点关注TCP三次握手的SYN、SYN-ACK、ACK三个包。如果只有SYN没有SYN-ACK说明服务端没有正常响应可能是防火墙拦截或者服务根本没监听如果SYN-ACK发了但客户端不回ACK可能是客户端内核参数问题或者客户端在收到响应前就超时关闭了。ncnetcat是另一个好用的小工具。调试服务端时可以直接用nc模拟客户端nc -vz 127.0.0.1 8080 nc 127.0.0.1 8080第一行是扫描端口连通性第二行进入交互模式你输入什么服务端就会收到什么。在没有完整客户端的情况下nc是验证服务端逻辑最快的方式。7.3 压测工具不只是ab和wrk服务写完之后至少要压一下才知道能不能扛住。简单的压测可以用abab -n 10000 -c 100 http://127.0.0.1:8080/这个命令表示发送10000个请求并发100。不过ab只能打HTTP协议如果你写的是自定义TCP协议更适合用wrk或自定义脚本。wrk支持lua脚本可以灵活构造请求压测能力比ab强很多wrk -t4 -c1000 -d30s http://127.0.0.1:8080/意思是使用4个线程模拟1000个并发连接持续30秒。如果压测过程中出现大量连接失败或超时就要回头检查你的服务端代码尤其是fd泄漏和事件处理逻辑这两块。压测环境的网络参数也要注意单机压测时经常是客户端先到瓶颈因为每次connect都要消耗本地端口默认范围有限。可以用sysctl net.ipv4.ip_local_port_range查看并调整可用端口范围。8. 写在最后的几点经验这四弹Linux网络编程写下来我自己也重新把整个体系过了一遍。有些人觉得网络编程就是背APIsocket、bind、listen、accept、read、write完事。但实际上真正的分水岭是在遇到异常、并发、半包、惊群、SIGPIPE这些“教科书不太讲”的问题时能不能快速定位并解决。我给新人的建议是先把单线程的阻塞TCP服务写熟再上多线程然后上epoll最后才是ET和性能优化。每一步都亲手写一遍不要只抄代码。你调通一个echo服务获得的正反馈比看十篇教程都管用。如果非要列几个“过来人”心得的话我提炼成三条第一所有read/write的返回值都是核心情报对了断连、错了报错、少了半包全都藏在返回值里。第二SIGPIPE一定要尽早全局忽略否则某天凌晨服务突然挂了你都找不到原因。第三调试多线程epoll时把惊群问题当成默认会踩的坑提前用EPOLLEXCLUSIVE堵住。这套内容写到这儿基本上把Linux网络编程从socket到epoll到异常处理的路径捋了一遍。每一段代码我都亲手编译运行过每一步踩过的坑都有记录。你照着写一遍再自己压一压遇到问题了可以回头翻这篇。如果这篇文章能让你少走几段弯路那我就没白写。

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

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

免费获取报价 →
↑