一、TCP三大核心特性:面向连接、可靠传输、流式服务

TCP(传输控制协议)是互联网核心协议之一,位于传输层,提供端到端的可靠数据传输服务。其三大核心特性构成了TCP协议的基石。

1. 面向连接

TCP是面向连接的协议,通信双方在传输数据前必须建立连接,传输结束后要释放连接。这个过程通过著名的三次握手四次挥手完成,确保通信双方都同意建立和终止连接。

实际应用:就像打电话前需要先拨号接通,通话结束后要挂断电话,TCP在数据传输前需要建立连接,确保对方准备好通信。

2. 可靠传输

TCP通过多种机制保证数据的可靠传输

  • 序号和确认机制:每个数据包都有唯一序号,接收方会发送确认应答

  • 超时重传:发送方在指定时间内未收到确认会重新发送数据

  • 流量控制:通过滑动窗口机制防止接收方缓冲区溢出

  • 拥塞控制:根据网络状况动态调整发送速率

记忆技巧:想象快递发货——有发货单号(序号)、收货确认(确认机制)、超时补发(重传机制)。

3. 流式服务

TCP将数据看作无结构的字节流,不保留应用层数据的边界。应用程序负责解析数据边界,这也是粘包问题的根源。

与UDP对比:UDP是面向数据报的协议,每个数据报都有明确的边界。

二、TCP头部结构:20字节的精密设计

TCP头部通常为20字节,包含控制TCP连接和数据传输所需的所有信息。

// TCP头部结构体
typedef struct _TCP_HEADER {
    short m_sSourPort;         // 源端口号16bit
    short m_sDestPort;         // 目的端口号16bit
    unsigned int m_uiSequNum;  // 序列号32bit
    unsigned int m_uiAcknowledgeNum; // 确认号32bit
    short m_sHeaderLenAndFlag; // 前4位:TCP头长度;中6位:保留;后6位:标志位
    short m_sWindowSize;       // 窗口大小16bit
    short m_sCheckSum;         // 检验和16bit
    short m_surgentPointer;    // 紧急数据偏移量16bit
} TCP_HEADER;

TCP头部关键字段详解:

字段 长度 功能说明
源端口/目的端口 各16位 标识发送和接收进程
序列号 32位 标识数据字节流中每个字节的位置
确认号 32位 期望收到的下一个报文段的序号
数据偏移 4位 TCP头部长度,以4字节为单位
控制位 6位 URG/ACK/PSH/RST/SYN/FIN,控制连接状态
窗口大小 16位 流量控制,表示接收方能接收的数据量
校验和 16位 检测数据传输过程中的错误
紧急指针 16位 当URG=1时有效,标识紧急数据位置

记忆口诀"T源目序缺首保,紧确推和复同终,窗校紧选数" - 对应源端口、目的端口、序号、确认号、首部长度、保留、URG、ACK、PSH、RST、SYN、FIN、窗口、校验和、紧急指针。

三、TCP编程流程:Socket API详解

TCP服务器和客户端有明确的编程流程,下面是典型的函数调用顺序:

服务器端流程

socket() → bind() → listen() → accept() → recv()/send() → close()

客户端流程

socket() → connect() → send()/recv() → close()

Socket API详细说明

