尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

《TCP 客户端代码逐行寻宝:三次握手、死循环 close 的谜底全拆解》

《TCP 客户端代码逐行寻宝:三次握手、死循环 close 的谜底全拆解》
📅 发布时间:2026/7/2 5:42:15

一、文档说明

很多同学入门 TCP 网络编程时,写下的第一份客户端代码往往只有短短几十行:创建套接字、连接服务端、循环收发数据。看似简单的代码里,却藏着好几个新手必问的灵魂问题:

  • 三次握手到底是哪个函数执行的?我没写 SYN 相关的代码啊?
  • 明明 TCP 和 UDP 代码长得差不多,为啥一个可靠一个不可靠?
  • 主逻辑是while(1)死循环,结尾的close根本跑不到,写了不是多余吗?
  • 调用close就等于直接触发四次挥手吗?

本文逐行拆解这份经典 TCP 客户端代码,把语法规则、系统调用、内核 TCP 协议行为全部讲透,顺便把所有疑问一次性解答清楚。

二、完整代码总览

这是一份 Linux 环境下标准的TCP 回显客户端:连接本地 8888 端口的服务端,读取用户键盘输入并发送,接收服务端返回的回显内容并打印,连接断开后自动退出。

c

运行

// tcp_client.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 8888 #define BUF_SIZE 1024 int main() { // 1. 创建通信socket int sock_fd = socket(AF_INET, SOCK_STREAM, 0); if (sock_fd < 0) { perror("socket创建失败"); exit(EXIT_FAILURE); } // 2. 配置服务端地址 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) { perror("IP地址格式错误"); close(sock_fd); exit(EXIT_FAILURE); } // 3. 发起连接(底层对应TCP三次握手) if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("连接服务端失败"); close(sock_fd); exit(EXIT_FAILURE); } printf("连接服务端成功,请输入要发送的内容:\n"); char buf[BUF_SIZE]; while (1) { memset(buf, 0, BUF_SIZE); // 从终端读取用户输入 fgets(buf, BUF_SIZE, stdin); // 发送数据到服务端 send(sock_fd, buf, strlen(buf), 0); // 接收服务端回显 memset(buf, 0, BUF_SIZE); ssize_t recv_len = recv(sock_fd, buf, BUF_SIZE - 1, 0); if (recv_len <= 0) { printf("服务端断开连接\n"); break; } printf("收到服务端回显:%s", buf); } close(sock_fd); // 关闭连接(底层对应TCP四次挥手) return 0; }

三、逐模块逐行深度详解

3.1 头文件与宏定义:编程的前置工具箱

c

运行

#include <stdio.h> // 标准输入输出:printf、perror、fgets #include <stdlib.h> // 标准库:程序退出exit、状态码宏 #include <string.h> // 内存/字符串操作:memset、strlen #include <unistd.h> // Unix系统调用:close、基础IO操作 #include <sys/socket.h> // Socket核心API:socket、connect、send、recv #include <netinet/in.h> // IPv4地址结构:sockaddr_in、字节序转换函数 #include <arpa/inet.h> // IP地址转换:inet_pton(字符串转二进制IP) #define PORT 8888 // 目标服务端端口 #define BUF_SIZE 1024 // 收发缓冲区最大字节数

这是 Linux Socket 编程的标配头文件,所有网络相关的系统调用、数据结构都定义在这里。缓冲区 1024 是入门示例的常用值,实际项目会根据业务消息大小灵活调整。

3.2 创建套接字:申请一条专属通信线路

c

运行

int sock_fd = socket(AF_INET, SOCK_STREAM, 0); if (sock_fd < 0) { perror("socket创建失败"); exit(EXIT_FAILURE); }
函数核心说明

socket()函数的作用是向操作系统内核申请一个网络套接字,返回一个文件描述符(fd)。Linux 下 “一切皆文件”,socket 本质也是一个文件,后续所有收发、连接操作都通过这个 fd 来标识。

三个参数分别对应:

  1. AF_INET:协议族,指定使用 IPv4 协议;对应 IPv6 则填AF_INET6
  2. SOCK_STREAM:套接字类型,指定流式传输,对应 TCP 协议
  3. 0:使用对应类型的默认协议,SOCK_STREAM默认就是 TCP,无需额外指定
灵魂追问:TCP 和 UDP 代码开头几乎一样,区别到底在哪?

很多同学觉得 TCP 和 UDP 代码长得像,核心原因是Socket 是操作系统设计的一套通用编程接口,不管底层用什么传输协议,创建套接字、绑定地址的 API 格式都是统一的。

