TCP相关讲解以及代码示例

发布于:2025-08-04 ⋅ 阅读:(13) ⋅ 点赞:(0)

一、TCP

        TCP(Transmission Control Protocol,传输控制协议)是一种面向连接、可靠的、基于字节流的传输层通信协议,位于 OSI 模型的第四层(传输层),与 IP 协议(网际协议)共同构成了 TCP/IP 协议族的核心,因此常被合称为 “TCP/IP”。

1. 核心特性

  1. 面向连接
    通信前必须通过 “三次握手” 建立连接,通信结束后通过 “四次挥手” 释放连接,确保双方状态同步。

    • 三次握手:客户端发送连接请求(SYN)→ 服务器确认并回应(SYN+ACK)→ 客户端再次确认(ACK),连接建立。
    • 四次挥手:主动方发送断开请求(FIN)→ 被动方确认(ACK)→ 被动方发送断开请求(FIN)→ 主动方确认(ACK),连接关闭。
  2. 可靠性保障

    • 序号与确认机制:每个字节都有唯一序号,接收方通过确认号(ACK)告知已接收的数据范围,未确认的数据会被重传。
    • 超时重传:发送方若在规定时间内未收到确认,会重新发送数据。
    • 流量控制:通过滑动窗口机制,接收方根据自身缓冲区大小调整窗口值,避免发送方发送过快导致溢出。
    • 拥塞控制:通过慢启动、拥塞避免、快重传、快恢复等算法,动态调整发送速率,减少网络拥塞。
  3. 字节流服务
    数据被视为连续的字节流,TCP 会处理数据的分段(MTU 限制)、重组,确保接收方按顺序完整接收。

与 UDP 的对比
特性 TCP UDP
连接性 面向连接(三次握手) 无连接
可靠性 可靠(重传、确认、排序) 不可靠(无重传、无序)
速度 较慢(开销大) 较快(开销小)
适用场景 数据完整性优先 实时性优先

2. TCP 协议的应用场景

TCP 的可靠性使其适用于对数据完整性、顺序性要求高的场景,典型应用包括:

  1. 万维网(HTTP/HTTPS)
    浏览网页时,需确保 HTML、图片、视频等资源完整传输,TCP 的重传机制可避免因丢包导致的内容缺失。HTTPS(基于 SSL/TLS 的 HTTP)同样依赖 TCP 作为传输基础。

  2. 文件传输(FTP/SFTP)
    FTP(文件传输协议)和 SFTP(SSH 文件传输协议)需保证大文件传输的完整性,TCP 能确保文件分片按顺序重组,避免数据损坏。

  3. 电子邮件(SMTP/POP3/IMAP)
    邮件发送(SMTP)、接收(POP3/IMAP)过程中,文字、附件等数据需准确送达,TCP 的可靠性可防止邮件内容丢失或乱序。

  4. 远程登录(Telnet/SSH)
    远程控制设备时,指令的顺序和完整性至关重要(如命令行输入),TCP 能确保指令按发送顺序执行,避免操作混乱。

  5. 即时通讯(文本消息)
    虽然语音 / 视频通话常用 UDP,但文本消息(如微信、WhatsApp 文字)需确保送达和顺序,通常基于 TCP 传输。

  6. 数据库通信(MySQL、PostgreSQL)
    数据库查询、数据写入需保证指令和数据的准确性,TCP 可防止因网络问题导致的查询错误或数据不一致。

  7. 金融交易(支付、银行系统)
    转账、支付等操作对数据可靠性要求极高,TCP 的确认机制可避免交易信息丢失导致的重复支付或交易失败。

3. TCP的C/S架构

客户端操作主要有
connect 请求服务器链接操作,建立和服务器直接的连接。
后续代码,利用 read write 函数,完成 IO 操作。
服务器操作主要有
bind 操作,明确当前服务器中,哪一个端口作为用户请求链接端口
listen 设置监听操作
accept 接收用户请求
后续代码,利用 read write 函数,完成 IO 操作。

 

4. 三次握手

5. 四次挥手

6.TCP连接管理到底层逻辑

一、连接管理的核心:半连接队列与全连接队列

