网络编程中的 TCP 与 UDP:原理、区别和适用场景

网络编程中的 TCP 与 UDP:原理、区别和适用场景

网络编程中的 TCP 与 UDP:原理、区别和适用场景

网络程序要先回答一个基础问题:数据怎样从一台设备送到另一台设备?在 IP 网络上,TCP 和 UDP 是两种常见的传输层协议。TCP 提供可靠、有序的字节流;UDP 提供面向数据报的传输,不承诺送达、顺序或去重。它们不是“快”和“慢”的简单二选一,选择取决于应用能否容忍丢失、是否需要消息边界、怎样处理拥塞,以及应用本身是否愿意实现重试和排序。

本文从通信过程讲起,比较二者的行为和边界,再用 Python 标准库分别实现 TCP、UDP 回显服务。示例适用于本机学习和测试;部署到真实网络前,还需要考虑认证、加密、限流和服务可用性。

先理解端口与套接字

IP 地址用于定位网络接口,传输层端口用于区分同一台设备上的网络服务。一个 TCP 或 UDP 通信端点通常由 IP 地址和端口共同标识。应用程序通过操作系统提供的套接字 API 收发数据;创建 TCP 套接字和 UDP 套接字时,所用的类型和后续操作不同。

下面的示例使用 127.0.0.1,这是 IPv4 回环地址,数据只在本机通信。服务端绑定 127.0.0.1 时,局域网中的其他设备不能直接连接它。若绑定 0.0.0.0,表示监听本机所有 IPv4 网络接口,并不意味着服务自动获得公网可达性;防火墙、路由器和云平台安全规则仍会影响访问范围。

TCP 是什么:可靠的字节流

TCP(Transmission Control Protocol,传输控制协议)在通信双方之间建立连接,并提供有序的字节流。应用写入的数据可能被拆分,也可能与相邻写入的数据合并后再传输。接收端读到的是一段字节,不会天然知道这段字节对应发送端的哪一次 send。

TCP 的“可靠”主要指协议会检测传输中的丢失和错误,并通过确认、重传、序号等机制尽力让连接中的字节按序交付给接收应用。它不等同于“应用一定成功”:对端进程可能已经退出,数据可能在连接故障前只到达了操作系统缓冲区,应用也可能处理失败。若业务要求确认某项操作已经完成,需要设计应用层响应和状态判断,不能只依赖 send 成功。

连接建立与关闭

典型 TCP 连接通过三次握手建立。双方协商初始序号等连接状态后,才能进行数据传输。关闭时,TCP 的两个方向可以分别结束,因此常见关闭过程会涉及双方各自发送结束信号;异常断开时,程序也可能观察到重置或超时。三次握手和连接状态会带来一定开销,短小、频繁的交互可能需要连接复用,避免反复建立连接。

TCP 不保留应用消息边界

假设发送端连续两次写入 hello 和 world,接收端可能一次读到 helloworld,也可能先读到 he,再读到其余字节。recv(1024) 中的数字表示这次最多读取的字节数,不表示一定收到 1024 字节,也不表示恰好收到一条消息。

因此,TCP 应用协议必须自行定义消息边界。常见方法有:

  • 固定长度:每条消息长度固定,适合格式简单的场景。
  • 长度前缀:先发送固定格式的长度,再发送指定长度的正文。读取时必须先读满长度字段,再读满正文。
  • 分隔符:例如以换行符结束一条文本消息。正文若允许出现分隔符,就必须规定转义方式或换用其他 framing。

读取固定字节数时要循环,直到收齐或发现连接结束;发送时应使用 sendall,或者自行处理只发送了部分数据的情况。以下 TCP 示例用“客户端发送完后关闭写方向,服务端读取到 EOF,再回送全部内容”的简单方式演示字节流。它不是适合所有业务的通用消息协议,但能直观体现 TCP 的读取与关闭行为。

UDP 是什么:无连接的数据报

UDP(User Datagram Protocol,用户数据报协议)不建立类似 TCP 的连接状态。应用每次发送一个数据报,接收端每次读取一个数据报。与 TCP 不同,数据报边界由 UDP 保留:一次接收操作取得一个数据报(或其被截取的部分),不会把两个独立数据报拼成一条字节流。

