当前位置: 首页 > ai >正文

网络通信与协议栈 -- TCP协议与编程

TCP 协议与编程:面向连接的可靠传输

TCP(Transmission Control Protocol,传输控制协议)是传输层 “面向连接、可靠传输” 的核心协议,通过三次握手建立连接、四次挥手关闭连接,配合确认重传、流量控制、拥塞控制机制,保障数据有序、不丢失、不重复传输,适用于对可靠性要求高的场景(如网页浏览、文件传输、即时通信)。以下从协议特性、通信框架、编程流程到代码示例,系统梳理 TCP 编程核心内容。

1. TCP 的核心特性(与 UDP 对比)

TCP 作为 “可靠传输协议”,核心特性围绕 “连接管理” 和 “可靠性保障” 设计,与 UDP 的无连接、不可靠形成显著差异:

特性维度TCP 协议UDP 协议
连接模式面向连接:通信前需通过 “三次握手” 建立专用连接,通信后需 “四次挥手” 关闭连接无连接:直接发送数据,无需建立 / 关闭连接
传输可靠性可靠:通过 “确认重传”(收到 ACK 才继续发)、“序号 / 确认号”(保证有序)、“校验和”(检测错误)实现数据不丢失、不重复、有序不可靠:仅校验头部,无重传 / 确认机制,可能丢包、乱序
数据边界无边界:数据以 “字节流” 形式传输,发送端多次 send (),接收端可一次 recv () 读取有边界:数据以 “数据报” 为单位,发送次数与接收次数需匹配
流量控制支持:通过 “滑动窗口” 机制,根据接收端缓冲区大小调整发送速率,避免接收端溢出不支持:按发送端速率传输,可能导致接收端缓冲区满丢包
拥塞控制支持:通过 “慢启动”“拥塞避免”“快速重传”“快速恢复” 机制,避免网络拥堵不支持:无拥塞控制,可能加剧网络拥堵
头部大小20-60 字节(含序号、确认号、窗口大小等字段)8 字节(仅含源端口、目标端口、长度、校验和)
适用场景对可靠性要求高的场景(网页 HTTP/HTTPS、文件传输 FTP、即时通信)对延迟敏感的场景(视频通话、游戏、DNS 查询)

2. TCP 连接管理:三次握手与四次挥手

TCP 的 “面向连接” 核心是通过 “三次握手建立连接” 和 “四次挥手关闭连接”,确保通信双方状态同步,避免无效数据传输。

(1)三次握手(建立连接,确保双方收发能力正常)

目的:确认客户端和服务器的 “发送能力” 和 “接收能力” 均正常,分配连接所需资源(如缓冲区、序号)。
流程(客户端主动发起,服务器被动监听):

  1. 客户端 → 服务器:发送 SYN(同步)报文,携带初始序号(如 seq=100),表示 “我要建立连接,我的初始序号是 100”;此时客户端进入SYN_SENT状态。
  2. 服务器 → 客户端:发送 SYN+ACK(同步 + 确认)报文,携带服务器初始序号(如 seq=200)和对客户端 SYN 的确认号(ack=101,即 “我收到你的 100,下次请发 101”);此时服务器进入SYN_RCVD状态。
  3. 客户端 → 服务器:发送 ACK(确认)报文,携带对服务器 SYN 的确认号(ack=201,即 “我收到你的 200,下次请发 201”);此时客户端进入ESTABLISHED(连接建立)状态,服务器收到后也进入ESTABLISHED状态,双方可开始传输数据。

(2)四次挥手(关闭连接,确保数据传输完成)

目的:确保双方已传输完所有数据,释放连接占用的资源(如缓冲区、端口)。
流程(通常客户端主动发起关闭,也可服务器发起):

  1. 客户端 → 服务器:发送 FIN(结束)报文,携带序号(如 seq=300),表示 “我已无数据要发,请求关闭连接”;此时客户端进入FIN_WAIT_1状态。
  2. 服务器 → 客户端:发送 ACK(确认)报文,携带确认号(ack=301,即 “我收到你的关闭请求,已停止接收你的数据”);此时服务器进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态(等待服务器发送剩余数据)。
  3. 服务器 → 客户端:服务器发送完所有剩余数据后,发送 FIN 报文,携带序号(如 seq=400),表示 “我也无数据要发,请求关闭连接”;此时服务器进入LAST_ACK状态。
  4. 客户端 → 服务器:发送 ACK(确认)报文,携带确认号(ack=401,即 “我收到你的关闭请求”);此时客户端进入TIME_WAIT状态(等待 2MSL,确保服务器收到 ACK,避免服务器重发 FIN),服务器收到后进入CLOSED状态;客户端等待 2MSL 后也进入CLOSED状态,连接完全关闭。