TCP 连接建立过程中,服务器需要通过两个队列管理并发连接请求,确保连接状态的有序过渡。

  1. 半连接队列(SYN 队列):未完成的三次握手
    当服务器收到客户端的 SYN 包(第一次握手)时,会创建临时连接条目并加入半连接队列,此时连接处于 SYN_RCVD 状态。队列中存储源 IP / 端口、目标 IP / 端口、服务器初始序列号(ISN)等元数据,用于等待客户端的 ACK 包(第三次握手)。

    • 触发条件:服务器处于 LISTEN 状态,收到客户端的 SYN 包并回复 SYN+ACK 后。
    • 超时处理:若未收到客户端 ACK,服务器会重传 SYN+ACK(重试次数由 net.ipv4.tcp_synack_retries 控制),超时后移除队列条目,避免资源浪费。
    • 大小限制:由内核参数 net.ipv4.tcp_max_syn_backlog 和应用层 backlog 参数共同决定,队列满时新 SYN 包会被丢弃,导致客户端连接延迟或失败。
  2. 全连接队列(Accept 队列):已完成的三次握手
    当服务器收到客户端的 ACK 包(第三次握手完成),连接会从半连接队列移除,转入全连接队列,此时连接状态变为 ESTABLISHED,等待应用层调用 accept() 函数获取。

    • 存储内容:完整的套接字(socket)信息,包括文件描述符、缓冲区指针等,供应用层直接使用。
    • 触发条件:三次握手完成,连接状态从 SYN_RCVD 转为 ESTABLISHED
    • 溢出处理:若队列满(由 net.core.somaxconn 和 backlog 最小值限制),服务器会忽略新的 ACK 包,客户端可能收到 RST 包或阻塞等待,导致连接失败。
二、保障网络稳定:TCP 拥塞控制机制

TCP 拥塞控制的核心目标是避免网络因数据量过大而拥塞,通过动态调整发送速率(拥塞窗口 cwnd)实现网络负载与容量的匹配。其核心算法包括:

 
  1. 慢启动(Slow Start)
    连接初始阶段,cwnd 以指数级增长(每收到一轮 ACK,cwnd 翻倍),快速探测网络容量。当 cwnd 达到慢启动阈值 ssthresh 时,进入拥塞避免阶段。

  2. 拥塞避免(Congestion Avoidance)
    cwnd 以线性增长(每轮 RTT 增加 1 个 MSS),避免突发流量导致拥塞。若检测到丢包,触发拥塞响应。

  3. 快速重传与快速恢复

    • 快速重传:当收到 3 个重复 ACK 时,无需等待超时,立即重传丢失的报文段,减少延迟。
    • 快速恢复:重传后,将 ssthresh 设为当前 cwnd 的一半,cwnd 调整为 ssthresh + 3*MSS,直接进入拥塞避免阶段,避免过度降低发送速率。
    • 若因超时触发重传,ssthresh 同样减半,但 cwnd 重置为 1 MSS,回到慢启动阶段(惩罚更严厉)。
三、数据传输的边界问题:粘包与分包

TCP 是面向字节流的协议,无天然消息边界,因此可能出现粘包或分包现象:

  1. 粘包(Sticky Packets)
    多个逻辑数据包被合并为一个数据流发送,原因包括:

    • 发送方缓冲区合并小数据(Nagle 算法优化);
    • 接收方未及时读取缓冲区,剩余数据与新数据合并。
  2. 分包(Packet Segmentation)
    一个逻辑数据包被拆分为多个 TCP 段,原因包括:

    • MSS(最大段大小)限制:单个 TCP 段数据载荷不超过 MSS(通常 1460 字节);
    • MTU(最大传输单元)限制:IP 层对超过 MTU 的数据报分片(如以太网 MTU 为 1500 字节)。
 

解决思路:应用层需自定义消息边界(如固定长度、分隔符、头部长度字段等),确保接收方正确解析数据。

四、连接生命周期:11 种状态迁移

TCP 状态机定义了连接从建立到关闭的 11 种状态及迁移逻辑,核心流程如下:

  1. 连接建立(三次握手)

    • 客户端:CLOSED → SYN_SENT → ESTABLISHED(发送 SYN → 收到 SYN+ACK 并回复 ACK)。
    • 服务器:CLOSED → LISTEN → SYN_RCVD → ESTABLISHED(监听 → 收到 SYN 并回复 SYN+ACK → 收到 ACK)。
  2. 数据传输(ESTABLISHED)
    双方处于 ESTABLISHED 状态,可双向传输数据。

  3. 连接关闭(四次挥手)

    • 主动关闭方:ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED(发送 FIN → 收到 ACK → 收到 FIN 并回复 ACK → 等待 2MSL 后关闭)。
    • 被动关闭方:ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED(收到 FIN 并回复 ACK → 发送 FIN → 收到 ACK 后关闭)。

