news 2026/10/18 11:54:59

Linux Socket编程实战:从socket()到close()的TCP通信与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux Socket编程实战:从socket()到close()的TCP通信与避坑指南

简介:这是一份面向Linux网络编程初学者与进阶开发者的Socket编程学习资料,围绕进程间通信、Socket接口原理与TCP连接管理展开,帮助读者理解跨主机进程通信的标识机制与套接字操作流程。内容涵盖socket()、bind()、listen()、connect()、accept()、read()/write()、close()等核心函数用法,并详解TCP三次握手建立连接与四次挥手释放连接的过程,最后附有实践示例供动手验证。资源包为1个docx文档,约77KB,以图文笔记形式组织,便于按章节顺序阅读与查阅。目前已有241人学习,适合希望系统掌握Linux Socket编程、为开发Web服务器或聊天应用打基础的读者参考。

1. 从一份“老派”Socket 讲义说起:为什么今天还值得翻出来跑一遍

很多人第一次写网络程序,是从socket()开始的,然后被bind、listen、accept这一串函数绕得云里雾里。这份《Linux Socket编程(附实例)》讲义,就是那种典型的“老派但扎实”的资料:它不急着教你写高并发框架,而是先把网络进程通信的标识问题讲清楚——IP 定位主机、协议加端口定位进程,三元组一摆,Socket 的定位就出来了。接着它把socket()、bind()、listen()、connect()、accept()、read/write、close逐个拆开,还配了 TCP 三次握手和四次挥手的函数级时序说明。最实在的是最后那个可编译的 C++ 小例子:一个mysocket类封装服务端和客户端,服务端监听 6666 端口,客户端连上去发消息。如果你正在学网络编程、准备面试、或者想带新人理解 TCP 连接到底发生在哪个函数调用里,这份资料能帮你把“玄学”变成可调试的代码。下面我就按自己复现的路径,把它拆成能直接抄作业的步骤。

2. 把 Socket 基本操作拆成可编译的最小闭环:从 socket() 到 close()

2.1 先理解“一切皆文件”在 Socket 上的映射

这份资料反复强调一个点:Socket 是 Unix“一切皆文件”哲学在网络层的实现。普通文件用open拿到文件描述符,然后read/write,最后close;Socket 用socket()拿到 socket 描述符,然后send/recv或read/write,最后close。这个类比不是修辞,它直接决定了你排错时的思路——文件描述符会耗尽、会泄漏,Socket 也一样。资料里给出的函数原型是:

int socket(int domain, int type, int protocol); int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); int listen(int sockfd, int backlog); int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数含义资料里讲得很细,我按自己的理解再压一遍:domain选AF_INET就是 IPv4,type选SOCK_STREAM就是 TCP,protocol填 0 让系统根据 type 自动选默认协议。这里有个容易翻车的点:SOCK_STREAM不能和IPPROTO_UDP组合,类型和协议必须匹配。bind的第二个参数是struct sockaddr *,实际传的是sockaddr_in,需要强制转换。listen的backlog是排队连接数上限,资料里例子用了 10,生产环境通常要结合somaxconn调整。accept返回的是一个全新的描述符,代表与某个客户端的连接,而原来的监听描述符继续留着接新连接——这个区分是后面写并发服务器的前提。

2.2 服务端和客户端的调用顺序不能乱

资料里把服务端和客户端的流程画得很清楚,我把它整理成可对照的步骤表:

角色调用顺序关键说明
服务端socket → bind → listen → accept → recv/send → closebind 绑定知名端口,listen 转被动,accept 阻塞等连接
客户端socket → connect → send/recv → close通常不 bind,connect 时内核随机分配本地端口

这个顺序不是建议,是硬约束。服务端如果先listen再bind,会直接报错;客户端如果先connect再bind,虽然语法上可能过,但绑定端口后 connect 的行为会变得不直观。资料里特别指出:客户端不调用bind,connect时由系统自动分配端口和 IP 组合。我一般会在客户端也显式bind一个端口 0,让内核选,这样调试时能看到本地端口,但这不是必须的。

2.3 字节序转换:那个引发过“血案”的 htons

资料里用了一段很重的语气讲字节序:“由于这个问题曾引发过血案!”主机字节序分大端小端,网络字节序统一是大端。bind和connect之前,端口必须用htons()转成网络字节序,IP 地址用inet_pton()或htonl()处理。我见过太多人直接写servaddr.sin_port = 6666;,在小端机器上跑起来端口就变成了 0x1A0A 之类的怪值,然后bind报EADDRINUSE或者连不上。正确写法:

servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(6666);

INADDR_ANY表示绑定本机所有网卡地址,htonl把它转成网络字节序。客户端侧用inet_pton(AF_INET, "192.168.1.18", &servaddr.sin_addr)把点分十进制转成二进制,这个函数比老的inet_addr更安全,能处理错误返回值。

2.4 把资料里的 mysocket 类跑起来

