TCP/UDP网络协议实践

2025/10/23

传输层里比较重要的两个协议,一个是 TCP,一个是 UDP;TCP 是面向连接的,UDP 是面向无连接的;

面对这种概念性的东西,甚至还要咬文嚼字,不免觉得枯燥,但其实网络也可以很好玩,最重要的是要结合实践,面对上面一句话,一字一句记下来就跟背电话号码没什么区别,甚至味同嚼蜡,如果要理解其中的一些机制,以及网络协议在设计时所需要解决的实际问题,则需要亲自动手体会,纸上得来终觉浅。

UDP

所谓面向连接,无连接,实际上就是在报文在传输之前,通信的两端是否需要提前建立连接,UDP 无需提前建立连接,简单粗暴,直接从源端传输到目标端;

从一个报文开始

首先借助 docker 启动两个容器,netshoot 是一个自带很多网络工具的镜像,用于测试网络

docker pull nicolaka/netshoot

# 172.17.0.3
docker run -d --name netshoot_container1 nicolaka/netshoot sleep infinity
# 172.17.0.4
docker run -d --name netshoot_container2 nicolaka/netshoot sleep infinity

docker ps

在 netshoot_container1 中使用 tcpdump 监听 12345 端口,并指定协议为 udp

tcpdump -i eth0 udp port 12345 -X -v
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes

然后在 netshoot_container2 中,发送一个 udp 的请求到 netshoot_container1 中:

echo test-udp |nc -u 172.17.0.3 12345

在netshoot_container1 中得到如下结果:

13:38:59.946566 IP (tos 0x0, ttl 64, id 18849, offset 0, flags [DF], proto UDP (17), length 37)
    172.17.0.4.35387 > 941541e59a01.12345: UDP, length 9
		0x0000:  4500 0025 49a1 4000 4011 98fd ac11 0004  E..%I.@.@.......
		0x0010:  ac11 0003 8a3b 3039 0011 584c 7465 7374  .....;09..XLtest
		0x0020:  2d75 6470 0a                             -udp.

在 tcpdump 的十六进制输出中,第一列(如 0x0000, 0x0010, 0x0020)表示的是数据包中的字节偏移量(byte offset),以十六进制表示。这是数据在数据包中的位置索引。 具体解释: 0x0000:数据包起始位置(第 0 字节) 0x0010:第 16 字节(十六进制 10 = 十进制 16) 0x0020:第 32 字节(十六进制 20 = 十进制 32)

完整的数据包结构解析:

  1. 第一行:0x0000: … 包含 IP 头部的前 16 个字节 偏移量 0x0000 到 0x000F(16 字节)
  2. 第二行:0x0010: … 包含 IP 头部的后 4 字节 + UDP 头部的前 12 字节 偏移量 0x0010 到 0x001F(16 字节)
  3. 第三行:0x0020: … 包含 UDP 数据的开始部分 偏移量 0x0020 到 0x0024(5 字节)

完整报文格式如下:

udp

MTU

在网络传输过程中,每次传输的报文都会有一个上限,这个上限就是 MTU;

MTU(Maximum Transmission Unit,最大传输单元)是网络接口在一次传输中能够承载的最大数据包大小,不包括数据链路层的头部(如以太网帧头)

ip link show eth0
759: eth0@if760: <BROADCAST,MULTICAST,UP,LOWER_UP> 
	mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff link-netnsid 0

当一次网络请求数据超过 MTU 会发生什么?

当一次网络请求的数据超过最大传输单元(MTU,Maximum Transmission Unit)时,数据会被分段并在网络上传输。

例如,发送一个 200k 的UPD 请求,分别在请求端,服务端抓包:

# client
tcpdump -i eth0 "host 172.17.0.4 and ip proto 17" -vvv
cat test.log |nc -u 172.17.0.4 1234
# server
tcpdump -i eth0 "host 172.17.0.3 and ip proto 17" -vvv

