资讯动态

Linux socket编程:文件描述符、TCP粘包与epoll实战

发布时间:2026/10/27 3:42:45 来源:尧图企业网站定制
1. 先把 socket 的本质说透它就是个文件描述符第一次接触 socket 那会儿我脑子里全是网络协议栈三次握手这类大词代码抄下来跑通了但完全不知道自己在干什么。真正让我开窍的是一句话在 Linux 里socket 的返回值就是一个整数和 open() 打开文件拿到的那个 fd 地位完全一样。这个认知一旦建立后面一大堆看起来玄学的东西就都顺了。1.1 一切皆文件不是一句口号read/write 真的能直接怼上去你可以在一个已经建立连接的 socket fd 上直接调read()和write()不需要什么socket_read之类的专用函数。原因很朴素内核给文件描述符挂的那张操作函数表里socket 对应的实现里同样填了read_iter和write_iter。你在用户态调read(fd, buf, n)内核顺着 fd 找到对应的struct file再找到struct socket最终落到 TCP 接收队列上。这意味着很多通用技巧可以直接迁移过来。比如select/poll/epoll能同时监听普通文件和 socket比如dup()能复制一个 socket fd比如close()和文件一样会触发引用计数递减。我在排查一个句柄泄漏问题时就是靠ls -l /proc/pid/fd看到同一个 socket 被 fork 出来的子进程继承了多份父进程 close 之后连接居然还在折腾了半天才想明白引用计数的逻辑。不过一切皆文件也有失灵的地方。lseek()对 socket 是无效的返回ESPIPE因为 TCP 是流没有偏移量这个概念。你不能像读文件那样随机跳到第 100 字节再读只能从当前读到的位置继续往后取。这一点想通了后面理解粘包问题就毫无障碍了。1.2 socket() 的三个参数每一笔都在决定后面能干什么socket(int domain, int type, int protocol)看着简单但每个参数都在给你的连接定终身选错了要么直接报错要么跑出诡异行为。domain决定地址家族。做本机进程间通信AF_UNIX比AF_INET快得多因为它绕过了整个网络层协议栈数据直接从内核缓冲区搬到对方缓冲区实测在本地高频小包场景下能省掉三成以上的 CPU。做跨机通信就老老实实AF_INET要 IPv6 就AF_INET6现在更推荐直接用AF_INET6加双栈选项一套代码兼顾两种地址。type里最常见的是SOCK_STREAM和SOCK_DGRAM。前者对应 TCP 的可靠字节流后者对应 UDP 的数据报。这里有个容易被忽略的位SOCK_NONBLOCK和SOCK_CLOEXEC可以按位或进type里。SOCK_CLOEXEC尤其重要它能保证exec系列函数执行新程序时这个 fd 自动关闭避免泄露给子进程。我见过一个服务在 fork 之后 exec 外部命令结果外部命令一直跑不完就是因为管道和 socket 的写端被意外继承对端永远等不到 EOF。用SOCK_CLOEXEC一次性解决比事后fcntl去设FD_CLOEXEC可靠得多因为那中间存在竞态窗口。protocol一般填 0让内核根据前两个参数自动推断。只有在SOCK_RAW这种场景下才需要显式指定普通业务代码里填 0 就对了。1.3 内核在你按下 socket() 的那一刻做了什么理解这一层排查问题时会快很多。调用socket()时内核会创建一个struct socket再挂一个struct sock上去TCP 的话就是tcp_sock然后把这个结构注册进文件系统分配一个没被占用的最小 fd 返回给你。注意这一步完全没有发生任何网络交互对面根本不知道你的存在。真正的连接建立发生在connect()或accept()。服务端listen()之后内核会维护两个队列一个是已完成三次握手的连接队列accept队列一个是半连接队列SYN 队列。accept()做的事情就是从已完成队列里摘一个出来新建一个 fd 返回。这解释了一个经典困惑为什么服务端accept()不及时客户端connect()也可能成功因为三次握手是内核替你完成的只要队列没满客户端握手完成后就认为连上了压根不管你的应用层有没有调accept。等accept队列被挤满新的 SYN 就会被丢掉客户端开始超时重传表现出来就是连不上但服务进程还活着。BACKLOG参数也常被误解。它设的不是最大连接数而是已完成队列的长度上限。Linux 上实际生效值取min(backlog, net.core.somaxconn)somaxconn默认值在老发行版上是 128现在多数是 4096。高并发服务上线前一定要检查这个值不然listen(fd, 1024)写得很漂亮实际生效的只有 128压测一上来就丢连接。2. 地址结构体和字节序翻车率最高的两个地方代码写不对报个错还好最怕的是不报错但行为诡异。地址结构和字节序就属于这一类填错了可能连到一台莫名其妙的机器上或者 bind 到一个你完全没想到的地址。2.1 sockaddr_in 的每个字段都别凭感觉填struct sockaddr_in一共四个字段看着少但每个都有讲究。sin_family必须是AF_INET这个大概没人会填错。sin_port必须经过htons()转换忘了转换的典型症状是你 bind 到 9001netstat里看到的却是 368719001 的字节互换结果。这个错误非常隐蔽因为 bind 不会失败只是监听在了别的端口上。sin_addr是struct in_addr里面就一个in_addr_t本质是 uint32。这里最容易搞混的是 地址的值和地址的字节序是两件事。你想表示 192.168.1.10写成inet_addr(192.168.1.10)得到的是网络字节序的整数你要是手写0x0A01A8C0这种十六进制那是把大端表示直接当小端整数用很容易错。sin_zero[8]这个填充字段现在很少有人提了它的作用是让sockaddr_in和sockaddr大小一致方便指针强转。关键点在于用之前必须memset清零。结构体里有未初始化字节时某些平台上的比较或者序列化会出问题而且这个 bug 极难复现。我现在的习惯是把memset当成肌肉记忆写多少遍都不嫌多。字段类型填法常见错误sin_familysa_family_tAF_INET填成 PF_INET虽然值一样但语义不对sin_portin_port_thtons(端口)忘记转字节序端口错乱sin_addrstruct in_addrinet_pton 或 INADDR_ANY手写十六进制导致地址反了sin_zerochar[8]memset 清零不清零导致比较异常2.2 字节序这件事不转就是等着出玄学网络字节序是大端x86 是小端。端口号是 16 位IP 地址是 32 位所以才有htons/htonl和ntohs/ntohl四个函数。名字里的 h 是 hostn 是 network中间的 s 是 shortl 是 long。很多人在本地测试时一切正常部署到另一台机器就炸往往就是字节序漏了——因为两端如果都是小端机器且都忘了转换反而能对上。这类 bug 一旦进了生产环境排查成本极高。更稳妥的写法是完全不要碰字节序地址用inet_pton()从字符串转端口用htons()转读回来的时候用inet_ntop()和ntohs()转。中间不要出现任何手写的数字常量。我现在的代码里基本看不到htonl直接操作 IP 的情况全是字符串进、字符串出可读性和正确性都好很多。另外提一句inet_ntoa它返回的是指向静态缓冲区的指针不可重入。在信号处理函数或者多线程里用它会出大问题。inet_ntop需要你自己提供缓冲区虽然麻烦一点但安全。这个坑我在一个多线程日志模块里踩过日志里打印的客户端 IP 偶尔会串成别人的地址查了两天才定位到。2.3 SO_REUSEADDR 到底解决了什么问题bind报Address already in use几乎是每个写网络程序的人都遇到过的第一个错误。原因通常是上一次的服务进程退出后连接进入TIME_WAIT状态端口还被占着。TIME_WAIT存在的意义是这样的主动关闭连接的一方在发完最后一个 ACK 之后要等 2MSL一般是 60 秒才能释放端口。为什么要等一是为了保证最后一个 ACK 能到达对面万一丢了对面会重传 FIN你还能重发 ACK二是为了让这条连接上残留的旧数据包在网络中自然消亡不至于串到新连接里。服务端重启时如果它是主动关闭方端口就会被占着。这时候加上int opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));就能在TIME_WAIT期间重新 bind。注意这里说的是重新 bind不是允许两个进程同时监听同一个端口那个是SO_REUSEPORT是另一回事多进程负载均衡场景才用。还有个高频误解SO_REUSEADDR能减少TIME_WAIT的数量。并不能。它只是让你能在TIME_WAIT存在的情况下成功 bind。真要减少TIME_WAIT得调整内核参数或者让对端先关。我个人的做法是服务端永远开启SO_REUSEADDR成本几乎为零收益是重启时不至于卡一分钟。3. 把一个能跑的服务端和客户端写出来原理说得再多不如亲手跑一遍。下面这套代码是我平时用来做快速验证的最小骨架剪掉了业务逻辑只保留网络部分方便你直接改。3.1 服务端的五步曲socket、bind、listen、accept、收发服务端的流程是有严格顺序的每一步失败都意味着后面不用走了。socket()拿到监听 fdbind()把地址和端口绑上去listen()把 fd 从主动变成被动内核开始替你接收 SYNaccept()从已完成队列里取连接拿到一个新的 fd 用于和这个客户端通信。这里有个很重要的点监听 fd 和连接 fd 是两个不同的东西。监听 fd 只负责接收新连接不会参与任何数据收发。连接 fd 才是每个客户端独有的那条通道。初学者常犯的一个错误是在循环里反复close(listen_fd)或者拿listen_fd去read前者导致服务只能接一个连接后者会报ENOTCONN或者读到乱七八糟的东西。accept()失败要分情况处理。EINTR是信号打断直接continue重试就行EMFILE/ENFILE是 fd 用完了说明有泄漏或者并发太高这时候不能简单 continue否则会陷入死循环疯狂重试CPU 直接打满。比较稳的做法是记一条日志sleep一小会儿再继续给系统一个喘息的机会。这个细节在压测时特别关键我见过一个服务就是因为没处理EMFILE一晚上把机器拖死了。3.2 完整可跑的服务端代码#include stdio.h #include stdlib.h #include string.h #include unistd.h #include errno.h #include arpa/inet.h #include sys/socket.h #include netinet/in.h #define PORT 9001 #define BACKLOG 128 #define BUF_SIZE 4096 int main(void) { int listen_fd socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); if (listen_fd 0) { perror(socket); exit(EXIT_FAILURE); } int opt 1; if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)) 0) { perror(setsockopt); close(listen_fd); exit(EXIT_FAILURE); } struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(PORT); addr.sin_addr.s_addr htonl(INADDR_ANY); if (bind(listen_fd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(bind); close(listen_fd); exit(EXIT_FAILURE); } if (listen(listen_fd, BACKLOG) 0) { perror(listen); close(listen_fd); exit(EXIT_FAILURE); } printf(listening on port %d\n, PORT); for (;;) { struct sockaddr_in cli; socklen_t cli_len sizeof(cli); int conn_fd accept(listen_fd, (struct sockaddr *)cli, cli_len); if (conn_fd 0) { if (errno EINTR) { continue; } perror(accept); usleep(100000); continue; } char ip[INET_ADDRSTRLEN] {0}; inet_ntop(AF_INET, cli.sin_addr, ip, sizeof(ip)); printf(client %s:%d connected\n, ip, ntohs(cli.sin_port)); char buf[BUF_SIZE]; ssize_t n; while ((n read(conn_fd, buf, sizeof(buf))) 0) { ssize_t off 0; while (off n) { ssize_t w write(conn_fd, buf off, (size_t)(n - off)); if (w 0) { if (errno EINTR) { continue; } fprintf(stderr, write failed: %s\n, strerror(errno)); break; } off w; } } if (n 0) { fprintf(stderr, read failed: %s\n, strerror(errno)); } printf(client %s:%d closed\n, ip, ntohs(cli.sin_port)); close(conn_fd); } close(listen_fd); return 0; }编译命令很朴素gcc -O2 -Wall -Wextra -g server.c -o server-Wall -Wextra这两个选项一定要加很多类型转换和未使用变量的警告能提前暴露问题。尤其是send/recv的第三个参数是size_t传有符号整数会有隐式转换警告这类警告背后往往是真实的 bug。3.3 客户端代码与调用顺序#include stdio.h #include stdlib.h #include string.h #include unistd.h #include errno.h #include arpa/inet.h #include sys/socket.h #include netinet/in.h #define PORT 9001 int main(int argc, char *argv[]) { const char *ip (argc 1) ? argv[1] : 127.0.0.1; int fd socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); if (fd 0) { perror(socket); exit(EXIT_FAILURE); } struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(PORT); if (inet_pton(AF_INET, ip, addr.sin_addr) ! 1) { fprintf(stderr, invalid ip: %s\n, ip); close(fd); exit(EXIT_FAILURE); } if (connect(fd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(connect); close(fd); exit(EXIT_FAILURE); } printf(connected to %s:%d\n, ip, PORT); char line[1024]; while (fgets(line, sizeof(line), stdin) ! NULL) { size_t len strlen(line); size_t off 0; while (off len) { ssize_t w write(fd, line off, len - off); if (w 0) { if (errno EINTR) { continue; } perror(write); close(fd); exit(EXIT_FAILURE); } off (size_t)w; } char buf[1024]; ssize_t n read(fd, buf, sizeof(buf) - 1); if (n 0) { printf(server closed\n); break; } buf[n] \0; printf(echo: %s, buf); } close(fd); return 0; }跑起来是两个终端的事先起服务端再起客户端。输入几行文字能看到回显就说明通路没问题。这一步看起来简单但它验证了地址填充、字节序、连接建立、读写循环四条链路任何一环有问题都会立刻暴露。3.4 短读短写为什么你的代码在本地没事上线就丢数据上面代码里我给write加了循环read那边只读了一次。严格来说两边都该循环但客户端这个例子是交互式的每次输入一行服务端一次性回显短读概率低。生产代码里绝对不能这么写。read返回的字节数可能小于你要求的数量这太正常了。TCP 是字节流内核缓冲区里有多少就给你多少可能你请求 4096 字节实际上只到了 100 字节。这不代表出错只代表对端还没发完。write也一样会短写。当你往一个非阻塞 socket 或者缓冲区快满的 socket 写大量数据时内核只接受一部分剩下的需要你自己接着写。判断 EOF 的正确姿势是read返回 0而不是-1。返回 -1 是出错要看errnoEINTR重试EAGAIN/EWOULDBLOCK是非阻塞下暂时没数据其他基本就是连接真出问题了。SIGPIPE这个坑必须单独说。当你往一个已经被对端关闭的连接上写数据内核默认会给你发SIGPIPE信号进程直接被杀掉连清理都来不及。解决方案有两种全局忽略这个信号代码里写signal(SIGPIPE, SIG_IGN);或者用send(fd, buf, len, MSG_NOSIGNAL)。我倾向于两个都做双保险。这个坑在长连接服务里迟早会踩早预防早省事。4. TCP 是字节流粘包不是包的问题几乎每个刚写完 echo 服务的人下一步都会尝试发结构体然后发现接收端解析出来的数据是乱的或者一次读到两条消息黏在一起。这就是所谓的粘包但这个名字其实有误导性——TCP 根本没有包的概念。4.1 粘包和拆包的真实成因TCP 是面向字节流的协议它在传输层做的是把字节切片塞进报文段接收端按序重组然后往接收缓冲区里堆。至于你的应用层是怎么划分消息边界的它完全不管。所以会出现两种情况你连着发了两个 100 字节的消息接收端一次read读了 200 字节这叫粘。你发了一个 1000 字节的消息接收端read只拿到 600 字节这叫拆。诱发因素主要有三个发送端的 Nagle 算法会把小包攒起来一起发接收端的缓冲区大小决定了单次能读多少网络 MTU 限制会让大包被切分。这三个都不受你的应用层控制所以任何一次 read 就是一条完整消息的假设都是错的。我踩过最典型的一次坑是协议里用\n分隔消息本地测试一次发一条完美运行压测时客户端批量发送接收端一次读到好几行代码只取了第一行剩下的丢弃导致大量消息丢失。修复方式很简单收到数据先追加到一个缓冲区里然后反复查找分隔符找到一个就切一条出来处理剩下的继续留在缓冲区等后续数据。4.2 三种常用的消息定界方案方案实现方式优点缺点适用场景分隔符用特定字符如 \n切分简单、可读分隔符不能出现在正文里需要转义文本协议、命令行交互长度前缀头部固定 4 字节表示长度高效、无转义问题需要处理头部本身的拆包二进制协议、内部服务固定长度每条消息长度恒定实现最简单灵活性极差特定硬件协议长度前缀是我最常用的方案。实现上有几个细节头部的 4 字节本身也可能被拆成两次到达所以要先攒够 4 字节解析出长度再攒够这个长度才能处理一条消息。另外长度字段一定要做上限校验不然一个恶意客户端发一个0x7FFFFFFF你的代码就去申请 2G 内存直接被打挂。我一般会设一个 1MB 左右的上限超过就断开连接。4.3 心跳和空闲检测别等 TCP 自己告诉你很多人以为连接断了read会立刻返回 0。实际情况是如果对面机器断电、网线拔掉、中间路由器挂了你的read会一直阻塞着可能十几分钟都没反应。TCP 自己有keepalive但默认是 2 小时才开始探测对业务来说太晚了。常规做法是在应用层做心跳客户端每隔固定时间发一个轻量的心跳包服务端记录每个连接最后一次活跃的时间后台起一个定时器扫描超过阈值就主动关掉。阈值一般设成心跳间隔的 2 到 3 倍既能容忍偶发的网络抖动又不会让死连接占着资源太久。SO_KEEPALIVE也可以顺手打开虽然它慢但作为兜底多一层保障。真要调的话TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT这三个选项能把探测间隔压到分钟级实测在跨机房场景下挺有用。5. 阻塞、非阻塞和并发模型选错了就是自找麻烦上面那个 echo 服务有个致命问题accept之后直接在循环里read一个客户端连上来不说话整个服务就卡死了别的客户端根本连不进来。这就是阻塞 IO 的天然缺陷。5.1 阻塞带来的问题不止一个默认创建的 socket 是阻塞的。accept会阻塞到有新连接read会阻塞到有数据connect会阻塞到握手完成write会阻塞到数据进了发送缓冲区。单连接场景下这很方便但一旦要处理多个连接就必须换思路。最粗暴的方案是每个连接开一个线程。这个方案在小规模下能跑几十上百个连接没问题但连接数上千之后线程切换的开销和每个线程默认 8MB 的栈空间就成了负担。而且线程模型下的资源竞争、连接生命周期管理都会变得很麻烦。更常见的方案是把 fd 设成非阻塞用一个事件循环统一管理。设置方式有两种socket()时直接或上SOCK_NONBLOCK或者事后用fcntl(fd, F_SETFL, flags | O_NONBLOCK)。前者是原子的后者有竞态窗口推荐前者。非阻塞之后所有调用都要处理EAGAIN。read返回 -1 且errno EAGAIN说明暂时没数据不是错误回去等下次可读事件accept返回 -1 且EAGAIN说明队列空了同样回去等。5.2 select、poll、epoll 的实际取舍这三个都是 IO 多路复用用途一样但机制和适用场景差很多。select是最老的用位图表示 fd 集合有FD_SETSIZE通常 1024的硬限制而且每次调用都要把整个集合从用户态拷到内核态返回后还要遍历一遍才知道谁就绪了。fd 数量一多性能下降非常明显。poll用数组替代了位图解除了数量限制但每次仍要全量拷贝和遍历。几百个连接时够用几千个就开始吃力。epoll是 Linux 特有的通过epoll_ctl把 fd 注册进内核的一个红黑树事件就绪时由内核通过回调把它们挂到就绪链表上epoll_wait直接取链表就行不需要遍历全部 fd。连接数越多优势越明显。对比项selectpollepollfd 数量限制有1024 左右无无每次调用是否全量拷贝是是否注册时拷贝一次就绪检测方式遍历遍历回调 就绪链表时间复杂度O(n)O(n)O(1)活跃数适用连接数少量中等大量epoll有两种触发模式水平触发LT和边沿触发ET。LT 是默认的只要缓冲区里有数据就会一直通知你写起来简单不容易漏事件。ET 只在状态变化时通知一次必须一次把数据读干净直到返回EAGAIN为止否则剩下的数据可能永远不再触发通知。ET 性能略好但代码要求高我一般在新项目里先上 LT压测发现瓶颈了再考虑换。还有个细节epoll的 fd 是可以被dup的但只有原 fd 在监控里复制出来的不在。这个在热更新或者多线程共享监听 fd 的场景下要注意。5.3 端口耗尽和连接数上限别等报警了才查高并发客户端场景下Cannot assign requested address这个错误很常见本质是本机临时端口用完了。Linux 默认的临时端口范围是 32768 到 60999差不多 2.8 万个。如果服务需要对外建大量短连接很容易打满。排查方式很直接ss -s cat /proc/sys/net/ipv4/ip_local_port_range第一个命令能看到当前的总连接数和各状态分布第二个看端口范围。如果确实是端口耗尽可以扩大范围同时让客户端尽量复用连接别每次请求都新建。服务端的连接数上限则受限于三处进程的RLIMIT_NOFILE、系统的fs.file-max、以及内存。ulimit -n看进程限制改的话在 systemd 服务文件里加LimitNOFILE65535比改/etc/security/limits.conf更可靠因为后者对 systemd 启动的服务经常不生效这个坑我踩过不止一次。6. 常见错误与排查实录写网络程序一半时间在写代码另一半时间在跟errno打交道。下面这些是我这些年反复遇到的整理成速查表遇到问题先对号入座。6.1 errno 速查表错误码出现位置常见原因处理方式EADDRINUSEbind端口被占用或处于 TIME_WAIT开 SO_REUSEADDR或换端口EADDRNOTAVAILbind绑定了本机不存在的 IP检查网卡地址或改用 INADDR_ANYEACCESbind端口小于 1024 且非特权用户换高位端口或用 capabilities 授权ECONNREFUSEDconnect目标端口没服务监听检查服务是否启动防火墙是否放行ETIMEDOUTconnect网络不通或对端无响应检查路由、安全组、抓包看 SYN 有无回包EMFILEaccept/socket进程 fd 用尽查 fd 泄漏提高 ulimitEPIPEwrite对端已关闭忽略 SIGPIPE检查连接状态EAGAINread/write非阻塞下暂时不可读写不是错误等下次事件EINTR各种系统调用被信号打断直接重试6.2 六个真实场景的排查路径场景一bind 一直报 Address already in use但ss -tlnp看不到占用。大概率是TIME_WAIT或者被别的用户空间的进程占了你看不到。用ss -tan | grep 端口看所有状态注意TIME_WAIT的本地地址那一列。开SO_REUSEADDR基本能解决。场景二客户端能连上但服务端 accept 不到。先看ss -tln的 Send-Q 那一列如果数字接近 backlog说明 accept 队列满了。要么是 accept 逻辑有阻塞要么是 backlog 设太小。场景三大批连接同时断开。检查是不是服务端accept之后没有正确close导致 fd 泄漏达到上限后所有操作失败。用lsof -p pid | wc -l能快速确认。场景四接收端读到的数据长度不对。先用tcpdump抓包确认数据是否完整到达然后检查应用层有没有正确处理拆包。抓包命令tcpdump -i any -nn -s 0 -A tcp port 9001-A参数能直接看 payload排查协议问题特别直观。场景五连接偶尔超时日志里什么都没有。看netstat -s | grep -i retrans的重传统计如果重传率明显偏高可能是网络质量或者接收窗口太小。再看dmesg有没有 conntrack 表满的告警这个在容器环境下很常见。场景六程序运行一段时间后 CPU 100%。优先怀疑是循环里没处理EAGAIN或者EMFILE导致不停重试空转。用strace -p pid -c看系统调用分布如果某个调用次数异常高基本就锁定了。6.3 几个省钱省时间的实操心得关于调试工具的选择。遇到连接问题先看状态再看代码。ss -tanp一条命令能看到所有连接的状态、队列、进程比读代码快十倍。strace -f -e tracenetwork能跟踪所有网络系统调用参数和返回值一目了然非常适合确认我到底传了什么参数进去。关于连接关闭的顺序。服务端主动关闭连接时如果还有数据没发完就close对端可能收不到完整数据。稳妥做法是先shutdown(fd, SHUT_WR)发 FIN等对端也关了再close。shutdown和close的区别在于前者只关一个方向后者释放整个 fd。关于零拷贝。如果是纯文件转发的大数据量场景sendfile能省掉两次数据拷贝性能提升很明显。但它对协议有要求不能加密、不能改内容适合静态文件服务这类场景。普通业务报文没必要上收益有限。关于缓冲区大小。SO_RCVBUF和SO_SNDBUF的默认值本身是内核自动调节的手动设死反而可能降低吞吐。真要调先压测看带宽延迟积再决定值。盲目设成一个很大的数可能造成内存浪费和延迟上升。关于内存对齐。如果要在 socket 上传结构体务必注意两端的内存布局。不同编译器、不同架构下的对齐规则可能不一致最稳的做法是定义明确的序列化格式一个字段一个字段地按字节写别直接memcpy整个结构体。这个坑在跨平台项目里几乎必踩。我个人在实际项目里的体会是socket 编程真正的门槛不在 API 有多少而在对失败路径的敏感度。每一条read都可能短读每一次write都可能被打断每一个accept都可能因为 fd 耗尽而失败。把这些当成常态而不是异常代码的健壮性会上一个大台阶。前面提到的那套最小骨架我建议你至少亲手改三遍一遍加长度前缀协议一遍换成非阻塞加 epoll一遍加上心跳和空闲回收。三遍走完后面再看任何网络框架的源码都是熟悉的味道。

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

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

免费获取报价 →
↑