两者真正的分水岭,就是第二个参数:

  • 填SOCK_STREAM:内核会加载完整的 TCP 协议栈,自动处理连接管理、可靠重传、流量控制、拥塞控制
  • 填SOCK_DGRAM:内核加载 UDP 协议栈,只负责发送数据报,不保证送达、不保证顺序

可以理解为:同样是 “买一部手机”,外观、按键操作都差不多,但一个插的是有线保障专线,一个插的是无保障广播频道,底层能力天差地别。

3.3 配置服务端地址:填好对方的 “通信门牌”

c

运行

struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) { perror("IP地址格式错误"); close(sock_fd); exit(EXIT_FAILURE); }

这一步是在内存中组装目标服务端的 “IP + 端口” 地址,后续connect要靠这个地址找到网络上的目标程序。

关键细节拆解
  1. 内存清零:memset把结构体内存全部置 0,避免内存脏数据导致地址解析错误,是网络编程的标准安全写法。
  2. 字节序转换htons: 不同 CPU 的内存存储字节顺序(大端 / 小端)不同,网络传输统一规定使用大端字节序(网络字节序)。htons= host to network short,把主机字节序的 16 位端口号转换成网络字节序,保证跨设备兼容性。
  3. IP 地址转换inet_pton: 把人类可读的点分十进制字符串 IP(如"127.0.0.1")转换成内核能识别的二进制网络地址。p代表字符串展示格式,n代表网络二进制格式。
  4. 出错兜底:IP 格式错误时,先调用close(sock_fd)再退出 —— 因为前面已经成功申请了套接字,直接退出会导致文件描述符泄漏。

3.4 发起连接:按下拨号键,完成三次握手

c

运行

if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("连接服务端失败"); close(sock_fd); exit(EXIT_FAILURE); } printf("连接服务端成功,请输入要发送的内容:\n");
函数核心说明

connect()是客户端发起 TCP 连接的入口函数,三个参数分别是套接字 fd、目标服务端地址、地址结构体长度。 注意地址参数必须强转为struct sockaddr*通用类型 —— 这是历史设计原因:Socket API 要兼容所有协议族,所以用通用地址结构体做入参,实际传入对应协议的地址结构体即可。

灵魂追问:三次握手是这个函数执行的吗?我没写发 SYN 的代码啊?

三次握手全程由操作系统内核的 TCP 协议栈自动完成,应用层代码不需要手动发任何控制报文,connect()只是触发三次握手的启动开关。

调用connect()后,内核会自动完成三步:

  1. 发送第一次握手的 SYN 报文,客户端进入SYN_SENT状态
  2. 阻塞等待服务端返回 SYN+ACK 报文
  3. 收到后自动回复第三次握手的 ACK 报文,客户端进入ESTABLISHED已连接状态,connect()函数返回

补充对应服务端的逻辑:

  • 服务端调用listen()后,内核就开始自动响应 SYN 请求,完成握手的前两步
  • 服务端的accept()根本不参与握手,只从内核的「已完成连接队列」里取出已经握手完成的连接
  • 半连接队列:存放只收到 SYN、还没完成三次握手的连接
  • 全连接队列:存放完成三次握手、等待应用层取用的连接

3.5 核心收发循环:24 小时待命的通信专线

c

运行

