Linux C语言TCP socket编程:服务端与客户端通信全流程详解
搞Linux下C/C网络编程的人迟早都会碰到这个经典需求服务端程序监听某个端口客户端连上来后双方互相收发数据。这个场景抽象成一句话就是“在Linux上用TCP协议实现客户端/服务端网络通信”但真正动手时会发现里面藏着socket编程最核心的一整套API调用流程。从创建套接字到bind、listen、accept再到connect、send、recv每一步背后都有对应的内核行为和状态变化把这套流程吃透了后面再去看多线程并发、epoll、异步网络库都会轻松很多。这篇文章我按自己实际撸代码的顺序来写会从最关键的基础概念讲起然后分别给出完整的服务端和客户端C语言实现附上编译命令、运行方法和联调测试全过程最后专门整理我在真实环境中反复踩过的坑。无论你是刚接触Linux网络编程的新手还是想快速回顾核心API的开发者照着这个流程走一遍至少能把“两端怎么连起来、数据怎么传过去”这件事彻底弄明白。1. 项目概述与前置知识盘点1.1 这个项目要解决什么问题很多初学者学网络编程时喜欢直接背代码背完能跑就觉得会了。但一旦环境变了、端口冲突了、客户端连不上了就完全不知道从哪里排查。这个项目存在的意义就是通过一个最小可用的完整demo把TCP通信最核心的调用链彻底讲清楚让读者不只是会敲代码还能理解每一步为什么这么写。本项目的核心目标是实现一个回显服务客户端在终端输入什么内容服务端收到后原样返回。虽然功能简单但它覆盖了TCP编程中绝大部分关键环节套接字创建、地址绑定、监听、接受连接、数据收发、连接关闭判断、资源释放。也就是说你把这个demo里的每个函数、每个返回值都搞懂就已经掌握了几乎所有同步TCP网络程序的基本骨架。1.2 TCP协议通信核心概念速览TCPTransmission Control Protocol是面向连接的、可靠的、基于字节流的传输层协议。“面向连接”意味着通信双方在正式收发数据之前必须先通过三次握手建立一条逻辑链路“可靠”意味着数据不丢失、不重复、按序到达“字节流”意味着它不关心消息边界只把数据看作一串连续的字节应用层在解析时自己负责切分消息。用生活化的类比来理解TCP就像打电话。先拨号connect对方接听服务端正常accept双方确认接通后开始说话send/recv最后任何一方挂断close链路断开。而UDP更像是发短信写完地址直接扔到邮筒里对方能不能收到、什么时候收到发件人并不了解。对于初学者我建议先把几个状态与API的对应关系建立起来未连接到已连接靠connect配合内核完成三次握手数据传输阶段靠send和recv收发字节流断开连接靠close发送FIN触发四次挥手。这些状态在操作系统的网络栈中都有对应数据结构初学阶段不用深入内核细节但用户态的API行为一定要对上号。1.3 开发环境与工具准备推荐环境是任意主流的Linux发行版本文示例在 Ubuntu 22.04 和 CentOS 7 上都能直接编译运行。核心工具只需要gcc和make没有的话一条命令装上即可# Debian/Ubuntu 系列 sudo apt update sudo apt install -y gcc make # RHEL/CentOS 系列 sudo yum install -y gcc make如果没有Linux物理机用虚拟机或者云主机都行。为了测试跨主机通信最好准备两台机器如果只有一台机器在同一台机器上开两个终端通过127.0.0.1环回地址测试也完全可以。环回地址在本地测试时基本模拟了真实网络通信的完整过程只是数据在内核内部完成转发没有真正经过网卡和物理链路对学习API流程没有任何影响。2. 核心API与设计思路拆解2.1 socket编程的五个核心函数一次讲透socket编程的服务端和客户端本质上用的是同一套API家族只是调用顺序不同。先把这张函数调用地图刻进脑子里后面理解任何代码都不会跑偏服务端调用链socket() - bind() - listen() - accept() - recv()/send() - close()客户端调用链socket() - connect() - send()/recv() - close()每个函数的具体职责我用一个表来总结方便对照记忆函数服务端客户端核心作用常见失败原因socket必调必调创建套接字描述符文件描述符耗尽bind必调非必须绑定本地地址与端口端口被占用listen必调不调进入监听状态内核维护连接队列未先绑定地址accept必调不调从已完成握手队列取出连接无新连接到达connect不调必调主动发起三次握手目标端口拒绝连接send/recv必调必调在已建立连接上收发数据对端已关闭socket(int domain, int type, int protocol)是最底层的入口负责创建一个套接字描述符。它可以类比为申请一条电话线参数分别是协议族IPv4用AF_INET、套接字类型流式TCP用SOCK_STREAM和具体协议TCP直接填0让内核根据前两个参数自动推断。bind是把地址和端口绑定到套接字上服务端必须调用否则客户端不知道去访问哪个端口客户端一般不显式调用由内核在connect时自动分配临时端口。listen宣告套接字进入监听状态内核会在该端口上维护连接队列backlog参数表示已完成连接队列的最大长度。accept从队列中取出一个已完成握手的连接返回一个全新的已连接套接字描述符后续收发数据都走这个新描述符原来的监听描述符仍继续接受新连接。connect在客户端发起三次握手调用成功就代表逻辑链路已建立。2.2 地址结构体与字节序转换容易忽略但必须吃透socket接口接收的地址参数类型是struct sockaddr *实际使用时绝大多数情况是struct sockaddr_inIPv4专用。这个结构体里有三个关键字段需要留意sin_family协议族固定填AF_INETsin_addr.s_addr存放32位IP地址sin_port存放16位端口号。后两个字段在网络编程中有一个容易踩坑的细节它们必须使用网络字节序。网络字节序是大端模式而x86等主流CPU在小端模式下工作内存中整数的高低字节存放顺序正好相反。所以给sin_port赋值时会写htons(SERVER_PORT)htons就是“Host TO Network Short”把主机字节序的端口号转换到网络字节序。同理取出端口时用ntohs转回主机字节序。IP地址字段长度是32位用htonl/ntohl处理例如htonl(INADDR_ANY)表示监听本机所有网卡的IP地址000.0.0.0。还有一个常见的坑很多人在客户端填了127.0.0.1connect仍然失败排查半天发现是inet_pton没有正确转换IP。推荐统一使用inet_pton(AF_INET, ip字符串, server_addr.sin_addr)不要在旧代码里使用已经废弃的inet_addr后者没有错误判断能力。inet_pton的返回值大于0表示转换成功等于0表示IP字符串格式不合法小于0表示协议族不支持这三个结果都要显式处理。2.3 为什么服务端必须bindlistenaccept三步走服务端的三个函数初学者最容易混淆。我建议继续用“电话接听”的场景来理解bind相当于把电话号码登记在册让别人找得到你listen相当于摘机待命内核开始记录打进来的电话accept相当于接通一个来电并拿起听筒接通后你手里就多了一个独立的通话通道。从内核角度看一个端口上的监听套接字维护着半连接队列和全连接队列。客户端发送SYN后进入半连接队列内核完成三次握手后把连接移入全连接队列而accept只是从全连接队列里取出一个已完成握手的连接并分配新的描述符。也就是说accept不会主动去和客户端握手握手在客户端connect配合内核协议栈时已经完成accept只是“取货”。这也解释了一个令很多人困惑的问题为什么accept的后两个参数不填目标端口和IP因为建立连接所需的地址信息在握手阶段已经协商完成accept只是把结果通过参数回填出来方便服务端打印客户端来源或者做访问控制。回填的信息包括客户端IP、客户端临时端口这些正是排查问题时的关键线索。3. 服务端程序完整实现3.1 创建套接字socket函数的三个参数怎么填先给出这一步的完整代码文件保存为server.c#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/socket.h #include sys/types.h #include netinet/in.h #include arpa/inet.h #define SERVER_PORT 8888 #define BUFFER_SIZE 1024 int main(void) { int listen_fd, conn_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len sizeof(client_addr); char buffer[BUFFER_SIZE]; ssize_t n; int opt 1; // 1. 创建TCP套接字 listen_fd socket(AF_INET, SOCK_STREAM, 0); if (listen_fd 0) { perror(socket); exit(EXIT_FAILURE); } // 2. 允许端口复用避免TIME_WAIT状态下重启失败 if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)) 0) { perror(setsockopt); close(listen_fd); exit(EXIT_FAILURE); } // 3. 初始化服务器地址结构 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(SERVER_PORT); // 4. 绑定地址与端口 if (bind(listen_fd, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { perror(bind); close(listen_fd); exit(EXIT_FAILURE); } // 5. 进入监听状态backlog为已完成连接队列长度 if (listen(listen_fd, 5) 0) { perror(listen); close(listen_fd); exit(EXIT_FAILURE); } printf(服务端已启动监听端口: %d\n, SERVER_PORT); // 6. 接受客户端连接 conn_fd accept(listen_fd, (struct sockaddr *)client_addr, client_len); if (conn_fd 0) { perror(accept); close(listen_fd); exit(EXIT_FAILURE); } printf(客户端接入: %s:%d\n, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 7. 循环收发数据实现一个最简单的回显服务 while (1) { memset(buffer, 0, sizeof(buffer)); n recv(conn_fd, buffer, sizeof(buffer) - 1, 0); if (n 0) { printf(客户端断开连接\n); break; } printf(客户端说: %s, buffer); if (strncmp(buffer, quit, 4) 0) { printf(收到退出指令关闭连接\n); break; } if (send(conn_fd, buffer, n, 0) 0) { perror(send); break; } } close(conn_fd); close(listen_fd); return 0; }socket函数本身很简单但开发时要搞清楚三个问题。第一个参数为什么是AF_INET而不是AF_INET6AF_INET对应IPv4AF_INET6对应IPv6两者用到的地址结构体、地址长度都不一样。本文按IPv4展开实际项目中还要考虑双栈支持。第二个参数SOCK_STREAM表示流式套接字TCP协议对应的就是它SOCK_DGRAM对应UDPSOCK_RAW是裸网络层套接字普通应用碰不到。第三个协议参数直接传0内核会推断为TCP这个写法最稳妥。返回值是一个非负整数描述符注意它和文件描述符共用同一套资源池理解这点对排查“描述符耗尽”问题很有帮助。3.2 绑定端口bind函数与SO_REUSEADDR的前置处理服务端必须先绑定地址再监听因为内核需要知道把发往哪个端口的数据分发给哪个套接字。这一步代码的核心就是初始化struct sockaddr_in并调用bind但有三处细节我特意强调。第一memset清零是必须的。struct sockaddr_in里除了重要字段外还有填充字节不清零可能导致bind行为不确定甚至在不同编译器下出现诡异差异。这段代码里我先memset整个结构体再把字段逐个赋值是最稳妥的顺序。第二INADDR_ANY表示监听本机所有网卡的连接请求。如果这里指定某个具体IP服务端就只接受发往该IP的请求其他网卡上的连接一律不响应。对内网穿透、多网卡服务器这些场景来说INADDR_ANY通常是最省心的选择。对应字段可以写成htonl(INADDR_ANY)也可以直接写成INADDR_ANY再让系统转换但显式htonl更清楚。第三SO_REUSEADDR就是那个“老手都知道”的选项。当服务端程序异常退出或者主动关闭连接后端口可能停留在TIME_WAIT状态立即重启会报Address already in use导致bind失败。设置SO_REUSEADDR允许内核在端口处于TIME_WAIT时直接复用地址端口。我在开发阶段遇到过好多次加了这行之后再也没为频繁重启发过愁。教学代码经常忽略它导致新手照着教程写完第二天启动就报错这里提前打上补丁。3.3 监听与接受连接listen和accept的分工逻辑listen这一步只需要两行代码但backlog参数的含义值得展开。backlog传5表示全连接队列最多容纳5个已完成握手的连接。如果队列满了后续connect会阻塞或失败由内核的SYN cookie机制决定具体行为。这个数字不是越大越好过大会让客户端长时间等待而服务端没及时accept反而掩盖了服务端处理能力不足的问题。生产环境会结合并发模型调优本文学习场景取5足够了。accept这一步同样只有几行但返回值的意义必须理解透。accept返回一个全新的描述符conn_fd之后所有针对这个客户端的数据收发都走conn_fd而listen_fd继续用于监听后续连接。也就是说一个套接字监听一个套接字通信职责完全不同。很多新手误以为accept返回的还是原来那个描述符于是往listen_fd上收发数据结果行为完全不符合预期。代码里我打印了inet_ntoa(client_addr.sin_addr)和ntohs(client_addr.sin_port)前者把32位IP转回点分十进制字符串后者把网络字节序端口转回主机字节序。这行日志不是可有可无的装饰联调时能立刻确认客户端是否真的接入以及接入来源是哪里。3.4 数据收发循环与完整服务端代码连接建立后进入数据收发循环。我选择实现回显服务原因很简单收到什么就原样返回什么能最直观地验证链路是否双向畅通。循环体内有几个必须掌握的细节。recv的返回值是本次实际收到的字节数。返回0表示对端已经关闭连接返回-1表示出错所以统一用n 0作为退出循环的条件。这是网络编程里最容易忽略但绝对核心的判据很多线上事故都是因为没检查recv返回值导致对端断开后程序还在循环里空转。buffer在recv前先清零能防止上一次残留数据混淆本次内容。recv的第四个参数flags通常传0即可特殊用途才涉及MSG_WAITALL这类标志。send的返回值是实际发送的字节数可能小于传入长度严谨做法是循环发送直到全部发送完毕。本demo为了方便直接发一次但至少检查了是否返回-1否则对端断开后send会触发SIGPIPE信号默认情况下直接把进程干掉新手往往一头雾水。strncmp(buffer, quit, 4) 0用于判断客户端输入的前4字符是否为quit。这里特意用strncmp而不是strcmp因为fgets会保留换行符buffer实际内容是quit\nstrncmp比较前4个字符恰好匹配如果用strcmp会因为多一个换行符而匹配失败。这种小地方就是“看起来能跑但实际有坑”的典型。完整代码已经在上文给出。最后close(conn_fd)和close(listen_fd)分别关闭通信描述符和监听描述符。很多人写服务端时只记得close通信描述符忘掉监听描述符长跑服务最终会因文件描述符耗尽而崩溃这也是一个隐蔽的坑。4. 客户端程序完整实现4.1 客户端主流程socket与connect连接服务器客户端的socket创建与服务端几乎一样区别在于它不需要bind和listen。客户端不关心自己用哪个端口内核会在connect时自动分配一个临时端口这个端口可以从accept回填的client_addr里看到。客户端更关键的是connect这一步它会触发三次握手在默认阻塞模式下会一直等到握手完成或者超时失败。完整代码保存为client.c#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/socket.h #include sys/types.h #include netinet/in.h #include arpa/inet.h #define SERVER_PORT 8888 #define BUFFER_SIZE 1024 int main(int argc, char *argv[]) { if (argc ! 2) { fprintf(stderr, 用法: %s 服务器IP\n, argv[0]); exit(EXIT_FAILURE); } int sock_fd; struct sockaddr_in server_addr; char buffer[BUFFER_SIZE]; ssize_t n; // 1. 创建TCP套接字 sock_fd socket(AF_INET, SOCK_STREAM, 0); if (sock_fd 0) { perror(socket); exit(EXIT_FAILURE); } // 2. 初始化服务器地址注意字节序转换 memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(SERVER_PORT); if (inet_pton(AF_INET, argv[1], server_addr.sin_addr) 0) { fprintf(stderr, 无效的IP地址: %s\n, argv[1]); close(sock_fd); exit(EXIT_FAILURE); } // 3. 发起连接触发三次握手 if (connect(sock_fd, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { perror(connect); close(sock_fd); exit(EXIT_FAILURE); } printf(已连接服务器: %s:%d\n, argv[1], SERVER_PORT); // 4. 循环收发数据 while (1) { printf( ); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) NULL) { break; } if (send(sock_fd, buffer, strlen(buffer), 0) 0) { perror(send); break; } if (strncmp(buffer, quit, 4) 0) { break; } memset(buffer, 0, sizeof(buffer)); n recv(sock_fd, buffer, sizeof(buffer) - 1, 0); if (n 0) { printf(服务器断开连接\n); break; } printf(回显: %s, buffer); } close(sock_fd); return 0; }connect的常见失败原因有两个。服务器没启动时对方会直接回RST包客户端立即收到Connection refused网络不可达时通常是ETIMEDOUT超时这时要先检查网络路由和防火墙。另外我在客户端用命令行参数argv[1]作为服务器IP而不是写死127.0.0.1。学习阶段大多数人在本机测试写死IP也能跑但这样的代码一旦拿到两台机器上就废了参数化是更通用的做法。4.2 客户端收发数据与完整代码客户端的收发逻辑和服务端对称区别在交互方向。客户端先从标准输入读取一行发给服务端然后等待回显。这个一收一发的模式恰好演示了TCP上最简单的请求响应模型。代码里我特意加了fflush(stdout)目的是让“ ”提示符立即显示出来。如果不加由于stdout默认是行缓冲交互式输入时提示符可能不会及时刷新到屏幕上新手容易误以为程序卡死了。这类小细节决定实际使用体验也是常规教程很少提到的。fgets读取一行后会保留末尾换行符send发送的长度是strlen(buffer)包含换行符。服务端那边接收到后直接printf(%s, buffer)显示时自带换行体验很自然。客户端在发送quit后直接break退出不等待服务端回显服务端收到quit后同样break并关闭连接。两边都把退出逻辑放在发送完数据、收到回显之前这样双方都能干净退出不会因等待回显而卡死。需要注意的另一个细节是服务端收到quit后没有走send回显直接break退出所以客户端在quit后也不会进入recv等待。如果将来扩展功能比如服务端要先回一个“bye”再关闭那么客户端quit后的逻辑就要相应调整这种分工协作正是应用层协议设计的一部分。5. 编译运行与联调测试5.1 两条编译命令与常见编译错误服务端和客户端是两个独立程序分别编译成两个可执行文件gcc server.c -o server gcc client.c -o client如果gcc报“storage size of server_addr isnt known”这类错误百分之百是头文件没引全。struct sockaddr_in、htons等定义都在netinet/in.h里inet_pton在arpa/inet.h里。我习惯把下面四个头文件一次性全部引入省去排查依赖的麻烦#include sys/socket.h #include sys/types.h #include netinet/in.h #include arpa/inet.h编译完成后可以用ls -l确认两个可执行文件生成成功。还有一个验证代码质量的小技巧编译时加上-Wall让编译器把潜在问题都提示出来。首次编译能零警告通过说明代码基本功很扎实。如果在调试阶段频繁改代码可以直接把两个编译命令写在Makefile里后续只需要执行make省时间。5.2 本地回环测试完整过程打开两个终端。终端A运行服务端./server正常输出服务端已启动监听端口: 8888终端B运行客户端./client 127.0.0.1正常输出已连接服务器: 127.0.0.1:8888 服务端同时打印出客户端接入信息客户端接入: 127.0.0.1:52431这里的52431是内核给客户端临时分配的源端口每次运行都可能不同属于正常现象。然后我在客户端输入hello完整交互过程如下 hello 回显: hello world 回显: world quit服务端这边同步打印客户端说: hello 客户端说: world 客户端说: quit 收到退出指令关闭连接 客户端断开连接这个结果说明两端收发链路完全打通退出逻辑也符合预期。我实际测试时还会开第三个终端用ss命令确认端口监听和连接状态ss -ant | grep 8888可以看到LISTEN状态的8888端口以及ESTAB状态的连接记录。这是排查网络问题时最实用的基本功比盲目猜测靠谱得多。如果系统里有netstat效果类似但新版Linux推荐优先使用ss。5.3 跨主机通信时的配置要点如果两台机器在同一局域网内客户端传服务器的内网IP即可代码不用改。但有三个前置条件容易被忽略。第一两台机器必须网络互通先ping目标IPping不通就谈不上TCP通信。第二服务端防火墙要放行TCP 8888端口云服务器还要在安全组里放行入方向规则。第三服务端监听的是INADDR_ANY也就是所有网卡客户端通过任意IP都能连上如果bind时写死了某网卡IP只有访问那个IP才能连上服务端。关于防火墙我习惯这么测先临时关闭防火墙测试程序能不能通通了再打开防火墙并配置放行规则。这样能快速判断问题出在程序还是出在防火墙测试完毕后立刻恢复防火墙配置。云服务器尤其要注意iptables防火墙和云平台安全组是两层过滤都要放行对应端口。很多时候代码一点问题都没有就是安全组没开端口连不上时先把这两层都查一遍。6. 常见问题与排查经验6.1 bind报“Address already in use”TIME_WAIT与端口复用最典型的场景服务端刚CtrlC退出立刻重启bind就报错。原因是主动关闭连接的一方会进入TIME_WAIT状态持续约2MSL也就是几十秒到几分钟不等。虽然这个demo里服务端正常退出时流程简单但TCP四次挥手的最后一步总会留下TIME_WAIT记录。排查方法很简单ss -ant | grep 8888如果看到TIME-WAIT状态的记录原因就确定了。解决方案有两个方向在代码里提前设置SO_REUSEADDR也就是服务端代码中的setsockopt那一行或者等TIME_WAIT超时后再重启。我在开发阶段频繁改代码重启加了SO_REUSEADDR之后就没再为这个问题发过愁。SO_REUSEADDR不会缩短TIME_WAIT的时间但允许新套接字绑定到同一个地址端口足以避开开发期最常见的重启冲突。6.2 connect报“Connection refused”先查监听再查防火墙这个问题十个人里九个人会碰到。Connection refused说明客户端发出的SYN收到了RST响应常见原因排序如下服务端程序没启动端口根本没有监听IP或端口写错了连到了没有服务的端口防火墙直接返回RST有些安全策略会这样配置。排查顺序建议是先ps确认服务端进程是否存在再ss确认端口处于LISTEN状态最后检查客户端传入的IP和端口是否与服务端一致。如果服务端和客户端在同一台机器上却依然refused多半是服务端根本没起来或者两个文件里的SERVER_PORT定义不一致。还有一种隐蔽情况服务端程序启动时bind了8888但客户端传了8889看起来像是连接被拒绝其实是端口不匹配。这种低级错误靠打印日志就能发现所以客户端代码里把连接目标IP和端口都打出来了联调时务必养成看日志的习惯。6.3 recv返回0和-1如何正确判断连接关闭网络编程里判断连接是否关闭几乎是所有逻辑的前提。recv返回值必须吃透返回大于0收到数据返回值就是字节数返回0对端主动关闭比如调用close或者进程正常退出内核发送了FIN返回-1出错需要进一步判断errno常见的有EINTR被信号中断、EAGAIN非阻塞模式下暂时无数据、ECONNRESET对端异常关闭。很多人的第一版代码只关心收到的内容不检查返回值客户端断开后服务端还在循环里傻等要么死循环要么读到一串垃圾数据。严谨的写法就是本文代码里n 0退出循环这是最基本但非常重要的网络编程素养。另外关于SIGPIPE信号我再补充一个坑。如果对一个已经关闭的连接继续send默认行为是触发SIGPIPE进程会直接退出。很多服务端不明不白地挂了日志里什么都没有最后发现是SIGPIPE作祟。生产级代码通常会调用signal(SIGPIPE, SIG_IGN)忽略这个信号然后靠send的EPIPE错误码感知连接异常。本demo不涉及高并发但提出来是想让大家知道这里有坑。6.4 粘包与拆包TCP字节流特性带来的经典问题TCP是字节流协议不保证一次send对应一次recv。客户端连续发送三个消息服务端可能一次recv就收到全部内容也可能三次recv才分别收到一部分这就是粘包和拆包。很多新手第一次遇到时百思不得其解以为是代码bug其实是TCP的固有特性。解决思路都围绕“应用层协议”展开本质是约定消息边界。常见做法有四种固定长度每条消息长度相同不够用补位填充特殊分隔符比如用\r\n分割消息HTTP协议就是这种思路长度前缀每条消息前追加4字节的消息长度或者直接用序列化框架如protobuf统一处理封装。本文的回显demo其实也有潜在粘包问题只是因为服务端收到什么就回什么客户端也是send一次就recv一次数据量小的时候问题不暴露。一旦数据量大或者并发上来就会真实遇到。我的建议是学完基础流程后把“长度前缀循环拆包”的改造当作练习能顺手写出来才算真正理解了字节流协议。这个练习会逼着你重新审视recv的循环调用和对缓冲区的管理能力提升非常明显。说实话把这一整套流程跑通只是网络编程万里长征的第一步。但反过来说后面所有进阶内容多线程并发、IO多路复用、异步网络库、协议设计都是在这套基础API上搭起来的。我见过不少同学demo能跑就急着去学高深框架结果遇到线上问题连基本的状态变化都分析不清楚。我的建议是先把每个函数的返回值想清楚把每一行代码背后的内核行为琢磨明白再谈优化和扩展这条路会顺得多。最后再说个小技巧把这个回显demo的端口改成自己不常用的高端口跑一遍再试试两台真实机器之间的通信你收获的细节会比单纯看本文多得多。

