简介这份资源面向Linux网络编程初学者与需要夯实Socket基础的开发者系统梳理了Socket通信的核心原理与常用接口。内容从网络中进程如何唯一标识讲起说明IP地址、协议与端口三元组的作用并解释Socket作为特殊文件的设计思想随后逐一介绍socket()、bind()、listen()、connect()、accept()、read()/write()及close()等基本操作配合TCP三次握手建立连接与四次挥手释放连接的详细过程最后给出一个可动手实践的服务端与客户端示例帮助读者把理论落到代码。资源包为1个docx文档约77KB结构紧凑适合作为随查随用的学习笔记或教学参考。目前已有241人学习内容覆盖从概念到实践的完整链路便于快速理解跨主机进程通信机制为后续开发Web服务器、聊天程序等网络应用打下基础。1. 从一份“转帖级”Socket 笔记说起它到底能不能跑起来很多人第一次搜 Linux Socket 编程翻到的都是同一类东西概念讲得挺全函数列了一堆真到编译运行就卡住。这份资源不一样它是一份带完整实例的转帖笔记原作者把 socket()、bind()、listen()、connect()、accept()、read()/write()、close() 这条链路串了一遍还配了一个能编译的 C 小工程mysocket 类封装服务端和客户端server.cpp 监听 6666 端口client.cpp 连上去发消息。它解决的不是“什么是 socket”这种科普问题而是“我照着写能不能跑通、跑不通该看哪一行”的落地问题。适合刚接触网络编程、想拿一份能直接编译的骨架改自己业务的人也适合工作几年但一直用框架、没亲手写过 accept 循环的人回头补课。2. 三元组与字节序Socket 通信绕不开的两个底层设定2.1 为什么是 IP 协议 端口而不是 PID本地进程通信靠 PID 就能定位但网络里 PID 只在单机内唯一跨主机就失效了。TCP/IP 给的方案是三元组IP 地址标识主机传输层协议加端口号标识主机上的应用进程。这三者组合起来才能在一堆并发连接里准确找到“这条数据该给谁”。理解这一点后面所有函数的行为就顺了。socket() 创建的是一个协议族空间里的“未命名端点”它知道自己属于 AF_INET 还是 AF_INET6但还没有具体地址。bind() 做的事就是把这个端点钉到某个 IP 和端口上。服务端必须 bind因为客户端需要一个众所周知的地址去连客户端通常不 bindconnect() 时内核会自动分配一个临时端口和本机 IP。这个不对称设计是很多新手第一次读代码时的困惑点。2.2 主机字节序转网络字节序一个曾引发血案的动作x86 是小端网络协议要求大端。你直接把sin_port 6666写进去在 x86 上跑起来可能“看起来正常”因为两端都是小端但一旦对端换成大端机器或者中间有设备按网络序解析端口号就完全错了。原笔记里那句“由于这个问题曾引发过血案”不是夸张我见过真实项目里因为漏了 htons调试了两天才定位到端口字段。正确做法是端口用htons()IPv4 地址用htonl()或inet_pton()不要对主机字节序做任何假定。下面这段是服务端 bind 前的标准准备动作struct sockaddr_in servaddr; memset(servaddr, 0, sizeof(servaddr)); servaddr.sin_family AF_INET; servaddr.sin_addr.s_addr htonl(INADDR_ANY); // 监听本机所有网卡 servaddr.sin_port htons(6666); // 端口必须转网络字节序INADDR_ANY表示不指定具体网卡 IP内核会帮你绑定到所有可用接口。如果你只想监听某一个网卡把htonl(INADDR_ANY)换成inet_pton(AF_INET, 192.168.1.18, servaddr.sin_addr)即可。参数上sin_family必须和 socket() 第一个参数一致sin_port和sin_addr必须走网络字节序memset清零不能省否则结构体里的填充字节可能是随机值。2.3 socket() 的三个参数怎么选socket(int domain, int type, int protocol)里domain 选 AF_INETIPv4或 AF_INET6type 选 SOCK_STREAMTCP或 SOCK_DGRAMUDPprotocol 一般填 0让系统根据 type 自动选默认协议。注意 type 和 protocol 不能乱配SOCK_STREAM 配 IPPROTO_UDP 是非法的。原实例里统一用socket(AF_INET, SOCK_STREAM, 0)这是 TCP 场景下最稳的写法。3. 把实例跑起来mysocket 类的编译、运行与参数调整3.1 文件结构与编译命令原实例给了四个文件mysocket.h、mysocket.cpp、server.cpp、client.cpp。server.h 和 client.h 是空的可以不管。编译时把 mysocket.cpp 分别和两个 main 一起编g -o server mysocket.cpp server.cpp g -o client mysocket.cpp client.cpp先起 server再起 client。server 会打印waiting for clients requestclient 连上后打印Begin sending msg to server然后循环发msg 0.、msg 1.这样的消息。server 端 recv 到后打印 connfd、消息内容和长度。3.2 客户端 IP 必须改原代码里写死了192.168.1.18这是最容易翻车的地方。如果你在本机测试改成127.0.0.1如果 server 在另一台机器改成那台机器的实际 IP。改的位置在createclient里的inet_pton调用// 本机测试用 127.0.0.1跨机测试换成 server 实际 IP if( inet_pton(AF_INET, 127.0.0.1, servaddr.sin_addr) 0){ printf(inet_pton error\n); return false; }inet_pton返回 1 表示成功0 表示地址格式不对-1 表示协议族不支持。比老的inet_addr好在能明确区分错误类型也不会把255.255.255.255误判为失败。3.3 accept 循环与 recv 循环的写法问题原实例的recvmsg()里accept 失败会 continue成功就 break 出 while(1)然后进入 recv 循环。这个写法只能处理一个客户端第二个客户端连上来时第一个连接没断accept 就不会再被调用。作为教学 demo 够用但你要改成能并发服务至少得把 accept 放进循环每个连接 fork 一个子进程或开线程。recv 循环里有个细节memset(buff, 0, 1024)放在 recv 之后、打印之前但buff[n] \0又依赖 n 是上一次 recv 的返回值。实际执行顺序是 recv 返回 n然后 memset 清空再buff[n] \0这会把第 n 个字节置零但前面 recv 写进去的数据已经被 memset 清掉了。正确顺序应该是先 recv再置零末尾再打印再清空。这个 bug 在原笔记里没被指出但你自己改的时候要注意。while((n recv(connfd, buff, 1023, 0)) 0){ buff[n] \0; // 先保证字符串结束 printf(recv: %s, len%d\n, buff, n); memset(buff, 0, 1024); // 再清空准备下一次 }recv 返回 0 表示对端关闭了连接返回 -1 表示出错需要看 errno。EINTR 表示被信号中断可以重试ECONNRESET 表示连接被对端重置。3.4 send 的返回值处理原实例的sedmsg()里send 返回值小于 0 时打印错误然后 continue没有区分 EINTR 和 EPIPE。EPIPE 表示对端已经关闭继续发没有意义应该 break 退出循环。EINTR 可以重试。另外 send 返回值大于 0 但不等于 strlen(buff) 时表示只发了一部分严格来说需要循环发送剩余部分。demo 里消息短一次发完的概率高但生产代码不能省这个判断。4. 三次握手与四次挥手connect 和 accept 到底卡在哪一步4.1 connect 在第二次握手返回accept 在第三次返回这是原笔记里最有价值的一句话。客户端调 connect 时内核发 SYN Jconnect 阻塞服务端收到 SYN Jaccept 被唤醒发 SYN K ACK J1accept 继续阻塞客户端收到 SYN K ACK J1connect 返回同时发 ACK K1服务端收到 ACK K1accept 返回。所以 connect 返回时连接对客户端来说已经建立accept 返回时服务端才拿到已连接 socket。理解这个时序你就能解释为什么有时候客户端 connect 成功了但服务端 accept 还没返回——第三次握手包还在路上。也能解释为什么 listen 的 backlog 参数重要如果半连接队列满了新的 SYN 会被丢弃客户端会重试。4.2 close 与引用计数close 一个 TCP socket 时如果引用计数不为 0不会立即发 FIN。只有最后一个引用被 close 时才触发四次挥手。这意味着如果你 fork 之后父子进程都持有同一个 socket父进程 close 不会关闭连接子进程还得再 close 一次。这个行为在写多进程服务器时经常被忽略导致连接泄漏。四次挥手的过程是主动关闭方发 FIN M被动方回 ACK M1同时把 FIN 作为文件结束符传给应用进程被动方处理完数据后 close发 FIN N主动方回 ACK N1。主动方最后会进入 TIME_WAIT 状态等 2MSL 才真正关闭。TIME_WAIT 是为了保证最后一个 ACK 能到达对端如果对端没收到会重发 FIN主动方还能再回一次 ACK。5. 避坑与排查这份实例改到能用之前会遇到的五件事5.1 bind 报 Address already in use现象server 刚跑完一次再启动就 bind 失败提示bind socket error。原因上一次的连接还处于 TIME_WAIT端口被占用。解决在 bind 之前加setsockopt设置 SO_REUSEADDRint opt 1; setsockopt(m_isocket, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));这个选项让内核允许绑定处于 TIME_WAIT 的端口是服务端代码的标配。5.2 客户端 connect 返回 Connection refused现象client 启动后立刻报connect to server error。原因server 没起或者 IP/端口写错或者防火墙拦了。解决先确认 server 在跑netstat -an | grep 6666看端口是否 LISTEN再确认 client 里的 IP 是 server 实际可达的地址本机测试用 127.0.0.1跨机测试确认网络连通。5.3 recv 收到乱码或长度不对现象server 打印的消息末尾有奇怪字符或者长度比预期大。原因recv 不保证按消息边界返回TCP 是字节流。你发两次msg 0.和msg 1.对端可能一次 recv 收到两条拼在一起。解决demo 里消息短且发送间隔小不容易复现但生产代码必须自己定协议比如固定长度、或者长度前缀、或者分隔符。原实例用\n结尾但没有按行解析只是打印所以看起来正常。5.4 accept 返回 -1 且 errno 为 EMFILE现象并发连接多了之后accept 开始失败errno 是 EMFILE。原因进程打开的文件描述符达到上限。每个 accept 返回的 connfd 都占一个 fd如果只 accept 不 close很快耗尽。解决ulimit -n看当前限制用close(connfd)及时关闭或者调大限制。原实例的 recv 循环里没有 close connfd客户端断开后 connfd 也没关长时间跑会泄漏。5.5 编译报 sockaddr_in 未定义现象g报storage size of servaddr isnt known或sockaddr_in was not declared。原因缺少头文件。原实例的 mysocket.cpp 里包含了netinet/in.h但如果你自己拆代码容易漏。解决确保包含sys/socket.h、netinet/in.h、arpa/inet.h。顺序上sys/socket.h要在netinet/in.h之前否则某些平台会报错。6. 从 demo 到可用把 mysocket 改成多连接服务的三个动作原实例只能服务一个客户端改到能同时处理多个连接最直接的办法是 accept 之后 fork 子进程。父进程继续 accept子进程处理 recv/send处理完 close connfd 退出。这样每个连接独立互不阻塞。但 fork 有代价进程创建开销、父子进程通信麻烦。更现代的做法是 epoll 或线程池不过那是另一个话题了。如果你只想先验证多连接fork 版最快。改动点在recvmsg()while(1){ int connfd accept(m_isocket, NULL, NULL); if(connfd -1){ continue; } if(fork() 0){ close(m_isocket); // 子进程不需要监听 socket char buff[1024]; int n; while((n recv(connfd, buff, 1023, 0)) 0){ buff[n] \0; printf(child recv: %s\n, buff); } close(connfd); // 子进程关闭已连接 socket exit(0); } close(connfd); // 父进程关闭已连接 socket引用计数减一 }这里有个关键点父进程 accept 返回的 connfd 和子进程继承的 connfd 指向同一个连接父进程必须 close 掉自己的那份否则引用计数不归零连接不会真正关闭。子进程也要 close 监听 socket否则监听 fd 泄漏。验证方法起 server同时起三个 clientserver 端应该能同时收到三路消息。用ps -ef | grep server能看到多个子进程。netstat -an | grep 6666能看到多个 ESTABLISHED 连接。从那以后我每次写 socket 服务端都会先把 SO_REUSEADDR、字节序转换、accept 循环、connfd 关闭这四件事过一遍再往下写业务逻辑。这份笔记的实例不算完美但它把从 socket() 到 close() 的完整链路摆出来了改一改就能当脚手架用。希望帮到你。本文还有配套的精品资源点击获取