2026-05-27 TCP 通信基础流程服务端客户端OS视角服务端六步走socket()→bind()→listen()→accept()→recv()/send()→close()客户端五步走socket()→connect()→send()→recv()→close()服务端完整代码#includeiostream#includesys/types.h#includesys/socket.h#includenetinet/in.h#includearpa/inet.h#includeunistd.h#includestring.h#definePORT8080#defineIP0.0.0.0#defineBACKLOG10intmain(){// 1 创建套接字(哪个协议簇,TCP/UDP,要不要指定协议)intsockfdsocket(AF_INET,SOCK_STREAM,0);if(sockfd0){perror(socket);return-1;}// 2 绑定套接字(IP 负责互联网级的寻址找到机器端口负责机器级的寻址找到进程)structsockaddr_inaddr;addr.sin_familyAF_INET;addr.sin_porthtons(PORT);addr.sin_addr.s_addrinet_addr(IP);intretbind(sockfd,(structsockaddr*)addr,sizeof(addr));if(ret0){perror(bind);return-1;}// 3 监听套接字retlisten(sockfd,BACKLOG);if(ret0){perror(listen);return-1;}// 4 获取新连接structsockaddr_inclient_addr;socklen_t client_lensizeof(client_addr);intnewfdaccept(sockfd,(structsockaddr*)client_addr,client_len);if(newfd0){perror(accept);return-1;}std::cout客户端 IP: inet_ntoa(client_addr.sin_addr) 端口: ntohs(client_addr.sin_port)std::endl;// 接收数据charbuf[1024]{0};ssize_t nrecv(newfd,buf,sizeof(buf),0);if(n0){perror(recv);close(newfd);close(sockfd);return-1;}// 发送数据constchar*msgserver recieved!\n;send(newfd,msg,strlen(msg),0);// 5 关闭套接字close(newfd);close(sockfd);}客户端完整代码#includeiostream#includesys/types.h#includesys/socket.h#includenetinet/in.h#includearpa/inet.h#includeunistd.h#includestring.h#definePORT8080#defineIP127.0.0.1// 客户端必须指定服务端具体 IP不能用 0.0.0.0intmain(){// 1 创建套接字intsockfdsocket(AF_INET,SOCK_STREAM,0);if(sockfd0){perror(socket);return-1;}// 2 建立连接隐式 bind 三次握手structsockaddr_inaddr;addr.sin_familyAF_INET;addr.sin_porthtons(PORT);addr.sin_addr.s_addrinet_addr(IP);intretconnect(sockfd,(structsockaddr*)addr,sizeof(addr));if(ret0){perror(connect);return-1;}// 3 发送数据constchar*msg我是客户端;send(sockfd,msg,strlen(msg),0);// 用 strlen 不是 sizeof// 4 接收数据charbuf[1024]{0};ssize_t nrecv(sockfd,buf,sizeof(buf),0);if(n0){perror(recv);close(sockfd);return-1;}std::cout客户端接收的数据bufstd::endl;// 5 关闭套接字close(sockfd);}客户端 vs 服务端关键区别服务端客户端IP 填什么INADDR_ANY接收所有网卡的连接具体 IP127.0.0.1或服务器地址端口谁绑显式bind()connect()隐式绑定随机端口要 listen 吗必须listen()不需要要 accept 吗必须accept()获取新连接不需要connect()返回后直接收发有几个 fd两个监听 fd 新连接 fd一个自身 fd各接口解决的问题socket() — 创建套接字intsocket(intdomain,inttype,intprotocol);domain (AF_INET)用什么网络协议族IPv4/IPv6/本地域type (SOCK_STREAM)传输层用什么方式TCP流式/UDP数据报protocol (0)要不要指定具体协议默认即可bind() — 绑定地址intbind(intsockfd,conststructsockaddr*addr,socklen_t addrlen);sockfd给哪个 socket 绑定地址addr (struct sockaddr_in)IP 端口IP 负责互联网级寻址找到机器端口负责机器级寻址找到进程INADDR_ANY 接收所有网络接口的连接htons()将端口转成网络字节序大端addrlen地址结构体的大小解决通用指针不知道具体结构多大的问题为什么用sockaddr*而不是sockaddr_in*C 语言的类型擦除一套接口适配 IPv4/IPv6/本地域多种地址listen() — 设置被动监听intlisten(intsockfd,intbacklog);把主动套接字变成被动套接字等着被连而不是主动连别人backlog内核等待队列的最大长度防流量洪峰accept() — 获取新连接intaccept(intsockfd,structsockaddr*addr,socklen_t*addrlen);从内核等待队列中取出一个连接返回全新的 fd用于通信原来的 sockfd 继续监听addr传出参数获取客户端 IP 和端口addrlen传入传出参数必须先初始化为 sizeof(client_addr)recv() / send() — 收发数据ssize_trecv(intsockfd,void*buf,size_t len,intflags);ssize_tsend(intsockfd,constvoid*buf,size_t len,intflags);recv 返回值0 读到字节数0 对端关闭0 出错send 用strlen(msg)而非sizeof(msg)后者是指针大小connect() — 客户端发起连接intconnect(intsockfd,conststructsockaddr*addr,socklen_t addrlen);向服务端发起三次握手SYN → SYNACK → ACK如果 socket 还没 bind内核自动分配随机端口隐式绑定addr 填服务端的 IP 和端口不是本机地址踩坑记录宏定义后面不能加分号#define PORT 8080不是#define PORT 8080;bind 必须检查返回值端口可能被占用inet_addr需要#include arpa/inet.hclose需要#include unistd.hIP 字符串不能直接赋给 s_addrs_addr是整数需要用inet_addr()转换客户端 connect 不能用 0.0.0.0必须写服务端具体 IP0.0.0.0只能用于服务端 bindsizeof(msg)不是字符串长度const char*的 sizeof 返回指针大小8用strlen()当前限制这个简单版本一次只能处理一个客户端——accept 后 recv 阻塞其他客户端连不上。需要用 epoll 非阻塞 I/O 解决。每一行代码执行时 OS 在做什么第 0 步程序启动前Shell 调用fork()execve()内核创建新进程分配 PID 和 task_struct进程控制块分配虚拟地址空间页表加载可执行文件到内存跳转到 main()此时进程的文件描述符表基本为空0/1/2 被 stdin/stdout/stderr 占用。socket() — 内核分配通信端点内核分配struct socket记录协议族IPv4、类型TCP流式、状态刚创建在进程的 fd 表中找最小空闲位置通常是 3挂上 socket 结构体指针内核预分配发送缓冲区和接收缓冲区变化仅在内存中多了内核数据结构无网络包发出端口未被占用。bind() — 给 socket 挂上地址内核将 IP0.0.0.0和端口8080写入之前分配的struct socket内核在内部的端口哈希表中注册端口 8080 → 这个 socket之后网卡收到目标端口 8080 的 TCP 包内核查表就能交给这个 socket变化端口 8080 被占用其他程序不能再 bind 同一端口。网络包过滤规则生效。listen() — 开启接客模式内核将 socket 状态从TCP_CLOSE改为TCP_LISTEN内核创建半连接队列SYN queue和全连接队列accept queue容量 backlogsocket 开始响应 SYN 包收到 SYN → 回 SYNACK → 对方回 ACK → 连接进入全连接队列变化端口真正开始工作客户端能三次握手了但连接请求只能排队等 accept。accept() — 取出一个已完成的连接根据 sockfd 找到监听 socket状态 TCP_LISTEN检查全连接队列队列为空→ 进程进入阻塞状态CPU 调度其他进程运行队列有东西→ 从队列取出第一个已完成三次握手的连接为这个新连接在进程 fd 表中分配全新的 fd如 4新 socket 状态是 TCP_ESTABLISHED填充客户端地址到client_addr唤醒进程返回新 fd此时进程的 fd 表fd用途状态3sockfd监听TCP_LISTEN继续等新连接4newfd通信TCP_ESTABLISHED可以收发数据类比餐厅前台分配座位号服务员按座位号服务前台继续接待下一个客人。fd 编号0/1/2 是 stdin/stdout/stderrsocket() 拿到 3accept() 拿到 4、5、6…recv() — 从接收缓冲区拷数据检查内核接收缓冲区有没有数据没有→ 进程阻塞睡觉CPU 切走有→ 内核把数据从内核缓冲区拷贝到用户空间 buf拷贝量 min(你要的, 缓冲区有的)已拷贝的数据从内核缓冲区移除腾空间返回实际拷贝的字节数返回值含义 0实际收到多少字节 0对端关闭连接优雅断开发来 FIN 0出错连接异常断开等TCP 粘包问题客户端发 “hello”recv 可能只收到 “hel”剩余 “lo” 下次 recv 才到。这也是为什么项目需要 Buffer 模块。send() — 把数据塞进发送缓冲区把用户空间的msg拷贝到内核发送缓冲区如果空间不够则阻塞等返回实际拷贝的字节数异步内核 TCP 协议栈后续处理切段 → 加 TCP 头 → 加 IP 头 → 网卡发出关键认知send() 返回成功 ≠ 对端收到了。send() 只保证数据进了内核缓冲区后续的传输、重传、确认是内核 TCP 协议栈的事。类比你把信丢进邮筒拷贝到内核缓冲区邮递员什么时候来取、邮件路上了几天——你不用管邮局保证送到。close() — 回收资源close(newfd) — 关闭通信连接检查发送缓冲区是否还有未发完的数据如果有直接关不等——所以项目需要优雅关闭发起四次挥手主动关闭方FIN → ACK ← FIN → ACK回收 socket 结构体、发送/接收缓冲区进程 fd 表中位置 4 被清空编号 4 可被复用close(sockfd) — 关闭监听TCP_LISTEN → TCP_CLOSE清空全连接队列和半连接队列所有排队请求被丢弃端口 8080 从内核端口哈希表注销其他程序可以 bind 了回收 fd 3关闭顺序先 close(newfd) 再 close(sockfd)先关个人连接再关大门。字节序与转换函数为什么需要转换不同 CPU 架构存整数的方式不同大端高位字节在低地址网络传输规定用大端小端低位字节在低地址Intel/AMD x86 是小端TCP/IP 协议规定网络上传输必须用大端。如果你的机器是小端发送前必须翻转字节序。函数方向作用例子htons(n)Host → Network Short16位端口号转换htons(8080)ntohs(n)Network → Host Short16位打印端口前转回来ntohs(client_addr.sin_port)htonl(n)Host → Network Long32位IP 转换很少直接用inet_addr(s)字符串 → 网络字节序 32 位整数“127.0.0.1” → 整数inet_addr(127.0.0.1)inet_ntoa(n)网络字节序整数 → 字符串整数 → “127.0.0.1”inet_ntoa(client_addr.sin_addr)原则存的时候转网络字节序htons/inet_addr读出来展示时转主机字节序ntohs/inet_ntoa。全流程回顾图你的程序 (用户空间) Linux 内核 ════════════════════════ ═══════════════════════════════ │ │ │ ① socket(AF_INET,SOCK_STREAM,0) │ │────────────────────────────────────→│ 分配 struct socket │ sockfd3 │ 协议IPv4, 类型TCP │ │ 分配 发送缓冲区 接收缓冲区 │ │ fd表: [3]→socket │ │ │ │ │ ② bind(3, 0.0.0.0:8080) │ │────────────────────────────────────→│ socket 写入 IP0.0.0.0, PORT8080 │ ret0 │ 端口哈希表注册: 8080→socket │ │ │ ┌────────────┤ 此后网卡收到目标端口8080的包 │ │ 端口哈希表 │ 内核能查表交给这个socket │ │ 8080→sock │ │ └────────────┤ │ │ │ ③ listen(3, backlog10) │ │────────────────────────────────────→│ TCP_CLOSE → TCP_LISTEN │ ret0 │ 创建 半连接队列(SYN Queue) │ │ 创建 全连接队列(Accept Queue) │ │ 容量10 │ │ │ │ 内核开始自动处理三次握手: │ 客户端──SYN──→ │ 进入半连接队列 │ 客户端←──SYNACK── │ │ 客户端──ACK──→ │ 握手完成→移入全连接队列 │ │ │ │ │ ④ accept(3) │ │────────────────────────────────────→│ 检查全连接队列 │ 阻塞等待... │ ┌──────────┐ │ (进程睡觉, CPU切走) │ │ 连接1 ✓ │ ← 取出 │ │ │ 连接2 ✓ │ │ newfd4 │ │ 连接3 ✓ │ │←────────────────────────────────────│ └──────────┘ │ │ 为连接1分配新 socket(fd4) │ 现在有两个fd: │ 状态: TCP_ESTABLISHED │ fd3监听(继续等新连接) │ fd表: [3]→监听socket │ fd4通信(和客户端1对话) │ [4]→通信socket │ │ │ │ │ ⑤ recv(4, buf, 1024, 0) │ │────────────────────────────────────→│ 检查 fd4 的接收缓冲区 │ 阻塞等待... │ ┌────────────────┐ │ (进程睡觉, CPU切走) │ │ 我是客户端 │ ← 客户端发的数据 │ │ └────────────────┘ │ n13 │ 拷贝到用户空间 buf │←────────────────────────────────────│ 清除已读数据(腾空间) │ buf 我是客户端 │ │ │ │ │ │ ⑥ send(4, server recieved!\n, 18) │ │────────────────────────────────────→│ 拷贝 msg 到 fd4 的发送缓冲区 │ ret18 │ ┌──────────────────┐ │ │ │server recieved! │ │ send返回≠对端收到! │ └────────┬─────────┘ │ (数据刚进内核缓冲区) │ │ 内核异步处理: │ │ │ 切段→加TCP头→加IP头 │ │ │ →网卡驱动→网线发出 │ │ ↓ │ │ 客户端收到 server recieved!\n │ │ │ │ │ ⑦ close(4) — 关通信连接 │ │────────────────────────────────────→│ 发起四次挥手: │ │ 我方 FIN → │ │ 对方 ACK ← │ │ 对方 FIN ← │ │ 我方 ACK → │ │ 回收 fd4 的 socket缓冲区 │ │ fd表: [4] 清空 │ │ │ │ │ ⑧ close(3) — 关监听 │ │────────────────────────────────────→│ TCP_LISTEN → TCP_CLOSE │ │ 清空全连接半连接队列 │ │ 端口哈希表注销 8080 │ │ 回收 fd3 的 socket │ │ fd表: [3] 清空 │ │ │ main() return 0 │ 进程退出回收所有资源 │ 进程结束 │ ════════════════════════ ═══════════════════════════════关键数据结构演变socket() 后: bind() 后: listen() 后: ┌─────────┐ ┌─────────────┐ ┌──────────────────┐ │ fd表 │ │ fd表 │ │ fd表 │ │ [3]→socket│ │ [3]→socket │ │ [3]→socket(LISTEN)│ └─────────┘ │ │ │ │ │ 端口哈希表 │ │ SYN Queue: [] │ │ 8080→[3] │ │ Accept Queue: [] │ └─────────────┘ └──────────────────┘ accept() 后: recv() 后: ┌─────────────────────┐ ┌─────────────────────────┐ │ fd表 │ │ fd表 │ │ [3]→socket(LISTEN) │ │ [3]→socket(LISTEN) │ │ [4]→socket(ESTABLISHED)│ │ [4]→socket(ESTABLISHED) │ │ │ │ 接收缓冲区: 空(已拷走) │ │ SYN Queue: [] │ │ 发送缓冲区: 空 │ │ Accept Queue: [连接2] │ ← 又来一个 │ buf[1024]我是客户端 │ └─────────────────────┘ └─────────────────────────┘编译运行Makefileall: server client server: server.cc g $^ -o $ -stdc11 client: client.cc g $^ -o $ -stdc11 .PHONY: clean clean: rm -rf server client运行make# 编译服务端和客户端./server# 终端1先启动服务端./client# 终端2再启动客户端