从 0 到 1 掌握 Java 网络通信:UDP/TCP 实战教程
在Java开发中,网络通信是核心技能之一,而UDP(用户数据报协议)和TCP(传输控制协议)作为TCP/IP协议族中最常用的两种通信协议,分别适用于不同的业务场景。本文将结合完整代码示例,从协议特点、核心API、实战实现到应用场景,全面拆解UDP与TCP的Java编程实践,帮助开发者快速掌握两种协议的开发技巧。
一、协议核心区别:UDP vs TCP
在开始编码前,我们先明确两种协议的本质差异,这是选择合适通信方式的关键:
| 对比维度 | UDP | TCP |
|---|---|---|
| 连接方式 | 无连接 | 面向连接(三次握手建立连接) |
| 可靠性 | 不可靠(发送后不确认,可能丢包、乱序) | 可靠(重传机制、流量控制、拥塞控制) |
| 数据限制 | 单数据包最大64KB | 无数据大小限制(基于流传输) |
| 通信效率 | 高(无连接开销,延迟低) | 中等(连接建立/释放有开销) |
| 适用场景 | 实时通信(直播、游戏、语音)、广播通知 | 可靠传输(文件传输、网页访问、登录认证) |
Java为两种协议提供了专门的API支持:UDP依赖DatagramSocket和DatagramPacket,TCP依赖Socket和ServerSocket。
二、UDP通信实战:无连接的快速传输
UDP通信的核心是"数据包",发送端直接封装数据、目标地址和端口后发送,接收端被动监听端口并接收数据包,无需提前建立连接。
1. 核心API解析
-
DatagramSocket:用于创建通信端点(客户端/服务端),提供发送(
send())和接收(receive())数据包的方法。- 客户端构造器:
DatagramSocket()(系统随机分配端口) - 服务端构造器:
DatagramSocket(int port)(绑定固定端口,监听请求)
- 客户端构造器:
-
DatagramPacket:封装待发送或接收的数据,包含数据字节数组、长度、目标地址(发送时)等信息。
- 发送数据包:
DatagramPacket(byte[] buf, int length, InetAddress address, int port) - 接收数据包:
DatagramPacket(byte[] buf, int length)(指定接收缓冲区)
- 发送数据包:
2. 实战1:UDP一发一收(基础版)
适用于单次数据传输场景,如传感器数据上报、简单指令下发。
客户端代码(发送数据)
package com.wmh.demo2udp1;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPClientDemo1 {
public static void main(String[] args) throws Exception {
System.out.println("===客户端启动===");
// 1. 创建发送端通信端点(随机端口)
DatagramSocket socket = new DatagramSocket();
// 2. 封装发送数据(字节数组形式,指定目标IP和端口)
byte[] sendData = "是客户端,约你啤酒、龙虾、小烧烤".getBytes();
DatagramPacket packet = new DatagramPacket(
sendData,
sendData.length,
InetAddress.getLocalHost(), // 目标IP(本地测试)
8080 // 服务端端口
);
// 3. 发送数据包
socket.send(packet);
// 4. 关闭资源
socket.close();
}
}
服务端代码(接收数据)
package com.wmh.demo2udp1;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("===服务端启动了===");
// 1. 创建接收端通信端点(绑定8080端口)
DatagramSocket socket = new DatagramSocket(8080);
// 2. 创建接收缓冲区(最大64KB,符合UDP协议限制)
byte[] receiveBuf = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(receiveBuf, receiveBuf.length);
// 3. 阻塞等待接收数据(无数据时会一直等待)
socket.receive(packet);
// 4. 解析数据(获取有效数据长度,避免缓冲区冗余)
int dataLength = packet.getLength();
String receiveData = new String(receiveBuf, 0, dataLength);
System.out.println("服务端收到了:" + receiveData);
// 5. 获取发送端信息(IP+端口)
String clientIp = packet.getAddress().getHostAddress();
int clientPort = packet.getPort();
System.out.println("发送端IP:" + clientIp + ",端口:" + clientPort);
// 6. 关闭资源
socket.close();
}
}
3. 实战2:UDP多发多收(交互版)
适用于持续通信场景,如聊天工具、实时监控,通过循环实现多次数据传输。
客户端代码(支持多次输入)
package com.wmh.demo3udp2;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class UDPClientDemo1 {
public static void main(String[] args) throws Exception {
System.out.println("===客户端启动===");
DatagramSocket socket = new DatagramSocket();
Scanner scanner = new Scanner(System.in);
while (true) {
// 循环读取用户输入
System.out.println("请输入要发送的内容(输入exit退出):");
String content = scanner.nextLine();
// 退出条件判断
if ("exit".equals(content)) {
System.out.println("客户端退出了...");
socket.close();
break;
}
// 封装并发送数据
byte[] sendData = content.getBytes();
DatagramPacket packet = new DatagramPacket(
sendData,
sendData.length,
InetAddress.getLocalHost(),
8080
);
socket.send(packet);
}
scanner.close();
}
}
服务端代码(持续接收)
package com.wmh.demo3udp2;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("===服务端启动了===");
DatagramSocket socket = new DatagramSocket(8080);
byte[] receiveBuf = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(receiveBuf, receiveBuf.length);
while (true) {
// 持续阻塞接收数据
socket.receive(packet);
// 解析数据和发送端信息
int dataLength = packet.getLength();
String receiveData = new String(receiveBuf, 0, dataLength);
String clientIp = packet.getAddress().getHostAddress();
int clientPort = packet.getPort();
// 打印接收结果
System.out.println("服务端收到了:" + receiveData);
System.out.println("发送端IP:" + clientIp + ",端口:" + clientPort);
System.out.println("---------------------------------------");
}
}
}
三、TCP通信实战:可靠的面向连接传输
TCP通信基于"流"传输,需先通过三次握手建立可靠连接,再进行数据交互,适用于对数据完整性要求高的场景。
1. 核心API解析
-
Socket:客户端通信端点,用于与服务端建立连接并传输数据。
- 构造器:
Socket(String host, int port)(指定服务端IP和端口,发起连接) - 核心方法:
getOutputStream()(获取发送流)、getInputStream()(获取接收流)
- 构造器:
-
ServerSocket:服务端监听端点,用于绑定端口、接收客户端连接请求。
- 构造器:
ServerSocket(int port)(绑定固定端口) - 核心方法:
accept()(阻塞等待客户端连接,返回与该客户端对应的Socket对象)
- 构造器:
2. 实战1:TCP一发一收(基础版)
适用于单次可靠数据传输,如文件片段上传、接口请求响应。
客户端代码(发送数据)
package com.wmh.demo4tcp1;
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
public class ClientDemo1 {
public static void main(String[] args) throws Exception {
System.out.println("客户端启动了...");
// 1. 与服务端建立连接(三次握手)
Socket socket = new Socket("127.0.0.1", 9999);
// 2. 获取输出流(发送数据)
OutputStream os = socket.getOutputStream();
// 包装为数据输出流,支持直接写入基本类型和字符串
DataOutputStream dos = new DataOutputStream(os);
// 3. 发送数据(先写int,再写字符串)
dos.writeInt(1); // 发送标识ID
dos.writeUTF("我想你了,你在哪儿?"); // 发送文本数据
// 4. 关闭资源(四次挥手释放连接)
socket.close();
}
}
服务端代码(接收数据)
package com.wmh.demo4tcp1;
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动了...");
// 1. 绑定端口,监听客户端连接
ServerSocket serverSocket = new ServerSocket(9999);
// 2. 阻塞等待客户端连接(返回与该客户端对应的Socket)
Socket socket = serverSocket.accept();
// 3. 获取输入流(接收数据)
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
// 4. 读取数据(需与发送顺序一致)
int id = dis.readInt();
String msg = dis.readUTF();
// 5. 打印结果和客户端信息
System.out.println("id=" + id + ",收到的客户端msg=:" + msg);
System.out.println("客户端IP=" + socket.getInetAddress().getHostAddress());
System.out.println("客户端端口=" + socket.getPort());
// 6. 关闭资源
serverSocket.close();
socket.close();
}
}
3. 实战2:TCP多发多收(交互版)
适用于持续可靠通信,如即时通讯、远程控制。
客户端代码(循环输入)
package com.wmh.demo5tcp2;
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class ClientDemo1 {
public static void main(String[] args) throws Exception {
System.out.println("客户端启动了...");
Socket socket = new Socket("127.0.0.1", 9999);
OutputStream os = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请说(输入exit退出):");
String msg = scanner.nextLine();
if ("exit".equals(msg)) {
System.out.println("退出成功");
dos.close();
socket.close();
break;
}
// 发送数据并刷新流(避免数据缓存)
dos.writeUTF(msg);
dos.flush();
}
scanner.close();
}
}
服务端代码(循环接收)
package com.wmh.demo5tcp2;
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动了...");
ServerSocket serverSocket = new ServerSocket(9999);
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
while (true) {
// 阻塞读取客户端数据
String msg = dis.readUTF();
System.out.println("收到的客户端msg=:" + msg);
System.out.println("客户端IP=" + socket.getInetAddress().getHostAddress());
System.out.println("客户端端口=" + socket.getPort());
System.out.println("-------------------------");
}
}
}
4. 实战3:TCP多客户端并发处理(进阶版)
上述服务端仅支持单个客户端连接,实际应用中需处理多客户端并发,解决方案是为每个客户端分配独立子线程。
服务端核心改造(子线程处理多客户端)
// 子线程类:负责处理单个客户端的通信
package com.wmh.demo6tcp3;
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.Socket;
public class ServerRead extends Thread {
private Socket socket; // 单个客户端的Socket连接
public ServerRead(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
while (true) {
String msg = dis.readUTF();
// 打印当前客户端的消息
System.out.println("收到客户端[" + socket.getInetAddress().getHostAddress() + "]的msg=:" + msg);
System.out.println("客户端端口=" + socket.getPort());
System.out.println("-------------------------");
}
} catch (Exception e) {
System.out.println("客户端[" + socket.getInetAddress().getHostAddress() + "]下线了");
}
}
}
// 主服务端:监听连接并分配子线程
package com.wmh.demo6tcp3;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动了...");
ServerSocket serverSocket = new ServerSocket(9999);
while (true) {
// 持续接收客户端连接
Socket socket = serverSocket.accept();
System.out.println("一个客户端上线了:" + socket.getInetAddress().getHostAddress());
// 为每个客户端分配独立子线程处理
new ServerRead(socket).start();
}
}
}
5. 实战4:TCP线程池优化
多线程方案在高并发时会创建大量线程,导致系统资源耗尽。通过线程池复用线程,可提升系统稳定性和性能。
线程池优化版服务端
// 任务类:实现Runnable接口,封装客户端通信逻辑
package com.wmh.demo7tcp4;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
public class ServerReaderRunnable implements Runnable {
private Socket socket;
public ServerReaderRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 响应HTTP格式数据(模拟BS架构)
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
// 必须遵循HTTP协议格式,否则浏览器无法识别
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=utf-8");
ps.println(); // 空行分隔响应头和响应体
ps.println("<html><body><h1 style='color:red'>欢迎访问TCP服务端</h1></body></html>");
ps.close();
socket.close();
} catch (Exception e) {
System.out.println("客户端[" + socket.getInetAddress().getHostAddress() + "]下线了");
}
}
}
// 线程池服务端
package com.wmh.demo7tcp4;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
public class ServerDemo {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动了...");
ServerSocket serverSocket = new ServerSocket(8080);
// 创建线程池(核心3线程,最大10线程,队列容量100)
ExecutorService threadPool = new ThreadPoolExecutor(
3,
10,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.AbortPolicy() // 队列满时拒绝新任务
);
while (true) {
Socket socket = serverSocket.accept();
System.out.println("一个客户端上线了:" + socket.getInetAddress().getHostAddress());
// 将客户端通信任务提交给线程池
threadPool.execute(new ServerReaderRunnable(socket));
}
}
}
四、关键注意事项与最佳实践
- 资源关闭:UDP的
DatagramSocket、TCP的Socket和ServerSocket需手动关闭,避免端口占用。 - 流刷新:TCP的输出流需调用
flush(),确保数据即时发送(避免缓存)。 - 数据边界:UDP数据包天然带边界(一次发送对应一次接收),TCP是流传输,需自定义边界(如固定长度、分隔符)。
- 异常处理:网络通信可能出现断连、超时等异常,需捕获并处理,避免程序崩溃。
- 端口选择:避免使用0-1023的系统端口,选择1024-65535之间的端口。
五、总结
- UDP:无连接、高效率、不可靠,适合实时性要求高的场景(直播、游戏、物联网数据上报)。
- TCP:面向连接、可靠传输、支持流传输,适合对数据完整性要求高的场景(文件传输、接口通信、网页服务)。
- 高并发场景下,TCP服务端需使用线程池优化,避免线程爆炸;UDP需注意数据包大小限制(64KB)和数据校验。
通过本文的代码示例和解析,相信你已掌握Java中UDP与TCP通信的核心实现。实际开发中,需根据业务场景选择合适的协议,并结合线程池、异常处理等机制,打造高效稳定的网络应用。
提示:在实际部署中,建议使用NIO(非阻塞IO)或Netty等高性能网络框架替代原生Socket编程,以获得更好的性能和可维护性。