UDP 本身不保证数据报送达、不保证到达顺序,也不自动重传或去重。数据报可能丢失、重复或乱序。接收端没有监听时,发送端通常也不会像 TCP 那样通过建立连接失败来确认服务不可用;发送调用成功通常只表示本机系统接受了数据,不代表对端收到或处理。

UDP 数据报有长度边界,但长度受到协议字段、网络路径和系统实现等因素制约。IPv4 的 UDP 长度字段限制了单个数据报的理论大小,常见描述是 UDP 负载最多 65,507 字节;这不是跨网络可安全发送的推荐大小。具体可发送上限还受地址族、操作系统和网络配置影响。更重要的是,较大的数据报可能需要 IP 分片;分片丢失会使整个数据报无法重组,部分网络设备也会过滤分片。应用通常应尽量控制数据报大小,避免依赖大数据报在任意路径上可靠通过。

UDP 的低开销不等于一定更快。它省去了 TCP 的连接管理和可靠传输机制,但丢包后应用若要重试、排序、拥塞控制、流量控制或确认,就必须自行设计这些机制。实现不当可能造成重复操作、网络拥塞,或让发送速度远超接收能力。

TCP 与 UDP 的核心区别

对比项TCPUDP
通信模型面向连接,维护连接状态无连接的数据报传输
数据形式有序字节流,不保留写入边界独立数据报,保留数据报边界
送达与顺序协议提供可靠、有序的字节交付;连接仍可能失败不保证送达、顺序或去重
拥塞与流量控制内置拥塞控制和接收端流量控制协议本身不提供相同机制,应用需按需求处理
常见开销连接建立、确认、重传及状态维护协议头和连接管理开销较少,但不代表总成本必然更低
典型用途网页、文件传输、数据库连接、需要完整有序数据的应用实时媒体、DNS 等请求应答、局域网发现、可容忍丢失或自行处理可靠性的应用

什么场景适合 TCP

  • 网页与 API:HTTP/1.1 和 HTTP/2 通常运行在 TCP 之上。应用一般希望请求和响应完整且有序。HTTP/3 则基于 QUIC,使用 UDP 作为底层传输,但 QUIC 在其上实现了可靠传输等机制,不能据此认为“使用 UDP 就是不可靠”。
  • 文件传输与备份:通常要求内容完整、顺序正确。TCP 可以承担可靠字节传输,但文件格式校验、断点续传和完整性校验仍需应用协议支持。
  • 数据库与消息服务连接:如果协议需要稳定的有序字节流,TCP 通常是合适的基础。连接池和请求超时等机制则由客户端、服务端及应用协议共同处理。
  • 自定义文本或二进制协议:当每条消息都必须被完整接收时,可以使用 TCP,再明确设计长度前缀、分隔符或其他消息 framing。

若业务要求“操作恰好执行一次”,单靠 TCP 不够。例如客户端发出扣款请求后连接中断,无法仅凭连接状态判断服务端是否已经完成扣款。常见做法是为请求增加唯一标识,在服务端持久化处理结果,并提供查询或幂等处理机制。

什么场景适合 UDP

  • 实时音视频与互动游戏:部分过期数据即使重传也已失去价值,应用可能更愿意使用较新的数据而非等待旧数据补齐。不过,媒体编码、抖动缓冲、丢包恢复和拥塞控制仍然重要,不能忽略。
  • DNS 等短请求应答:UDP 可减少建立连接的往返开销。具体 DNS 行为还取决于记录大小、配置和协议扩展;需要可靠传输时,DNS 也可以使用 TCP。
  • 局域网发现与状态广播:可以用广播或组播向多个接收方发送小型通知。网络范围、路由和防火墙策略可能限制广播或组播的传播,应用也要容忍接收方缺席。
  • 应用自定义可靠传输:某些协议在 UDP 上实现确认、重传、加密和拥塞控制,以满足特定需求。这样的设计复杂且容易出错,应优先考虑已有成熟协议,而不是从零拼装。