3. TCP 通信框架(C/S 模式)

TCP 采用 “客户端 - 服务器(C/S)” 模型,服务器需提前启动并监听指定端口,客户端主动发起连接请求,连接建立后双方通过 “字节流” 传输数据,流程如下:

  • 服务器端:启动 → 创建套接字 → 绑定 IP + 端口 → 监听端口 → 接受客户端连接 → 收发数据 → 关闭连接 ;
  • 客户端:启动 → 创建套接字 → 发起连接(连接服务器) → 收发数据 → 关闭连接;

核心差异(与 UDP 对比)

  • 服务器需多一步 “监听(listen)” 和 “接受连接(accept)” 操作,UDP 无需;
  • 连接建立后,双方通过read()/write()recv()/send()收发数据(基于已连接套接字),无需每次指定目标地址(UDP 需每次用sendto()/recvfrom()指定地址);
  • 数据传输为 “字节流”,接收端需根据应用层协议(如 HTTP 的Content-Length)判断数据是否接收完整,UDP 无需(数据报有边界)。

4. TCP 编程流程(以 C 语言 Socket API 为例)

TCP 编程依赖标准 Socket API,核心函数包括socket()(创建套接字)、bind()(绑定地址)、listen()(监听端口)、accept()(接受连接)、connect()(发起连接)、send()/recv()(收发数据)、close()(关闭连接)。

(1)核心函数说明

函数原型功能描述关键参数与返回值
int socket(int domain, int type, int protocol);创建 TCP 套接字(向内核申请网络通信端点)- domain:AF_INET(IPv4);
- type:SOCK_STREAM(TCP 流式套接字);
- protocol:0(自动适配 TCP);
- 返回值:成功返回套接字 ID(int),失败返回 - 1。
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);服务器端:将套接字与指定 IP + 端口绑定(确定监听地址)- sockfd:socket () 返回的套接字 ID;
- my_addr:sockaddr_in 结构体(含 IP、端口、协议族);
- addrlen:结构体长度(sizeof (struct sockaddr_in));
- 返回值:成功返回 0,失败返回 - 1。
int listen(int sockfd, int backlog);服务器端:将套接字设为 “监听状态”,等待客户端连接- sockfd:绑定后的套接字 ID;
- backlog:监听队列大小(未完成连接队列 + 已完成连接队列的最大长度,通常设 5-10);
- 返回值:成功返回 0,失败返回 - 1。
int accept(int sockfd, struct sockaddr *cli_addr, socklen_t *addrlen);服务器端:从监听队列中接受一个客户端连接,返回 “已连接套接字”(用于与该客户端通信)- sockfd:监听状态的套接字 ID(仅用于接受连接,不直接收发数据);
- cli_addr:存储客户端地址(可选,NULL 表示不关心);
- addrlen:客户端地址长度指针;
- 返回值:成功返回 “已连接套接字 ID”,失败返回 - 1。
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);客户端:向服务器发起 TCP 连接请求(触发三次握手)- sockfd:socket () 返回的套接字 ID;
- serv_addr:服务器地址结构体(含服务器 IP、端口);
- addrlen:结构体长度;
- 返回值:成功返回 0(连接建立),失败返回 - 1。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);收发双方:通过已连接套接字发送数据(TCP 字节流)- sockfd:accept ()(服务器)或 connect ()(客户端)返回的 “已连接套接字”;
- buf:发送数据缓冲区;
- len:数据长度;
- flags:0(阻塞发送);
- 返回值:成功返回发送字节数,失败返回 - 1。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);收发双方:通过已连接套接字接收数据(TCP 字节流)- 参数同 send (),buf 为接收缓冲区;
- 返回值:成功返回接收字节数,0 表示对方关闭连接,失败返回 - 1。
int close(int sockfd);关闭套接字,释放资源(触发四次挥手)- sockfd:已连接套接字或监听套接字;
- 返回值:成功返回 0,失败返回 - 1。

(2)TCP 编程流程对比(服务器端 vs 客户端)

