系列: 从 0 开始学习计算机网络
适合读者: 计算机专业学生、网络基础薄弱的开发者、Java 后端学习者
本文关键词: TCP、UDP、可靠传输、字节流、数据报、Java Socket
一、引言:为什么网络通信需要两种传输协议?
访问 Spring Boot 接口、连接 MySQL、进行实时语音通话,都需要在网络上交换数据。但不同应用的要求并不一样:文件下载希望数据完整、有序;实时语音更在意及时性;DNS 查询通常希望用较小的消息快速获得结果。
传输层提供了不同的通信方式,其中最常见的两种协议是 TCP 和 UDP。本文会从实际需求出发,讲清它们的区别、适用场景,以及 Java 中如何使用 Socket 进行简单通信。
二、先看结论:TCP 与 UDP 的核心区别

| 对比项 | TCP | UDP |
|---|---|---|
| 通信方式 | 面向连接 | 无连接 |
| 数据形式 | 有序字节流 | 数据报 |
| 是否由协议保证可靠、有序交付 | 是,连接正常时提供可靠、有序的字节流服务 | 否 |
| 是否保留应用消息边界 | 不保留 | 保留数据报边界 |
| 确认与重传 | TCP 提供相关机制 | UDP 本身不提供 |
| 流量控制与拥塞控制 | TCP 提供相关机制 | UDP 本身不提供;上层可另行实现 |
| 常见用途 | Web 通信、数据库连接、文件传输 | DNS 常见查询、实时媒体、部分游戏通信 |
不要简单地记成"TCP 一定慢,UDP 一定快"。真实应用的速度和延迟还受网络状况、数据大小、连接复用、实现方式和上层协议影响。
三、TCP 是什么?为什么它能可靠、有序地传输数据?
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的传输层协议,为应用提供可靠、有序的字节流。
3.1 面向连接
通信双方在传输数据前,需要建立 TCP 连接。建立连接通常涉及三次握手,连接结束也有相应的关闭过程。"面向连接"不意味着两台电脑之间必须有一条独占的物理线路,而是指 TCP 双方维护连接状态,并按照 TCP 规则进行通信。
3.2 可靠、有序
TCP 使用序列号、确认应答、超时重传等机制处理丢失或乱序的数据。接收方会按字节序号整理数据,并向应用交付有序字节流。TCP 还使用接收窗口进行流量控制,并通过拥塞控制机制调节发送负载。

需要准确理解:TCP 的可靠性是传输层提供的服务,不代表网络故障、进程崩溃或连接中断时,应用一定能完成业务。 连接中断时,应用仍需要处理异常、超时和业务重试。
3.3 字节流:TCP 不保留消息边界
例如客户端连续写入两段数据:
java
outputStream.write("hello".getBytes());
outputStream.write("world".getBytes());
接收端读取时,可能一次读到 helloworld,也可能先读到部分内容,再读到剩余内容。TCP 不保证一次 write() 对应一次对端 read(),也不保证一次写入对应一个 TCP 段。
如果业务需要明确的消息边界,应用层必须自行设计协议,例如使用固定长度、长度字段、分隔符,或采用已有的消息协议。
四、UDP 是什么?为什么它适合一些实时场景?
UDP(User Datagram Protocol,用户数据报协议)是一种无连接的数据报传输协议。应用可以把数据作为一个个数据报发送,接收方也按数据报接收。
UDP 的主要特点:
- 发送前不需要像 TCP 那样建立连接;
- 保留数据报边界;
- UDP 本身不保证数据一定到达或按发送顺序到达;
- UDP 本身不负责重传丢失的数据报,也不提供 TCP 那样的流量控制和拥塞控制。
这并不意味着使用 UDP 的应用完全不能可靠。应用或更高层协议可以在 UDP 之上增加确认、重传、拥塞控制等机制;这些能力不是 UDP 基础协议本身提供的。
实时语音或游戏状态更新有时更重视及时性。如果旧状态已经过时,应用可能选择忽略它,而不是等待重传。具体是否使用 UDP,要结合业务协议和实现判断。
五、TCP 和 UDP 的应用场景