可以看到数据被分成多个包发送:

# client
03:52:06.252714 IP (tos 0x0, ttl 64, id 41053, offset 0, flags [+], proto UDP (17), length 1500)
    941541e59a01.35352 > 172.17.0.4.12345: UDP, length 16384
03:52:06.252732 IP (tos 0x0, ttl 64, id 41053, offset 1480, flags [+], proto UDP (17), length 1500)
    941541e59a01 > 172.17.0.4: udp
03:52:06.252736 IP (tos 0x0, ttl 64, id 41053, offset 2960, flags [+], proto UDP (17), length 1500)
    941541e59a01 > 172.17.0.4: udp
03:52:06.252738 IP (tos 0x0, ttl 64, id 41053, offset 4440, flags [+], proto UDP (17), length 1500)
    941541e59a01 > 172.17.0.4: udp
03:52:06.252739 IP (tos 0x0, ttl 64, id 41053, offset 5920, flags [+], proto UDP (17), length 1500)
    941541e59a01 > 172.17.0.4: udp
03:52:06.252741 IP (tos 0x0, ttl 64, id 41053, offset 7400, flags [+], proto UDP (17), length 1500)
    941541e59a01 > 172.17.0.4: udp
03:52:06.252742 IP (tos 0x0, ttl 64, id 41053, offset 8880, flags [+], proto UDP (17), length 1500)
    941541e59a01 > 172.17.0.4: udp
03:52:06.252743 IP (tos 0x0, ttl 64, id 41053, offset 10360, flags [+], proto UDP (17), length 1500)
    941541e59a01 > 172.17.0.4: udp
03:52:06.252745 IP (tos 0x0, ttl 64, id 41053, offset 11840, flags [+], proto UDP (17), length 1500)
    941541e59a01 > 172.17.0.4: udp
03:52:06.252746 IP (tos 0x0, ttl 64, id 41053, offset 13320, flags [+], proto UDP (17), length 1500)
    941541e59a01 > 172.17.0.4: udp
03:52:06.252748 IP (tos 0x0, ttl 64, id 41053, offset 14800, flags [+], proto UDP (17), length 1500)
    941541e59a01 > 172.17.0.4: udp
03:52:06.252749 IP (tos 0x0, ttl 64, id 41053, offset 16280, flags [none], proto UDP (17), length 132)
    941541e59a01 > 172.17.0.4: udp

正常来说,UDP 分成多个包发送,当 UDP 服务端接收到多个报文且它们到达时间有差异时,处理方式完全取决于应用程序的实现。UDP 协议本身不提供任何排序或重传机制,因此服务端需要自行处理这些问题。 这也是为什么 UDP 不是可靠的传输协议。

TCP

同样地,从一次 tcp 请求报文开始。

在 netshoot_container1 中,向 netshoot_container2 中发起一个 tcp 请求:

# netshoot_container2
nc -l -p 12345
# netshoot_container1
echo 'test-tcp' |nc 172.17.0.4 12345

在netshoot_container1中,抓包:

# netshoot_container1
tcpdump -i eth0 tcp port 12345 -v

12:43:55.167496 IP (tos 0x0, ttl 64, id 16012, offset 0, flags [DF], proto TCP (6), length 60)
    941541e59a01.36396 > 172.17.0.4.12345: Flags [S], cksum 0x5858 (incorrect -> 0x5209), seq 597377402, win 64240, options [mss 1460,sackOK,TS val 3065366699 ecr 0,nop,wscale 7], length 0
12:43:55.167587 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    172.17.0.4.12345 > 941541e59a01.36396: Flags [S.], cksum 0x5858 (incorrect -> 0xd2f4), seq 1871059573, ack 597377403, win 65160, options [mss 1460,sackOK,TS val 569102212 ecr 3065366699,nop,wscale 7], length 0
12:43:55.167597 IP (tos 0x0, ttl 64, id 16013, offset 0, flags [DF], proto TCP (6), length 52)
    941541e59a01.36396 > 172.17.0.4.12345: Flags [.], cksum 0x5850 (incorrect -> 0xfe53), ack 1, win 502, options [nop,nop,TS val 3065366699 ecr 569102212], length 0
