TCP协议
TCP 协议核心知识
TCP 概述
- 全称:Transmission Control Protocol(传输控制协议)。
- 核心定位:面向连接的可靠传输协议。
- 「可靠」定义:并非数据一定能送达,而是发送方能可靠知晓对方是否收到数据。
TCP 基本特征
- 有连接:通信前需通过「三次握手」建立连接,通信后通过「四次挥手」释放连接。
- 有确认:接收方收到数据后,会向发送方返回确认回执(ACK)。
- 数据完整性:保证数据有序、不重复,丢失数据会自动重发。
- 流量控制:网络拥堵时,通过滑动窗口机制自动调节发送速率。
- 流式通信:帧异步传输,收发双方每次数据量不必相等。
TCP 应用场景
- 对传输质量要求高、不允许丢数据的场景(如文件下载、邮件发送)。
- 大数据量通信(连接/断开的开销可忽略)。
- 安全相关功能(如用户登录、账户管理)。
TCP 与 UDP 对比
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(确认、重传) | 不可靠(无确认) |
| 数据传输方式 | 流式传输(异步) | 数据报传输(同步) |
| 有序性 | 保证数据有序 | 不保证有序 |
| 流量控制 | 支持(滑动窗口) | 不支持 |
| 速度 | 较慢 | 较快 |
| 应用场景 | 文件、登录、邮件 | 视频、直播、音频 |
TCP 通信流程
传输协议
整体流程(客户端-服务器)
关键阶段说明
- 建立连接:客户端发起
connect,与服务器通过三次握手同步序列号和确认号,建立可靠连接。 - 数据传输:通过
read/write或send/recv函数收发数据,TCP 自动保证数据完整性和有序性。 - 释放连接:双方通过四次挥手关闭连接,确保所有数据都已传输完成。
TCP 编程 API 接口
核心 API 详解
创建套接字(socket)
- 功能:创建一个网络通信的端点(文件描述符)。
- 函数原型:
int socket(int domain, int type, int protocol); - 参数:
domain:网络层协议(AF_INET 表示 IPv4)。type:传输层协议(SOCK_STREAM 表示 TCP)。protocol:协议属性(默认 0)。
- 返回值:成功返回套接字描述符(非负整数),失败返回 -1。
绑定地址(bind)
- 功能:将套接字与服务器的 IP 地址和端口号绑定。
- 函数原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); - 参数:
sockfd:待绑定的套接字描述符。addr:服务器地址结构体(struct sockaddr_in)。addrlen:地址结构体的长度(sizeof(struct sockaddr_in))。
- 返回值:成功返回 0,失败返回 -1。
监听连接(listen)
- 功能:将套接字设置为监听模式,等待客户端连接。
- 函数原型:
int listen(int sockfd, int backlog); - 参数:
sockfd:监听套接字描述符。backlog:最大等待连接队列长度(默认受系统/proc/sys/net/core/somaxconn限制,默认 128,需大于该值才生效)。
- 返回值:成功返回 0,失败返回 -1。
接受连接(accept)
- 功能:阻塞等待客户端连接请求,建立新连接。
- 函数原型:
int accept(int sockfd, struct sockaddr *_Nullable restrict addr, socklen_t *_Nullable restrict addrlen); - 参数:
sockfd:监听套接字描述符。addr:存储客户端地址信息的结构体(可设为 NULL,不获取客户端地址)。addrlen:客户端地址结构体的长度指针。
- 返回值:成功返回新的已连接套接字(用于与该客户端通信),失败返回 -1。
发起连接(connect)
- 功能:客户端向服务器发起连接请求(三次握手)。
- 函数原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); - 参数:
sockfd:客户端套接字描述符。addr:服务器地址结构体。addrlen:服务器地址结构体的长度。
- 返回值:成功返回 0,失败返回 -1。
收发数据(read/write 或 send/recv)
- 功能:通过已连接套接字收发数据。
- 函数原型(read/write):
ssize_t read(int fd, void *buf, size_t count); // 接收数据 ssize_t write(int fd, const void *buf, size_t count); // 发送数据 - 函数原型(send/recv):
ssize_t send(int sockfd, const void buf[.len], size_t len, int flags); ssize_t recv(int sockfd, void buf[.len], size_t len, int flags); - 参数:
fd/sockfd:已连接套接字描述符(accept 返回的新套接字)。buf:存储数据的缓冲区。count/len:缓冲区长度。flags:传输标志(默认 0)。
- 返回值:成功返回实际收发的字节数,失败返回 -1。
关闭连接(close)
- 功能:关闭套接字,释放相关资源。
- 函数原型:
int close(int fd); - 参数:
fd为套接字描述符。 - 返回值:成功返回 0,失败返回 -1。
地址结构体与转换函数
IPv4 地址结构体(struct sockaddr_in)
struct sockaddr_in {
sa_family_t sin_family; // 地址族(AF_INET 表示 IPv4)
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP 地址结构体
};
struct in_addr {
uint32_t s_addr; // IP 地址(网络字节序)
};
字节序转换函数
- 本地字节序 ↔ 网络字节序(端口):
uint16_t htons(uint16_t hostshort); // 本地 → 网络 uint16_t ntohs(uint16_t netshort); // 网络 → 本地 - IP 地址转换函数:
in_addr_t inet_addr(const char *cp); // 字符串 IP → 网络字节序 IP char *inet_ntoa(struct in_addr in); // 网络字节序 IP → 字符串 IP
TCP 编程示例代码
客户端代码(主动发起连接)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
// 1. 创建 TCP 套接字
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_socket < 0) {
perror("tcp socket error");
return -1;
}
perror("tcp socket success");
// 2. 设置服务器地址信息
struct sockaddr_in ServerAddr = {
.sin_family = AF_INET,
.sin_port = htons(65000), // 服务器端口(网络字节序)
.sin_addr.s_addr = inet_addr("192.168.46.1") // 服务器 IP
};
socklen_t addrlen = sizeof(ServerAddr);
// 3. 发起连接(三次握手)
int ret_val = connect(tcp_socket, (const struct sockaddr *)&ServerAddr, addrlen);
if (ret_val < 0) {
perror("connect error");
return -1;
}
perror("connect success");
// 4. 循环发送消息
char SendMsg[128];
while (1) {
bzero(SendMsg, sizeof(SendMsg)); // 清空缓冲区
printf("请输入需要发送的消息:\n");
fgets(SendMsg, sizeof(SendMsg), stdin); // 读取用户输入
// 发送数据
ret_val = write(tcp_socket, SendMsg, strlen(SendMsg));
if (ret_val < 0) {
perror("Send msg error");
break;
}
printf("成功发送%d字节数据...\n", ret_val);
}
// 5. 关闭套接字
close(tcp_socket);
return 0;
}
服务器代码(被动接收连接)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
// 1. 创建 TCP 套接字
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_socket < 0) {
perror("tcp socket error");
return -1;
}
perror("tcp socket success");
// 2. 设置服务器地址信息
struct sockaddr_in ServerAddr = {
.sin_family = AF_INET,
.sin_port = htons(65000), // 绑定端口(网络字节序)
.sin_addr.s_addr = INADDR_ANY // 绑定所有网卡 IP
};
socklen_t addrlen = sizeof(ServerAddr);
// 3. 绑定地址
if (bind(tcp_socket, (struct sockaddr *)&ServerAddr, addrlen)) {
perror("bind error");
return -1;
}
perror("bind success");
// 4. 设置监听模式
listen(tcp_socket, 5); // 最大等待队列长度 5
perror("listen success");
// 5. 等待客户端连接(阻塞)
struct sockaddr_in ClientAddr;
int ConnectFd = accept(tcp_socket, (struct sockaddr *)&ClientAddr, &addrlen);
if (ConnectFd < 0) {
perror("accept error");
return -1;
}
// 打印客户端地址信息
char *client_ip = inet_ntoa(ClientAddr.sin_addr);
int client_port = ntohs(ClientAddr.sin_port);
printf("accept success: client IP=%s, port=%d\n", client_ip, client_port);
// 6. 循环接收消息
char RecvMsg[128];
while (1) {
bzero(RecvMsg, sizeof(RecvMsg)); // 清空缓冲区
// 接收数据(使用 accept 返回的新套接字)
int ret_val = read(ConnectFd, RecvMsg, sizeof(RecvMsg));
if (ret_val <= 0) {
perror("read msg error");
break;
}
printf("成功收到%d字节数据: %s\n", ret_val, RecvMsg);
}
// 7. 关闭套接字
close(ConnectFd);
close(tcp_socket);
return 0;
}
拓展
TCP 三次握手与四次挥手细节
三次握手(建立连接)
- 目的:同步双方的序列号和确认号,确保双方收发能力正常。
- 过程:
- 客户端 → 服务器:SYN 包(seq = x),请求建立连接。
- 服务器 → 客户端:SYN + ACK 包(seq = y,ack = x+1),确认客户端请求并发起连接。
- 客户端 → 服务器:ACK 包(seq = x+1,ack = y+1),确认服务器请求,连接建立。
四次挥手(释放连接)
- 目的:确保双方都已完成数据传输,安全释放连接资源。
- 过程:
- 客户端 → 服务器:FIN 包(seq = x),请求关闭连接。
- 服务器 → 客户端:ACK 包(ack = x+1),确认客户端关闭请求。
- 服务器 → 客户端:FIN 包(seq = y),服务器数据传输完成,请求关闭。
- 客户端 → 服务器:ACK 包(ack = y+1),确认服务器关闭请求,连接释放。