资料最后给了一个完整的 C++ 例子,文件结构是mysocket.h、mysocket.cpp、server.cpp、client.cpp。我把它整理成可直接编译的版本,关键部分保留原逻辑,补上必要的头文件和错误处理:

// mysocket.h #ifndef _MYSOCKET_H #define _MYSOCKET_H #include <sys/socket.h> class mysocket { public: mysocket(); ~mysocket(); bool createclient(int domain, int soctype, int protype, int port); bool createserver(int domain, int soctype, int protype, int port); void recvmsg(); void sedmsg(); private: int m_idomain; int m_isoctype; int m_iprotype; int m_isocket; }; #endif
// mysocket.cpp 关键片段 bool mysocket::createserver(int domain, int soctype, int protype, int port) { m_idomain = domain; m_isoctype = soctype; m_iprotype = protype; 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; 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; } if (listen(m_isocket, 10) == -1) { printf("listen socket error\n"); return false; } printf("======waiting for client's request======\n"); return true; }
// server.cpp #include "mysocket.h" int main() { mysocket server; if (server.createserver(AF_INET, SOCK_STREAM, 0, 6666)) { server.recvmsg(); } return 1; }
// client.cpp #include "mysocket.h" int main() { mysocket client; if (client.createclient(AF_INET, SOCK_STREAM, 0, 6666)) { client.sedmsg(); } return 1; }

编译命令:

g++ -o server server.cpp mysocket.cpp g++ -o client client.cpp mysocket.cpp

先跑./server,再跑./client。服务端会打印======waiting for client's request======,客户端连上后循环发送msg = 0.、msg = 1.这样的消息,服务端recvmsg里打印收到的内容和长度。这里有个细节:资料里recvmsg的循环写法是while((n = recv(connfd, buff, 1023, 0)) > 0),但循环体内先memset再buff[n] = '\0',顺序上memset把刚收到的数据清掉了,实际打印的是空。我复现时把memset挪到recv之前,或者干脆用n控制打印长度,才能看到正确消息。这个坑后面还会展开。

3. 三次握手和四次挥手到底发生在哪个函数里:用 strace 验证

3.1 connect 和 accept 的返回时机

资料里有一句总结很关键:“客户端的 connect 在三次握手的第二次返回,而服务器端的 accept 在三次握手的第三次返回。”这句话如果只背结论很容易忘,我建议用strace跟一遍。服务端跑起来后,用strace -f -e trace=network ./server观察,客户端connect时你会看到sendto发出 SYN,然后recvfrom收到 SYN+ACK,connect返回。服务端这边,accept在收到第三次 ACK 后才返回。这个时序解释了为什么connect返回成功不代表服务器应用层已经accept——连接已经在内核里建立了,但服务器进程可能还没取走。

3.2 四次挥手和 close 的引用计数

资料里讲close时提到:“close 操作只是使相应 socket 描述字的引用计数-1,只有当引用计数为 0 的时候,才会触发 TCP 客户端向服务器发送终止连接请求。”这个点很多人忽略。如果你用fork或dup复制了 socket 描述符,只close一次不会发 FIN。四次挥手的流程是:主动关闭方发 FIN,被动方回 ACK,被动方应用层读到 EOF 后调用close发 FIN,主动方回 ACK。资料里的图 2 把这个过程画得很清楚。实际调试时,如果发现连接没有正常关闭,先检查是不是有地方dup了描述符没关干净。

3.3 用 tcpdump 看握手包

想更直观,可以在回环口抓包:

tcpdump -i lo port 6666 -nn -S

-S让序列号显示绝对值,方便对照。你会看到Flags [S]、Flags [S.]、Flags [.]三个包,对应三次握手。关闭时看到Flags [F.]、Flags [.]、Flags [F.]、Flags [.]四个包。这个验证方法比看图更可靠,因为你能确认自己的代码真的触发了这些包。

4. 避坑与排查:那些年我在 Socket 上翻过的车

4.1 bind 报 Address already in use

现象:服务端重启时bind返回 -1,errno是EADDRINUSE。原因:TCP 连接关闭后,主动关闭方会进入TIME_WAIT状态,默认持续 2MSL(通常 60 秒),期间端口不能被新 socket 绑定。如果服务端是主动关闭方,重启就会撞上。解决:在bind之前设置SO_REUSEADDR:

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

这允许绑定处于TIME_WAIT的端口。注意SO_REUSEPORT是另一个选项,用于多进程负载均衡,不要混用。

4.2 accept 返回的 connfd 和监听 fd 搞混

现象:服务端只能处理一个客户端,第二个客户端连上来没反应。原因:把accept返回的connfd当成监听描述符继续accept,或者recv时用了m_isocket而不是connfd。解决:记住m_isocket只负责accept,一旦拿到connfd,所有读写都用connfd。资料里recvmsg函数用的是connfd,这是对的。如果要支持多客户端,需要把connfd交给线程或epoll,不能在一个循环里串行处理。

4.3 recv 返回 0 被当成正常数据

