
简介面向Linux网络编程初学者与需要夯实底层通信原理的开发者这份资料系统梳理了Socket编程的核心知识体系帮助读者理解网络中进程如何借助IP地址、协议与端口三元组实现唯一标识与跨主机通信。内容从Socket“一切皆文件”的设计哲学切入依次讲解socket()、bind()、listen()、connect()、accept()、read()/write()与close()等基本操作函数并深入剖析TCP三次握手建立连接与四次挥手释放连接的全过程最后给出服务端与客户端配合的实践示例兼顾原理理解与动手验证。资源包共1个docx文档约77KB以文字讲解与代码片段为主结构紧凑便于通读与查阅。目前已有241人学习适合作为网络编程入门笔记或复习提纲使用。1. 从一份能跑通的 Socket 实例说起很多人学网络编程卡在“看得懂函数原型写不出能跑的程序”这一步。这份资源给的不是概念拼盘而是一套能直接编译运行的 Linux Socket 编程实例一个mysocket类把服务端和客户端的连接建立过程封装起来server.cpp监听 6666 端口client.cpp连上去循环发消息编译完先跑服务端再跑客户端终端里就能看到收发日志。它适合两类人刚接触网络编程、想照着跑通第一个 TCP 程序的新手以及需要一份干净骨架、拿去做二次开发底座的熟手。整份材料从进程间通信讲到 socket 基本函数再落到三次握手、四次挥手和完整代码路径很直。2. 项目定位与技术选型为什么是 C 封装的 Socket 骨架2.1 这份资源解决的是什么问题网络编程入门最尴尬的地方在于教材把socket()、bind()、listen()、accept()讲得很细但真到动手时服务端和客户端怎么配合、哪个函数会阻塞、地址结构体怎么填全是空白。这份实例的价值就在于把“函数清单”变成了“可执行链路”。它没有引入任何第三方网络库直接基于 Linux 系统调用和 POSIX socket API用 C 类做了一层薄封装。mysocket类持有四个成员协议域m_idomain、socket 类型m_isoctype、协议m_iprotype、socket 描述符m_isocket。对外暴露createserver()、createclient()、recvmsg()、sedmsg()四个方法分别对应服务端初始化、客户端初始化、收消息循环、发消息循环。这种设计的好处是职责清晰server.cpp和client.cpp的main函数各自只有几行把“建连接”和“跑业务”分开。坏处也很明显——它是个教学骨架不是生产级封装后面改造章节会细说。2.2 技术栈与运行环境项目说明语言C兼容 C 风格系统调用网络 APIPOSIX socketTCP 协议关键头文件sys/socket.h、netinet/in.h、arpa/inet.h、unistd.h编译工具g运行平台LinuxmacOS 需微调头文件通信模型阻塞式 TCP单连接循环收发选 TCP 而不是 UDP是因为这份材料的重点在“连接建立与释放”的完整过程。三次握手、四次挥手这些概念只有落在connect()、accept()、close()的具体调用上才有体感。UDP 没有连接状态讲握手就无从谈起。提示代码里客户端硬编码了服务端 IP192.168.1.18这是原实例留下的坑。跑之前必须改成你本机或目标机器的实际 IP否则connect()直接失败。2.3 目录结构与文件职责整个实例只有四个源文件加两个空头文件结构极简mysocket/ ├── mysocket.h # 类声明四个成员变量 四个方法 ├── mysocket.cpp # 类实现socket/bind/listen/connect/recv/send 全在这里 ├── server.cpp # 服务端入口创建 mysocket 对象并调 createserver recvmsg ├── client.cpp # 客户端入口创建 mysocket 对象并调 createclient sedmsg ├── server.h # 空文件占位 └── client.h # 空文件占位server.h和client.h是空的说明作者原本可能想按“一头一实现”的方式组织后来简化掉了。二次开发时可以直接删掉这两个空文件不影响编译。真正承载逻辑的是mysocket.cpp服务端和客户端的所有系统调用都集中在这一个文件里。3. 核心链路拆解从 socket() 到 accept() 的完整调用顺序3.1 服务端初始化socket → bind → listen服务端的入口是createserver()它把三个系统调用串成一条线。先看代码bool mysocket::createserver(int domain, int soctype, int protype, int port) { m_idomain domain; m_isoctype soctype; m_iprotype protype; // 1. 创建 socket 描述符 m_isocket socket(m_idomain, m_isoctype, m_iprotype); if (-1 m_isocket) { printf(create socket error\n); return false; } // 2. 填充地址结构体并绑定 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; } // 3. 转为被动监听状态backlog 为 10 if (listen(m_isocket, 10) -1) { printf(listen socket error\n); return false; } printf(waiting for clients request\n); return true; }逐段看。socket(AF_INET, SOCK_STREAM, 0)三个参数分别指定 IPv4 协议族、流式套接字即 TCP、协议自动选择。第三个参数传 0 是常见做法系统会根据前两个参数推导出IPPROTO_TCP。如果这里传IPPROTO_UDP配SOCK_STREAM调用会失败——类型和协议不是随便组合的。bind()之前必须把sockaddr_in清零否则残留字段可能导致绑定到意外地址。sin_addr.s_addr htonl(INADDR_ANY)表示监听本机所有网卡INADDR_ANY的值是 0经过htonl转换后填入。sin_port htons(port)这一步是血泪经验端口号在内存里是主机字节序网络传输要求大端序不转就会绑到一个完全不相干的端口上。原材料里专门强调过“对主机字节序不要做任何假定”说的就是这里。listen()的第二个参数backlog设为 10表示已完成三次握手、等待accept()取走的连接队列最大长度。这个值不是并发上限只是排队上限生产环境一般调到 128 或SOMAXCONN。3.2 客户端连接socket → connect客户端走的是另一条路径不调bind()直接connect()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); // 关键这里必须改成服务端的实际 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; } 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()把点分十进制的 IP 字符串转成二进制网络序地址返回值小于等于 0 都算失败。客户端不调bind()是正常的内核会在connect()时自动分配一个临时端口和本机 IP。这也是为什么服务端必须bind()而客户端通常不用——服务端需要一个众所周知的地址供人连接客户端只需要一个能用的源地址。connect()触发 TCP 三次握手客户端发 SYN收到服务端 SYNACK 后connect()返回同时发出 ACK。也就是说connect()返回成功时三次握手已经完成了两次半最后一个 ACK 由内核补上。服务端的accept()则要等到收到这个 ACK 才返回。原材料里那句“客户端的 connect 在三次握手的第二次返回而服务器端的 accept 在第三次返回”说的就是这个时序差。3.3 收发循环accept 返回新描述符recv/send 跑业务服务端的recvmsg()先accept()再循环recv()void mysocket::recvmsg() { int connfd -1; char buff[1024] {0}; int n 0; // 阻塞等待客户端连接失败则重试 while (1) { if ((connfd accept(m_isocket, (struct sockaddr*)NULL, NULL)) -1) { printf(accept socket error: %s(errno: %d), strerror(errno), errno); continue; } break; } // 循环接收直到对端关闭recv 返回 0 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()返回的是connfd一个全新的描述符代表这条具体连接m_isocket是监听描述符始终留在原地等下一个连接。原材料的说法是“一个服务器通常只创建一个监听 socket 描述字内核为每个接受的客户连接创建一个已连接 socket 描述字”。这份代码只处理了一个连接就退出循环属于单连接演示版。recv()的返回值有三种情况大于 0 是实际读到的字节数等于 0 表示对端调用了close()小于 0 是出错。循环条件写成 0对端一关就自然退出。缓冲区大小 1024recv最多读 1023 字节留一个位置给\0这是 C 字符串处理的常规操作。客户端的sedmsg()更简单一个死循环里sprintf拼消息再sendvoid 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()返回小于 0 时打印错误但继续循环这在演示里没问题生产环境要区分EINTR被信号中断可重试和EPIPE对端已关闭应退出。strlen(buff)作为发送长度是对的因为消息是文本且以\n结尾不含\0。3.4 编译与运行两条命令跑起来编译命令很直接两个可执行文件分别链接mysocket.cpp# 编译服务端 g server.cpp mysocket.cpp -o server # 编译客户端 g client.cpp mysocket.cpp -o client运行顺序不能反先起服务端它会阻塞在accept()上打印等待提示再起客户端connect()成功后开始循环发消息。服务端终端会持续打印rec msg from client: msg N.客户端终端打印send msg succ。想停就 CtrlC。注意如果客户端报connect to server error先确认服务端 IP 改对了、服务端已启动、6666 端口没被占用。用ss -tlnp | grep 6666可以看端口监听状态。4. 避坑与排查这份实例跑不起来时的五个常见问题4.1 客户端连不上IP 硬编码是头号元凶现象客户端启动后立刻打印connect to server error 192.168.1.18服务端毫无反应。原因createclient()里inet_pton写死了192.168.1.18这个地址大概率不是你当前机器的 IP。connect()往一个不存在的地址发 SYN要么超时要么直接被拒。解决用ip addr或hostname -I查到本机实际 IP替换掉字符串。如果服务端和客户端在同一台机器上直接改成127.0.0.1最省事。改完重新编译客户端。4.2 绑定失败端口被占用或权限不足现象服务端打印bind socket error后退出。原因6666 端口已被其他进程占用或者你试图绑定 1024 以下的特权端口但没用 root 运行。解决先ss -tlnp | grep 6666看谁占着杀掉或换端口。换端口要同时改server.cpp和client.cpp里的createserver/createclient调用参数两边必须一致。低于 1024 的端口需要sudo但演示程序没必要用特权端口。4.3 收不到消息字节序没转或地址结构体没清零现象bind()成功但客户端连不上或者连上了收到的数据是乱码。原因sin_port忘了htons()端口被解释成另一个值或者sockaddr_in没memset清零残留的sin_zero字段干扰绑定。解决检查bind()前是否有memset(servaddr, 0, sizeof(servaddr))检查端口赋值是否走了htons()。IP 赋值用inet_pton而不是直接赋整数能避开大部分字节序问题。4.4 服务端只服务一个连接就退出现象第一个客户端断开后服务端进程直接结束第二个客户端连不上。原因recvmsg()里accept()只调了一次recv循环退出后函数返回main随之结束。这是单连接演示的设计不是 bug。解决想支持多连接把accept()放进外层while(1)每个connfd用fork()或线程处理。改造章节会给具体写法。4.5 编译报错找不到头文件现象g报sys/socket.h: No such file or directory。原因在非 Linux 环境比如某些精简容器或 Windows 的 MinGW下编译POSIX socket 头文件不存在。解决换到 Linux 或 WSL 下编译。macOS 上大部分头文件兼容但accept()的参数类型和close()的行为略有差异需要小改。这份实例的目标平台就是 Linux别在环境上硬扛。5. 二次开发改造把单连接演示改成能扛多客户端的小服务5.1 从单连接到多连接fork 还是线程原版recvmsg()只accept()一次服务完一个客户端就退出。要改成多客户端最省事的做法是在accept()外面套循环每来一个连接fork()一个子进程void mysocket::recvmsg() { char buff[1024] {0}; int connfd -1; while (1) { connfd accept(m_isocket, (struct sockaddr*)NULL, NULL); if (connfd -1) { printf(accept error: %s\n, strerror(errno)); continue; } pid_t pid fork(); if (pid 0) { // 子进程关闭监听描述符只处理这条连接 close(m_isocket); int n; while ((n recv(connfd, buff, 1023, 0)) 0) { buff[n] \0; printf(child %d recv: %s\n, getpid(), buff); memset(buff, 0, 1024); } close(connfd); exit(0); } else if (pid 0) { // 父进程关闭已连接描述符继续 accept close(connfd); } else { printf(fork error\n); } } }关键点在两处close()子进程关掉监听描述符m_isocket父进程关掉已连接描述符connfd。不关的话描述符引用计数永远不归零连接释放不了跑久了就是文件描述符耗尽。这是 fork 模型的经典写法也是我踩过坑之后每次都会检查的地方。线程模型同理把fork()换成pthread_create()注意connfd要传值而不是传地址否则多个线程可能拿到同一个描述符。5.2 把硬编码参数抽成配置原版 IP 和端口写死在代码里改一次要重编译。二次开发时至少把端口和 IP 抽成main的参数int main(int argc, char* argv[]) { int port (argc 1) ? atoi(argv[1]) : 6666; mysocket server; if (server.createserver(AF_INET, SOCK_STREAM, 0, port)) { server.recvmsg(); } return 0; }客户端同理把 IP 和端口都做成命令行参数。这样同一份二进制可以在不同环境跑不用每次改源码。再进一步可以读配置文件但对这个体量的骨架来说命令行参数已经够用。5.3 验证改造是否生效改完之后按这个顺序验证先起服务端开两个终端各起一个客户端观察服务端是否同时打印两个客户端的消息然后关掉其中一个客户端确认服务端另一个连接不受影响最后用ss -tnp | grep 6666看连接状态确认关闭的客户端进入TIME_WAIT而不是一直挂着。如果第二个客户端连上后服务端没反应多半是accept()循环没写对或者fork后描述符没关干净。5.4 一个容易忽略的收尾习惯这份实例的close()调用其实不完整recvmsg()和sedmsg()里都没有显式关闭connfd和m_isocket靠进程退出时内核回收。演示程序无所谓但二次开发时如果进程长期运行描述符泄漏是迟早的事。我现在的习惯是每个socket()和accept()返回的描述符都配一个明确的close()路径哪怕当前用不到也先把释放逻辑写在那里。从那以后我每次改这类网络骨架都强制走一遍“创建—使用—关闭”的配对检查希望帮到你。本文还有配套的精品资源点击获取