滑动窗口机制示例代码(基于 TCP 模拟实现)
由于 TCP 协议栈已内置滑动窗口逻辑(操作系统内核实现),应用层无法直接操作内核的滑动窗口,因此以下示例通过 用户态模拟 实现滑动窗口的核心流程(发送方+接收方),帮助理解“窗口协商、数据发送、ACK 反馈、窗口滑动”的完整逻辑。
示例代码核心思路
- 模拟对象:实现发送方(Sender)和接收方(Receiver)两个角色,模拟 TCP 单向数据传输。
- 核心机制:
- 接收方维护「接收窗口」,根据缓冲区剩余容量动态调整窗口大小。
- 发送方维护「发送窗口」,按接收窗口大小连续发送数据,收到 ACK 后滑动窗口。
- 支持「累计确认」:接收方无需逐个确认数据段,仅确认已连续接收的最大序列号。
- 模拟「窗口动态调整」:接收方可因“处理延迟”缩小窗口,模拟实际场景中的流量控制。
完整示例代码(C 语言实现)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h> // 新增:引入uint32_t的定义
// 配置参数(可调整)
#define MAX_BUFFER_SIZE 4096 // 接收方缓冲区总大小(字节)
#define INIT_WINDOW_SIZE 1024 // 初始窗口大小(字节)
#define DATA_SEG_SIZE 512 // 单个数据段大小(字节)
#define SIM_PROCESS_DELAY 1 // 接收方处理数据的模拟延迟(秒)
// 数据段结构体(模拟 TCP 数据段)
typedef struct {
uint32_t seq; // 数据段起始序列号
uint32_t len; // 数据段长度(字节)
char data[DATA_SEG_SIZE]; // 数据内容
} TcpSegment;
// ACK 结构体(模拟 TCP ACK 包)
typedef struct {
uint32_t ack; // 确认号(期待下一个字节的序列号)
uint32_t window; // 接收方当前窗口大小
} TcpAck;
// 发送方状态
typedef struct {
uint32_t next_seq; // 下一个要发送的序列号
uint32_t send_window;// 当前发送窗口大小(= 接收方反馈的窗口大小)
uint32_t unack_seq; // 已发送但未确认的最小序列号
} SenderState;
// 接收方状态
typedef struct {
uint32_t exp_seq; // 期待接收的下一个序列号
uint32_t recv_window;// 当前接收窗口大小(= 缓冲区剩余容量)
char buffer[MAX_BUFFER_SIZE]; // 接收缓冲区
uint32_t buffer_used;// 缓冲区已使用字节数
} ReceiverState;
// 初始化发送方
void sender_init(SenderState *sender) {
sender->next_seq = 0; // 初始序列号从 0 开始
sender->send_window = INIT_WINDOW_SIZE; // 初始窗口大小
sender->unack_seq = 0; // 初始无未确认数据
printf("[发送方] 初始化完成,初始发送窗口大小:%d 字节\n", sender->send_window);
}
// 初始化接收方
void receiver_init(ReceiverState *receiver) {
receiver->exp_seq = 0; // 期待接收序列号从 0 开始
receiver->buffer_used = 0; // 缓冲区初始为空
receiver->recv_window = MAX_BUFFER_SIZE - receiver->buffer_used; // 初始接收窗口=缓冲区总大小
printf("[接收方] 初始化完成,初始接收窗口大小:%d 字节\n", receiver->recv_window);
}
// 发送方:生成数据段(模拟发送数据)
TcpSegment sender_gen_segment(SenderState *sender, uint32_t seg_len) {
TcpSegment seg;
seg.seq = sender->next_seq; // 数据段起始序列号
seg.len = seg_len; // 数据段长度(不超过剩余窗口大小)
// 填充模拟数据(用序列号标识数据内容)
snprintf(seg.data, seg_len, "Data_Seq_%d", seg.seq);
// 更新发送方状态
sender->next_seq += seg_len; // 下一个序列号 = 当前序列号 + 数据段长度
printf("[发送方] 发送数据段:Seq=%d, 长度=%d 字节,内容:%s\n",
seg.seq, seg.len, seg.data);
return seg;
}
// 接收方:处理数据段(模拟接收数据)
TcpAck receiver_process_segment(ReceiverState *receiver, TcpSegment *seg) {
TcpAck ack;
// 1. 检查数据段是否符合预期(序列号是否连续)
if (seg->seq != receiver->exp_seq) {
printf("[接收方] 警告:数据段序列号不连续(预期=%d,实际=%d),丢弃该数据段\n",
receiver->exp_seq, seg->seq);
// 反馈 ACK=预期序列号,窗口大小不变(告知发送方重传)
ack.ack = receiver->exp_seq;
ack.window = receiver->recv_window;
return ack;
}
// 2. 检查缓冲区是否有足够空间
if (seg->len > receiver->recv_window) {
printf("[接收方] 警告:数据段长度(%d)超过接收窗口(%d),丢弃该数据段\n",
seg->len, receiver->recv_window);
ack.ack = receiver->exp_seq;
ack.window = receiver->recv_window;
return ack;
}
// 3. 接收数据并写入缓冲区
memcpy(receiver->buffer + receiver->buffer_used, seg->data, seg->len);
receiver->buffer_used += seg->len;
receiver->exp_seq += seg->len; // 更新期待的下一个序列号
// 4. 模拟处理数据(延迟后释放缓冲区)
printf("[接收方] 成功接收数据段:Seq=%d,长度=%d 字节,缓冲区已用:%d 字节\n",
seg->seq, seg->len, receiver->buffer_used);
sleep(SIM_PROCESS_DELAY); // 模拟处理延迟(如写入文件、解析数据)
receiver->buffer_used = 0; // 处理完成,释放缓冲区
printf("[接收方] 数据处理完成,缓冲区已清空\n");
// 5. 计算新的接收窗口大小(缓冲区剩余容量)
receiver->recv_window = MAX_BUFFER_SIZE - receiver->buffer_used;
// 6. 生成 ACK:确认号=已接收的最大序列号+1,窗口=新接收窗口
ack.ack = receiver->exp_seq;
ack.window = receiver->recv_window;
printf("[接收方] 发送 ACK:确认号=%d,新接收窗口大小=%d 字节\n",
ack.ack, ack.window);
return ack;
}
// 发送方:处理 ACK(更新发送窗口,滑动窗口)
void sender_process_ack(SenderState *sender, TcpAck *ack) {
// 1. 更新已确认的序列号(滑动窗口的左边界)
if (ack->ack > sender->unack_seq) { // 修复:ack.ack → ack->ack
printf("[发送方] 收到 ACK:确认号=%d,窗口大小=%d 字节\n",
ack->ack, ack->window); // 修复:ack.ack → ack->ack;ack.window → ack->window
sender->unack_seq = ack->ack; // 修复:ack.ack → ack->ack
// 2. 更新发送窗口大小(由接收方窗口决定)
sender->send_window = ack->window; // 修复:ack.window → ack->window
printf("[发送方] 窗口滑动完成:新的未确认序列号=%d,新发送窗口大小=%d 字节\n",
sender->unack_seq, sender->send_window);
} else {
printf("[发送方] 收到重复 ACK:确认号=%d(无新确认),窗口大小=%d 字节\n",
ack->ack, ack->window); // 修复:ack.ack → ack->ack;ack.window → ack->window
}
}
// 模拟滑动窗口通信流程
int main() {
SenderState sender;
ReceiverState receiver;
// 1. 初始化发送方和接收方
sender_init(&sender);
receiver_init(&receiver);
printf("=====================================\n");
// 2. 模拟多轮数据传输(直到发送方完成所有数据)
const uint32_t TOTAL_DATA_SIZE = 4096; // 总传输数据量
while (sender.unack_seq < TOTAL_DATA_SIZE) {
// 2.1 发送方:在发送窗口内连续发送数据段
uint32_t remaining_window = sender.send_window; // 当前剩余窗口大小
while (remaining_window > 0 && sender.next_seq < TOTAL_DATA_SIZE) {
// 每个数据段大小 = min(剩余窗口, 最大数据段大小, 剩余总数据)
uint32_t seg_len = remaining_window;
if (seg_len > DATA_SEG_SIZE) seg_len = DATA_SEG_SIZE;
if (seg_len > TOTAL_DATA_SIZE - sender.next_seq) {
seg_len = TOTAL_DATA_SIZE - sender.next_seq;
}
// 生成并发送数据段
TcpSegment seg = sender_gen_segment(&sender, seg_len);
remaining_window -= seg_len; // 剩余窗口减少
// 2.2 接收方:处理数据段并反馈 ACK
TcpAck ack = receiver_process_segment(&receiver, &seg);
// 2.3 发送方:处理 ACK 并滑动窗口
sender_process_ack(&sender, &ack);
}
// 2.4 若发送窗口用尽,等待下一轮 ACK(模拟网络延迟)
if (sender.next_seq < TOTAL_DATA_SIZE) {
printf("[发送方] 发送窗口用尽,等待下一轮 ACK...\n");
sleep(SIM_PROCESS_DELAY);
}
}
// 3. 传输完成
printf("=====================================\n");
printf("[发送方] 所有数据(%d 字节)已发送并确认,传输完成!\n", TOTAL_DATA_SIZE);
return 0;
}
代码说明与运行效果
核心模块解析
| 模块 | 功能 |
|---|---|
| 数据结构定义 | TcpSegment(模拟 TCP 数据段)、TcpAck(模拟 ACK 包)、SenderState/ReceiverState(维护双方状态)。 |
| 初始化函数 | sender_init/receiver_init:设置初始序列号、窗口大小、缓冲区状态。 |
| 发送方逻辑 | sender_gen_segment:生成数据段(按窗口大小控制发送量);sender_process_ack:处理 ACK 并滑动窗口。 |
| 接收方逻辑 | receiver_process_segment:检查数据段合法性、写入缓冲区、模拟处理延迟、生成 ACK+新窗口。 |
| 主流程 | 循环发送数据段→接收处理→反馈 ACK→滑动窗口,直到总数据传输完成。 |
编译与运行
- 编译命令:
gcc tcp_sliding_window.c -o tcp_sliding_window - 运行命令:
./tcp_sliding_window - 关键输出(示例):

