
简介这是一份面向Linux网络编程初学者与进阶开发者的Socket编程学习资料围绕网络进程通信这一核心主题展开帮助读者理解跨主机进程如何借助TCP/IP协议栈完成数据交互。内容涵盖Socket概念起源与设计哲学、socket()、bind()、listen()、connect()、accept()、read()、write()、close()等基本操作函数并深入讲解TCP三次握手建立连接与四次挥手释放连接的过程最后附有实践示例与思考问题便于边学边练。资源包内含1个docx文档约77KB以文字讲解为主结构清晰适合作为系统梳理Socket原理与接口用法的参考笔记。目前已有241人学习适合希望夯实网络编程基础、准备面试或开发网络应用的读者查阅。1. 从一份“老派”Socket 实例说起为什么它仍是网络编程的活地图翻到这份《Linux Socket编程附实例》的时候我第一反应是这年头谁还手写socket()、bind()、listen()但真把代码拉下来跑一遍你会发现一个反直觉的事实——现在所有花哨的网络框架底层干的事跟这份实例里那几十行 C 代码一模一样。它解决的不是“怎么调库”而是“连接到底怎么建立、数据到底怎么流动”这个黑匣子问题。适合谁刚学完 Linux 常用命令、想搞懂socket网络编程到底在传什么的人也适合写了几年 Java/Python 却从没看过accept()返回值的熟手。这份资源用mysocket类把服务端和客户端封装成两个可执行文件服务端监听 6666 端口客户端连上就循环发消息麻雀虽小三次握手四次挥手全在里面。2. 拆开 mysocket 类socket()、bind()、listen() 到底在干什么2.1 从三元组到文件描述符socket 的选型逻辑网络进程通信绕不开一个死结本地靠 PID 认进程到了网络上 PID 根本不够用。TCP/IP 给的答案是三元组——IP 地址认主机协议加端口认进程。这份实例里createserver和createclient两个函数本质上就是在拼这个三元组。socket()函数返回的那个int值就是后续所有操作的“身份证”跟fopen返回的FILE*是一个哲学——Unix 的“一切皆文件”。选型上实例用的是AF_INET SOCK_STREAM 0。AF_INET决定地址是 32 位 IPv4 加 16 位端口SOCK_STREAM对应 TCP保证有序可靠第三个参数给 0让系统自动选IPPROTO_TCP。常见做法是只要不是做原始套接字抓包或 UDP 广播这三个参数基本就是默认组合。注意SOCK_STREAM不能跟IPPROTO_UDP硬凑类型和协议必须匹配否则socket()直接返回 -1。// mysocket.cpp 中创建服务端 socket 的核心片段 m_isocket socket(m_idomain, m_isoctype, m_iprotype); if (-1 m_isocket) { printf(create socket error\n); return false; }这段代码里m_isocket是类成员保存的就是那个文件描述符。逻辑说明先创建失败立刻返回不往后走。参数说明m_idomain传AF_INETm_isoctype传SOCK_STREAMm_iprotype传 0。我一般会在这里加一句perror(socket)比printf多带 errno 信息排错快得多。2.2 bind 与字节序那个曾引发血案的地方bind()把地址赋给 socket。服务端必须 bind因为要绑定众所周知的端口客户端不 bindconnect()时系统随机分配。实例里服务端绑定的是INADDR_ANY加htons(port)意思是监听本机所有网卡。这里有个血泪经验主机字节序和网络字节序不是一回事。x86 是小端网络字节序是大端。htons()和htonl()就是干转换的。原文里说“由于这个问题曾引发过血案”我信——端口号不转绑上去的端口跟你以为的完全不是同一个客户端连半天连不上查半天查不出。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; }逻辑说明先清零结构体再填 family、地址、端口最后强转成struct sockaddr*传给bind。参数说明INADDR_ANY表示任意本机地址htons(port)把主机序端口转网络序。memset不能省否则sin_zero填充字段是脏数据某些系统上bind会失败。2.3 listen 与 accept监听 socket 和已连接 socket 的分工listen()把主动 socket 变被动第二个参数backlog是排队上限实例给的是 10。这个值不是“最大连接数”而是“已完成三次握手但还没被 accept 取走的队列长度”。给太小高并发下客户端会收到连接重置给太大内核会截断到系统上限。accept()是整份实例里最容易被误解的函数。它返回的是一个全新的描述符代表与某个客户端的 TCP 连接。原来的监听 socket 继续监听生命周期贯穿整个服务端进程。实例里recvmsg()先accept拿到connfd然后循环recv。while (1) { 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失败不退出continue重试成功后跳出循环进入recv循环。参数说明accept后两个参数传 NULL表示不关心客户端地址。recv的 1023 是留一个字节给\0。注意memset放在recv之后、打印之前顺序反了会把刚收到的数据清掉。3. 把实例跑起来编译、连接与消息循环的完整复现3.1 文件结构与编译命令实例给了四个文件mysocket.h、mysocket.cpp、server.cpp、client.cpp。server.h和client.h是空的可以不管。编译分两步先生成目标文件再链接。# 编译服务端 g -c mysocket.cpp -o mysocket.o g -c server.cpp -o server.o g mysocket.o server.o -o server # 编译客户端 g -c client.cpp -o client.o g mysocket.o client.o -o client逻辑说明-c只编译不链接生成.o最后一步把mysocket.o和各自的 main 链接成可执行文件。参数说明如果mysocket.cpp里用了strerror需要#include string.h用了inet_pton需要#include arpa/inet.h。原文代码里这两个头文件都有但顺序上arpa/inet.h放在最后某些老编译器会报隐式声明挪到前面更稳。3.2 客户端 IP 与端口参数怎么改createclient里硬编码了192.168.1.18这是服务端的 IP。如果你在本机跑改成127.0.0.1如果服务端在另一台机器改成那台机器的实际 IP。端口两边都是 6666必须一致。// 红底色 ip 需要修改为 server 的 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; }逻辑说明inet_pton把点分十进制转成二进制返回值小于等于 0 说明 IP 格式不对。connect返回 -1 说明连接失败常见原因是服务端没起、IP 写错、防火墙拦了。参数说明AF_INET对应 IPv4servaddr里已经填好 family、port、addr。3.3 消息循环与发送节奏客户端sedmsg()是个死循环每轮sprintf一条msg N.然后send。服务端recvmsg()收到后打印。这个设计有个明显问题客户端发太快服务端打印跟不上缓冲区会堆。实际调试时我一般会在客户端send后加sleep(1)或者服务端recv后加个计数方便观察。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; } }逻辑说明memset清空缓冲区sprintf格式化消息send发送。失败不退出continue重试。参数说明strlen(buff)是实际消息长度不要用sizeof(buff)否则会把 1024 字节全发出去接收端读到一堆\0。4. 避坑与排查accept 返回值、字节序和 close 的引用计数4.1 accept 返回 -1 但 errno 是 EINTR现象服务端跑着跑着accept返回 -1打印Interrupted system call。原因信号中断了阻塞的系统调用。解决判断errno EINTR就continue不要当致命错误退出。实例里已经这么做了但很多人抄代码时把continue改成return服务端直接挂掉。4.2 客户端连不上报 Connection refused现象connect返回 -1errno是ECONNREFUSED。原因服务端没启动或者端口被防火墙拦了或者 IP 写错。解决先在服务端机器上netstat -anp | grep 6666确认监听存在再从客户端telnet 服务端IP 6666测连通性。如果telnet不通问题在网络层不在代码。4.3 收到乱码或端口对不上现象服务端打印的端口号跟客户端连的不一样或者收到的数据是乱码。原因htons漏了或者sin_family没设对。解决检查servaddr.sin_port htons(port)和servaddr.sin_family AF_INET。字节序问题不会报错只会让行为诡异属于典型的玄学翻车点。4.4 close 之后对方还能发数据现象一端close了另一端send居然还成功。原因close只是引用计数减一TCP 层面还有四次挥手的过程。解决理解close不等于立即断开主动关闭方会发 FIN被动方收到后回 ACK数据还能在反向通道上走一会儿。要强制立即关闭用shutdown(fd, SHUT_RDWR)再close。4.5 客户端循环发消息导致服务端缓冲区堆积现象服务端打印越来越慢最后卡死。原因客户端send没有节流服务端recv处理速度跟不上TCP 接收缓冲区满了之后send阻塞。解决客户端加usleep(100000)或者服务端用多线程/多进程处理连接。实例是单线程循环recv只适合演示不适合压测。5. 从实例到生产用 strace 验证三次握手与四次挥手5.1 用 strace 看 socket 系统调用实例跑起来之后别急着关。开一个终端strace -f -e tracenetwork ./server再跑客户端你能看到socket、bind、listen、accept、recv、send的完整调用序列。这比看任何时序图都直观——三次握手发生在connect和accept之间四次挥手发生在close之后。strace -f -e tracenetwork ./server 21 | grep -E socket|bind|listen|accept|recv|send逻辑说明-f跟踪子进程-e tracenetwork只看网络相关系统调用。参数说明输出里accept返回的新 fd 就是connfd跟监听 fd 不是同一个数字。我一般会对比accept前后的 fd 变化确认“监听 socket 和已连接 socket 是两个东西”这个结论。5.2 用 tcpdump 抓包看 SYN 和 FIN更底层一点tcpdump -i lo port 6666 -nn抓本机回环流量。客户端一跑你能看到[S]、[S.]、[.]三个包就是三次握手close之后看到[F.]、[.]、[F.]、[.]就是四次挥手。这个验证方法比背八股文管用因为包是你自己发出来的。抓包标志含义对应函数[S]SYN客户端connect触发[S.]SYNACK服务端accept前内核回复[.]ACK客户端确认connect返回[F.]FINACK主动关闭方close触发[P.]PSHACKsend/recv数据5.3 一个我常犯的错误早些年我总以为accept返回的 fd 跟监听 fd 是同一个直到有次写多连接服务端把connfd和m_isocket搞混recv一直读监听 socket返回 -1 还查不出原因。从那以后我每次写 socket 代码都强制在accept后面打一行日志把监听 fd 和连接 fd 都打出来确认是两个不同的数字。这个习惯帮我省了至少两次通宵排查。希望帮到你。本文还有配套的精品资源点击获取