特殊状态

  • CLOSING:双方同时发送 FIN 时的过渡状态,等待对方 ACK 后进入 TIME_WAIT
  • TIME_WAIT:主动关闭方等待 2MSL(最大段生存时间),确保旧数据包消失,避免干扰新连接。
五、常见异常状态解析
  1. 大量 CLOSE_WAIT 状态
    被动关闭方收到 FIN 并回复 ACK 后,若应用层未调用 close() 释放连接,连接会一直停留在 CLOSE_WAIT 状态,导致资源泄漏(如文件描述符耗尽)。需检查代码中连接释放逻辑,确保及时调用关闭接口。

  2. TIME_WAIT 累积
    短时间内大量连接关闭可能导致 TIME_WAIT 状态端口占用,可通过 SO_REUSEADDR 选项允许端口复用缓解。

六、TCP连接建立、状态管理以及销毁的完整过程

        在 TCP 连接建立过程中,当客户端发起第一次握手,SYN 包到达服务器时,服务器会创建 TCB(TCP Control Block)并将该连接放入半连接队列(SYN 队列)。此时,连接处于未完全建立状态。待客户端发送 ACK 完成第三次握手,服务器会把连接从半连接队列移至全连接队列(accept 队列),同时将 TCB 标记为 ESTABLISHED 状态,意味着连接已成功建立。这里需要注意,连接进入全连接队列时状态就已变为 ESTABLISHED,而非在 accept () 调用之后。accept () 函数的作用是从全连接队列中取出连接,将 TCB 与应用层 socket 关联,分配 fd 和文件结构体,把已建立的连接交付给应用层。即使应用程序未调用 accept (),全连接队列中的连接依然维持 ESTABLISHED 状态,直到队列满时会触发 SYN 重传或拒绝新连接。在 Linux 系统中,半连接队列和全连接队列的大小可分别通过 net.ipv4.tcp_max_syn_backlog 和 somaxconn 参数进行配置。当连接断开,如完成四次挥手,主动关闭方会进入 TIME_WAIT 状态,此时 TCB 不会立即销毁,而是会保留一段时间(默认 60 秒,可配置),以避免旧数据包对新连接造成干扰。

7. TCP 操作相关 API

1. socket():创建套接字
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
 
  • 函数功能
    创建用于网络通信的套接字(Socket),为后续操作分配内核资源。
  • 函数参数
    • domain:协议族,TCP 用 AF_INET(IPv4)或 AF_INET6(IPv6)。
    • type:套接字类型,TCP 用 SOCK_STREAM(流式套接字,可靠连接)。
    • protocol:协议,TCP 填 0(由 type 推导)。
  • 返回值
    成功返回套接字描述符(非负整数),失败返回 -1 并置 errno
2. bind():绑定地址(服务器专用)
#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 函数功能
    将套接字与 IP 地址、端口绑定,让服务器在指定地址监听连接。
  • 函数参数
    • sockfdsocket() 创建的套接字描述符。
    • addr:要绑定的地址结构体(struct sockaddr_in 或 struct sockaddr_in6),需强转为 struct sockaddr *
    • addrlen:地址结构体长度(如 sizeof(struct sockaddr_in))。
  • 返回值
    成功返回 0,失败返回 -1 并置 errno(常见错误:端口被占用 EADDRINUSE)。
3. listen():监听连接(服务器专用)
#include <sys/socket.h>

int listen(int sockfd, int backlog);
  • 函数功能
    将套接字转为被动监听状态,开始接受客户端连接请求,维护全连接队列backlog 限制队列长度)。
  • 函数参数
    • sockfd:已绑定的套接字描述符。
    • backlog:全连接队列最大长度(实际受系统 somaxconn 限制,填 5/10 等常用值即可)。
  • 返回值
    成功返回 0,失败返回 -1 并置 errno
4. accept():接受连接(服务器专用)
#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 函数功能
    从监听套接字的全连接队列取出已完成三次握手的连接,创建新套接字用于与客户端通信(原监听套接字仍可继续监听)。
  • 函数参数
    • sockfd:监听状态的套接字描述符(listen() 调用后的套接字)。
    • addr:用于存储客户端地址信息的结构体(struct sockaddr_in),可 NULL(不关心客户端地址)。
    • addrlen:地址结构体长度的指针(传入前需赋值,如 sizeof(struct sockaddr_in))。
  • 返回值
    成功返回新套接字描述符(用于收发数据),失败返回 -1 并置 errno
