news 2026/10/6 11:16:00

Linux Socket编程实战:从TCP基础到多客户端与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux Socket编程实战:从TCP基础到多客户端与避坑指南

简介:这是一份面向初、中级开发者的 Linux Socket 编程学习笔记,核心讲解网络进程通信、Socket 接口原理及 TCP 连接管理,适合正在学习网络编程、准备相关面试或希望补齐套接字 API 使用细节的读者。资源仅 1 个 docx 文档,压缩包约 77KB,内容集中,便于按章节查阅。已有 241 人学习利用该笔记。文中依次梳理了进程间通信标识方式、socket()、bind()、listen()、connect()、accept()、read()/write()、close() 等关键函数的作用与调用关系,并对 TCP 三次握手建立连接、四次握手释放连接的过程进行了分步解读;同时提供了一个可运行的服务端/客户端通信示例,帮助读者串联完整编程流程。结尾留下的讨论问题也可用于检验理解深度,整体兼具原理讲解、代码实践与思考引导。

1. Linux Socket编程:从“能通”到“能用”的必经之路

写网络服务的人,迟早要跟 Socket 打交道。不管是嵌入式设备上报数据、网关转发消息,还是写一个简单的聊天室,底层绕不开 Linux 的 socket 接口。很多新手照着博客敲一遍 TCP 回显代码,发现“能跑”,但换到真实场景——多客户端接入、对端异常断开、端口被占用——马上就翻车。Socket 编程的难点不在那十几个 API 的名字,而在理解内核帮你做了什么、没帮你做什么。这篇文章就把 socket 网络编程从原理到实例拆开讲,配一份可直接编译运行的 TCP 回显服务器和客户端代码,再往后讲多客户端处理、超时控制、缓冲区这些真实项目里必须面对的参数细节。适合刚入门 Linux 网络编程、或者写了几版 demo 但总在边界情况上踩坑的开发者。

2. 先建立直觉:Socket 编程的核心模型与 TCP/UDP 选型

2.1 一个 socket 描述符从创建到关闭,内核在背后做了什么

Socket 在 Linux 里本质是一个文件描述符。创建 socket 后,你拿到一个 int,对它调用 read/write 就能收发数据,这一点常常让新手困惑:我明明没打开任何文件,怎么就有个 fd?实际上 socket 是内核网络协议栈暴露给用户态的接口,fd 这个整数的背后是一套内核数据结构:套接字对象、发送缓冲区、接收缓冲区、等待队列,以及关联的四元组信息(本地 IP、本地端口、远端 IP、远端端口)。

调用 socket(AF_INET, SOCK_STREAM, 0) 时,内核只是创建了一套协议控制块,这时候它还“没接电话线”。真正建立连接的是 connect(客户端)和 accept(服务器)这两个动作。很多人以为 accept 完成了 TCP 三次握手,其实不是。Linux 内核在收到 SYN 后会自动完成三次握手,把完成连接的 socket 放进一个队列,accept 只是从队列里取一个“已经握手完成”的连接交给你。所以服务器端即使不调用 accept,客户端也可能已经 connect 成功,只是没人接待它。

看一段最简单的服务端骨架,能帮你把整个生命周期串起来:

int lfd = socket(AF_INET, SOCK_STREAM, 0); // 创建监听套接字 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡 addr.sin_port = htons(9000); // 端口号转网络字节序 bind(lfd, (struct sockaddr *)&addr, sizeof(addr)); listen(lfd, 8); // 未 accept 的连接队列上限 int cfd = accept(lfd, NULL, NULL); // 从完成队列取一个连接

这段代码里有两个值得注意的地方。第一,sockaddr_in 是 IPv4 专用结构体,bind/connect/accept 这些函数统一接收 sockaddr 指针,所以代码里要强转。第二,htons/htonl 做的是字节序转换,x86 机器是小端,网络字节序是大端,端口和 IP 必须转,否则端口号对不上号。我见过有人手写一个不转换的版本,本机测试 curl 能通,跨机器永远连不上,查了半天网卡配置。

2.2 TCP 与 UDP 选型:选错协议,后面全是补丁

