简介这是一份面向Linux网络编程初学者与进阶开发者的Socket编程学习资料围绕TCP/IP协议栈下进程间通信的核心机制展开帮助读者理解Socket接口原理并掌握实际编码方法。内容涵盖网络中进程通信的标识方式、Socket概念起源与文件抽象模型以及socket()、bind()、listen()、connect()、accept()、read()/write()、close()等基本函数的用法并详细解析TCP三次握手建立连接与四次挥手释放连接的过程最后附有实践示例供动手演练。资源包为1个docx文档约77KB以图文笔记形式组织结构清晰便于按章节顺序阅读或作为查阅手册。目前已有241人学习适合希望系统梳理Socket编程知识、为网络应用开发打基础的读者参考。1. 从一份“老派”Socket 实例说起为什么它仍是网络编程的活教材翻到一份 Linux Socket 编程的实例包第一反应往往是“这年头谁还手写 socket”。但真到排查线上服务连接超时、半连接队列溢出、或者客户端connect返回EINTR的时候能救命的恰恰是这些底层函数的行为细节。这份资源围绕 TCP 场景把socket()、bind()、listen()、connect()、accept()、recv()/send()、close()串成一条完整链路并配了一个可直接编译运行的服务端/客户端 Demo。它适合两类人一是刚接触网络编程、想搞懂“三次握手到底发生在哪个函数里”的新手二是写过业务代码、但对accept返回的新描述符和监听描述符的区别始终模糊的熟手。下面按“资源是什么 → 怎么跑起来 → 参数怎么调 → 坑在哪”的顺序拆一遍。2. 拆解实例代码结构mysocket 封装与编译链路2.1 四个文件的分工与依赖关系这份实例没有把逻辑堆在一个main.cpp里而是拆成mysocket.h、mysocket.cpp、server.cpp、client.cpp四个文件。mysocket类把服务端和客户端的连接建立、消息收发封装成成员函数server.cpp和client.cpp只负责调用。这种拆法在课程设计里很常见好处是服务端和客户端复用同一套 socket 操作改端口或改 IP 时只动一处。先看头文件暴露的接口// mysocket.h #ifndef _MYSOCKET_H #define _MYSOCKET_H #include sys/socket.h class mysocket { public: mysocket(); ~mysocket(); // 客户端创建 socket 并 connect 到指定端口 bool createclient(int domain, int soctype, int protype, int port); // 服务端创建 socket、bind、listen bool createserver(int domain, int soctype, int protype, int port); void recvmsg(); // 服务端循环接收 void sedmsg(); // 客户端循环发送 private: int m_idomain; // 协议族如 AF_INET int m_isoctype; // socket 类型如 SOCK_STREAM int m_iprotype; // 协议通常传 0 int m_isocket; // socket 描述符 }; #endif这里把domain、soctype、protype三个参数存成成员变量是为了在createclient和createserver里统一传给socket()。注意m_isocket只保存了监听或客户端主 socket服务端accept返回的已连接描述符是局部变量connfd没有存进类里——这是后面要重点说的一个设计取舍。2.2 服务端创建流程bind 与 listen 的参数落点createserver的职责是把 socket 变成被动监听状态。核心代码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) { // backlog 10 printf(listen socket error\n); return false; } printf(waiting for clients request\n); return true; }几个参数值得单独拎出来。INADDR_ANY表示绑定到0.0.0.0也就是本机所有网卡地址这样局域网内其他机器也能连进来如果只想本机测试可以换成inet_addr(127.0.0.1)。htons(port)把主机字节序转成网络字节序这一步漏掉的话端口号会变成另一个值服务端和客户端对不上。listen的第二个参数backlog传了 10它控制的是已完成连接队列和半连接队列的长度上限不是“最大连接数”。高并发场景下这个值偏小会导致新连接被丢弃或客户端收到 RST后面避坑章节会展开。2.3 客户端创建流程inet_pton 与 connect 阻塞点客户端侧的关键是地址转换和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把点分十进制的 IPv4 字符串转成二进制网络序地址比老式的inet_addr更安全因为它能区分255.255.255.255和错误返回值。connect在 TCP 下会触发三次握手默认是阻塞的内核发 SYN等对方回 SYNACK再回 ACK然后connect才返回。如果服务端没开或者防火墙拦了这里会卡住一段时间才报错。实例里把 IP 硬编码成192.168.1.18换环境必须改否则连不上。2.4 编译命令与运行顺序四个文件用 g 直接编译即可不需要额外链接库# 编译服务端 g -o server server.cpp mysocket.cpp # 编译客户端 g -o client client.cpp mysocket.cpp # 先启动服务端再启动客户端 ./server # 另开一个终端 ./client编译时如果报undefined reference to mysocket::...通常是漏掉了mysocket.cpp。运行顺序不能反先跑客户端会直接connect失败因为服务端还没listen。服务端启动后打印waiting for clients request客户端启动后打印Begin sending msg to server服务端侧会看到rec msg from client和递增的msg N。3. 从函数调用看 TCP 三次握手与四次挥手accept 到底卡在哪3.1 三次握手在 socket 函数中的映射很多人背过“三次握手”但说不清它对应代码里的哪一步。按这份实例的调用顺序映射关系是这样的握手步骤内核动作应用层函数状态第一次客户端发 SYN Jconnect()阻塞中第二次服务端回 SYN K ACK J1accept()阻塞中第三次客户端回 ACK K1connect()返回accept()随后返回关键结论客户端的connect在第二次握手后、收到 SYNACK 时就返回了此时它已经可以发数据服务端的accept要等到第三次握手的 ACK 到达才返回。这意味着服务端在accept返回之前连接其实已经在内核里建立好了只是应用层还没拿到那个已连接描述符。实例里recvmsg的while(1)循环中accept失败会continue这个写法在真实服务里要小心因为accept失败可能是EINTR也可能是资源耗尽无脑重试会打满 CPU。3.2 四次挥手与 close 的引用计数释放连接比建立多一次因为 TCP 是全双工的两个方向要各自关闭。实例里客户端sedmsg是死循环服务端recvmsg在recv返回 0 时退出内层循环但外层while(1)又会回到accept等下一个连接。close的行为容易被误解// close 只是把描述符引用计数减一 // 计数归零才真正发 FIN int close(int fd);服务端accept返回的connfd是内核新建的描述符和监听描述符m_isocket是两个独立对象。关闭connfd不影响m_isocket继续监听。实例里connfd是局部变量recvmsg内层循环结束后没有显式close(connfd)依赖进程退出或下一次循环覆盖——这在短生命周期 Demo 里能跑但长跑服务会泄漏描述符。常见做法是在内层循环结束后加close(connfd)把每个连接的生命周期收干净。3.3 recv 返回值的三种含义与处理recvmsg里的接收循环是理解 TCP 流式语义的好例子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; }recv返回值分三种大于 0 表示读到的字节数等于 0 表示对端关闭了连接收到 FIN小于 0 表示出错。这里只处理了大于 0 的情况等于 0 时循环退出小于 0 时也退出但没有打印错误。真实场景下要区分EINTR被信号中断可重试、ECONNRESET对端复位、EAGAIN非阻塞模式下无数据。另外recv不保证一次读完一条“消息”TCP 是字节流msg 1.\n和msg 2.\n可能被合并成一次recv返回也可能被拆成两次。实例里靠\n和printf观察生产代码需要自己定长度前缀或分隔符来切包。4. 参数调优与边界backlog、缓冲区与阻塞模式4.1 backlog 的真实含义与设置建议listen(m_isocket, 10)里的 10 经常被误认为“最多 10 个连接”。实际上内核为监听 socket 维护两个队列半连接队列收到 SYN、还没完成三次握手和已完成连接队列三次握手完成、等accept取走。backlog影响的是已完成队列的长度上限不同内核版本对半连接队列的处理还不一样。当队列满时新来的 SYN 可能被丢弃客户端会重传表现为连接变慢或超时。设置建议Demo 里 10 够用真实服务按并发量估算常见做法是设成somaxconn的值或略小。Linux 上可以用sysctl net.core.somaxconn查看系统上限默认常见是 128 或 4096。如果accept调用不够快队列会堆积此时应该优化accept后的处理逻辑而不是单纯调大backlog。4.2 发送与接收缓冲区的查看和调整send和recv的第三个参数是缓冲区长度但内核还有一层 socket 缓冲区。可以用setsockopt调整int sndbuf 64 * 1024; int rcvbuf 64 * 1024; setsockopt(m_isocket, SOL_SOCKET, SO_SNDBUF, sndbuf, sizeof(sndbuf)); setsockopt(m_isocket, SOL_SOCKET, SO_RCVBUF, rcvbuf, sizeof(rcvbuf));SO_SNDBUF影响send在阻塞模式下能一次性写入多少数据而不阻塞SO_RCVBUF影响内核能缓存多少未读数据。调大能提升吞吐但会占用更多内存。实例里没设这两个选项用的是系统默认值小消息场景没问题。如果发送大块数据发现send频繁部分返回可以先查SO_SNDBUF当前值再决定是否调整。4.3 阻塞与非阻塞的切换时机实例全程用阻塞模式connect阻塞到握手完成accept阻塞到有新连接recv阻塞到有数据。阻塞模式代码简单但一个连接卡住会影响后续连接处理。常见改造是把监听 socket 设成非阻塞用select/poll/epoll做多路复用int flags fcntl(m_isocket, F_GETFL, 0); fcntl(m_isocket, F_SETFL, flags | O_NONBLOCK);设成非阻塞后accept在没有新连接时返回 -1 并置errno为EAGAIN或EWOULDBLOCK需要配合事件循环使用。这份实例作为入门材料保持阻塞模式是合理的先理解同步流程再过渡到多路复用。如果直接拿它改高并发服务第一刀就是给监听 socket 加非阻塞并引入epoll。5. 避坑与排查那些让 Demo 跑不起来的细节5.1 端口被占用导致 bind 失败现象服务端启动打印bind socket error程序退出。原因6666 端口已被其他进程占用或者上一次运行的服务端没退干净。解决先用ss -tlnp | grep 6666或netstat -tlnp | grep 6666查占用进程杀掉或换端口。换端口时记得客户端createclient的port参数同步改两边不一致会表现为客户端connect失败。5.2 客户端 IP 写死导致 connect 超时现象客户端启动后卡住过很久打印connect to server error。原因inet_pton里的192.168.1.18不是当前服务端所在机器的 IP或者服务端和客户端不在同一网段。解决把 IP 改成服务端实际地址本机测试直接用127.0.0.1。如果服务端在另一台机器还要确认防火墙放行了对应端口否则 SYN 包被丢弃connect会一直重传到超时。5.3 字节序转换遗漏导致端口错乱现象服务端bind成功客户端connect也成功但双方打印的端口对不上或者连到了奇怪的服务。原因sin_port赋值时忘了htons或者sin_addr忘了htonl。解决牢记“赋给 socket 地址结构体的整数必须转网络序”。htons用于 16 位端口htonl用于 32 位地址。inet_pton内部已经处理了地址的字节序不需要再手动转。5.4 accept 返回的新描述符被误当监听描述符现象服务端处理完一个连接后第二个客户端连不上或者recv报错。原因把accept返回的connfd存进了m_isocket覆盖了监听描述符导致后续accept在已连接 socket 上调用。解决监听描述符和已连接描述符分开保存accept的第一个参数永远是监听描述符返回值才是新连接。实例里connfd是局部变量没有覆盖问题但改造时容易踩。5.5 recv 返回 0 未处理导致死循环现象客户端关闭后服务端 CPU 占用飙升。原因recv返回 0 表示对端关闭如果循环条件写成while(1)并在recv返回 0 时继续调用会一直返回 0形成忙等。解决recv返回值小于等于 0 时都要跳出接收循环等于 0 关闭连接小于 0 根据errno决定重试还是关闭。实例里while((n recv(...)) 0)已经处理了等于 0 的情况但内层循环退出后没有close(connfd)长跑会泄漏。6. 进阶改造把单连接 Demo 改成可复用的收发骨架6.1 给 recvmsg 加上连接关闭与错误分支原始recvmsg在内层循环退出后直接回到外层acceptconnfd没有关闭。改造如下void mysocket::recvmsg() { while (1) { int connfd accept(m_isocket, (struct sockaddr*)NULL, NULL); if (connfd -1) { if (errno EINTR) continue; // 被信号打断重试 perror(accept error); break; // 其他错误退出监听 } char buff[1024]; while (1) { memset(buff, 0, sizeof(buff)); ssize_t n recv(connfd, buff, sizeof(buff) - 1, 0); if (n 0) { printf(recv from connfd%d: %s (len%zd)\n, connfd, buff, n); } else if (n 0) { printf(client connfd%d closed\n, connfd); break; // 对端关闭 } else { if (errno EINTR) continue; // 中断重试 perror(recv error); break; } } close(connfd); // 关键释放已连接描述符 } }改动点有三个accept失败区分EINTR和其他错误recv返回值分三种情况处理内层循环结束后close(connfd)。这样服务端可以连续处理多个客户端不会泄漏描述符。6.2 用 SO_REUSEADDR 解决重启时的地址占用服务端主动关闭后连接会进入TIME_WAIT状态持续 2MSL通常 60 秒左右。这期间重新启动服务端bind会报Address already in use。加一行setsockopt即可int opt 1; setsockopt(m_isocket, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));这行要放在bind之前。SO_REUSEADDR允许绑定处于TIME_WAIT的地址对调试和快速重启很实用。注意它不影响正在监听的端口两个进程同时监听同一端口仍然会失败。6.3 验证改造是否生效的观察点改完之后按这个顺序验证启动服务端用ss -tlnp | grep 6666确认监听状态启动一个客户端观察服务端打印连接和消息按 CtrlC 关掉客户端服务端应打印client closed并回到waiting再启动一个客户端确认能正常收发。最后用ls /proc/server_pid/fd | wc -l看描述符数量反复连接断开后数量应保持稳定不持续增长。我一般改完这类骨架都会强制走一遍“连、发、断、再连”的循环确认没有描述符泄漏才算过。希望这份拆解帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?