可运行示例一:TCP 回显

下面分为服务端和客户端两个程序。服务端只处理一个连接:客户端发送字节后调用 shutdown(SHUT_WR),表示不再发送数据;服务端读到 EOF 后回送收到的全部字节并关闭连接。这样避免把一次 recv 错当成一条完整消息,也便于在示例中使用 EOF 作为结束标记。

tcp_server.py

import socket

HOST = "127.0.0.1"
PORT = 50000

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((HOST, PORT))
    server.listen()
    print(f"TCP server listening on {HOST}:{PORT}")

    conn, addr = server.accept()
    with conn:
        print("connected:", addr)
        chunks = []

        while True:
            data = conn.recv(4096)
            if not data:
                break
            chunks.append(data)

        message = b"".join(chunks)
        conn.sendall(b"echo: " + message)
        print("received", len(message), "bytes")

tcp_client.py

import socket

HOST = "127.0.0.1"
PORT = 50000
message = "你好,TCP".encode("utf-8")

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
    client.connect((HOST, PORT))
    client.sendall(message)
    client.shutdown(socket.SHUT_WR)

    chunks = []
    while True:
        data = client.recv(4096)
        if not data:
            break
        chunks.append(data)

response = b"".join(chunks)
print(response.decode("utf-8"))

把两段代码分别保存为 tcp_server.py 和 tcp_client.py。先在一个终端运行 python tcp_server.py,再在另一个终端运行 python tcp_client.py。客户端应输出 echo: 你好,TCP。服务端一次只接受一个连接,是为了保持示例简洁;生产服务通常还要采用线程、异步 I/O 或进程等方式并发处理,并设置连接数、请求大小和超时限制。

此处用“半关闭”表达客户端发送结束,不代表整个连接立刻销毁。TCP 可以在一个方向结束发送后,继续在另一个方向接收数据。实际协议也可以使用长度前缀或分隔符来判断单条消息结束,从而让同一个连接承载多条请求。

可运行示例二:UDP 回显

UDP 服务端通过 recvfrom 接收一个数据报及其来源地址,再用 sendto 回送数据报。客户端的每次 sendto 都发送一个数据报;服务端的响应也只是示例程序主动发送的回包,不是 UDP 自动提供的送达确认。

udp_server.py

import socket

HOST = "127.0.0.1"
PORT = 50001

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
    server.bind((HOST, PORT))
    print(f"UDP server listening on {HOST}:{PORT}")

    while True:
        data, addr = server.recvfrom(2048)
        print("received", len(data), "bytes from", addr)
        server.sendto(b"echo: " + data, addr)

udp_client.py

import socket

HOST = "127.0.0.1"
PORT = 50001
message = "你好,UDP".encode("utf-8")

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
    client.settimeout(2.0)
    client.sendto(message, (HOST, PORT))

    try:
        response, addr = client.recvfrom(2048)
        print("response from", addr)
        print(response.decode("utf-8"))
    except socket.timeout:
        print("没有在 2 秒内收到响应")

先运行 python udp_server.py,再运行 python udp_client.py。客户端设置两秒超时,避免在没有响应时无限等待。这只是演示性的处理:超时不等于确定请求丢失,也可能是响应延迟、被过滤或服务端已处理但回包丢失。若在真实业务里重试,必须评估重复处理风险,并使用请求 ID、幂等键或其他去重设计。

示例中的 recvfrom(2048) 为接收缓冲区指定最大长度。若数据报比缓冲区大,接收端不能通过下一次读取拿到同一个数据报剩余部分;超出的部分会被截断。不同平台对截断情况的报告方式可能有差异。因此,接收缓冲区大小应和应用协议的最大数据报约定匹配。不要为了“保险”而盲目发送很大的 UDP 数据报。

容易混淆的边界

TCP 确认不等于业务完成

TCP 确认针对传输层的数据接收,不证明对端业务代码已经执行成功。需要业务确认时,应定义响应消息及其语义,例如“请求已受理”“处理完成”或“结果可查询”,并处理响应丢失后的重试。