char buf[BUF_SIZE]; while (1) { memset(buf, 0, BUF_SIZE); fgets(buf, BUF_SIZE, stdin); // 读取用户键盘输入 send(sock_fd, buf, strlen(buf), 0); // 发送数据到服务端 memset(buf, 0, BUF_SIZE); ssize_t recv_len = recv(sock_fd, buf, BUF_SIZE - 1, 0); if (recv_len <= 0) { printf("服务端断开连接\n"); break; // 跳出循环的唯一出口 } printf("收到服务端回显:%s", buf); }

这是程序的核心业务逻辑,进入死循环后,一直重复「读输入→发消息→等回复→打印」的流程,就像 24 小时值班的客服坐席。

关键细节拆解
  1. 缓冲区清空:每次收发前用memset清空缓冲区,避免上一次的数据残留导致内容错乱。
  2. send函数:把应用层数据拷贝到内核的 TCP 发送缓冲区。注意:send成功返回,不代表对方已经收到数据,只代表数据已经成功交给内核,后续的发送、重传、流量控制都由内核在后台自动完成。
  3. recv返回值的三种含义:
    • 返回值 > 0:正常收到数据,值为实际读取到的字节数
    • 返回值 = 0:对端主动关闭了连接(收到了 FIN 报文),属于正常断开
    • 返回值 < 0:连接异常,比如网络中断、连接被强制重置
灵魂追问:while (1) 是死循环,正常运行走不到后面的 close,这行是不是多余的?

绝对不是多余的,反而是必须写的规范操作。我们觉得 “跑不到”,只是只考虑了 “程序正常运行、连接永远稳定” 的理想场景,而真实编程必须覆盖所有退出路径:

  1. 服务端主动断开:服务端关闭、重启、主动踢掉客户端时,会发送 FIN 报文,客户端recv返回 0,触发break跳出循环,顺理成章走到close。这是最常见的正常退出场景。
  2. 网络异常中断:网线断开、防火墙拦截、网络波动导致连接失效,recv返回 - 1,同样触发break跳出循环。
  3. 后续扩展退出逻辑:如果要加 “输入 quit 退出” 的功能,只需要在循环里加判断执行break,结尾的close可以直接复用,不需要重复写关闭逻辑。
  4. 资源泄漏兜底:就算极端场景下永远走不到,这行也是安全底线。就像大楼的消防通道,平时可能用不上,但必须有 —— 一旦有退出路径走到这里,就能保证释放文件描述符,避免资源耗尽。

3.6 关闭套接字:优雅挂断,走完收尾流程

c

运行

close(sock_fd); return 0;

跳出循环后,程序最终执行close关闭套接字,正常结束。

灵魂追问:调用 close 就立刻触发四次挥手吗?

close是发起关闭流程的开关,不是 “立刻掐断线路”,真正的四次挥手依然由内核在后台自动完成:

  1. 调用close后,内核会先把发送缓冲区里残留的数据全部发送完毕
  2. 数据发完后,内核向对端发送 FIN 报文,代表 “我这边不再发数据了”,触发四次挥手流程
  3. 后续的 ACK 回复、对端 FIN 回复、最终确认,都由内核自动完成

补充一个易错点:TCP 是全双工协议,FIN 只代表 “关闭发送方向”,接收方向依然可以正常收数据。完整的四次挥手,就是双方各自关闭一次发送通道,每次关闭都需要「发 FIN + 回 ACK」,所以一共四次交互。

四、新手避坑清单

  1. 出错分支别忘了 close:创建套接字失败、地址错误、连接失败时,只要前面已经成功申请了资源,退出前都要记得释放,避免文件描述符泄漏。
  2. 不要迷信 send 的返回值:send 成功只代表数据进了内核缓冲区,不代表对方已经收到,可靠交付由 TCP 内核保证,但应用层业务级确认需要自己设计。
  3. 不要忽略 recv 返回 0 的情况:这是对端正常断开的信号,不是错误,需要做好连接释放的收尾逻辑。
  4. 不要以为 TCP 天然有消息边界:TCP 是字节流协议,这段示例能正常运行是因为回显简单、数据量小;复杂业务中必须处理粘包问题,通过包头长度、分隔符等方式拆分完整消息。
谢谢

相关新闻

  • 【课程设计/毕业设计】基于 Java 的高中生多元素质评价管理系统的设计与实现【附源码、数据库、万字文档】
  • 计算机毕业设计之基于弹幕文本大数据的情感分析与可视化
  • IPv6改造后,如何验证全国用户是否都能正常访问

最新新闻

  • 基于Playwright的环境监测数据自动化采集系统实战
  • DBeaver ER图建模避坑指南:3 类常见元数据缺失导致反向工程失败的修复方案
  • 【IDEA代码覆盖率实战指南】:3步精准定位测试盲区,提升覆盖率至95%+的权威方法论
  • 西安代买跑腿平台开发?骑手定位实时同步技术方案
  • dpu-utilities社区贡献指南:从问题报告到代码提交的完整流程
  • 为什么92%的Java工程师从未用对IDEA的Database Diagram?揭秘官方未公开的3个性能陷阱与绕过方案

日新闻

  • Python Playwright录制功能:从零到一构建自动化测试脚本
  • 如何用开源工具永久保存你心爱的小说:novel-downloader全攻略
  • In-Context Learning不是教知识,而是模式对齐:从5个示例到100个工业级样本的真相

周新闻

  • Windows字体自定义终极方案:No!! MeiryoUI完全指南
  • Deepin Boot Maker:告别命令行,3分钟制作Linux启动盘的智能解决方案
  • Plain Craft Launcher 2:重新定义你的Minecraft游戏体验

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号