相关新闻

2026年靠谱建站公司怎么选?六大特征+梯队榜单+避坑指南

2026年靠谱建站公司怎么选?六大特征+梯队榜单+避坑指南

做网站这块,每年都有朋友来问我“榜单靠不靠谱”,今年问的人格外多。原因也简单:建站需求在这几年经历了“官网没用了”到“官网又值钱了”的循环之后,2026 年回到官网的中小企业明显变多了。但打开任何一个“TOP10 榜单”&#x…

2026/10/18 18:48:02 阅读更多 →
用真实界面做30秒AI演示视频:UI保真度驱动的产品信任构建

用真实界面做30秒AI演示视频:UI保真度驱动的产品信任构建

1. 项目概述:为什么“用真实界面做30秒演示”正在成为AI视频工具的分水岭最近在帮几个做ToB SaaS产品的团队做产品演示优化,发现一个特别有意思的现象:几乎所有团队都试过用主流AI视频工具生成App功能介绍视频——输入一段文字描述&#xff0…

2026/10/18 18:48:02 阅读更多 →
边缘检测从基础到进阶:Sobel、Canny与OpenCV实战解析

边缘检测从基础到进阶:Sobel、Canny与OpenCV实战解析

边缘检测这种话题,放在计算机视觉里,看着像个入门基本功,实际上是把信号处理和工程取舍绑得最紧的一环。我见过不少人背熟了Sobel的卷积核,也看懂了Canny的几步流程,但一上真实图片就发现边缘断的断、花的碎&#xff0…

