news 2026/10/27 3:07:32

从 socket 到 accept:TCP 协议常用 API 与客户端服务端实现拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从 socket 到 accept:TCP 协议常用 API 与客户端服务端实现拆解

服务端进程重启,bind直接报Address already in use,明明上一个进程已经退出了,ps也查不到,端口却像是被谁焊死在那里。这个场景我见过太多次,提问的人通常还会补一句“我代码没改过,昨天还是好的”。问题往往不在代码改没改,而在于对 TCP 服务端从socket到accept这条调用链的内核行为缺少一层理解。这篇就把TCP 协议常用 API和TCP 客户端服务端的实现拆开讲透:每个调用的入参在表达什么、返回值在告诉你什么、哪些事情内核替你做了、哪些必须你自己兜住。适合刚写完第一个 echo 服务的同学,也适合写过不少但一直靠改参数试出来的朋友。

1. 先搞清楚内核和你的代码各自负责哪一段

写 TCP 程序最容易产生的一个错觉,是以为send就是“把数据发到网线上”、recv就是“从网线上把数据收下来”。真实的模型更像是你去寄快递:你把包裹交到网点,签收单上写的只是“网点已收件”,不代表收件人已经拿到。TCP 的“可靠”是由内核的发送缓冲区、重传定时器、滑动窗口、拥塞控制这一整套机制共同保证的,应用层能做的只有两件事——把数据正确地交给内核,以及正确地理解内核交回来的结果。想明白这个分层,后面所有 API 的行为都能推导出来,而不是靠背。

1.1 三次握手被拆散到了哪几个调用里

教科书上讲三次握手是一张时序图,落到代码上它其实是被切碎分配到不同调用里的。服务端的listen是把 socket 从“主动连接”状态切换为“被动监听”状态的分界线,调用之后内核才开始接受 SYN 报文;客户端调用connect发出 SYN,此时内核开始重传定时;服务端内核收到 SYN 后回 SYN+ACK,这条连接进入“半连接队列”;客户端收到 SYN+ACK 回 ACK,同时connect返回成功;服务端内核收到最后一个 ACK,把连接从半连接队列挪到全连接队列,等待accept把它取走。

这里有两个非常关键的推论。第一个:connect返回成功,只代表客户端这一侧的握手完成了,服务端的应用层可能压根还没调用accept。所以客户端连上就发数据、服务端还没准备好读取,是完全正常的,数据会躺在服务端的接收缓冲区里。第二个:服务端listen之后不调用accept,连接照样能建立,最多堆到全连接队列满,之后新来的 SYN 会被丢弃或收到 RST。这解释了很多“服务端起没起来”的误判——端口在监听不代表业务逻辑在跑。

1.2 缓冲区是这个模型里最容易被忽略的角色

每个 TCP socket 在内核里都挂着发送缓冲区和接收缓冲区。send干的事情是把用户态数据拷进发送缓冲区,然后就返回了,真正什么时候发、发多少、要不要重传,全由内核决定。recv则是从接收缓冲区把已经到达并且按序排好的数据拷到用户态。缓冲区的大小直接决定了send会不会阻塞、recv一次能拿到多少。

我见过一个很典型的误解:有人以为recv传入 4096 字节的缓冲区,对端发 100 字节就会返回 100,对端发 8000 字节就会分两次各返回 4096。前半句大致成立,后半句在边界上不成立——TCP 是字节流,不保留任何消息边界。对端连续调用了三次send,每次 100 字节,你这边一次recv完全可能拿到 300 字节,也可能拿到 100、50、50 这样拆开的片段。这不是实现 bug,是协议本来的设计。理解这一条,后面“粘包”那一节就不用再纠结了。

2. 服务端这五个调用的边界,比参数本身更重要

一个最简 TCP 服务端的骨架是socket → bind → listen → accept → 读写 → close。代码短到可以背下来,但每个调用都有几个“不看文档就会踩”的细节。我把它们按调用顺序拆开说,顺带解释每个参数为什么是这个值。