函数 头文件 参数说明 返回值 示例参数 示例含义
socket() <sys/socket.h> int domain, int type, int protocol 成功:套接字描述符,失败:-1 AF_INET, SOCK_STREAM, 0 IPv4协议,TCP流式套接字
bind() <sys/socket.h> int sockfd, const struct sockaddr* addr, socklen_t addrlen 成功:0,失败:-1 sockfd, &server_addr, sizeof(server_addr) 将套接字绑定到指定IP和端口
listen() <sys/socket.h> int sockfd, int backlog 成功:0,失败:-1 sockfd, 5 设置最大等待连接数为5
accept() <sys/socket.h> int sockfd, struct sockaddr* addr, socklen_t* addrlen 成功:新套接字描述符,失败:-1 sockfd, &client_addr, &addr_len 接受客户端连接,获取客户端地址
connect() <sys/socket.h> int sockfd, const struct sockaddr* addr, socklen_t addrlen 成功:0,失败:-1 sockfd, &server_addr, sizeof(server_addr) 连接到指定服务器
send() <sys/socket.h> int sockfd, const void* buf, size_t len, int flags 成功:发送字节数,失败:-1 sockfd, buffer, strlen(buffer), 0 发送缓冲区数据
recv() <sys/socket.h> int sockfd, void* buf, size_t len, int flags 成功:接收字节数,0:连接关闭,-1:失败 sockfd, buffer, BUFFER_SIZE, 0 接收数据到缓冲区
close() <unistd.h> int fd 成功:0,失败:-1 sockfd 关闭套接字连接

简单TCP服务器代码示例

#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 8080
#define BUFFER_SIZE 1024
int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    char *hello = "Hello from server";
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    printf("Client connected\n");
    read(new_socket, buffer, BUFFER_SIZE);
    printf("Message from client: %s\n", buffer);
    send(new_socket, hello, strlen(hello), 0);
    printf("Hello message sent\n");
    close(new_socket);
    close(server_fd);
    return 0;
}

四、三次握手与四次挥手详解

三次握手:建立连接

三次握手过程确保通信双方都具备数据发送和接收能力:

  1. 第一次握手:客户端发送SYN=1,Seq=J,进入SYN_SENT状态

  2. 第二次握手:服务器回复SYN=1,ACK=1,Ack=J+1,Seq=K,进入SYN_RCVD状态

  3. 第三次握手:客户端发送ACK=1,Ack=K+1,Seq=J+1,双方进入ESTABLISHED状态

为什么需要三次握手?

  • 防止已失效的连接请求报文突然到达服务器,导致服务器错误打开连接

  • 确保双方都具备发送和接收能力

  • 同步双方的初始序列号

四次挥手:释放连接

四次挥手过程确保数据完整传输后才释放连接:

  1. 第一次挥手:主动关闭方发送FIN=1,Seq=U,进入FIN_WAIT_1状态

  2. 第二次挥手:被动关闭方发送ACK=1,Ack=U+1,Seq=V,进入CLOSE_WAIT状态

  3. 第三次挥手:被动关闭方发送FIN=1,ACK=1,Ack=U+1,Seq=W,进入LAST_ACK状态

  4. 第四次挥手:主动关闭方发送ACK=1,Ack=W+1,Seq=U+1,进入TIME_WAIT状态

为什么需要四次挥手?
TCP是全双工协议,每个方向必须单独关闭。四次挥手确保双方都完成数据发送后再完全关闭连接。

五、TIME_WAIT状态的作用与意义

当主动关闭连接的一方发送完最后一个ACK后,会进入TIME_WAIT状态,持续2MSL(最大报文段生存时间)。

TIME_WAIT状态存在的两个主要原因:

  1. 可靠地终止TCP连接:确保最后一个ACK能够到达对方,如果ACK丢失,对方会重传FIN,主动方还能再次响应ACK

  2. 让旧连接报文完全消失:确保网络中该连接的旧报文都过期,防止被新连接误收

MSL(Maximum Segment Lifetime):报文段在网络中的最大生存时间,通常是2分钟,因此TIME_WAIT状态一般持续2-4分钟。

实际开发问题:服务器端出现大量TIME_WAIT连接可能导致端口耗尽。解决方案包括设置SO_REUSEADDR套接字选项,允许重用处于TIME_WAIT状态的套接字地址。

六、粘包问题及解决方案

什么是粘包问题?

由于TCP是流式协议,不保留消息边界,多个小数据包可能被合并成一个大数据包发送(粘包),或者一个大包被拆分成多个小包(拆包)。

产生粘包的原因:

  1. 应用层写入数据小于套接字缓冲区大小,TCP会将多个写入合并发送

  2. 接收方应用层没有及时读取缓冲区数据,导致多个数据包在缓冲区累积

  3. 数据包被分片传输,接收方重组时可能出现乱序

解决方案:

  1. 固定长度法:所有消息采用相同长度,不够用空格填充

    // 固定消息长度为100字节
    char message[100] = "Hello";
    memset(message + strlen(message), ' ', 100 - strlen(message));
  2. 长度前缀法:在消息前添加长度字段

    // 发送方
    uint32_t msg_len = htonl(strlen(actual_message));
    send(sock, &msg_len, sizeof(uint32_t), 0);  // 先发送长度
    send(sock, actual_message, strlen(actual_message), 0);  // 再发送内容
    
    // 接收方
    uint32_t msg_len;
    recv(sock, &msg_len, sizeof(uint32_t), 0);  // 先接收长度
    msg_len = ntohl(msg_len);
    char* buffer = malloc(msg_len + 1);
    recv(sock, buffer, msg_len, 0);  // 按长度接收内容
    buffer[msg_len] = '\0';
  3. 分隔符法:使用特殊字符作为消息边界,如\r\n

    // 发送方
    send(sock, "Hello\r\n", 7, 0);
    send(sock, "World\r\n", 7, 0);
    
    // 接收方需要按字符解析,遇到\r\n则分割

七、常见面试题精选

1. TCP与UDP的区别是什么?

特性 TCP UDP
连接性 面向连接 无连接
可靠性 可靠传输,有确认机制 不可靠传输,尽最大努力交付
顺序性 保证数据顺序 不保证顺序
速度 相对慢 相对快
头部大小 最小20字节 8字节
控制机制 有流量控制、拥塞控制 无控制机制
适用场景 文件传输、网页浏览、邮件 视频会议、DNS查询、实时游戏

2. 为什么TCP建立连接需要三次握手,而不是两次或四次?

三次握手是最小次数,能够:

        防止历史连接请求导致服务器错误打开连接

        确保双方都具备数据发送和接收能力

        同步双方的初始序列号

两次握手无法防止失效的连接请求,四次握手则冗余。

3. TCP的可靠传输是如何实现的?

通过多种机制保证可靠性:

        序号和确认机制:每个数据包有序号,接收方发送确认

        超时重传:未收到确认会自动重传

        滑动窗口:流量控制,防止接收方缓冲区溢出

        拥塞控制:根据网络状况调整发送速率

4. 如果第三次握手丢失了会发生什么?

服务器在发送SYN+ACK后未收到ACK,会重传SYN+ACK包(默认重试5次)。如果始终未收到ACK,服务器最终会关闭这个半连接。

5. CLOSE_WAIT状态过多是什么原因?如何解决?

原因:应用程序没有及时调用close()关闭连接。

解决方案

        检查代码逻辑,确保socket正确关闭

        使用setsockopt设置SO_LINGER选项

        优化资源释放流程

6. TCP的流量控制和拥塞控制有什么区别?

       流量控制:点对点通信,防止发送方数据发送过快导致接收方缓冲区溢出,通过滑动窗口实现

       拥塞控制:全局性控制,防止网络过载,通过慢启动、拥塞避免、快重传和快恢复算法实现

总结

TCP协议作为互联网通信的基石,其设计精巧而复杂。理解TCP的三大特性、头部结构、连接管理机制和可靠传输原理,对于网络编程和故障排查都至关重要。通过本文的学习,你应该掌握了:

  1. TCP三大特性的具体表现和应用场景

  2. TCP头部各字段的功能和作用

  3. Socket编程的基本流程和API使用方法

  4. 三次握手和四次挥手的详细过程及原理

  5. TIME_WAIT状态的意义和实际问题解决方法

  6. 粘包问题的成因和多种解决方案

  7. 常见的TCP相关面试题和解答思路

应用建议:在学习理论的同时,多使用Wireshark等工具观察实际的TCP数据包,加深对协议的理解。在编程实践中,注意正确处理连接建立和关闭,以及消息边界的处理。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