2026/10/18 18:47:02 阅读更多 →

最新新闻

智能座舱语音交互核心技术术语解析:从ASR到TTS的完整链路

智能座舱语音交互核心技术术语解析:从ASR到TTS的完整链路

智能座舱的发布会上,“语音交互”这几个字被反复提及,但真正看参数页和产品通稿时,满屏的ASR、NLU、AEC、TTS、唤醒率、误唤醒、全双工、可见即可说,说实话我刚接触那会儿也一头雾水。车载语音交互和手机上的语音助手完全是两码事…

2026/10/19 4:29:45 阅读更多 →
从数据驱动到函数表映射:轻量级GUI组件注册机制设计实践

从数据驱动到函数表映射:轻量级GUI组件注册机制设计实践

写GUI框架最痛苦的日子,不是画界面,而是给界面里的每个控件找回调。我早期做过一个不到两千行的小型界面库,按钮不过十几个、页面三四个,看起来工作量不大,可每加一个页面就要去改那个全局的switch-case分发函数&#…

2026/10/19 4:29:45 阅读更多 →
上下文工程实战:从记忆分层到检索调优的Agent一致性指南

上下文工程实战:从记忆分层到检索调优的Agent一致性指南

我最早意识到上下文工程是独立问题,是在一个内部工具的第五次联调上。功能明明都对齐了,模型却能在一段对话里反复失忆:上午刚确认的筛选规则,下午就问第二遍;用户上一轮给了三条约束,模型下一轮只记得两条…