2.1 socket 与 bind:地址、端口、字节序三处容易翻车

socket(AF_INET, SOCK_STREAM, 0)这三个参数分别定的是协议族、套接字类型和具体协议。第三个参数填 0 表示由前两个推导,AF_INET + SOCK_STREAM推导出来就是 TCP,所以绝大多数情况下填 0 即可。返回的是一个文件描述符,本质就是进程 fd 表里的一个索引,所以用完一定要close,否则 fd 泄漏,跑到EMFILE的时候accept会一直失败。

bind的坑主要集中在地址结构体上。sin_port必须用htons转成网络字节序,sin_addr用INADDR_ANY表示监听本机所有网卡。字节序这件事,如果不是从小就写网络程序,很容易忘。判断方法很简单:小端机器上htons(8080)得到的值跟 8080 完全不一样,你printf一下就知道自己有没有转。

#include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> int main(void) { int listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd < 0) { perror("socket"); return 1; } int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(9000); addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind"); close(listen_fd); return 1; } printf("bound on port %d\n", ntohs(addr.sin_port)); close(listen_fd); return 0; }

这段代码里SO_REUSEADDR是故意加的,它的作用放到第 6 节展开。地址转换不要再用inet_addr,它返回的是in_addr_t,失败时返回INADDR_NONE,跟广播地址255.255.255.255撞车,判断不了错误。用inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr),返回值 1 表示成功,0 表示地址格式不合法,-1 表示协议族不支持,三种情况分得清清楚楚。反向转换用inet_ntop,比inet_ntoa安全,因为后者返回的是静态缓冲区,多线程下会互相覆盖。

2.2 listen 的 backlog 不是“最大连接数”

listen(fd, backlog)这个 backlog 被误解的频率极高。它控制的不是“服务端最多能服务多少个客户端”,而是已完成三次握手、等待应用层accept取走的连接队列长度上限。Linux 上实际生效的值是min(backlog, /proc/sys/net/core/somaxconn),很多发行版somaxconn默认是 4096,老版本是 128。你传 1024,但系统上限是 128,那实际就是 128。

为什么这个值重要?如果应用层accept的速度跟不上新连接涌入的速度,全连接队列会满。队列满了之后,Linux 的默认行为(tcp_abort_on_overflow=0)是静默丢弃客户端的 ACK,让客户端以为握手还没完成,触发重传。表现出来就是客户端偶发连接超时,而服务端日志里什么异常都没有,因为连接根本没进到应用层。这种问题极难从应用日志看出来,得看ss -lnt输出里Send-Q那一列,或者netstat -s | grep -i listen里的 overflow 计数。

实践中我的做法是:backlog 传SOMAXCONN,同时保证accept循环里不要在取到连接后做耗时操作。取到连接立刻扔给工作线程或事件循环,accept本身要尽快回到下一轮。

2.3 accept 返回的是新 fd,监听 fd 一直留在原地

accept成功后返回一个全新的文件描述符,代表这条具体连接。原监听 fd 什么都不变,继续监听。很多初学者在这里写出这样的代码:拿到返回值之后把它赋给了listen_fd,或者在循环外只accept一次,结果第二个客户端连不上。

for (;;) { struct sockaddr_in peer; socklen_t len = sizeof(peer); int conn_fd = accept(listen_fd, (struct sockaddr *)&peer, &len); if (conn_fd < 0) { if (errno == EINTR) continue; /* 被信号打断,重试 */ if (errno == EMFILE) { /* fd 耗尽,见下文 */ } perror("accept"); continue; } char ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &peer.sin_addr, ip, sizeof(ip)); printf("client %s:%d connected\n", ip, ntohs(peer.sin_port)); /* 处理这条连接 */ close(conn_fd); }

accept的错误处理里有三个必须区分的情况。EINTR表示被信号打断,直接continue重试就行,不重试就等于丢了一次连接机会。ECONNABORTED表示客户端在握手完成前后放弃了,也重试。EMFILE表示进程 fd 用完,这个最麻烦——此时连接还在队列里,你accept不出来,只能等有 fd 释放。业界有个小技巧:预留一个 fd(启动时open("/dev/null")),遇到EMFILE就close掉它,accept一条连接立刻close,然后再把预留 fd 打开,这样能用最小代价把堆积的连接清掉,避免把监听端口拖死。