协议选型是 socket 编程里最容易被轻视的决策。TCP 是字节流,UDP 是报文。这四个字决定了你写代码的方式完全不一样。TCP 保证数据不丢、不乱序、不重复,但你调用 read 读回来的字节数,和你对端调用 write 写下去的字节数没有一毛钱关系。你 write 了 1000 字节,对端 read 可能一次返回 300 字节、一次返回 700 字节,也可能一次性 1000 字节,这取决于内核缓冲区、MTU 和调度。这就是常说的“粘包/半包问题”,后面用代码细讲。

UDP 则简单粗暴:sendto 一个报文,对端 recvfrom 拿到的就是一个完整的报文,边界由内核帮你守着。但 UDP 不保证到达、不保证顺序,丢了就是丢了。选 TCP 还是 UDP,常见的判断标准是看你对数据完整性和实时性的容忍度:文件传输、远程命令、数据库协议,无脑选 TCP;音视频通话、游戏位置同步、局域网设备发现,优先考虑 UDP 或 QUIC。

维度TCP (SOCK_STREAM)UDP (SOCK_DGRAM)
数据边界无边界,字节流有边界,报文
可靠性可靠,丢包重传不可靠,丢了不管
连接状态有连接,需要 accept/connect无连接,直接 sendto/recvfrom
内核开销高(维护状态机、重传、拥塞控制)低
典型场景HTTP、数据库、远程 ShellDNS、音视频、设备发现

有一点值得多说:UDP 的“无连接”不意味着 socket 不能 connect。UDP 客户端可以调用 connect 绑定对端地址,之后就能用 read/write 替代 recvfrom/sendto,内核还会帮你过滤掉来自其他地址的包。这个技巧在做请求-响应模型时挺实用,省得每次传地址参数。当然,connect 之后 UDP 依然不保证可靠,该丢还是丢。

2.3 阻塞与非阻塞:你以为程序卡死了,其实它在等数据

Linux 下默认创建的 socket 是阻塞模式。阻塞的意思是:调用 read 时,如果接收缓冲区没有数据,进程就睡在那里,直到有数据可读才返回。这个“睡眠”对单线程程序来说就是卡死。所以初学者写一个阻塞模式的服务器,只能一次服务一个客户端——处理完第一个连接的所有数据之前,accept 不会被第二次调用。

非阻塞模式不是让操作立即返回结果,而是让操作在没有数据时返回一个错误码。设置方式有 fcntl 和设置 O_NONBLOCK 标志,或者用 epoll 的 EPOLLET 边缘触发。非阻塞模式下 read 返回 -1 且 errno 是 EAGAIN/EWOULDBLOCK 时,表示“现在没数据,你等会再来”。这里是最容易写错的地方:很多新手把 EAGAIN 当错误处理,直接退出循环,导致数据没读完。正确的做法是把 EAGAIN 当作“缓冲区空了”的提示,继续等待下一次就绪通知。

在实际项目里,连接建立用阻塞模式配合超时,数据收发用非阻塞模式配合 epoll,是最常见的组合。超时用 setsockopt 的 SO_RCVTIMEO 和 SO_SNDTIMEO 控制,或者用 poll/select 自己维护超时逻辑。后面实例部分,先写一个最简单的阻塞版本,把流程走通,再谈升级。

3. 最小可复现实例:写一个 TCP 回显服务器和客户端

3.1 服务端:从 socket() 到 accept() 的完整生命周期

回显服务器(echo server)是 socket 编程里的 Hello World:客户端发什么,服务端原样回什么。别小看这个例子,它覆盖了 socket 编程的完整生命周期:创建、绑定、监听、接受连接、循环收发、关闭。绝大多数网络服务的骨架都是这套流程。

