从入门到实战:Java Socket 实现 TCP/UDP 双协议网络通信系统(带心跳检测)

前言

在网络编程领域,TCP 和 UDP 是两种最基础也最重要的传输协议,它们分别对应"可靠通信"和"高效传输"两种不同的设计哲学。但在实际开发中,我们常常需要同时使用这两种协议来满足多样化的业务需求,并且需要解决长连接中的客户端离线检测问题。

本文将结合一套完整的 Java 代码,详细讲解如何实现一个同时支持 TCP(带心跳检测)和 UDP 的网络通信系统,帮助你彻底理解这两种协议的工作原理和应用场景。

一、系统整体架构

这套代码实现了一个完整的网络通信 demo,包含以下核心模块:

  1. TCP 服务端:支持多客户端连接,实现心跳检测和超时离线管理

  2. TCP 客户端:实现主动心跳发送机制,保持长连接稳定

  3. UDP 服务端:基于数据报的无连接通信

  4. 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 已经实现了一个完整的网络通信系统,但在实际项目中,还可以进行以下扩展优化:

  1. 消息序列化:使用 JSON/Protobuf 等方式替代简单字符串

  2. 异常重试机制:在客户端实现断线重连逻辑

  3. 负载均衡:扩展为多服务端集群模式

  4. 监控报警:添加服务端状态监控和报警机制

  5. 数据加密:对传输数据进行加密处理,保证通信安全

总结

本文通过一套完整的代码,深入讲解了 Java 中 TCP 和 UDP 协议的实现原理和应用场景。心跳检测机制解决了长连接中的客户端离线问题,双协议架构展示了如何在项目中灵活组合不同的传输协议。

希望这篇文章能帮助你彻底理解网络编程的核心概念,为你的项目开发提供有价值的参考。

相关推荐
朱朱没烦恼yeye6 分钟前
java基础学习
java·python·学习
她和夏天一样热27 分钟前
【观后感】Java线程池实现原理及其在美团业务中的实践
java·开发语言·jvm
郑州光合科技余经理33 分钟前
技术架构:上门服务APP海外版源码部署
java·大数据·开发语言·前端·架构·uni-app·php
篱笆院的狗1 小时前
Java 中的 DelayQueue 和 ScheduledThreadPool 有什么区别?
java·开发语言
2501_941809141 小时前
面向多活架构与数据地域隔离的互联网系统设计思考与多语言工程实现实践分享记录
java·开发语言·python
qualifying1 小时前
JavaEE——多线程(4)
java·开发语言·java-ee
better_liang2 小时前
每日Java面试场景题知识点之-DDD领域驱动设计
java·ddd·实体·领域驱动设计·架构设计·聚合根·企业级开发
li.wz2 小时前
Spring Bean 生命周期解析
java·后端·spring
czlczl200209252 小时前
深入解析 ThreadLocal:架构演进、内存泄漏与数据一致性分析
java·jvm·架构
盖世英雄酱581363 小时前
不是所有的this调用会导致事务失效
java·后端