3. 客户端代码短,但 connect 的返回值不能只看 -1

客户端骨架是socket → connect → 读写 → close,比服务端少两个调用,陷阱却一点不少。最核心的一条是:connect返回 -1 不代表连接一定失败,得分阻塞和非阻塞两种模式看。

3.1 阻塞模式下 connect 的超时到底有多长

阻塞connect返回 0 表示握手完成,返回 -1 表示失败。但这里的“失败”有个时间尺度问题:如果没有SO_SNDTIMEO干预,内核会按tcp_syn_retries重传 SYN,默认 6 次,总耗时大约 127 秒。也就是说,对一个不可达的地址调用connect,你的程序可能卡两分多钟。生产环境里这基本不可接受。

想控制连接超时,标准做法是把 socket 设成非阻塞,connect立刻返回 -1 且errno == EINPROGRESS,然后用select或poll等这个 fd 可写,配一个自己定的超时时间。可写之后不能直接认为成功,必须用getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len)把待处理的错误取出来,err == 0才算连上。

int connect_with_timeout(const char *ip, int port, int timeout_ms) { int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) return -1; 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) { close(fd); return -1; } int flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK); int ret = connect(fd, (struct sockaddr *)&addr, sizeof(addr)); if (ret == 0) { fcntl(fd, F_SETFL, flags); return fd; } if (errno != EINPROGRESS) { close(fd); return -1; } fd_set wfds; FD_ZERO(&wfds); FD_SET(fd, &wfds); struct timeval tv; tv.tv_sec = timeout_ms / 1000; tv.tv_usec = (timeout_ms % 1000) * 1000; ret = select(fd + 1, NULL, &wfds, NULL, &tv); if (ret <= 0) { close(fd); return -1; } /* 超时或出错 */ int err = 0; socklen_t len = sizeof(err); getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len); if (err != 0) { close(fd); return -1; } fcntl(fd, F_SETFL, flags); /* 恢复阻塞,后续读写按阻塞模型走 */ return fd; }

注意最后那行恢复O_NONBLOCK的操作。很多人忘了这一步,后面recv直接返回 -1 加EAGAIN,然后误判成对端断开,排查半天。另外select的可写条件在出错时也会触发,所以那一步必须用SO_ERROR复核,不能省。

3.2 客户端什么时候必须自己 bind

绝大多数客户端不需要bind,内核会自动选一个临时端口。但有几种场景必须自己绑:需要固定源端口做访问控制的、需要绑到特定网卡做多路径分流的、以及某些协议要求源端口在特定范围内(比如按端口规则做的白名单)。一旦自己bind,就要处理端口被占用的问题,还要注意SO_REUSEADDR在客户端侧的行为和服务端不完全一样——它解决的是本地端口冲突,不是服务端那种 TIME_WAIT 占用。

还有一种情况是 IPv4/IPv6 双栈。用AF_INET6加IPV6_V6ONLY=0可以让一个 socket 同时接受两种地址,省掉两套代码。但如果对端地址可能是 v4 映射地址,getaddrinfo返回的地址结构要按ai_family分别处理,不能统一往sockaddr_in里塞,这是崩溃和乱码的高发区。

4. send 和 recv 的返回值才是真正的“协议约定”

写完能连上之后,下一个坎就是收发。这个地方的判断逻辑如果写错,表现出来的现象是“数据偶尔少一段”“偶尔收到乱码”“进程莫名被杀”,都很隐蔽。

4.1 短写:send 返回了不代表请求的量都发出去了

send(fd, buf, len, 0)的返回值是实际拷进发送缓冲区的字节数,可能是len,也可能小于len。阻塞模式下如果缓冲区满了,send会一直卡着直到有空间;非阻塞模式下会直接返回 -1 且errno == EAGAIN或EWOULDBLOCK。还有一种情况是返回小于len的正数,说明只写进去一部分。

所以一个能用的发送函数必须带循环:

ssize_t send_all(int fd, const void *buf, size_t len) { size_t sent = 0; const char *p = buf; while (sent < len) { ssize_t n = send(fd, p + sent, len - sent, MSG_NOSIGNAL); if (n > 0) { sent += n; continue; } if (n < 0 && (errno == EINTR || errno == EAGAIN)) continue; return -1; } return (ssize_t)sent; }

这里MSG_NOSIGNAL是关键,它避免了向已关闭连接写入时触发SIGPIPE。如果不加这个标志,也必须在进程启动时signal(SIGPIPE, SIG_IGN),否则默认行为是直接杀掉进程。我见过线上服务因为一个客户端提前断开、服务端继续推送心跳,整个进程被SIGPIPE干掉,日志里没有任何痕迹,只有一句被内核记录的信号退出。这次经历之后,我所有网络服务的main函数第一行都是忽略SIGPIPE。

那为什么不干脆全部用MSG_NOSIGNAL就行了?MSG_NOSIGNAL是 Linux 特有的(send系列支持),用write的时候没法传标志。所以更保险的组合是:进程级忽略SIGPIPE兜底,发送时再加MSG_NOSIGNAL。双保险,多花不了什么成本。

4.2 recv 返回 0 的语义只有一种:对端关闭了写方向

recv的返回值有三种情况,必须分开处理:

返回值含义正确处理
大于 0读到了这么多字节累加到缓冲区,继续读或处理
等于 0对端发送了 FIN,关闭了写方向关闭本端连接,不要重试
小于 0出错按 errno 分支:EINTR/EAGAIN 重试,ECONNRESET 等直接关闭

把返回 0 当成“暂时没数据”去continue,是一个能烧满一个 CPU 核的死循环。而把EAGAIN当成断连去close,是另一个方向的错误。这两个错误我在不同项目里都见过,前者表现为进程 CPU 打满,后者表现为连接频繁无故断开。

另外要特别注意recv的flags参数。传MSG_WAITALL会让recv尽量读到请求的字节数,但对端关闭或出错时仍可能提前返回,不能依赖它来“读满一个包”。传MSG_PEEK只窥探不消费,适合判断连接是否还活着。这两个标志都不是什么场景都合适,尤其是MSG_WAITALL,如果协议对端发的是变长数据,用它会让整个线程卡住。

4.3 粘包是应用层没定边界,不是 TCP 的毛病

TCP 是字节流协议,只保证字节按序到达,不保证“消息边界”。对端两次send的内容,接收方一次recv全部拿到,这叫“粘包”;对端一次send的内容,接收方分两次recv拿到,这其实连“拆包”都不能算,就是正常的字节流读取。两者都是协议允许的,所以应用层必须自己定义边界。

常见的三种定界方式,各有适用面:

  • 定长消息:每条消息固定 N 字节,读满 N 字节就算一条。实现最简单,但长度不可变,浪费带宽,只适合控制指令类场景。
  • 分隔符:用\n或\r\n之类不会出现在正文里的字符分隔,HTTP 的头部、Redis 的 RESP 都用了类似思路。要处理“分隔符恰好被切在两个 recv 之间”的情况,所以接收缓冲要能跨包保留。
  • 长度前缀(TLV 的 L):包头固定 4 字节大端整数表示后面的正文长度,先读 4 字节算出长度,再读正文。这是最通用、最高效的方案,绝大多数 RPC 协议都这么设计。

我一般用长度前缀加一个魔数校验,结构大概是:magic(2字节) + version(1字节) + type(1字节) + length(4字节) + payload。魔数用来快速发现“两边协议版本对不上”或者“连错了端口”这类问题,比拿到乱七八糟的 payload 再去反序列化报错要容易排查得多。解析状态机要能处理任意切分,所以实现上是一个循环:尽量从缓冲区头部取,取不够就等下一次recv。

还要注意长度字段的合法性校验。如果不校验length,攻击者发一个length = 0xFFFFFFFF,你的程序可能直接malloc4GB 然后挂掉。设一个业务上合理的上限,比如 16MB,超了直接断开,这是必须写的防御。

5. 从单连接到多连接:阻塞模型撑不住的那一天

只有一个客户端的时候,阻塞模型完美工作。第二个客户端连进来,你会发现它一直卡着,因为服务端还在第一条连接的recv上阻塞。这是所有网络编程入门者都会撞上的墙。

5.1 为什么 fork 一个进程或线程不是长久之计

最直观的改法是每条连接fork一个子进程或者pthread_create一个线程。几百个连接的时候还行,上到几千个,线程栈(默认 8MB)和上下文切换开销就会变成主导。而且线程之间的数据共享和生命周期管理会迅速变复杂,尤其在需要广播、需要统计全局状态的时候。

真正适合高并发的模型是“一个线程管很多个 socket”,靠 IO 多路复用把“哪个 fd 有新事件”这件事交给内核。这就是select、poll、epoll这一族系统调用的意义。

5.2 select / poll / epoll 怎么选

机制数据结构每次调用的开销fd 数量上限适用场景
select位图需要把 fd 集合从用户态拷到内核,返回后还要遍历受 FD_SETSIZE 限制,通常 1024教学、古董代码、跨平台兼容
poll结构体数组同样要拷贝整个数组没有固定上限连接数不大、需要跨平台
epoll内核红黑树 + 就绪链表只在epoll_ctl时注册,epoll_wait只返回就绪的受 fd 上限约束Linux 下高并发首选

epoll的核心优势是“事件注册一次,之后只取就绪的”,所以每次epoll_wait的开销跟你监听了多少个 fd 基本无关。它的两个触发模式要分清楚:水平触发(LT,默认)只要缓冲区还有数据就会一直通知你,写起来不容易漏;边缘触发(ET)只在状态变化时通知一次,必须配合非阻塞 fd 和循环读到底,否则数据会一直留在缓冲区里没人处理。ET 效率略高但坑更多,我的建议是除非确实在追性能,否则先用 LT 把逻辑写对。

int epfd = epoll_create1(0); struct epoll_event ev, events[64]; ev.events = EPOLLIN; ev.data.fd = listen_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev); for (;;) { int n = epoll_wait(epfd, events, 64, -1); for (int i = 0; i < n; i++) { int fd = events[i].data.fd; if (fd == listen_fd) { int conn = accept(listen_fd, NULL, NULL); if (conn < 0) continue; set_nonblocking(conn); ev.events = EPOLLIN | EPOLLET; ev.data.fd = conn; epoll_ctl(epfd, EPOLL_CTL_ADD, conn, &ev); } else { /* 读事件处理,ET 模式下要循环到 EAGAIN */ } } }

这段骨架里,set_nonblocking是必须的。ET 模式下如果用阻塞 fd,读到没有数据时就会卡死整个事件循环。另外连接关闭时的清理要用epoll_ctl(EPOLL_CTL_DEL)显式移除,虽然close会自动移除,但要考虑同一个 fd 号被复用的时序问题,先 DEL 再 close 更稳。

6. 上线前值得逐条过的几件事

代码跑通和能上线之间还隔着一层,我把这一层里跟 TCP API 直接相关的几条列一下。

6.1 SO_REUSEADDR 到底解决什么,和 SO_REUSEPORT 有什么区别

回到开头那个Address already in use。原因通常是上一条连接处于TIME_WAIT状态,主动关闭方会在这个状态停留 2MSL(Linux 上大约 60 秒),期间这个四元组不能复用。如果服务端是被动关闭方,一般不会遇到;但如果你在服务端里主动close了客户端连接(比如超时踢人),服务端就成了主动关闭方,重启时bind就会撞上。

SO_REUSEADDR的作用是允许bind到一个处于TIME_WAIT状态的本地地址,但它不允许多个进程同时监听同一个端口。很多人把它和SO_REUSEPORT搞混。SO_REUSEPORT才是真正的“多进程共享监听”,多个进程可以bind同一个端口,内核在它们之间做负载均衡。做多进程 worker 模型时,主进程创建监听 fd 后fork给子进程共享,是不需要SO_REUSEPORT的;让每个子进程自己bind同一端口,才需要它。两者的粒度不同,别混用。

顺带说一个常见的错误配置:有人为了“避免端口冲突”,在服务端和客户端都加SO_REUSEADDR,然后发现端口还是冲突。原因是他把两个不同的 socket 绑到了完全相同的地址和端口上——SO_REUSEADDR处理的是TIME_WAIT和通配地址(0.0.0.0与具体 IP)之间的冲突,不是让两个活跃 socket 共享端口。

6.2 TCP_NODELAY 与 Nagle:小包延迟从哪来

TCP 有个 Nagle 算法,会把多个小数据合并成一个大的段发出去,以提升带宽利用率。这个行为对大批量传输友好,对交互式场景就是灾难:你发一个小请求,内核说“等会儿,看还有没有更多数据”,等了几十毫秒才发出去。表现为请求-响应式的协议(比如数据库客户端、RPC)延迟莫名偏高。

设置setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one))可以关掉 Nagle。但也不是所有场景都该关——如果是持续的大流量传输,关掉反而会让网络里多出一堆小包。判断标准是:你的协议是不是请求-响应式的、单条消息是不是通常小于 MSS(约 1460 字节),是就关掉。

还有个容易忽略的组合问题:Nagle 和延迟 ACK 同时生效时,会出现互相等待的死锁,延迟被放大到 200ms 量级。这类问题在本地测不出来,只有真实网络才出现,排查的时候不要只盯着自己的代码,抓包看 TCP 层的时间间隔会快很多。

6.3 关闭连接:close 之前该做什么

close是个很粗暴的操作:它把 fd 的引用计数减一,减到 0 才真正关闭,同时如果还有未发送的数据,内核会尝试发完再发 FIN。但如果你只是想让对端知道“我不再发数据了”,应该用shutdown(fd, SHUT_WR),这会立刻发一个 FIN,同时本端仍然可以继续recv。HTTP 里的半关闭、一些文件传输协议都会用到这种模式。

还有个坑:多线程环境下,一个线程在recv阻塞,另一个线程直接close(fd),行为是未定义的,可能卡住也可能崩溃。正确做法是先用shutdown唤醒阻塞的recv(它会返回 0),等线程退出后再close。这个问题我在做服务优雅退出的时候踩过,重启时偶发挂住不退出,查了很久才发现是这个原因。

另外别忘了设置SO_LINGER的默认行为:默认情况下close立即返回,内核在后台把数据发完。如果进程紧接着退出,数据可能丢。对可靠性要求高的场景,要么等业务层确认,要么显式设置SO_LINGER让close阻塞到数据发完(l_onoff=1, l_linger=正数),但这会让关闭变慢,要权衡。

7. 我排查 TCP 连接类问题的固定动作

最后分享一套我自己的排查顺序,基本上从现象能快速定位到是哪一层的问题,比漫无目的地看代码高效得多。

先看连接到底建没建起来。ss -antp看状态分布,ESTABLISHED数量对不对,SYN_RECV堆积说明服务端在丢握手包,CLOSE_WAIT堆积说明应用层没有正确close,TIME_WAIT数量大说明主动关闭方在本地。这四个状态各自的成因完全不同,看到哪一个就往对应的方向想。

再看队列有没有溢出。ss -lnt里Recv-Q是全连接队列当前长度,Send-Q是 backlog 上限,Recv-Q逼近Send-Q就说明accept跟不上。这时候别急着调大 backlog,先看看accept循环里是不是干了什么耗时的事。

接着抓包。tcpdump -i any -nn port 9000 -w tcp.pcap抓下来用工具看时序,重点看三件事:SYN 有没有回 SYN+ACK、有没有 RST、有没有重传。RST 出现的位置特别有信息量——如果客户端刚连上就被 RST,多半是服务端accept队列满了或者程序崩了;如果传输中途出现 RST,通常是某一方在已关闭的连接上写了数据。