// echo_server.c - 单连接TCP回显服务器 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define PORT 9000 int main() { int lfd, cfd; struct sockaddr_in srv_addr; char buf[1024]; ssize_t n; // 1. 创建IPv4 TCP套接字 lfd = socket(AF_INET, SOCK_STREAM, 0); if (lfd < 0) { perror("socket"); exit(1); } // 2. 绑定IP和端口,IP用INADDR_ANY表示接受任意网卡连接 memset(&srv_addr, 0, sizeof(srv_addr)); srv_addr.sin_family = AF_INET; srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); srv_addr.sin_port = htons(PORT); if (bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0) { perror("bind"); close(lfd); exit(1); } // 3. 进入监听状态,backlog=8含义见下方说明 if (listen(lfd, 8) < 0) { perror("listen"); close(lfd); exit(1); } printf("listening on 0.0.0.0:%d ...\n", PORT); // 4. 接收一个客户端连接(阻塞在这里) cfd = accept(lfd, NULL, NULL); if (cfd < 0) { perror("accept"); close(lfd); exit(1); } printf("client connected\n"); // 5. 循环读取客户端数据,原样写回 while ((n = read(cfd, buf, sizeof(buf))) > 0) { // 注意:write的返回值要检查,短写是可能发生的 if (write(cfd, buf, n) != n) { perror("write"); break; } } if (n < 0) perror("read"); // 6. 关闭连接和监听套接字 close(cfd); close(lfd); return 0; }

这段代码的逻辑说明:socket 函数三个参数分别指定协议族、套接字类型和协议号,第三个参数传 0 表示让内核根据前两个参数选取默认协议。bind 把 IP 和端口绑定到套接字上,之后 listen 把这个套接字变成监听套接字。backlog 参数是“已完成三次握手、等待 accept”的连接队列长度,不是最大连接数。在高并发场景,backlog 太小会导致客户端 connect 成功但服务端 accept 不过来,表现为连接建立慢或失败;设太大也没用,内核还受 somaxconn 系统参数限制。

循环里 read 返回 0 表示对端关闭了连接,返回 -1 表示出错。read 一次最多读 sizeof(buf) 即 1024 字节,对端发了 5000 字节的话,这个循环会分多次读出来,每次读到的都不是完整业务消息,这就是字节流的特性。

3.2 客户端:连接、收发与优雅关闭的边界条件

客户端代码相对简单,但同样有讲究。connect 之前要手动设置好服务器地址结构体,这是和服务端代码唯一对称但写法不同的地方。

// echo_client.c - TCP回显客户端 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define PORT 9000 int main(int argc, char *argv[]) { int fd; struct sockaddr_in srv_addr; char buf[1024]; ssize_t n; const char *msg = (argc > 1) ? argv[1] : "hello socket"; // 1. 创建socket,参数与服务端一致 fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { perror("socket"); exit(1); } // 2. 配置服务器地址:这里换成目标机器IP memset(&srv_addr, 0, sizeof(srv_addr)); srv_addr.sin_family = AF_INET; srv_addr.sin_port = htons(PORT); if (inet_pton(AF_INET, "127.0.0.1", &srv_addr.sin_addr) <= 0) { perror("inet_pton"); close(fd); exit(1); } // 3. 发起连接:这里触发三次握手 if (connect(fd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0) { perror("connect"); close(fd); exit(1); } // 4. 发送消息 if (write(fd, msg, strlen(msg)) != (ssize_t)strlen(msg)) { perror("write"); close(fd); exit(1); } // 5. 读取回显:一次read不一定读完,这里简化只读一次 n = read(fd, buf, sizeof(buf)); if (n < 0) { perror("read"); close(fd); exit(1); } if (n > 0) write(STDOUT_FILENO, buf, n); // 6. 关闭连接 close(fd); return 0; }

这里的逻辑说明集中在两个地方。第一,inet_pton 把点分十进制的 IP 字符串转换成二进制网络字节序,替代了早期代码常用的 inet_addr——后者没法区分错误和 255.255.255.255 这个特殊地址,建议别用。第二,客户端 read 只调用了一次,如果服务器回显的数据超过 1024 字节,这里只能读到一部分。严格的做法是循环 read,直到返回 0 或达到预期字节数。在这个例子里因为服务端收到的就是客户端发的一条短消息,一次 read 通常能拿全,但作为工程习惯,这个“只读一次”的写法是隐患,后面避坑章节细说。

编译方法没有什么特殊的,两个文件分别 gcc 编译即可。建议加上 -Wall 开警告,不写 -g 调试时回头找不到行号。

gcc -Wall -g -o echo_server echo_server.c gcc -Wall -g -o echo_client echo_client.c ./echo_server & ./echo_client "hello socket"

3.3 冒烟测试:用 nc 和 ss 验证程序不是在“假跑”

写完代码先别急着写业务逻辑,用系统自带工具做一轮冒烟测试,能确认你的程序行为符合预期。nc(netcat)是 socket 调试最常用的工具,ss 用来查看端口监听状态。

# 查看端口是否在监听,ss 是 netstat 的现代替代 ss -lntp | grep 9000 # 用 nc 作为客户端发送数据,观察回显 printf 'test from nc\n' | nc 127.0.0.1 9000 # 观察 TCP 连接状态,ESTAB 表示已建立 ss -tnp | grep 9000

第一次跑服务端时,很可能遇到 bind 失败,提示 Address already in use。这不是程序写错了,而是上一个服务端实例关闭后,端口进入了 TIME_WAIT 状态,内核默认不允许立即绑定同一个端口。这时候可以等 60 秒,或者给套接字设置 SO_REUSEADDR 选项规避。这个坑太常见了,避坑章节专门展开。

4. 多客户端接入:从单连接到 select/poll 与线程模型

4.1 多线程方案:一个连接一个线程,简单但别开太多

回显服务器只能服务一个客户端,这在真实项目里基本不可用。最简单的升级是每来一个连接就开一个线程去处理,主线程继续 accept。这个模型代码好写,逻辑直观,适合连接数少、每个连接处理时间长的场景。

// 每个客户端连接用独立线程处理 void *handle_client(void *arg) { int cfd = *(int *)arg; free(arg); char buf[1024]; ssize_t n; while ((n = read(cfd, buf, sizeof(buf))) > 0) { write(cfd, buf, n); } close(cfd); return NULL; } // accept 循环里,把 cfd 传给 pthread_create int *conn_fd = malloc(sizeof(int)); *conn_fd = cfd; pthread_t tid; pthread_create(&tid, NULL, handle_client, conn_fd); pthread_detach(tid); // 分离线程,不用 join

一个连接一个线程的代价是每个线程默认栈空间 8MB,1000 个连接就 8GB 虚拟内存。更严重的是,线程切换和锁竞争在高并发下会吞掉性能。所以这种模型的适用边界很清楚:连接数几十到几百,能接受每连接固定开销。连接数上千、长连接居多、单连接流量不大,就该换事件驱动了。

pthread_create 之前那个 malloc 是刻意为之:如果直接传 &cfd,accept 下一次调用就会覆盖这个变量的值,新线程拿到的可能是别人的连接。这个 bug 用“玄学”两个字形容不过分,因为它时好时坏——取决于新线程是否抢在下一轮 accept 之前取到了值。

4.2 select 模型:内核帮你盯着 fd,一次处理多路 IO

select 是 Linux 历史上最经典的多路 IO 模型。你把自己关心的文件描述符放进一个集合,select 阻塞等待,直到至少一个 fd 可读可写,你再逐个检查哪些 fd 就绪了。

// select版多客户端回显 - 只展示核心逻辑 fd_set readfds; int maxfd = lfd; int client_fds[FD_SETSIZE]; int clients[FD_SETSIZE]; // 用数组记录已连接的fd while (1) { FD_ZERO(&readfds); FD_SET(lfd, &readfds); maxfd = lfd; for (int i = 0; i < FD_SETSIZE; i++) { if (client_fds[i] > 0) { FD_SET(client_fds[i], &readfds); if (client_fds[i] > maxfd) maxfd = client_fds[i]; } } int ready = select(maxfd + 1, &readfds, NULL, NULL, NULL); if (ready < 0) { perror("select"); break; } if (FD_ISSET(lfd, &readfds)) { // 有新连接,accept 后存入 client_fds 数组 } for (int i = 0; i < FD_SETSIZE; i++) { if (client_fds[i] > 0 && FD_ISSET(client_fds[i], &readfds)) { // 读数据,处理业务 } } }

