TCP协议

TCP 协议核心知识

TCP 概述

  • 全称:Transmission Control Protocol(传输控制协议)。
  • 核心定位:面向连接的可靠传输协议。
  • 「可靠」定义:并非数据一定能送达,而是发送方能可靠知晓对方是否收到数据。

TCP 基本特征

  • 有连接:通信前需通过「三次握手」建立连接,通信后通过「四次挥手」释放连接。
  • 有确认:接收方收到数据后,会向发送方返回确认回执(ACK)。
  • 数据完整性:保证数据有序、不重复,丢失数据会自动重发。
  • 流量控制:网络拥堵时,通过滑动窗口机制自动调节发送速率。
  • 流式通信:帧异步传输,收发双方每次数据量不必相等。

TCP 应用场景

  • 对传输质量要求高、不允许丢数据的场景(如文件下载、邮件发送)。
  • 大数据量通信(连接/断开的开销可忽略)。
  • 安全相关功能(如用户登录、账户管理)。

TCP 与 UDP 对比

特性 TCP UDP
连接方式 面向连接(三次握手) 无连接
可靠性 可靠(确认、重传) 不可靠(无确认)
数据传输方式 流式传输(异步) 数据报传输(同步)
有序性 保证数据有序 不保证有序
流量控制 支持(滑动窗口) 不支持
速度 较慢 较快
应用场景 文件、登录、邮件 视频、直播、音频

TCP 通信流程

传输协议

传输协议

整体流程(客户端-服务器)

整体流程

关键阶段说明

  • 建立连接:客户端发起 connect,与服务器通过三次握手同步序列号和确认号,建立可靠连接。
  • 数据传输:通过 read/writesend/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 三次握手与四次挥手细节

三次握手(建立连接)

  • 目的:同步双方的序列号和确认号,确保双方收发能力正常。
  • 过程:
    1. 客户端 → 服务器:SYN 包(seq = x),请求建立连接。
    2. 服务器 → 客户端:SYN + ACK 包(seq = y,ack = x+1),确认客户端请求并发起连接。
    3. 客户端 → 服务器:ACK 包(seq = x+1,ack = y+1),确认服务器请求,连接建立。

四次挥手(释放连接)

  • 目的:确保双方都已完成数据传输,安全释放连接资源。
  • 过程:
    1. 客户端 → 服务器:FIN 包(seq = x),请求关闭连接。
    2. 服务器 → 客户端:ACK 包(ack = x+1),确认客户端关闭请求。
    3. 服务器 → 客户端:FIN 包(seq = y),服务器数据传输完成,请求关闭。
    4. 客户端 → 服务器:ACK 包(ack = y+1),确认服务器关闭请求,连接释放。

image

滑动窗口机制示例代码(基于 TCP 模拟实现)

由于 TCP 协议栈已内置滑动窗口逻辑(操作系统内核实现),应用层无法直接操作内核的滑动窗口,因此以下示例通过 用户态模拟 实现滑动窗口的核心流程(发送方+接收方),帮助理解“窗口协商、数据发送、ACK 反馈、窗口滑动”的完整逻辑。

示例代码核心思路

  1. 模拟对象:实现发送方(Sender)和接收方(Receiver)两个角色,模拟 TCP 单向数据传输。
  2. 核心机制
    • 接收方维护「接收窗口」,根据缓冲区剩余容量动态调整窗口大小。
    • 发送方维护「发送窗口」,按接收窗口大小连续发送数据,收到 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
  • 关键输出(示例):

image

代码扩展方向(模拟更真实的 TCP 滑动窗口)

  1. 增加丢包重传逻辑

    • 在接收方随机丢弃部分数据段,发送方未收到 ACK 时,超时后重传“已发送未确认”的数据段。
    • 实现“快速重传”:收到 3 个重复 ACK 时,立即重传丢失的数据段,无需等待超时。
  2. 支持拥塞控制

    • 为发送方增加「拥塞窗口(cwnd)」,初始按指数增长(慢启动),遇到丢包后减半(拥塞避免)。
    • 实际发送窗口大小 = min(接收窗口, 拥塞窗口),模拟 TCP 同时应对“接收方能力”和“网络拥堵”。
  3. 窗口扩大选项(Window Scaling)

    • 在初始化阶段协商“窗口扩大因子”,实际窗口大小 = 头部窗口字段值 × (2^扩大因子),支持超过 64KB 的大窗口。
  4. 用 Wireshark 关联真实 TCP 流量

    • 运行代码时,用 Wireshark 捕获本地回环网卡(lo)的 TCP 流量,对比模拟窗口与真实 TCP 窗口的变化规律(过滤规则:tcp.port == 自定义端口)。

关键注意事项

  1. 与内核 TCP 窗口的区别

    • 本代码是用户态模拟,实际 TCP 滑动窗口由操作系统内核实现(如 Linux 的 tcp_windows.c),应用层无法直接修改窗口大小,只能通过 setsockopt 调整缓冲区大小(间接影响接收窗口)。
  2. 缓冲区配置建议

    • 实际开发中,若需优化 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 是流式传输,可能出现多个数据包合并接收的情况,需在应用层定义分隔符(如换行符)或固定包长。
  • 断连重连:检测到连接断开后,客户端自动重新发起连接。
posted @ 2025-12-10 08:43  Jaklin  阅读(69)  评论(0)    收藏  举报