12:43:55.167635 IP (tos 0x0, ttl 64, id 16014, offset 0, flags [DF], proto TCP (6), length 61)
    941541e59a01.36396 > 172.17.0.4.12345: Flags [P.], cksum 0x5859 (incorrect -> 0x7b84), seq 1:10, ack 1, win 502, options [nop,nop,TS val 3065366699 ecr 569102212], length 9
12:43:55.167644 IP (tos 0x0, ttl 64, id 50515, offset 0, flags [DF], proto TCP (6), length 52)
    172.17.0.4.12345 > 941541e59a01.36396: Flags [.], cksum 0x5850 (incorrect -> 0xfe43), ack 10, win 509, options [nop,nop,TS val 569102212 ecr 3065366699], length 0

相较于udp简单的一次请求,tcp 的报文分为了多次;

  1. SYN 请求 (客户端 → 服务端)
12:43:55.167496 IP
源: 941541e59a01.36396
目标: 172.17.0.4.12345
标志: [S] (SYN)
序列号: 597377402
窗口大小: 64240
选项: mss 1460, sackOK, TS val 3065366699, wscale 7
  1. SYN-ACK 响应 (服务端 → 客户端)
12:43:55.167587 IP
源: 172.17.0.4.12345
目标: 941541e59a01.36396
标志: [S.] (SYN-ACK)
序列号: 1871059573
确认号: 597377403 (客户端 SYN 序列号+1)
窗口大小: 65160
选项: mss 1460, sackOK, TS val 569102212, ecr 3065366699, wscale 7
  1. ACK 确认 (客户端 → 服务端)
12:43:55.167597 IP
源: 941541e59a01.36396
目标: 172.17.0.4.12345
标志: [.] (ACK)
确认号: 1 (服务端 SYN 序列号+1 = 1871059573 + 1)
窗口大小: 502
选项: TS val 3065366699, ecr 569102212

客户端确认服务端的 SYN-ACK 完成 TCP 三次握手 连接正式建立

  1. PSH-ACK 数据传输 (客户端 → 服务端)
12:43:55.167635 IP
源: 941541e59a01.36396
目标: 172.17.0.4.12345
标志: [P.] (PSH-ACK)
序列号: 1:10 (相对序列号)
确认号: 1
长度: 9
选项: TS val 3065366699, ecr 569102212
  1. ACK 确认 (服务端 → 客户端)
12:43:55.167644 IP
源: 172.17.0.4.12345
目标: 941541e59a01.36396
标志: [.] (ACK)
确认号: 10 (客户端数据结束序列号)
窗口大小: 509
选项: TS val 569102212, ecr 3065366699

一个完整的 TCP 报文格式如下:

tcp

tcp 建立连接过程

首先服务端需要先开启端口监听请求,表示可以与客户端建立连接

  1. SYN 请求 (客户端 → 服务端)
  2. SYN-ACK 响应 (服务端 → 客户端)
  3. ACK 确认 (客户端 → 服务端)

为什么需要3次请求建立,两次不行吗?

TCP 默认网络传输是不可靠的,如果是 (1,2) 两步,server 在响应了 client 的请求后,并不知道客户端是否接收到了,所以 client 还需要进行一次 ACK 的确认;

那 (3) 步骤中,为什么不需要 server 确认是否收到了 ACK 呢?

如果要确认 ACK 的 ACK,那就进入循环了,永远也没办法建立连接了,另外,请求是 client 发起的,在 (1,2) 中,就已经可以确认 server 是可连接的状态,无需再次确认;

一次建立连接的过程:

tcp

可以反推一下,基于网络不可靠的前提,当三次握手的每一个步骤失败,会发生生么?

  1. SYN 请求 (客户端 → 服务端) 请求失败时

