资讯详情

Linux Socket编程实战:从TCP服务端客户端实例到避坑指南

📅 2026/10/18 20:36:59 | 华诺云谱 👁 阅读
Linux Socket编程实战:从TCP服务端客户端实例到避坑指南
简介这是一份面向Linux网络编程初学者与进阶开发者的Socket编程学习资料围绕进程间通信、Socket概念与基本操作展开帮助读者理解TCP/IP协议栈下跨主机通信的实现机制。内容涵盖socket()、bind()、listen()、connect()、accept()、read()/write()、close()等核心函数用法并详细解析TCP三次握手建立连接与四次挥手释放连接的过程最后附有实践示例供动手练习。资源包内含1个docx文档约77KB以图文笔记形式组织结构清晰便于按章节顺序阅读与查阅。目前已有241人学习适合正在学习网络编程、准备面试或需要快速回顾Socket接口细节的开发者参考可帮助读者建立从理论到代码实现的完整认知为后续开发Web服务器、聊天程序等网络应用打下基础。1. 从一份“老派”Socket 实例说起为什么网络编程绕不开它翻到这份《Linux Socket编程附实例》的时候我第一反应是这玩意儿太经典了经典到几乎每个做后端或嵌入式的人都手写过一遍。它不讲框架、不依赖任何第三方库就靠socket()、bind()、listen()、accept()、recv()、send()这几个系统调用硬生生把 TCP 通信的骨架搭出来。项目里给了一个mysocket类把服务端和客户端的连接建立、消息收发封装成几个成员函数再配两个极简的main文件编译完先跑 server 再跑 client就能看到消息在两端来回蹦。适合谁刚接触 Linux 网络编程、想搞懂“三次握手到底发生在哪个函数里”的新手以及需要一份干净代码当脚手架、往上叠业务逻辑的熟手。它不炫技但每一行都能对应到 TCP/IP 协议栈的真实行为。2. 核心模块拆解mysocket 类如何把 TCP 服务端和客户端串起来2.1 服务端链路createserver 里的 socket、bind、listen 三件套服务端的入口是createserver它把创建套接字、绑定地址、开启监听三步压进一个函数。先看代码bool mysocket::createserver(int domain, int soctype, int protype, int port) { m_idomain domain; m_isoctype soctype; m_iprotype protype; // 创建套接字返回描述符存入 m_isocket m_isocket socket(m_idomain, m_isoctype, m_iprotype); if (-1 m_isocket) { printf(create socket error\n); return false; } struct sockaddr_in servaddr; memset(servaddr, 0, sizeof(servaddr)); servaddr.sin_family domain; // INADDR_ANY 表示绑定本机所有网卡地址 servaddr.sin_addr.s_addr htonl(INADDR_ANY); // 端口号必须从主机字节序转网络字节序 servaddr.sin_port htons(port); if (bind(m_isocket, (struct sockaddr*)servaddr, sizeof(servaddr)) -1) { printf(bind socket error\n); return false; } // backlog 设为 10允许最多 10 个未 accept 的连接排队 if (listen(m_isocket, 10) -1) { printf(listen socket error\n); return false; } printf(waiting for clients request\n); return true; }逻辑说明很直白socket()返回的描述符此时还没有地址bind()把sockaddr_in结构里的 IP 和端口赋给它listen()把主动套接字转为被动套接字内核开始维护连接队列。参数上domain传AF_INET走 IPv4soctype传SOCK_STREAM走 TCPprotype传 0 让系统自动选协议。INADDR_ANY是个常见做法服务器不挑网卡哪个网卡收到请求都能处理。htons(port)和htonl(INADDR_ANY)是血泪经验——字节序不转端口号在网络上就是乱的当年我在一个跨平台项目里漏了htons本地跑得好好的一上真机就连接超时查了一整天才定位到。2.2 客户端链路createclient 里的 connect 与地址填充客户端不需要 bind系统会在connect()时自动分配临时端口。代码里把服务器 IP 硬编码成192.168.1.18实际用时改成你的 server 地址bool mysocket::createclient(int domain, int soctype, int protype, int port) { m_idomain domain; m_isoctype soctype; m_iprotype protype; struct sockaddr_in servaddr; m_isocket socket(m_idomain, m_isoctype, m_iprotype); if (-1 m_isocket) { printf(create socket error\n); return false; } memset(servaddr, 0, sizeof(servaddr)); servaddr.sin_family domain; servaddr.sin_port htons(port); // inet_pton 把点分十进制 IP 转成二进制网络字节序 if (inet_pton(AF_INET, 192.168.1.18, servaddr.sin_addr) 0) { printf(inet_pton error for 192.168.1.18\n); return false; } // connect 触发三次握手成功返回 0 if (-1 connect(m_isocket, (struct sockaddr*)servaddr, sizeof(servaddr))) { printf(connect to server error 192.168.1.18\n); return false; } printf(Begin sending msg to server\n); return true; }inet_pton比老式的inet_addr更安全它明确指定了地址族返回值小于等于 0 就说明 IP 格式不对。connect()是阻塞调用内核发 SYN、收 SYNACK、再回 ACK三次握手走完才返回。如果服务器没开或者防火墙拦了这里会卡住直到超时。我一般会在connect外面加个alarm或者用非阻塞套接字配select做超时控制但这份示例代码为了简洁没加新手跑的时候注意先启动 server。2.3 消息收发recvmsg 与 sedmsg 的循环处理服务端的recvmsg先accept一个连接拿到connfd然后循环recvvoid mysocket::recvmsg() { int connfd -1; char buff[1024] {0}; int n 0; while (1) { // accept 返回已连接套接字失败则重试 if ((connfd accept(m_isocket, (struct sockaddr*)NULL, NULL)) -1) { printf(accept socket error: %s(errno: %d), strerror(errno), errno); continue; } break; } while ((n recv(connfd, buff, 1023, 0)) 0) { memset(buff, 0, 1024); buff[n] \0; printf(connfd%d; rec msg from client: %s.msglen%d\n, connfd, buff, n); n 0; } }注意accept的第一个参数是监听套接字m_isocket返回的是全新的已连接套接字connfd。监听套接字在整个服务器生命周期里一直存在而每个客户连接对应一个独立的connfd。recv返回 0 表示对端关闭了连接小于 0 表示出错。这里有个小坑memset放在recv之后、打印之前其实应该放在recv之前把缓冲区清干净否则上一轮残留数据可能干扰。不过示例里每次recv返回后都重新置零并补\0勉强能用。客户端的sedmsg更简单死循环拼字符串发出去void mysocket::sedmsg() { int msgid 0; char buff[1024] {0}; while (1) { memset(buff, 0, 1024); sprintf(buff, msg %d.\n, msgid); if (send(m_isocket, buff, strlen(buff), 0) 0) { printf(send msg error: %s(errno: %d)\n, strerror(errno), errno); continue; } else { printf(send msg succ\n); msgid; } } }send返回实际发送的字节数可能小于strlen(buff)但在这个小消息场景下一般一次发完。如果要做可靠传输得循环发送直到全部字节写出。msgid自增让每条消息可区分方便观察。3. 编译与运行从源码到两端通信的完整操作3.1 文件组织与编译命令项目文件结构很扁平mysocket/ ├── mysocket.h // 类声明 ├── mysocket.cpp // 类实现 ├── server.cpp // 服务端 main └── client.cpp // 客户端 main编译用 g 一条命令搞定g -o server server.cpp mysocket.cpp g -o client client.cpp mysocket.cpp如果提示找不到inet_pton在mysocket.cpp里加上#include arpa/inet.h。有些老编译器需要链接-lsocket -lnsl但现代 Linux 上通常不需要。3.2 运行顺序与观察要点先在一个终端跑./server看到waiting for clients request说明监听已就绪。再开另一个终端跑./client客户端会打印Begin sending msg to server然后不断send msg succ。切回 server 终端能看到类似connfd4; rec msg from client: msg 0. .msglen9 connfd4; rec msg from client: msg 1. .msglen9connfd4是内核分配的已连接套接字描述符每次运行可能不同。msglen9对应msg 0.\n的长度。如果 client 报connect to server error先确认 server 是否在跑、IP 是否填对、防火墙是否放行 6666 端口。3.3 参数调整与行为变化把createserver里的SOCK_STREAM改成SOCK_DGRAM代码不会报错但行为完全变了——UDP 没有连接概念accept和connect的语义不同recv会收到数据报而不是字节流。端口号改成 1024 以下需要 root 权限。backlog从 10 改成 1并发连接稍多就会看到客户端连接被拒。这些改动能帮你直观感受每个参数的作用。4. 避坑与排查Socket 编程里那些让人抓狂的瞬间4.1 连接被拒绝Connection refused现象客户端connect返回 -1打印connect to server error。原因服务器没启动、端口写错、或者服务器绑定了127.0.0.1而客户端连的是外网 IP。解决先netstat -anp | grep 6666确认监听状态再检查INADDR_ANY是否被误改成具体地址。4.2 地址已在使用Address already in use现象server 重启时bind失败提示bind socket error。原因TCP 连接关闭后内核默认会保留端口一段时间TIME_WAIT 状态防止旧数据包干扰新连接。解决在bind之前设置SO_REUSEADDRint opt 1; setsockopt(m_isocket, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));这行代码几乎是我每个 server 程序的标配不加的话调试期间反复重启能烦死人。4.3 字节序导致的“玄学”乱码现象端口号明明填的 6666抓包看到的是 0x1A0A 之类的怪值。原因主机字节序和网络字节序不一致x86 是小端网络是大端。解决所有多字节整数进出网络前必须过htons/htonl从网络读出来用ntohs/ntohl。别偷懒别假定平台。4.4 recv 返回 0 被误判为错误现象服务端recv返回 0程序继续循环疯狂打印空消息。原因recv返回 0 表示对端正常关闭连接不是错误。解决在循环里判断if (n 0) break;跳出然后close(connfd)释放资源。示例代码里没处理这个实际用的时候必须补上。4.5 客户端发送太快导致服务端缓冲区溢出现象客户端send一直成功但服务端只收到前几条消息。原因TCP 有流量控制接收缓冲区满了之后recv不及时发送方会被阻塞或丢包取决于套接字选项。解决确保服务端recv循环足够快或者用多线程/多进程处理每个连接。简单场景下把recv缓冲区调大setsockopt设置SO_RCVBUF。5. 二次开发改造从示例到可用服务的几步这份代码是个好起点但离生产可用还有距离。我一般会按下面几步改。第一步把单连接改成多连接。现在的recvmsg只accept一次就死循环收消息第二个客户端连不上。最简做法是accept之后fork()一个子进程处理父进程继续accept。或者用pthread_create开线程注意线程里别直接访问共享的m_isocket每个连接用独立的connfd。第二步加上错误处理和资源回收。每个socket、accept、recv、send的返回值都要判断失败时close掉已打开的描述符。SIGPIPE信号要忽略掉否则客户端异常断开时send会直接杀掉进程signal(SIGPIPE, SIG_IGN);第三步把消息协议规范化。现在发的是msg %d.\n实际项目里至少得有个包头标明长度避免粘包。常见做法是前 4 个字节放网络字节序的消息长度后面跟实际数据。收的时候先recv4 字节解析出长度再循环recv直到收满。第四步用select/poll/epoll替换阻塞循环。单线程处理多连接的标准方案epoll在 Linux 上性能最好。把监听套接字和所有已连接套接字都注册进去事件触发时分别处理accept或recv。第五步加日志和配置。把硬编码的 IP、端口、backlog抽到配置文件或命令行参数里用getopt解析。日志别再用printf换成syslog或者简单的日志级别控制。验证改造是否成功我习惯用nc或者写个脚本开多个客户端同时发消息观察服务端是否都能正确响应、有没有内存泄漏valgrind跑一遍、close之后端口是否正常释放。从那以后我每次写 socket 程序都强制自己先加SO_REUSEADDR和SIGPIPE忽略再开始写业务逻辑——这两行能省掉后面无数个抓狂的下午。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