select 有三个短板,面试常问,实战常踩。第一,fd_set 的大小有限制,默认 FD_SETSIZE 是 1024,超过这个数量 FD_SET 越界,不可靠。第二,select 每次调用都要把集合从用户态拷贝到内核态,几百个 fd 之后开销明显。第三,select 返回后你不知道哪些 fd 就绪,要自己遍历所有 fd 逐个 FD_ISSET,复杂度 O(n)。这三个短板导致它在业界基本被 epoll 替代,但 select 可移植性好,Windows、BSD、Linux 都有,写跨平台代码时仍有价值。

poll 解决了 fd 数量上限的问题,但没解决遍历效率和拷贝开销问题。epoll 则是 Linux 专属的终极方案:内核帮你维护就绪列表,你只处理真正有事件的那些 fd。写成熟的网络库,譬如 libevent、libuv、Netty 的底层,用的就是这类机制。如果在生产环境做长连接服务,建议直接学 epoll,select 作为理解模型用就好。判断依据很简单:连接数几十个,select/poll 足够;连接数上千、要求低延迟,直接 epoll;需要跨平台,用封装好的事件库。

5. Socket 编程避坑指南:5 个让老手也翻车的细节

5.1 端口占用与 TIME_WAIT:bind 失败最常见的元凶

现象:服务端重启时,bind 返回 EADDRINUSE,提示端口已被占用。用 ss 查看,端口状态是 TIME_WAIT。原因:TCP 四次挥手中主动关闭方要等待 2MSL(默认 60 秒)再彻底关闭连接,防止最后一个 ACK 丢失。服务端程序重启时旧连接还处于 TIME_WAIT,内核不允许新 socket 立即绑定。解决方法:在 socket 创建后、bind 之前设置 SO_REUSEADDR 选项。这个选项允许内核把 TIME_WAIT 状态下的端口重新绑定,前提是协议支持。

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

注意,SO_REUSEADDR 不是什么都管。两台机器同时监听同一个端口(比如负载均衡时的端口复用)需要用 SO_REUSEPORT,那是另一个选项,别混用。设置完 SO_REUSEADDR 后,服务端频繁重启不会再报端口占用,但如果程序崩溃前没来得及正常关闭,仍有极少数情况需要手动确认没有僵尸进程占着端口,用ss -lntp | grep 9000检查 PID。

5.2 SIGPIPE:对端关闭后 write 直接杀死你的进程

现象:客户端异常断开(拔网线、强制 kill),服务端继续向该连接 write 数据,进程莫名其妙退出,没有任何错误日志。原因:向已关闭的连接写数据时,内核发送 SIGPIPE 信号给当前进程。这个信号的默认动作是终止进程,而不是让你的 write 返回 EPIPE 错误码。服务端对每个连接都开着独立线程时,SIGPIPE 只杀死当前线程?不对,SIGPIPE 默认终止整个进程。解决方法分两层:第一层是忽略这个信号,让 write 正常返回错误码,由代码处理;第二层是发送时带上 MSG_NOSIGNAL 标志,更精准。

signal(SIGPIPE, SIG_IGN); // 忽略后write返回-1,errno=EPIPE // 或者直接用send替代write,并设置MSG_NOSIGNAL ssize_t ret = send(cfd, buf, n, MSG_NOSIGNAL); if (ret < 0) { // 对端已关闭,关闭fd并从连接表中移除 }

这个坑的危险之处在于进程死得无声无息。写服务端代码时,我一般第一行就忽略 SIGPIPE,这是多年 Socket 编程血泪换来的默认动作。

5.3 read 一次读不完一条消息:TCP 是流不是包

现象:客户端一条消息 2000 字节,服务端一次 read 只取到 1024 字节,剩余数据还躺在内核缓冲区里。如果服务端按“一条消息”来解析,就会解析出半条错误数据。原因:TCP 不维护消息边界。write 2000 字节,在网络层可能被切成两个或多个 TCP 段,接收端内核把数据按序放进缓冲区,read 只能读到当前缓冲区里已有的一部分。这就是网络编程里的半包问题。

