前言
在网络编程领域,TCP 和 UDP 是两种最基础也最重要的传输协议,它们分别对应"可靠通信"和"高效传输"两种不同的设计哲学。但在实际开发中,我们常常需要同时使用这两种协议来满足多样化的业务需求,并且需要解决长连接中的客户端离线检测问题。
本文将结合一套完整的 Java 代码,详细讲解如何实现一个同时支持 TCP(带心跳检测)和 UDP 的网络通信系统,帮助你彻底理解这两种协议的工作原理和应用场景。
一、系统整体架构
这套代码实现了一个完整的网络通信 demo,包含以下核心模块:
-
TCP 服务端:支持多客户端连接,实现心跳检测和超时离线管理
-
TCP 客户端:实现主动心跳发送机制,保持长连接稳定
-
UDP 服务端:基于数据报的无连接通信
-
UDP 客户端:轻量级的数据发送与接收
通过多线程方式同时启动 TCP 和 UDP 服务,展示了两种协议在实际项目中的协同工作方式。
完整版代码如下:
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
public class NetworkDemoWithHeartbeat {
// 在线客户端管理:clientId -> ClientInfo
private static final ConcurrentHashMap<String, ClientInfo> onlineClients = new ConcurrentHashMap<>();
// TCP 线程池
private static final ExecutorService tcpThreadPool = Executors.newCachedThreadPool();
// 心跳间隔(秒)
private static final int HEARTBEAT_INTERVAL = 10;
// 超时阈值(秒):超过此时间未收到心跳视为离线
private static final int HEARTBEAT_TIMEOUT = 30;
public static void main(String[] args) {
// 启动 TCP 服务端(带心跳)
new Thread(() -> startTcpServer(8888)).start();
// 启动 UDP 服务端(保持原样)
new Thread(() -> startUdpServer(9999)).start();
try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
// 启动 TCP 客户端(带心跳)
new Thread(() -> startTcpClient("127.0.0.1", 8888)).start();
// 启动 UDP 客户端
new Thread(() -> startUdpClient("127.0.0.1", 9999)).start();
}
/***************************** TCP 服务端实现(带心跳 & 客户端管理)*****************************/
private static void startTcpServer(int port) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("[TCP Server] 启动成功,端口:" + port);
// 启动心跳检测线程
tcpThreadPool.execute(NetworkDemoWithHeartbeat::heartbeatMonitor);
while (true) {
Socket clientSocket = serverSocket.accept();
String clientId = clientSocket.getInetAddress() + ":" + clientSocket.getPort();
ClientInfo clientInfo = new ClientInfo(clientId, clientSocket);
onlineClients.put(clientId, clientInfo);
tcpThreadPool.execute(() -> handleTcpClient(clientInfo));
}
} catch (IOException e) {
System.err.println("TCP 服务端异常: " + e.getMessage());
}
}
// 心跳监控线程:定期检查客户端是否超时
private static void heartbeatMonitor() {
while (!Thread.currentThread().isInterrupted()) {
long now = System.currentTimeMillis();
onlineClients.entrySet().removeIf(entry -> {
ClientInfo info = entry.getValue();
if (now - info.lastHeartbeatTime > HEARTBEAT_TIMEOUT * 1000L) {
System.out.println("[TCP Server] 客户端超时离线: " + info.clientId);
info.close();
return true; // 从 map 中移除
}
return false;
});
try {
Thread.sleep(5000); // 每5秒检查一次
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
private static void handleTcpClient(ClientInfo clientInfo) {
String clientId = clientInfo.clientId;
Socket socket = clientInfo.socket;
AtomicBoolean running = clientInfo.running;
try (DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
System.out.println("[TCP Server] 客户端连接: " + clientId);
while (running.get()) {
int messageLength = in.readInt();
if (messageLength <= 0 || messageLength > 1024 * 1024) {
System.out.println("[TCP Server] 收到无效长度消息,断开: " + clientId);
break;
}
byte[] buffer = new byte[messageLength];
in.readFully(buffer);
String message = new String(buffer, StandardCharsets.UTF_8);
// 心跳消息特殊处理
if ("PING".equals(message)) {
clientInfo.lastHeartbeatTime = System.currentTimeMillis();
// 回复 PONG
byte[] pong = "PONG".getBytes(StandardCharsets.UTF_8);
out.writeInt(pong.length);
out.write(pong);
out.flush();
continue;
}
System.out.println("[TCP Server] 收到消息: " + message + " from " + clientId);
// 普通消息回复
String response = "已收到: " + message;
byte[] responseData = response.getBytes(StandardCharsets.UTF_8);
out.writeInt(responseData.length);
out.write(responseData);
out.flush();
}
} catch (EOFException e) {
System.out.println("[TCP Server] 客户端正常断开: " + clientId);
} catch (IOException e) {
System.err.println("[TCP Server] 客户端异常 (" + clientId + "): " + e.getMessage());
} finally {
onlineClients.remove(clientId);
clientInfo.close();
}
}
/***************************** TCP 客户端实现(带心跳)*****************************/
private static void startTcpClient(String host, int port) {
try (Socket socket = new Socket(host, port);
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
DataInputStream in = new DataInputStream(socket.getInputStream())) {
System.out.println("[TCP Client] 连接服务器成功");
// 启动心跳发送线程
Thread heartbeatThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
byte[] ping = "PING".getBytes(StandardCharsets.UTF_8);
synchronized (out) {
out.writeInt(ping.length);
out.write(ping);
out.flush();
}
Thread.sleep(HEARTBEAT_INTERVAL * 1000L);
} catch (IOException | InterruptedException e) {
break;
}
}
});
heartbeatThread.setDaemon(true); // 随主线程退出
heartbeatThread.start();
// 发送业务消息
String[] messages = {"第一条消息", "第二条较长内容的消息", "第三条消息"};
for (String msg : messages) {
byte[] data = msg.getBytes(StandardCharsets.UTF_8);
synchronized (out) {
out.writeInt(data.length);
out.write(data);
out.flush();
}
System.out.println("[TCP Client] 已发送: " + msg);
// 读响应
int responseLength = in.readInt();
byte[] responseBuffer = new byte[responseLength];
in.readFully(responseBuffer);
String response = new String(responseBuffer, StandardCharsets.UTF_8);
System.out.println("[TCP Client] 收到响应: " + response);
Thread.sleep(2000);
}
// 等待一会儿再退出,观察心跳
Thread.sleep(5000);
heartbeatThread.interrupt();
} catch (IOException | InterruptedException e) {
System.err.println("TCP 客户端异常: " + e.getMessage());
}
}
/***************************** 客户端信息封装 *****************************/
private static class ClientInfo {
final String clientId;
final Socket socket;
final AtomicBoolean running = new AtomicBoolean(true);
volatile long lastHeartbeatTime = System.currentTimeMillis();
ClientInfo(String clientId, Socket socket) {
this.clientId = clientId;
this.socket = socket;
}
void close() {
running.set(false);
try {
socket.close();
} catch (IOException ignored) {}
}
}
/***************************** UDP 服务端(保持不变)*****************************/
private static void startUdpServer(int port) {
try (DatagramSocket socket = new DatagramSocket(port)) {
System.out.println("[UDP Server] 启动成功,端口:" + port);
byte[] buffer = new byte[1024];
while (true) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
String message = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
System.out.println("[UDP Server] 收到: " + message + " 来自 " + packet.getAddress());
String response = "UDP 响应: " + message;
byte[] responseData = response.getBytes(StandardCharsets.UTF_8);
DatagramPacket resp = new DatagramPacket(responseData, responseData.length, packet.getAddress(), packet.getPort());
socket.send(resp);
}
} catch (IOException e) {
System.err.println("UDP 服务端异常: " + e.getMessage());
}
}
/***************************** UDP 客户端(保持不变,仅加超时)*****************************/
private static void startUdpClient(String host, int port) {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setSoTimeout(3000);
InetAddress addr = InetAddress.getByName(host);
System.out.println("[UDP Client] 准备发送数据");
String[] msgs = {"UDP消息1", "第二条UDP消息"};
for (String msg : msgs) {
byte[] data = msg.getBytes(StandardCharsets.UTF_8);
socket.send(new DatagramPacket(data, data.length, addr, port));
System.out.println("[UDP Client] 已发送: " + msg);
byte[] buf = new byte[1024];
DatagramPacket resp = new DatagramPacket(buf, buf.length);
try {
socket.receive(resp);
String r = new String(resp.getData(), 0, resp.getLength(), StandardCharsets.UTF_8);
System.out.println("[UDP Client] 收到响应: " + r);
} catch (SocketTimeoutException e) {
System.err.println("[UDP Client] 响应超时");
}
Thread.sleep(1000);
}
} catch (IOException | InterruptedException e) {
System.err.println("UDP 客户端异常: " + e.getMessage());
}
}
}
二、核心模块详解
1. TCP 服务端实现(带心跳 & 客户端管理)
1.1 核心思路
TCP 是面向连接的可靠协议,但在长连接场景下,如何判断客户端是否离线是一个常见痛点。本实现通过以下机制解决这一问题:
-
心跳包机制:客户端定期发送 PING 包
-
超时检测:超过指定时间未收到心跳包则判定为离线
-
在线客户端管理:使用 ConcurrentHashMap 实时维护客户端连接状态
1.2 关键代码解读
// 在线客户端管理:clientId -> ClientInfo
private static final ConcurrentHashMap<String, ClientInfo> onlineClients = new ConcurrentHashMap<>();
// 心跳监控线程:定期检查客户端是否超时
private static void heartbeatMonitor() {
while (!Thread.currentThread().isInterrupted()) {
long now = System.currentTimeMillis();
onlineClients.entrySet().removeIf(entry -> {
ClientInfo info = entry.getValue();
if (now - info.lastHeartbeatTime > HEARTBEAT_TIMEOUT * 1000L) {
System.out.println("[TCP Server] 客户端超时离线: " + info.clientId);
info.close();
return true; // 从 map 中移除
}
return false;
});
try {
Thread.sleep(5000); // 每5秒检查一次
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
这段代码实现了心跳检测的核心逻辑:
-
使用 ConcurrentHashMap 保证多线程环境下的并发安全
-
每5秒扫描一次在线客户端列表
-
根据 lastHeartbeatTime 判断客户端是否超时
-
自动清理超时客户端资源
1.3 客户端连接处理
private static void handleTcpClient(ClientInfo clientInfo) {
// ... 省略部分代码 ...
try (DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
while (running.get()) {
int messageLength = in.readInt();
if (messageLength <= 0 || messageLength > 1024 * 1024) {
System.out.println("[TCP Server] 收到无效长度消息,断开: " + clientId);
break;
}
byte[] buffer = new byte[messageLength];
in.readFully(buffer);
String message = new String(buffer, StandardCharsets.UTF_8);
// 心跳消息特殊处理
if ("PING".equals(message)) {
clientInfo.lastHeartbeatTime = System.currentTimeMillis();
// 回复 PONG
byte[] pong = "PONG".getBytes(StandardCharsets.UTF_8);
out.writeInt(pong.length);
out.write(pong);
out.flush();
continue;
}
// 普通消息处理
System.out.println("[TCP Server] 收到消息: " + message + " from " + clientId);
String response = "已收到: " + message;
byte[] responseData = response.getBytes(StandardCharsets.UTF_8);
out.writeInt(responseData.length);
out.write(responseData);
out.flush();
}
} catch (EOFException e) {
System.out.println("[TCP Server] 客户端正常断开: " + clientId);
} finally {
onlineClients.remove(clientId);
clientInfo.close();
}
}
这段代码实现了几个关键功能:
-
基于长度的消息解析,避免粘包问题
-
心跳消息和业务消息的区分处理
-
异常处理和资源自动释放
-
客户端连接的生命周期管理
2. TCP 客户端实现(带心跳)
2.1 心跳发送机制
// 启动心跳发送线程
Thread heartbeatThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
byte[] ping = "PING".getBytes(StandardCharsets.UTF_8);
synchronized (out) {
out.writeInt(ping.length);
out.write(ping);
out.flush();
}
Thread.sleep(HEARTBEAT_INTERVAL * 1000L);
} catch (IOException | InterruptedException e) {
break;
}
}
});
heartbeatThread.setDaemon(true); // 随主线程退出
heartbeatThread.start();
客户端通过独立的心跳线程,定期向服务端发送 PING 包,保持连接活性。使用 synchronized 关键字确保消息发送的原子性,避免多线程竞争问题。
3. UDP 协议实现
3.1 UDP 服务端
UDP 是无连接的协议,每个数据报都是独立的通信单元。服务端实现相对简单:
private static void startUdpServer(int port) {
try (DatagramSocket socket = new DatagramSocket(port)) {
System.out.println("[UDP Server] 启动成功,端口:" + port);
byte[] buffer = new byte[1024];
while (true) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
String message = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
System.out.println("[UDP Server] 收到: " + message + " 来自 " + packet.getAddress());
String response = "UDP 响应: " + message;
byte[] responseData = response.getBytes(StandardCharsets.UTF_8);
DatagramPacket resp = new DatagramPacket(responseData, responseData.length, packet.getAddress(), packet.getPort());
socket.send(resp);
}
} catch (IOException e) {
System.err.println("UDP 服务端异常: " + e.getMessage());
}
}
3.2 UDP 客户端
private static void startUdpClient(String host, int port) {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setSoTimeout(3000); // 设置超时时间
InetAddress addr = InetAddress.getByName(host);
System.out.println("[UDP Client] 准备发送数据");
String[] msgs = {"UDP消息1", "第二条UDP消息"};
for (String msg : msgs) {
byte[] data = msg.getBytes(StandardCharsets.UTF_8);
socket.send(new DatagramPacket(data, data.length, addr, port));
System.out.println("[UDP Client] 已发送: " + msg);
byte[] buf = new byte[1024];
DatagramPacket resp = new DatagramPacket(buf, buf.length);
try {
socket.receive(resp);
String r = new String(resp.getData(), 0, resp.getLength(), StandardCharsets.UTF_8);
System.out.println("[UDP Client] 收到响应: " + r);
} catch (SocketTimeoutException e) {
System.err.println("[UDP Client] 响应超时");
}
Thread.sleep(1000);
}
} catch (IOException | InterruptedException e) {
System.err.println("UDP 客户端异常: " + e.getMessage());
}
}
UDP 客户端通过设置超时时间,避免无限等待响应。这也是 UDP 编程中的一个常见技巧。
三、运行效果演示
1. 服务端启动
[TCP Server] 启动成功,端口:8888
[UDP Server] 启动成功,端口:9999
2. 客户端连接
[TCP Client] 连接服务器成功
[TCP Client] 已发送: 第一条消息
[TCP Client] 收到响应: 已收到: 第一条消息
[TCP Client] 已发送: 第二条较长内容的消息
[TCP Client] 收到响应: 已收到: 第二条较长内容的消息
[UDP Client] 准备发送数据
[UDP Client] 已发送: UDP消息1
[UDP Client] 收到响应: UDP 响应: UDP消息1
3. 心跳检测
客户端会每隔 10 秒自动发送心跳包,服务端收到后更新客户端的最后心跳时间。如果客户端超过 30 秒未发送心跳,服务端会自动清理该客户端连接。
四、核心技术亮点
1. 并发安全设计
-
使用 ConcurrentHashMap 管理在线客户端,保证多线程环境下的并发安全
-
使用 AtomicBoolean 实现线程安全的状态管理
-
使用 synchronized 关键字确保消息发送的原子性
2. 资源自动管理
-
使用 try-with-resources 语法自动释放资源
-
在客户端断开时自动清理连接和状态
-
心跳监控线程自动处理超时客户端
3. 性能优化
-
心跳检测线程每隔 5 秒执行一次,减少系统开销
-
心跳间隔可配置,根据实际业务场景调整
-
使用线程池管理 TCP 连接,提高并发处理能力
五、扩展思考
这套 demo 已经实现了一个完整的网络通信系统,但在实际项目中,还可以进行以下扩展优化:
-
消息序列化:使用 JSON/Protobuf 等方式替代简单字符串
-
异常重试机制:在客户端实现断线重连逻辑
-
负载均衡:扩展为多服务端集群模式
-
监控报警:添加服务端状态监控和报警机制
-
数据加密:对传输数据进行加密处理,保证通信安全
总结
本文通过一套完整的代码,深入讲解了 Java 中 TCP 和 UDP 协议的实现原理和应用场景。心跳检测机制解决了长连接中的客户端离线问题,双协议架构展示了如何在项目中灵活组合不同的传输协议。
希望这篇文章能帮助你彻底理解网络编程的核心概念,为你的项目开发提供有价值的参考。