2026/10/19 4:29:45 阅读更多 →
Halcon焊球检测实战:Blob分析从例程到产线的参数调优与避坑指南

Halcon焊球检测实战:Blob分析从例程到产线的参数调优与避坑指南

1. 从一张焊球照片说起:这个例程到底在解决什么问题球焊,也就是业内常说的Bond Ball焊接,是半导体封装和精密电子组装里绕不开的一道工序。一根细细的金线或铜线,末端被电弧烧熔成一个自由空气球,然后压焊到焊盘上&…

2026/10/19 4:29:45 阅读更多 →
代码评审为何总流于形式?open-code-review用静态分析帮你抓重点

代码评审为何总流于形式?open-code-review用静态分析帮你抓重点

上个月我给一支后端团队做代码评审流程梳理,发现他们平均一次PR的评审时间是3分钟。三分钟能干什么?点开diff,扫一眼变量命名,说句LGTM,然后合入。代码评审本应是质量防线,结果成了“点头仪式”。后来我把一…

2026/10/19 4:29:45 阅读更多 →
Keras实现DCGAN:从零搭建稳定图像生成模型

Keras实现DCGAN:从零搭建稳定图像生成模型

1. 从零理解 DCGAN:为什么它比普通 GAN 更值得上手搞深度学习的人多少都碰过 GAN,但真正自己动手从零搭一个能跑出结果的,比例其实不高。原因也简单——GAN 的训练出了名的难伺候,判别器和生成器稍微不平衡就崩,要么生…

2026/10/19 4:28:45 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →