3. TCP协议深入与可靠传输

TCP(Transmission Control Protocol,传输控制协议)是互联网中使用最广泛的传输层协议,它为应用层提供面向连接的、可靠的、基于字节流的传输服务。理解TCP的可靠传输机制,是编写健壮网络程序的基础。本章将从连接建立与释放、流量控制、拥塞控制三个维度深入分析TCP的工作原理,并用Python实现一个带超时重传的可靠消息传输模块。

3.1 三次握手:连接的建立

TCP是面向连接的协议,通信双方在传输数据之前必须先建立连接。连接建立过程被称为"三次握手",其核心目的是同步双方的初始序列号(ISN),并确认双方都具备收发能力。

第一次握手:客户端向服务器发送一个SYN报文段,其中SYN标志位置1,同时携带客户端随机生成的初始序列号seq=x。此时客户端进入SYN_SENT状态。第二次握手:服务器收到SYN后,回复一个SYN+ACK报文段,其中SYN=1、ACK=1,确认号ack=x+1,同时携带服务器自己生成的初始序列号seq=y。服务器进入SYN_RCVD状态。第三次握手:客户端收到SYN+ACK后,发送一个ACK报文段,确认号ack=y+1,序列号seq=x+1。客户端进入ESTABLISHED状态,服务器收到该ACK后也进入ESTABLISHED状态,连接正式建立。

为什么需要三次而不是两次?关键在于防止已失效的连接请求报文段突然又传到服务器,导致服务器错误地建立连接并浪费资源。三次握手让双方都能确认对方的收发能力,并同步序列号,为后续可靠传输奠定基础。

3.2 四次挥手:连接的释放

TCP连接是全双工的,因此释放连接需要双方各自关闭自己方向的传输通道,这个过程称为"四次挥手"。

第一次挥手:主动关闭方(假设为客户端)发送FIN报文段,FIN=1,seq=u,表示客户端没有数据要发送了,进入FIN_WAIT_1状态。第二次挥手:服务器收到FIN后,回复ACK,ack=u+1,seq=v,服务器进入CLOSE_WAIT状态,客户端收到后进入FIN_WAIT_2状态。此时连接处于半关闭状态,服务器仍可向客户端发送数据。第三次挥手:服务器数据发送完毕后,发送FIN报文段,FIN=1,ACK=1,seq=w,ack=u+1,服务器进入LAST_ACK状态。第四次挥手:客户端收到FIN后,回复ACK,ack=w+1,seq=u+1,客户端进入TIME_WAIT状态,等待2MSL(Maximum Segment Lifetime,报文最大生存时间)后关闭连接。服务器收到ACK后进入CLOSED状态。

TIME_WAIT状态的存在有两个原因:一是确保最后一个ACK能够到达服务器,若丢失则服务器会重传FIN,客户端可再次响应;二是让本次连接产生的所有报文段在网络中消失,避免影响后续新连接。

TCP 三次握手与四次挥手状态时序 客户端 Client 服务器 Server — 三次握手 — SYN=1, seq=x SYN_SENT SYN=1, ACK=1, seq=y, ack=x+1 SYN_RCVD ACK=1, seq=x+1, ack=y+1 ESTABLISHED ESTABLISHED — 四次挥手 — FIN=1, seq=u FIN_WAIT_1 ACK=1, seq=v, ack=u+1 CLOSE_WAIT FIN_WAIT_2 FIN=1, ACK=1, seq=w, ack=u+1 LAST_ACK ACK=1, seq=u+1, ack=w+1 TIME_WAIT (2MSL) CLOSED TIME_WAIT 等待 2MSL:确保最后 ACK 到达服务器,并让旧报文在网络中消散 半关闭状态:服务器收到 FIN 后仍可继续发送未传完的数据

3.3 流量控制:滑动窗口机制

流量控制解决的是"发送方发送太快,接收方来不及处理"的问题。TCP通过滑动窗口机制实现流量控制。接收方在ACK报文段中通过窗口字段(Window Size)告知发送方自己当前还能接收多少字节的数据,这个值称为接收窗口rwnd。

发送方的发送窗口大小取接收窗口rwnd和拥塞窗口cwnd中的较小值。当接收方应用层读取数据后,接收缓冲区腾出空间,接收方会通过窗口更新通知发送方扩大窗口。如果接收方缓冲区满,会通告零窗口,发送方停止发送数据,并启动持续计时器(Persist Timer),定期发送窗口探测报文,防止死锁。

滑动窗口的核心思想是:窗口内的数据可以连续发送而不必等待每个字节的确认,从而大幅提高传输效率。窗口左沿随确认到达而右移,右沿随窗口通告而调整。

3.4 拥塞控制:四种算法协同

流量控制关注接收方能力,而拥塞控制关注的是网络本身的承载能力。TCP拥塞控制包含四个核心算法:慢启动、拥塞避免、快重传和快恢复。

慢启动阶段,拥塞窗口cwnd从1个MSS(Maximum Segment Size,最大报文段长度)开始,每收到一个ACK,cwnd翻倍,呈指数增长。当cwnd达到慢启动阈值ssthresh后,进入拥塞避免阶段,cwnd每个RTT只增加1个MSS,呈线性增长。当发生超时重传时,ssthresh设为当前cwnd的一半,cwnd重置为1,重新进入慢启动。当收到三个重复ACK时,触发快重传,立即重传丢失的报文段,并将ssthresh和cwnd都设为当前cwnd的一半,进入快恢复阶段,避免回到慢启动。

3.5 Python实现带超时重传的可靠消息传输

下面基于UDP套接字实现一个简化版的可靠传输协议,包含序列号、确认应答、超时重传和滑动窗口的基本思想。该示例用于教学演示TCP可靠传输的核心机制。

import socket
import struct
import time
import threading

# 报文格式: | seq(4B) | ack(4B) | flags(1B) | length(2B) | payload |
HEADER_FMT = '!IIBH'
HEADER_SIZE = struct.calcsize(HEADER_FMT)
FLAG_DATA = 0x01
FLAG_ACK  = 0x02
FLAG_FIN  = 0x04

class ReliableChannel:
    def __init__(self, sock, peer_addr, timeout=1.0, window=4):
        self.sock = sock
        self.peer = peer_addr
        self.timeout = timeout
        self.window = window          # 发送窗口大小
        self.send_base = 0            # 窗口左沿
        self.next_seq = 0             # 下一个待发序列号
        self.recv_base = 0            # 期望接收的序列号
        self.buffer = {}              # 乱序缓存
        self.lock = threading.Lock()

    def _pack(self, seq, ack, flags, payload=b''):
        return struct.pack(HEADER_FMT, seq, ack, flags, len(payload)) + payload

    def _unpack(self, pkt):
        seq, ack, flags, length = struct.unpack(HEADER_FMT, pkt[:HEADER_SIZE])
        return seq, ack, flags, pkt[HEADER_SIZE:HEADER_SIZE+length]

    def send_message(self, data, chunk=512):
        """将消息分片后通过滑动窗口发送,带超时重传"""
        segments = [data[i:i+chunk] for i in range(0, len(data), chunk)]
        pending = {}   # seq -> (packet, send_time)
        idx = 0
        while self.send_base < len(segments):
            # 窗口未满则继续发送
            while self.next_seq < self.send_base + self.window \
                  and self.next_seq < len(segments):
                pkt = self._pack(self.next_seq, 0, FLAG_DATA,
                                 segments[self.next_seq])
                self.sock.sendto(pkt, self.peer)
                pending[self.next_seq] = (pkt, time.time())
                self.next_seq += 1

            # 等待ACK
            self.sock.settimeout(self.timeout)
            try:
                ack_pkt, _ = self.sock.recvfrom(2048)
                _, ack_num, flags, _ = self._unpack(ack_pkt)
                if flags & FLAG_ACK:
                    with self.lock:
                        if ack_num > self.send_base:
                            self.send_base = ack_num
                            for s in list(pending):
                                if s < self.send_base:
                                    del pending[s]
            except socket.timeout:
                # 超时:重传窗口内所有未确认报文
                now = time.time()
                for seq, (pkt, ts) in list(pending.items()):
                    if now - ts >= self.timeout:
                        self.sock.sendto(pkt, self.peer)
                        pending[seq] = (pkt, now)
                        print(f"[重传] seq={seq}")

    def recv_message(self):
        """接收数据并返回按序拼接的完整消息"""
        while True:
            pkt, addr = self.sock.recvfrom(2048)
            seq, _, flags, payload = self._unpack(pkt)
            if flags & FLAG_DATA:
                if seq == self.recv_base:
                    self.buffer[seq] = payload
                    self.recv_base += 1
                    # 连续交付
                    while self.recv_base in self.buffer:
                        self.recv_base += 1
                elif seq > self.recv_base:
                    self.buffer[seq] = payload   # 乱序缓存
                # 回复累积ACK
                ack_pkt = self._pack(0, self.recv_base, FLAG_ACK)
                self.sock.sendto(ack_pkt, addr)
                if flags & FLAG_FIN:
                    break
        return b''.join(self.buffer[i] for i in sorted(self.buffer))

上述实现中,send_base和next_seq共同维护发送窗口,pending字典记录已发送但未确认的报文及其发送时间。当socket.timeout触发时,遍历pending中超过超时时间的报文进行重传。接收端使用累积确认,并通过buffer处理乱序到达的报文段。

超时时间的选择直接影响性能:过短会导致不必要的重传,过长则降低吞吐量。实际TCP实现中采用自适应重传算法(如Jacobson/Karels算法),根据RTT的测量值动态调整RTO(Retransmission Timeout)。示例中固定为1秒仅用于教学演示。

3.6 关键参数与观察指标

参数/指标含义典型值或观察方式
MSS最大报文段长度以太网通常1460字节
rwnd接收窗口由接收方缓冲区决定
cwnd拥塞窗口慢启动指数增长,拥塞避免线性增长
RTO重传超时时间基于RTT动态计算
ssthresh慢启动阈值超时后设为cwnd一半
2MSLTIME_WAIT等待时间Linux默认60秒

在实际调试中,可以使用ss -ti命令查看TCP连接的cwnd、rtt、retrans等指标,或通过Wireshark抓包观察三次握手、四次挥手以及重传行为。理解这些机制后,在编写网络程序时就能更好地设置超时、缓冲区大小和重试策略,从而构建真正可靠的通信模块。