5. connect():发起连接(客户端专用)
#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 函数功能
    客户端主动向服务器发起 TCP 连接,执行三次握手,连接成功后可收发数据。
  • 函数参数
    • sockfd:客户端套接字描述符(socket() 创建后未绑定的套接字)。
    • addr:服务器地址结构体(struct sockaddr_in,需填服务器 IP + 端口)。
    • addrlen:地址结构体长度(如 sizeof(struct sockaddr_in))。
  • 返回值
    成功返回 0,失败返回 -1 并置 errno(常见错误:服务器未监听 ECONNREFUSED、网络不可达 ENETUNREACH)。
6. send() / recv():收发数据
#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 函数功能
    • send():通过套接字发送数据(基于 TCP 流式传输,可靠有序)。
    • recv():从套接字接收数据。
  • 函数参数
    • sockfd:已连接的套接字描述符(accept() 或 connect() 返回的套接字)。
    • buf:数据缓冲区(send 填要发送的数据,recv 填接收数据的缓冲区)。
    • len:数据长度(send 填要发送的字节数,recv 填缓冲区最大容量)。
    • flags:标志位,一般填 0(默认行为)。
  • 返回值
    成功返回实际收发的字节数,失败返回 -1 并置 errnorecv 收到 EOF(连接关闭)返回 0
7. close():关闭套接字
#include <unistd.h>

int close(int fd);
  • 函数功能
    关闭套接字,释放资源,触发 TCP 四次挥手(主动关闭方发 FIN)。
  • 函数参数
    fd:套接字描述符(socket()accept() 返回的描述符)。
  • 返回值
    成功返回 0,失败返回 -1 并置 errno(极少失败,通常可忽略)。

八、完整代码案例

1. 服务器代码(tcp_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8888       // 监听端口
#define BACKLOG 5       // 全连接队列长度
#define BUF_SIZE 1024   // 缓冲区大小

int main() {
    // 1. 创建套接字
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 2. 绑定地址(端口+IP)
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;  // 绑定所有网卡 IP
    server_addr.sin_port = htons(PORT);        // 端口转换为网络字节序

    if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        close(listen_fd);
        exit(EXIT_FAILURE);
    }

    // 3. 监听连接
    if (listen(listen_fd, BACKLOG) == -1) {
        perror("listen");
        close(listen_fd);
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);

    // 4. 接受客户端连接
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len);
    if (conn_fd == -1) {
        perror("accept");
        close(listen_fd);
        exit(EXIT_FAILURE);
    }
    printf("Connected to client: %s:%d\n", 
           inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    // 5. 收发数据
    char buf[BUF_SIZE];
    ssize_t recv_len = recv(conn_fd, buf, sizeof(buf), 0);
    if (recv_len > 0) {
        buf[recv_len] = '\0';  // 手动加字符串结束符
        printf("Received from client: %s\n", buf);

        // 回复客户端
        const char *reply = "Hello from Server!";
        send(conn_fd, reply, strlen(reply), 0);
        printf("Sent reply to client.\n");
    } else if (recv_len == 0) {
        printf("Client closed connection.\n");
    } else {
        perror("recv");
    }

    // 6. 关闭套接字
    close(conn_fd);
    close(listen_fd);
    return 0;
}
2. 客户端代码(tcp_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"  // 服务器 IP(本地测试用回环地址)
#define SERVER_PORT 8888       // 服务器端口
#define BUF_SIZE 1024          // 缓冲区大小

int main() {
    // 1. 创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        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_addr.s_addr = inet_addr(SERVER_IP);  // 转换为网络字节序
    server_addr.sin_port = htons(SERVER_PORT);           // 端口转换为网络字节序

    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("Connected to server %s:%d\n", SERVER_IP, SERVER_PORT);

    // 3. 发送数据
    const char *msg = "Hello from Client!";
    if (send(sockfd, msg, strlen(msg), 0) == -1) {
        perror("send");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("Sent message to server.\n");

    // 4. 接收回复
    char buf[BUF_SIZE];
    ssize_t recv_len = recv(sockfd, buf, sizeof(buf), 0);
    if (recv_len > 0) {
        buf[recv_len] = '\0';
        printf("Received from server: %s\n", buf);
    } else if (recv_len == 0) {
        printf("Server closed connection.\n");
    } else {
        perror("recv");
    }

    // 5. 关闭套接字
    close(sockfd);
    return 0;
}

https://github.com/0voice


网站公告

今日签到

点亮在社区的每一天
去签到