现象:客户端关闭后,服务端recv返回 0,程序继续循环,打印空消息。原因:recv返回 0 表示对端关闭了连接(收到 FIN),不是读到空数据。资料里while((n = recv(...)) > 0)在 n=0 时会退出循环,这是对的。但如果在循环体内先memset再判断,可能把 0 当成有效长度。解决:recv返回值小于等于 0 都要处理:等于 0 关闭连接,小于 0 检查errno是否为EINTR,是就重试,否则报错。

4.4 客户端 connect 的 IP 写死导致换环境就挂

现象:资料例子里的客户端写死了192.168.1.18,换台机器或换网段就连不上。原因:硬编码 IP 只适合本机回环或固定实验环境。解决:把 IP 做成命令行参数或配置文件读取。本机测试直接用127.0.0.1。如果服务端和客户端不在同一台机器,确保防火墙放行 6666 端口,并且服务端绑定的是INADDR_ANY而不是127.0.0.1。

4.5 忘记处理 EINTR 导致程序莫名退出

现象:accept或recv被信号中断,返回 -1,errno是EINTR,程序直接报错退出。原因:慢系统调用在收到信号时会中断,这是 POSIX 行为。解决:对accept、recv、send的 -1 返回值判断errno == EINTR,如果是就继续循环。资料里accept的错误处理用了continue,但没有区分EINTR和其他错误,严格来说应该分开处理。

5. 从单连接例子到能用的服务端:几个我常加的改进

资料里的例子是单连接、阻塞式的,适合理解流程,但直接拿去用会很快遇到瓶颈。我一般会在它的基础上加三样东西。第一,把accept放到循环里,每个connfd开一个std::thread或丢进线程池,避免一个客户端占住整个服务端。第二,给recv加超时,用setsockopt设置SO_RCVTIMEO,防止客户端连上不发数据把服务端卡死:

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

第三,把send的返回值检查做完整。资料里sedmsg对send失败用了continue,但如果是EPIPE(对端关闭),继续发只会一直报错。正确做法是EPIPE时跳出循环并close。另外,send返回小于请求长度时,剩余部分需要继续发,不能假设一次send全部发完。这些改进不改变资料的核心逻辑,但能让代码从“能跑”变成“敢用”。我每次把这类例子交给新人,都会让他们先跑通原版,再自己加线程和超时,最后用tcpdump验证关闭流程。从那以后我每次写 Socket 代码,都强制走一遍strace看握手和挥手,确认没有描述符泄漏。希望帮到你。

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

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

文件安全分析实战:静态特征、沙箱监控与完整性保护

直接处理一个陌生文件之前&#xff0c;哪怕它是同事转发过来的一份报表&#xff0c;或是刚从官网下载的安装包&#xff0c;我都会下意识地先过一遍分析流程。这些年帮多个团队做过文件安全相关的检测与加固&#xff0c;越来越确信一件事&#xff1a;文件层的攻击是绕过传统边界…

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

Sublime Text 编辑器核心原理与实战价值解析

简介&#xff1a;本资源为Sublime Text编辑器的完整本地化安装与扩展包&#xff0c;面向广大程序员、前端开发者及代码初学者&#xff0c;解决跨平台轻量级开发环境快速部署与个性化定制需求。压缩包共2000个文件&#xff0c;主体包含798个sublime-snippet代码片段、140个Pytho…

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

遥感道路分割数据集实战:大分辨率影像划分与训练避坑指南

简介&#xff1a;面向深度学习和遥感图像分析需求&#xff0c;这份资源提供大分辨率遥感影像的道路分割数据集&#xff0c;已按标准划分训练集与测试集。训练集包含4981张原始影像及对应Mask标注&#xff0c;测试集包含1245对&#xff0c;前景标注精细&#xff0c;可直接用于U-…

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

数字员工业务处理能力升级:AI销冠系统实战落地与避坑指南

干了这么多年企业服务&#xff0c;我一直觉得有个问题特别拧巴&#xff1a;老板花大价钱搞数字化&#xff0c;结果一线销售和客服的日常活儿&#xff0c;还是靠人肉背话术、翻文档、问老员工。团队里真正干活儿的“数字员工”听起来很智能&#xff0c;实际上要么是只会机械应答…

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

Vue大文件上传插件开发:分片、断点续传与秒传全解析

做Vue大文件上传插件&#xff0c;这活儿我前后折腾过两轮。第一轮是给某个后台管理系统做附件模块&#xff0c;硬编码把上传逻辑写死在页面里&#xff0c;结果换个项目连复制粘贴都没法用。第二轮彻底重写&#xff0c;把它拆成一个可插拔的上传核心&#xff0c;用的时候传配置、…

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

用C++和OpenGL从零实现我的世界简易版:体素引擎核心解析

简介&#xff1a;这是一份面向C初学者与游戏开发爱好者的《我的世界》简易版项目&#xff0c;用C实现简化版沙盒玩法&#xff0c;涵盖方块生成、玩家移动等基础机制&#xff0c;既能作为编程练手案例&#xff0c;也可借此了解游戏循环、渲染与交互的粗略实现。压缩包共4个文件&…

作者头像 李华