解决思路有三种。定长协议最简单:每条消息固定 4 字节、16 字节、或其他长度,不足就是没读完,继续读。分隔符协议:按 \n 或自定义特殊字符切分,适合文本协议。长度前缀协议最通用:消息头固定几字节,其中包含整个消息体长度,先读头得到长度,再循环读体直到读满。

// 读取完整消息的循环写法 ssize_t read_full(int fd, char *buf, size_t len) { size_t got = 0; while (got < len) { ssize_t n = read(fd, buf + got, len - got); if (n == 0) return got; // 对端关闭 if (n < 0) { if (errno == EINTR) continue; // 被信号打断 return -1; } got += n; } return got; }

这段循环是所有 TCP 业务的基础函数,值得背下来。EINTR 单独处理是因为信号打断 read 时,数据没读到也不算错误,继续读就行。很多初学者在这上面纠结,其实是没理解 EINTR 的真实含义。

5.4 非阻塞 connect:返回 -1 不代表连接失败

现象:socket 设置为非阻塞后,调用 connect 返回 -1,errno 是 EINPROGRESS。新手直接报错退出,但连接实际正在后台建立。原因:非阻塞模式下 connect 不能立即完成三次握手,内核返回 EINPROGRESS 表示“操作还在进行”。解决方法:用 select/poll/epoll 等该 socket 的 EPOLLOUT 事件,或者等待 SO_ERROR 变为 0。一旦可写,再检查 getsockopt 的 SO_ERROR 是否为 0,为 0 表示连接成功。