代码扩展方向(模拟更真实的 TCP 滑动窗口)
-
增加丢包重传逻辑:
- 在接收方随机丢弃部分数据段,发送方未收到 ACK 时,超时后重传“已发送未确认”的数据段。
- 实现“快速重传”:收到 3 个重复 ACK 时,立即重传丢失的数据段,无需等待超时。
-
支持拥塞控制:
- 为发送方增加「拥塞窗口(cwnd)」,初始按指数增长(慢启动),遇到丢包后减半(拥塞避免)。
- 实际发送窗口大小 =
min(接收窗口, 拥塞窗口),模拟 TCP 同时应对“接收方能力”和“网络拥堵”。
-
窗口扩大选项(Window Scaling):
- 在初始化阶段协商“窗口扩大因子”,实际窗口大小 = 头部窗口字段值 × (2^扩大因子),支持超过 64KB 的大窗口。
-
用 Wireshark 关联真实 TCP 流量:
- 运行代码时,用 Wireshark 捕获本地回环网卡(lo)的 TCP 流量,对比模拟窗口与真实 TCP 窗口的变化规律(过滤规则:
tcp.port == 自定义端口)。
- 运行代码时,用 Wireshark 捕获本地回环网卡(lo)的 TCP 流量,对比模拟窗口与真实 TCP 窗口的变化规律(过滤规则:
关键注意事项
-
与内核 TCP 窗口的区别:
- 本代码是用户态模拟,实际 TCP 滑动窗口由操作系统内核实现(如 Linux 的
tcp_windows.c),应用层无法直接修改窗口大小,只能通过setsockopt调整缓冲区大小(间接影响接收窗口)。
- 本代码是用户态模拟,实际 TCP 滑动窗口由操作系统内核实现(如 Linux 的
-
缓冲区配置建议:
- 实际开发中,若需优化 TCP 传输效率,可通过以下接口调整套接字缓冲区大小:
// 设置发送缓冲区大小(单位:字节)
int send_buf = 65536;
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &send_buf, sizeof(send_buf));
// 设置接收缓冲区大小
int recv_buf = 65536;
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recv_buf, sizeof(recv_buf));
常见 TCP 异常处理
- 连接超时:设置
connect超时时间,避免程序无限阻塞。 - 数据粘包:由于 TCP 是流式传输,可能出现多个数据包合并接收的情况,需在应用层定义分隔符(如换行符)或固定包长。
- 断连重连:检测到连接断开后,客户端自动重新发起连接。

浙公网安备 33010602011771号