角色核心步骤(按执行顺序)关键说明
服务器端1. socket():创建 TCP 套接字
2. bind():绑定服务器 IP + 端口
3. listen():将套接字设为监听状态
4. accept():阻塞等待并接受客户端连接(返回通信套接字)
5. recv()/send():通过通信套接字与客户端收发数据
6. close():关闭通信套接字(与该客户端断开),可循环执行 4-6 接受新客户端
- 监听套接字(socket()返回)仅用于接受连接,不直接收发数据;
- 每个客户端连接对应一个 “通信套接字”,服务器需通过多线程 / 多进程处理多客户端(避免单客户端阻塞其他客户端)。
客户端1. socket():创建 TCP 套接字
2. connect():发起连接(连接服务器 IP + 端口)
3. send()/recv():与服务器收发数据
4. close():关闭套接字(断开连接)
- 客户端无需bind():系统自动分配临时端口;
connect()失败表示三次握手未完成(如服务器未启动、端口错误)。

5. TCP服务器/客户端 代码示例

服务器端代码

#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <time.h>
typedef struct sockaddr *(SA);  // 定义sockaddr指针别名int main(int argc, char **argv)
{// 创建监听套接字int listfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == listfd){perror("scoket error\n");return 1;}// 初始化服务器地址结构struct sockaddr_in ser, cli;bzero(&ser, sizeof(ser));  // 清零结构体bzero(&cli, sizeof(cli));ser.sin_family = AF_INET;          // IPv4协议ser.sin_port = htons(50000);       // 端口号转换为网络字节序ser.sin_addr.s_addr = INADDR_ANY;  // 监听所有网络接口// 绑定地址和端口int ret = bind(listfd, (SA)&ser, sizeof(ser));if (-1 == ret){perror("bind");return 1;}// 开始监听(队列长度3)listen(listfd, 3);socklen_t len = sizeof(cli);// 接受客户端连接int conn = accept(listfd, (SA)&cli, &len);if (-1 == conn){perror("accept");return 1;}time_t tm;while (1){char buf[1024] = {0};  // 初始化接收缓冲区// 接收客户端数据int ret = recv(conn, buf, sizeof(buf), 0);if (ret <= 0)  // 连接关闭或出错{break;}time(&tm);  // 获取当前时间// 在消息后附加时间戳sprintf(buf, "%s %s", buf, ctime(&tm));// 发送响应数据send(conn, buf, strlen(buf), 0);}// 关闭所有套接字close(listfd);close(conn);return 0;
}

运行结果

  • 服务器启动后阻塞在accept()等待连接
  • 客户端连接后,服务器接收消息并添加时间戳返回
  • 示例输出:hello,this is tcp test Wed Jun 12 10:30:45 2024

客户端代码 

#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
typedef struct sockaddr *(SA);  // 定义sockaddr指针别名int main(int argc, char **argv)
{// 创建通信套接字int conn = socket(AF_INET, SOCK_STREAM, 0);if (-1 == conn){perror("socket");return 1;}// 初始化服务器地址结构struct sockaddr_in ser;bzero(&ser, sizeof(ser));ser.sin_family = AF_INET;        // IPv4协议ser.sin_port = htons(50000);     // 服务器端口ser.sin_addr.s_addr = INADDR_ANY; // 本地回环地址// 连接服务器int ret = connect(conn, (SA)&ser, sizeof(ser));if (-1 == ret){perror("connect error\n");return 1;}int i = 10;while (i--){char buf[1024] = "hello,this is tcp test";  // 发送消息send(conn, buf, strlen(buf), 0);            // 发送数据bzero(buf, sizeof(buf));                   // 清空缓冲区recv(conn, buf, sizeof(buf), 0);           // 接收响应printf("from ser:%s\n", buf);              // 打印服务器响应sleep(1);                                  // 间隔1秒}close(conn);  // 关闭连接return 0;
}

运行结果

  • 客户端连接服务器成功
  • 每秒发送"hello,this is tcp test"并接收带时间戳的响应
  • 示例输出:from ser:hello,this is tcp test Wed Jun 12 10:30:45 2024

6.TCP数据粘包问题

问题描述

  • TCP粘包问题是指由于TCP是面向流的传输协议,数据在传输过程中没有明确的消息边界,导致:

  • 多条消息合并:接收方可能一次性收到发送方多次发送的数据

  • 消息被拆分:一个完整的消息可能被拆分成多个数据包接收

  • 数据混乱:无法准确区分每条消息的起始和结束位置