int err = 0; socklen_t len = sizeof(err); getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len); if (err == 0) { // 连接建立成功 }

同理,非阻塞 accept 的 EAGAIN 表示“当前没有已完成的连接”,不能当错误处理。非阻塞模式下,几乎每个 API 的返回码都有两层含义:要么成功,要么“等下次通知再试”。

5.5 服务器 accept 后忘记关闭旧连接,fd 泄漏到进程被杀

现象:服务器运行几天后,用 ss 看连接数暴涨,但实际在线用户没那么多,程序响应变慢甚至卡死。原因:处理完一个连接后忘记 close,或者把 close 放在提前 return 的分支里没执行到。Linux 的 fd 是有限资源,默认上限可以查 ulimit -n,泄漏完了新 socket 就创建不出来,表现为 socket 返回 -1 且 errno 是 EMFILE。解决方法:代码审查时重点检查每个 accept 出的 fd 是否在路径末段被 close,顺手在开发阶段用脚本统计 fd 数:

ls /proc/$(pgrep echo_server)/fd | wc -l

fd 泄漏不像内存泄漏那么好查,因为它不报错,只是悄悄把资源耗尽。我曾经遇到过一个服务,每个客户端连接关闭后服务端多出两个 fd,排查了两天才找到是一个分支里返回前漏了 close。后来养成的习惯是,accept 后立刻把 cfd 和相关释放逻辑写在注释里,所有退出路径都检查一遍。

6. 让 Socket 程序更稳的三个小技巧:超时、缓冲与优雅关闭

最后这三个技巧不属于基础 API,但真实项目里几乎必用,宁可提前写进代码,也不要在线上故障后再补。

第一个是超时控制。阻塞 socket 默认没有超时,read 会无限期等下去。用 setsockopt 控制收发超时是最省事的方式,适合请求-响应型协议:

struct timeval tv = { .tv_sec = 5, .tv_usec = 0 }; setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

超时后 read 返回 -1,errno 是 EAGAIN/EWOULDBLOCK。注意不要和“无数据可读”混淆——两者 errno 相同,所以超时场景下你要做的是关闭连接或者重新进入等待,而不是重试读。

第二个是 TCP_NODELAY。默认 TCP 启用 Nagle 算法,小数据会合并成大包发送,交互式程序会感到明显延迟。set TCP_NODELAY 为 1 可禁用该算法,每次 write 都会立即发送。代价是网络包数量增加,适合小消息、低延迟场景,不适合大流量传输。

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

第三个是优雅关闭。close 一个 fd 时,内核会立刻丢弃发送缓冲区里未发送的数据,然后发送 FIN。如果你还有数据要发给对方,close 会造成数据丢失。正确的顺序是调用 shutdown(fd, SHUT_WR) 发送 FIN 表示“我发完了”,等对端读完数据后关闭连接,再 close。shutdown 和 close 的区别经常被忽略:shutdown 只关读写方向,不释放 fd;close 是释放 fd,但不保证数据发完。真实场景里,服务端要回完最后一个响应再关闭,就应该先 shutdown 再 close。

做 Socket 编程最深的体会是:大部分问题不在 API 而在于边界。你有没有处理半包?对端掉线怎么办?缓冲区满了要不要阻塞?这些细节决定了程序能不能在线跑一年。我自己早期写网络服务,被 SIGPIPE 坑过、被 TIME_WAIT 卡过、被 fd 泄漏折磨过,后来把所有教训沉淀成一套默认起始代码:忽略 SIGPIPE、开 SO_REUSEADDR、读循环封装、所有出口检查 fd。这套习惯帮我省了太多排查时间,希望帮到你。

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

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

FPGA作为Root Complex:AXI转PCIe核的实战调试与枚举指南

做FPGA的兄弟&#xff0c;应该都有过类似的经历&#xff1a;看着一本几百页的IP手册&#xff0c;照着官方例程把工程跑起来&#xff0c;心里挺美&#xff0c;结果一到自己的板子上、自己的场景里&#xff0c;各种链路不up、地址不通、中断不触发的问题就全冒出来了。尤其是 PCI…

作者头像 李华
网站建设 2026/10/6 11:14:55

COZE平台AI应用开发实战:从工作流编排到API集成全指南

简介&#xff1a;《COZE 从入门到精通实战指南》是一份面向AI应用开发入门者与业务人员的系统教程&#xff0c;围绕低代码开发、自然语言处理与API集成三大方向&#xff0c;帮助读者快速掌握基于大模型的对话机器人、自动化工作流和数据分析助手搭建方法。内容从账号注册、创建…

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

Unity Shader动态屏幕遮罩:后处理UV变换与实测避坑

简介&#xff1a;该资源为Unity3D开发者提供一套完整的动态屏幕遮罩Shader实现方案&#xff0c;适用于需要实现跟随目标物体的可视范围遮罩、边缘渐变、遮罩颜色自定义等效果的游戏或交互项目。压缩包仅含1个PDF文档&#xff0c;大小约66KB&#xff0c;文档内完整展示了Shader源…

作者头像 李华
网站建设 2026/10/6 11:11:20

AI Agent 从 Demo 到生产:并发状态、工具调用与可观测性工程实践

1. 半年卡壳的真相&#xff1a;AI Agent 从 Demo 到生产之间隔着什么 去年秋天我接了一个内部工具项目&#xff0c;目标很明确&#xff1a;做一个能自动处理工单的 AI Agent&#xff0c;用户提交问题后&#xff0c;Agent 自主判断类型、检索知识库、调用内部 API 完成操作、最后…

作者头像 李华
网站建设 2026/10/6 11:11:19

Agent、LLM、RAG、GraphRAG、MCP:大模型落地工程化实战与避坑指南

1. 从一份日报标题里&#xff0c;我看到了 Agent 与 LLM 生态的真实切面拿到“Agent / LLM 技术精选日报”这个标题的时候&#xff0c;我第一反应不是“又一份资讯汇总”&#xff0c;而是它背后那串热搜词暴露出来的东西——Agent、LLM、RAG、GraphRAG、MCP 这五个词几乎把当下…

作者头像 李华
网站建设 2026/10/6 11:11:18

Claude Opus 5.5 智能体编程工作流:CLAUDE.md 与 Sub-agent 实战指南

1. 从“焚诀”说起&#xff1a;这个标题到底在讲什么第一次看到“焚诀”这个词&#xff0c;我愣了两秒。这不是什么玄幻小说里的功法&#xff0c;而是圈内人对 Claude 系列模型一次重大版本更新的戏称——意思是“烧掉旧套路、重写新规则”的那种大版本迭代。标题里挂着“Claud…

作者头像 李华