Java 提供 java.net 包支援 TCP (可靠流式) 和 UDP (不可靠數據報) 兩種傳輸層協議編程。本文通過完整客戶端/服務端代碼實例,深度對比兩種協議的程式設計模型、適用場景與關鍵注意點。
TCP (Transmission Control Protocol) - 面向連接、可靠字節流
核心特徵
- 三次握手建立連接:SYN → SYN+ACK → ACK
- 字節流無邊界:應用層需自定義協議分幀 (長度前綴、分隔符、固定長度)
- 可靠交付:序列號、確認應答、重傳、滑動窗口流控
- 全雙工通信:雙向獨立 InputStream/OutputStream
服務端編程模型
public class TcpServer {
public static void main(String[] args) throws IOException {
try (ServerSocket serverSocket = new ServerSocket(10001)) {
System.out.println("TCP Server started on port 10001");
while (true) {
// 阻塞等待客戶端連接
Socket clientSocket = serverSocket.accept();
System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress());
// 每個客戶端啟動獨立線程處理 (生產環境用線程池)
new Thread(() -> handleClient(clientSocket)).start();
}
}
}
private static void handleClient(Socket socket) {
try (socket;
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true)) {
String line;
while ((line = in.readLine()) != null) {
System.out.println("Received: " + line);
// 業務處理...
out.println("Server echo: " + line);
}
} catch (IOException e) {
System.err.println("Client handling error: " + e.getMessage());
}
}
}
客戶端編程模型
public class TcpClient {
public static void main(String[] args) throws IOException {
try (Socket socket = new Socket("localhost", 10001);
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
BufferedReader console = new BufferedReader(new InputStreamReader(System.in))) {
System.out.println("Connected to server. Type messages (empty to quit):");
String line;
while ((line = console.readLine()) != null && !line.isEmpty()) {
out.println(line);
System.out.println("Server reply: " + in.readLine());
}
}
}
}
TCP 關鍵注意點
| 問題 | 原因 | 解決 |
| 粘包/半包 | 字節流無消息邊界 | 應用層協議:長度前綴(4字節)、分隔符(\n)、固定長度、Netty LengthFieldBasedFrameDecoder |
| 阻塞 I/O 線程浪費 | 每連接一線程 | NIO (Selector) / Netty / 虛擬線程 (JDK 21+) |
| 連接泄露 | 未正確關閉 Socket | try-with-resources / finally 關閉 |
| 服務端端口佔用 | TIME_WAIT 狀態 | serverSocket.setReuseAddress(true) |
UDP (User Datagram Protocol) - 無連接、不可靠數據報
核心特徵
- 無連接建立:直接發送 DatagramPacket
- 保留消息邊界:每次 send/receive 一個完整數據報 (最大 64KB)
- 不可靠:可能丟失、重複、亂序、無流控
- 低延遲、開銷小:適合即時性要求高、容忍丟包場景
服務端編程模型
public class UdpServer {
public static void main(String[] args) throws IOException {
try (DatagramSocket socket = new DatagramSocket(7778)) {
System.out.println("UDP Server started on port 7778");
byte[] buffer = new byte[1024];
while (true) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet); // 阻塞接收
String msg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
InetAddress clientAddr = packet.getAddress();
int clientPort = packet.getPort();
System.out.println("From " + clientAddr + ":" + clientPort + " - " + msg);
// 回複 (使用收到包的地址/端口)
String reply = "Server received: " + msg;
byte[] replyBytes = reply.getBytes(StandardCharsets.UTF_8);
DatagramPacket replyPacket = new DatagramPacket(replyBytes, replyBytes.length, clientAddr, clientPort);
socket.send(replyPacket);
}
}
}
}
客戶端編程模型
public class UdpClient {
public static void main(String[] args) throws IOException {
try (DatagramSocket socket = new DatagramSocket()) {
InetAddress serverAddr = InetAddress.getByName("localhost");
int serverPort = 7778;
// 發送
String msg = "Hello UDP Server";
byte[] sendBuf = msg.getBytes(StandardCharsets.UTF_8);
DatagramPacket sendPacket = new DatagramPacket(sendBuf, sendBuf.length, serverAddr, serverPort);
socket.send(sendPacket);
System.out.println("Sent: " + msg);
// 接收回複 (設置超時防止永久阻塞)
socket.setSoTimeout(5000); // 5秒超時
byte[] recvBuf = new byte[1024];
DatagramPacket recvPacket = new DatagramPacket(recvBuf, recvBuf.length);
try {
socket.receive(recvPacket);
String reply = new String(recvPacket.getData(), 0, recvPacket.getLength(), StandardCharsets.UTF_8);
System.out.println("Reply: " + reply);
} catch (SocketTimeoutException e) {
System.err.println("No reply within timeout");
}
}
}
}
UDP 關鍵注意點
| 問題 | 原因 | 解決 |
| 數據報丟失 | 不可靠協議 | 應用層實現 ACK/重傳/序列號 (如 QUIC、RUDP) |
| 接收緩衝區溢出 | 處理速度 < 到達速度 | 增大緩衝區 socket.setReceiveBufferSize()、加快處理 |
| 廣播/多播 | 需特殊地址/權限 | socket.setBroadcast(true)、MulticastSocket |
| NAT 穿透 | 客戶端在內網 | STUN/TURN/ICE、打洞技術 |
TCP vs UDP 全方位對比
| 維度 | TCP | UDP |
| 連接建立 | 三次握手 (1.5 RTT) | 無 |
| 可靠性 | 保證有序、不重複、不丟失 | 不保證 |
| 消息邊界 | 無 (字節流) | 有 (數據報) |
| 流控/擁塞控制 | 滑動窗口、慢啟動、快速恢復 | 無 |
| 首部開銷 | 20-60 字節 | 8 字節 |
| 適用場景 | HTTP、DB、RPC、文件傳輸、郵件 | 視頻會議、直播、遊戲、DNS、IoT、日誌傳輸 |
| Java 類 | ServerSocket, Socket | DatagramSocket, DatagramPacket |
進階:NIO 非阻塞 I/O (Java 1.4+) 與 Netty
NIO 核心組件
Channel (SocketChannel, ServerSocketChannel, DatagramChannel) - 雙向、可非阻塞
Buffer (ByteBuffer) - 讀寫數據容器、位置/限制/容量
Selector - 多路複用器,單線程管理成千上萬 Channel
Netty 架構優勢 (生產環境首選)
// Netty TCP Server 簡例
public class NettyTcpServer {
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4)) // 解決粘包
.addLast(new StringDecoder(StandardCharsets.UTF_8))
.addLast(new SimpleChannelInboundHandler<String>() {
@Override protected void channelRead0(ChannelHandlerContext ctx, String msg) {
ctx.writeAndFlush("Echo: " + msg + "\n");
}
});
}
})
.bind(10001).sync().channel().closeFuture().sync();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
總結
TCP 提供可靠字節流,適合數據完整性至關重要的場景,但需自行處理粘包/半包;UDP 提供低延遲數據報,保留邊界但不可靠,適合即時性優先場景。Java 傳統 BIO (Socket/DatagramSocket) 適合學習與低併發,生產環境高併發建議使用 Netty (NIO 封裝) 或 虛擬線程 (JDK 21+)。理解協議本質、選對模型、處理邊界條件,是網絡編程的核心童子功。