5.1 常见 TCP 场景
- HTTP/1.1、HTTP/2: 通常运行在 TCP 之上;
- MySQL 连接: 常见部署中使用 TCP;
- 文件传输: 通常需要完整、有序的数据;
- 登录、支付等业务通信: 常需要可靠传输,但业务正确性仍需应用层保障。
5.2 常见 UDP 场景
- DNS: 常见 DNS 查询使用 UDP,但 DNS 也可以使用 TCP;
- 实时音视频: 部分实时媒体协议会使用 UDP,并由上层协议处理相关问题;
- 游戏状态更新: 某些实时状态消息可使用 UDP,具体取决于游戏协议;
- 局域网发现: 某些发现协议会使用 UDP 广播或组播。
注意:HTTP/3 使用 QUIC,而 QUIC 基于 UDP 传输,并在 QUIC 层实现可靠传输等能力。 不能把"HTTP/3 基于 UDP"理解为"HTTP/3 不可靠",也不能把 QUIC 的能力直接归到 UDP 本身。
六、Java 示例:通过 TCP Socket 发送一条消息
Java 提供 Socket 和 ServerSocket 等类用于 TCP 通信。下面是一个本机教学示例:服务端接收一行文本,并返回一行响应。
6.1 TCP 服务端
保存为 TcpServer.java:
java
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class TcpServer {
public static void main(String[] args) throws Exception {
try (ServerSocket serverSocket = new ServerSocket(8088)) {
System.out.println("TCP 服务端正在监听 8088 端口");
try (Socket socket = serverSocket.accept();
BufferedReader in = new BufferedReader(
new InputStreamReader(
socket.getInputStream(),
StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(
socket.getOutputStream(), true,
StandardCharsets.UTF_8)) {
String message = in.readLine();
System.out.println("收到客户端消息:" + message);
out.println("服务端已收到:" + message);
}
}
}
}
6.2 TCP 客户端
保存为 TcpClient.java:
java
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class TcpClient {
public static void main(String[] args) throws Exception {
try (Socket socket = new Socket("127.0.0.1", 8088);
BufferedReader in = new BufferedReader(
new InputStreamReader(
socket.getInputStream(),
StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(
socket.getOutputStream(), true,
StandardCharsets.UTF_8)) {
out.println("hello TCP");
System.out.println("服务端响应:" + in.readLine());
}
}
}
运行步骤
- 使用 JDK 8 或更高版本,将两个类分别保存为
TcpServer.java和TcpClient.java。 - 在文件所在目录编译:
bash
javac TcpServer.java TcpClient.java
- 先在一个终端启动服务端:
bash
java TcpServer
- 再打开另一个终端启动客户端:
bash
java TcpClient
在服务端正常运行、端口可用的前提下,客户端会发送一行文本,并等待服务端返回一行响应。这里用 println() 与 readLine() 配合,是因为双方约定以换行符作为消息结束标记。
注意:这只是单客户端的教学示例,不是生产级服务器。生产服务通常还需要循环接收连接、并发处理、超时、异常处理等设计。
七、Java 示例:发送一个 UDP 数据报
UDP 使用 DatagramSocket 和 DatagramPacket。下面展示发送端的基本写法:
java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
public class UdpSender {
public static void main(String[] args) throws Exception {
byte[] data = "hello UDP".getBytes(StandardCharsets.UTF_8);
InetAddress address = InetAddress.getByName("127.0.0.1");
try (DatagramSocket socket = new DatagramSocket()) {
DatagramPacket packet = new DatagramPacket(
data, data.length, address, 9090);
socket.send(packet);
System.out.println("已调用 UDP 发送接口");
}
}
}
这个示例只演示发送端。要真正接收数据,需要另一个程序使用 DatagramSocket(9090) 监听端口并调用 receive()。打印信息只说明程序执行到了发送调用,不代表接收方一定收到数据报。
这正是 UDP 与 TCP 在基础协议语义上的区别之一:UDP 的 send() 调用成功,不等于对端应用已经收到并处理数据报。
八、常见误区
误区 1:TCP 永远比 UDP 慢
不准确。TCP 有连接管理、确认和重传等机制,但实际性能取决于网络状况、数据量、连接复用、实现方式和应用需求。
误区 2:UDP 不可靠,所以不能用于重要业务
不准确。UDP 本身不提供可靠、有序交付,但应用协议可以在 UDP 之上增加所需机制。
误区 3:TCP 一次 write(),对端就会一次 read() 完整读到
不对。TCP 是字节流,不保留应用层消息边界。应用协议需要自己定义消息如何分隔或组装。
误区 4:HTTP 一定使用 TCP
不对。HTTP/1.1 和 HTTP/2 通常使用 TCP;HTTP/3 使用基于 UDP 的 QUIC。
误区 5:TCP 保证业务一定成功
不对。TCP 处理传输层可靠性,但无法保证服务端业务逻辑执行成功、数据库事务提交成功,或客户端一定收到最终业务结果。
九、本篇总结
- TCP: 面向连接,提供可靠、有序的字节流;不保留应用写入的消息边界。
- UDP: 无连接,提供数据报服务;本身不保证送达、顺序和重传。
- 如何选择: 看业务对完整性、顺序、及时性和拥塞处理的要求,而不是简单地认为 TCP 慢、UDP 快。
- Java 开发: TCP 常用
Socket/ServerSocket,UDP 常用DatagramSocket/DatagramPacket。 - 重要边界: TCP 的可靠传输不等于业务一定成功;UDP 的基础协议语义也不妨碍上层协议自行实现可靠机制。
下一篇预告:《【计算机网络】第 6 篇:TCP 三次握手和四次挥手,为什么不能少?》。我们会结合连接建立与关闭的状态变化,解释 SYN、ACK、FIN 分别代表什么,以及常见的连接问题。
我是程序员大海,我们下期不见不散.
参考资料
- RFC 9293:Transmission Control Protocol (TCP)
- RFC 768:User Datagram Protocol (UDP)
- RFC 9114:HTTP/3
- RFC 9000:QUIC: A UDP-Based Multiplexed and Secure Transport
本文中的地址、端口和代码均为教学示例;没有声称已经在特定环境中运行,也不预设固定的运行结果。