消息拆分是 TCP “适配底层” 的必然结果

TCP 拆分消息并非 “漏洞”,而是为了适配网络环境和保证传输可靠性的设计逻辑

  • 底层有 MTU/MSS 的硬件限制,必须拆分长数据;
  • 接收方有缓冲区(窗口)的能力限制,需动态拆分;
  • 网络有拥塞风险,需通过拥塞窗口控制拆分;
  • 本质是 “面向流” 协议对 “无边界字节” 的传输适配,完全不感知应用层的 “消息” 概念。
底层限制:MTU(最大传输单元)与 MSS(最大分段大小)的强制切割

TCP 数据包最终需要通过 IP 层传输,而 IP 层有一个关键限制:MTU(Maximum Transmission Unit,最大传输单元),即物理网络(如以太网、WiFi)能承载的 “单个 IP 数据包的最大长度”(默认以太网 MTU 为 1500 字节)。

TCP 为了避免 IP 层对数据包进行 “分片”(IP 分片会增加丢包风险和重组开销),会主动遵守MSS(Maximum Segment Size,最大分段大小) 规则

  • MSS = MTU - IP 头部长度(通常 20 字节) - TCP 头部长度(通常 20 字节)
  • 以太网环境下,MSS 默认值为 1500 - 20 - 20 = 1460字节,即 TCP 单个数据段(Segment)的数据部分最大不能超过 1460 字节

如果应用层发送的 “一条消息” 长度超过 MSS(比如发送 2000 字节的消息),TCP 会强制将消息拆分成多个 Segment,每个 Segment 的数据部分不超过 MSS,再分别封装成 IP 数据包发送。

案例:发送 2000 字节消息,MSS=1460 字节。TCP 会拆分为:

  • 第 1 个 Segment:1460 字节数据
  • 第 2 个 Segment:2000 - 1460 = 540 字节数据
    接收方会分两次收到这两个片段,需自行拼接成完整消息。

正因为拆分的必然性,应用层必须通过 “自定义消息边界”(如加长度字段、分隔符)来解决粘包 / 拆包问题,才能准确还原完整消息。

解决方案

  1. 设置边界:使用特殊字符(如\0)作为消息结束标志
  2. 固定大小:每次发送固定长度的数据块
  3. 自定义协议:在消息头部添加长度字段
http://www.xdnf.cn/news/19821.html

相关文章:

  • [Java]PTA:求最大值
  • 财务文档处理优化:基于本地运行的PDF合并解决方案
  • 入行FPGA选择国企、私企还是外企?
  • Ansible高效管理大项目实战技巧
  • 【Python】数据可视化之点线图
  • Android 渐变背景色绘制
  • Git在idea中的实战使用经验(二)
  • 基于SpringBoot的宠物咖啡馆平台
  • 在DDPM(扩散模型)中,反向过程为什么不能和前向一样一步解决,另外实际公式推导时反向过程每一步都能得到一个预测值,为什么还要一步一步的推导?
  • 前端-Vue的生命周期和生命周期的四个阶段
  • 缠论笔线段画线,文华财经期货指标公式,好用的缠论指标源码
  • 特斯拉三代灵巧手:演进历程与核心供应链梳理
  • Spring AI调用sglang模型返回HTTP 400分析处理
  • 前端学习 10-2 :验证中的SV
  • Qt使用Maintenance添加、卸载组件(未完)
  • Java 技术支撑 AI 系统落地:从模型部署到安全合规的企业级解决方案(四)
  • 嵌入式学习 51单片机(2)
  • 【C++】string类完全解析与实战指南
  • centos 压缩命令
  • (二)文件管理-基础命令-mkdir命令的使用
  • Linux应用(1)——文件IO
  • 部署jenkins并基于ansible部署Discuz应用
  • 嵌入式|RTOS教学——FreeRTOS基础3:消息队列
  • Unity之Spine动画资源导入
  • 小游戏公司接单难?这几点原因与破局思路值得看看
  • 聚焦诊断管理(DM)的传输层设计、诊断服务器实现、事件与通信管理、生命周期与报告五大核心模块
  • RTSP流端口占用详解:TCP模式与UDP模式的对比
  • 面向深层语义分析的公理化NLP模型:理论可行性、关键技术与应用挑战
  • 大语言模型领域最新进展
  • 如何将JPG图片批量转为PDF?其实可用的方法有很多种