此时 client 没有接收到 server 的ACK,并不知道 server 是否能建立连接,client 会重试一段时间,如果依然没有回复,则认为无法连接,建立连接失败;

  1. SYN-ACK 响应 (服务端 → 客户端) 请求失败

此时,server 接收到请求,知道有 client 想建立连接,于是向客户端响应,当请求发生失败时,server 也会进行重试,此时状态还不是建立连接的状态,直到 client 响应了 (2) 的请求;

  1. ACK 确认 (客户端 → 服务端) 请求失败

此时,client 发送完请求后,认为连接建立成功,但 server 仍然可能没接收到,但是 client 可能已经开始传输数据了;

tcp 释放连接过程

tcp 协议既然传输之前要建立连接,那么数据传输完之后,连接需要关闭,就涉及释放连接的过程。

一次完整的释放连接过程如下,一般称为四次挥手:

tcp

为什么在释放连接要比建立连接更复杂?

tcp 协议是可靠连接,为了实现可靠连接,必须假定网络传输是不可靠的,会有累计确认重传机制,需要尽可能保证数据传输的稳定,就需要额外通信去维护连接的状态。

在连接释放时,需要保证数据已经传输完成,client 知道数据传输完了,但此时并不意味着 server 端都接收到了数据,因此连接的释放需要服务端的确认,这也就是为什么会比建立连接多一次请求。

client 在回复server 的最后一次 ACK 之后,为什么需要等待一段时间?

2MSL,MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。

如果 client 不等待,则端口会直接释放,端口释放 server 端并不知道,可能还有往 client 的历史端口发送的报文,为了防止发生报文错乱,需要等待所有报文都被丢弃后,再释放端口。

socket 编程

虽然在传输层协议为两种,但是在网络编程层面,抽象成了 socket 一套 API,通过 socket API 可实现 tcp 和 udp 两种通信;

tcp 相对于 udp 来说,通信之前多了一步 client_socket.connect((HOST, PORT)) connect ,这就是建立连接的过程,同时,对于 tcp 的服务端而言,由于连接是有状态的, 服务端可以维护客户端的多个连接。

udp

服务端

#!/usr/bin/env python3
"""
UDP Server - Receives messages from clients and sends responses
"""
import socket

HOST = '127.0.0.1'  # Bind to localhost
PORT = 5001         # Listen port


def start_server():
    """Start UDP server"""
    # Create UDP socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    
    try:
        server_socket.bind((HOST, PORT))
        print(f"[START] UDP server started, listening on {HOST}:{PORT}")
        print("[INFO] Waiting for client messages...")
        
        while True:
            # Receive data from client (blocking)
            data, client_address = server_socket.recvfrom(1024)
            
            message = data.decode('utf-8')
            print(f"[RECEIVE] Message from {client_address}: {message}")
            
            # Send response back to client
            response = f"Server received: {message}"
            server_socket.sendto(response.encode('utf-8'), client_address)
            print(f"[SEND] Replied to {client_address}")
            
    except KeyboardInterrupt:
        print("\n[STOP] Server is shutting down...")
    except Exception as e:
        print(f"[ERROR] Server error: {e}")
    finally:
        server_socket.close()
        print("[CLOSE] Server closed")


if __name__ == '__main__':
    start_server()

客户端

#!/usr/bin/env python3
"""
UDP Client - Sends messages to server and receives responses
"""
import socket
import time

HOST = '127.0.0.1'  # Server address
PORT = 5001         # Server port