UDP 的连接形式不改变其可靠性

某些系统允许对 UDP 套接字调用 connect,这通常用于指定默认对端、简化收发接口,或让操作系统按平台行为筛选来源;它不会把 UDP 变成 TCP,也不会自动提供握手、可靠交付和顺序保证。

丢包和延迟不应只靠重试解决

网络拥塞时,无节制地重试可能加重拥塞。需要重试的应用通常应设置超时、退避策略、最大重试次数和请求去重逻辑。对实时数据而言,旧数据可能已经过期,盲目重传反而增加排队延迟。

端口号不是安全边界

使用不常见端口不能替代身份认证或访问控制。绑定到本机回环地址适合本机测试;要对外提供服务,应明确监听接口,并通过防火墙、网络策略和服务端鉴权限制访问。连接数、单条消息大小和处理时间也应设上限,避免资源耗尽。

常见错误清单

  • 认为一次 recv 就是一条 TCP 消息。 TCP 是字节流,必须使用明确的 framing,并处理不完整读取。
  • 用 send 后假设所有字节都已发送。 应使用 sendall,或循环处理返回值;同时记住,这仍不表示对端业务已完成。
  • 认为 UDP 一定比 TCP 快。 实际延迟受网络路径、重传策略、拥塞、应用逻辑和数据大小影响,需通过目标环境测量。
  • 把 UDP 超时直接当成请求失败。 响应可能只是晚到或丢失;设计重试时要防止重复执行。
  • 发送超大 UDP 数据报。 分片、路径 MTU 和接收缓冲区都会影响结果,优先使用较小数据报,必要时采用经过验证的分块与重组方案。
  • 忽略资源上限。 TCP 服务要管理连接数和缓冲区,UDP 服务要限制单个数据报和处理速率。来自网络的输入都应视为不可信数据并进行长度、格式和权限检查。

安全:TCP 和 UDP 本身都不加密

TCP 和 UDP 都不负责应用层身份认证或数据加密。网络上的观察者在没有其他保护时,可能读取或篡改数据;仅仅从 TCP 改用 UDP 并不会获得安全性。TCP 应用通常使用 TLS 建立经过认证、加密的通信。UDP 应用可以使用 DTLS,或采用集成了安全机制的成熟协议,例如基于 TLS 1.3 的 QUIC。选择协议时要核对实际使用的库、版本和部署配置,而不能只看协议名称。

UDP 还需要考虑来源地址伪造和反射放大风险。若服务端向未验证的来源地址发送明显大于请求的数据,攻击者可能借服务端把流量放大并转向受害者。公开 UDP 服务应避免不必要的放大响应,限制请求与响应大小及速率,并在合适场景下验证客户端可达性。具体措施要结合协议设计,不能用简单的“换一个端口”解决。

如何做选择

可以从四个问题开始判断:

  1. 数据丢失或乱序后,业务能否继续?如果不能,优先考虑 TCP,或选择已实现可靠传输的成熟协议。
  2. 应用是否需要清晰的数据报边界?UDP 原生保留边界;TCP 需要在应用协议中定义消息 framing。
  3. 是否需要低延迟、广播或组播等特性?UDP 可能更合适,但必须处理丢包、拥塞和接收方差异。
  4. 是否需要加密、认证、重试或恰好一次语义?这些不是 TCP 或 UDP 自动提供的完整业务能力,需要采用 TLS、DTLS、QUIC 等成熟方案,或在应用层谨慎设计。

对大多数需要完整、有序数据的常规客户端—服务端应用,TCP 是可靠的起点;对允许丢失、对时效敏感、需要广播,或使用成熟 UDP 上层协议的应用,UDP 更有吸引力。真正的选择不在于哪个协议“更好”,而在于应用愿意依赖协议提供什么,又准备自己承担哪些复杂度。

无论选择哪一种,都应先定义消息格式、失败语义和安全边界,再编写收发代码。理解 TCP 的字节流与 UDP 的数据报边界,正确处理超时、截断、重试和资源上限,往往比单纯比较协议头大小更能决定程序是否可靠。

© 版权声明

相关文章