最后才看代码。把send和recv的返回值都打印出来,包括errno。我见过太多“数据没收到”的案例,最后发现是send返回了 -1 加EPIPE,而代码里压根没检查返回值。网络编程里,每一个系统调用的返回值都必须检查,这不是代码洁癖,是这个领域唯一可靠的调试手段。

写 TCP 程序写到最后,你会发现真正难的从来不是那几个 API 怎么调,而是对“字节流 + 缓冲区 + 半开半关”这套模型的理解是否到位。API 参数查文档就有,但什么时候该关 Nagle、什么时候该用shutdown、粘包该怎么定边界,这些得靠一次次踩坑攒出来的判断。我自己现在的习惯是:任何新的 TCP 服务,先把send_all、recv_some、定界解析这三块写成可复用的独立模块,然后写一个能自动重连、能发随机长度消息的压力小工具跑一轮,很多边界问题在这一步就暴露了,比上线之后再查要省太多事。

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

数据库考试高频考点与真题解析:三级模式、关系代数与SQL

简介&#xff1a;这份中南大学数据库试题资料面向高校计算机相关专业学生及数据库课程备考者&#xff0c;围绕数据库管理系统基础、数据模型、关系模型、SQL语言、数据库保护与设计理论等核心考点组织内容&#xff0c;可用于期末复习、考研自测与知识点查漏补缺。资源包共1个do…

作者头像 李华
网站建设 2026/10/27 3:05:41

事件相机与REFID框架:视频插帧与去模糊技术解析

1. 项目概述&#xff1a;事件相机驱动的视觉增强革命去年在苏黎世参加计算机视觉会议时&#xff0c;我第一次亲眼目睹了事件相机&#xff08;Event Camera&#xff09;拍摄的高速场景。与传统相机每秒30-60帧的固定采样不同&#xff0c;事件相机能以微秒级精度捕捉亮度变化&…

作者头像 李华
网站建设 2026/10/27 3:04:28

Rust 复刻星际迷航舰载电脑:语音 AI Agent 全链路实战

1. 从科幻到终端&#xff1a;这个项目到底在做什么第一次在社区里刷到这个项目的时候&#xff0c;我盯着标题看了好几秒——用 Rust 复刻《星际迷航》舰载电脑&#xff0c;还要做完全语音交互的 AI Agent。这个组合本身就很有意思&#xff1a;一边是几十年前科幻作品里对"…

作者头像 李华
网站建设 2026/10/27 3:04:22

VSCode配置C语言环境:工具链、三份配置与断点调试

1. 先把工具链的分工理清楚&#xff1a;编辑器不背编译的锅1.1 一次“代码没错但跑不起来”的现场复盘前段时间帮一个刚入门的同学看代码&#xff0c;他把一段不到三十行的 C 语言程序发过来&#xff0c;说“编辑器报了一堆错&#xff0c;是不是我语法写错了”。我打开一看&…

作者头像 李华
网站建设 2026/10/27 3:03:51

代驾行业数据安全架构设计与隐私保护实践

1. 代驾行业的数据安全现状与挑战代驾服务作为O2O领域的重要分支&#xff0c;每天处理着海量敏感用户数据。根据我过去三年为多个代驾平台提供安全咨询的经验&#xff0c;一个日均订单量5000单的中型平台&#xff0c;每天产生的用户隐私数据包括&#xff1a;约8000条精准定位记…

作者头像 李华
网站建设 2026/10/27 3:02:29

激光雷达点云去拖尾:强度信息与几何特征融合算法

1. 激光雷达去拖尾现象的本质与挑战激光雷达在动态场景扫描时&#xff0c;运动物体表面会形成点云"拖尾"现象。这种现象本质上源于激光雷达的扫描机制与物体运动的叠加效应。以常见的机械旋转式激光雷达为例&#xff0c;单个激光器在旋转过程中以固定频率发射激光脉冲…

作者头像 李华