def connect_to_server():
    """Connect to UDP server and send messages"""
    # Create UDP socket
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    
    try:
        # Send multiple messages
        messages = [
            "Hello, this is the first message",
            "This is the second message",
            "UDP communication test completed"
        ]
        
        print(f"[CONNECT] Sending to server {HOST}:{PORT}...")
        
        for msg in messages:
            # Send message to server (no connection needed for UDP)
            print(f"\n[SEND] {msg}")
            client_socket.sendto(msg.encode('utf-8'), (HOST, PORT))
            
            # Receive response from server with timeout
            client_socket.settimeout(2)  # 2 second timeout
            try:
                response, server_address = client_socket.recvfrom(1024)
                print(f"[RECEIVE] {response.decode('utf-8')}")
            except socket.timeout:
                print("[WARNING] No response from server (timeout)")
            
            time.sleep(1)  # Delay 1 second
        
        print("\n[DONE] All messages sent")
        
    except Exception as e:
        print(f"[ERROR] Client exception: {e}")
    finally:
        client_socket.close()
        print("[CLOSE] Client connection closed")


if __name__ == '__main__':
    connect_to_server()

tcp

服务端

#!/usr/bin/env python3
"""
TCP Server - Receives client connections and handles messages
"""
import socket
import threading
import time

HOST = '127.0.0.1'  # Bind to localhost
PORT = 5000         # Listen port


def handle_client(client_socket, client_address):
    """Handle a single client connection"""
    print(f"[CONNECT] Client connected: {client_address}")
    
    try:
        while True:
            # Receive data from client
            data = client_socket.recv(1024)
            
            if not data:
                print(f"[DISCONNECT] Client disconnected: {client_address}")
                break
            
            message = data.decode('utf-8')
            print(f"[RECEIVE] Message from {client_address}: {message}")
            
            # Send response to client
            response = f"Server received: {message}"
            client_socket.send(response.encode('utf-8'))
            print(f"[SEND] Replied to {client_address}")
            
    except Exception as e:
        print(f"[ERROR] Error handling client {client_address}: {e}")
    finally:
        client_socket.close()
        print(f"[CLOSE] Client connection closed: {client_address}")


def start_server():
    """Start TCP server"""
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    try:
        server_socket.bind((HOST, PORT))
        server_socket.listen(5)
        print(f"[START] TCP server started, listening on {HOST}:{PORT}")
        print("[INFO] Waiting for client connections...")
        
        while True:
            # Accept client connection
            client_socket, client_address = server_socket.accept()
            
            # Create a separate thread to handle each client
            client_thread = threading.Thread(
                target=handle_client,
                args=(client_socket, client_address)
            )
            client_thread.daemon = True
            client_thread.start()
            
    except KeyboardInterrupt:
        print("\n[STOP] Server is shutting down...")
    except Exception as e:
        print(f"[ERROR] Server error: {e}")
    finally:
        server_socket.close()
        print("[CLOSE] Server closed")


if __name__ == '__main__':
    start_server()

客户端

#!/usr/bin/env python3
"""
TCP Client - Connects to server and sends messages
"""
import socket
import time

HOST = '127.0.0.1'  # Server address
PORT = 5000         # Server port


def connect_to_server():
    """Connect to TCP server"""
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    try:
        # Connect to server
        print(f"[CONNECT] Connecting to server {HOST}:{PORT}...")
        client_socket.connect((HOST, PORT))
        print("[SUCCESS] Connected to server")
        
        # Send multiple messages
        messages = [
            "Hello, this is the first message",
            "This is the second message",
            "TCP communication test completed"
        ]
        
        for msg in messages:
            # Send message to server
            print(f"\n[SEND] {msg}")
            client_socket.send(msg.encode('utf-8'))
            
            # Receive server response
            response = client_socket.recv(1024)
            print(f"[RECEIVE] {response.decode('utf-8')}")
            
            time.sleep(1)  # Delay 1 second
        
        print("\n[DONE] All messages sent")
        
    except ConnectionRefusedError:
        print("[ERROR] Cannot connect to server. Please ensure server is running.")
    except Exception as e:
        print(f"[ERROR] Client exception: {e}")
    finally:
        client_socket.close()
        print("[CLOSE] Client connection closed")


if __name__ == '__main__':
    connect_to_server()