基于Java的Socket.IO服务端基础演示

前言

socket.io是一套通信组件,支持即时、双向与基于事件的交流。它可以在每个平台、每个浏览器和每个设备上工作,可靠性和速度同样稳定。socket.io有一套自己的交互协议,可以基于该基础协议,实现相应的应用功能,使得功能开发将会更加便捷。本文限于篇幅,对于协议不会做过多介绍,主要从实现服务端和客户端的交互来介绍,对协议感兴趣的可以查看官方的文档。

服务端开发

首先添加必要的依赖,我们以基于netty的netty-socketio为基础,采用Java实现一个服务端,为了方便起见,写在了一个方法中。

xml 复制代码
<dependency>
    <groupId>com.corundumstudio.socketio</groupId>
    <artifactId>netty-socketio</artifactId>
    <version>2.0.13</version>
</dependency>
<!-- Netty 核心依赖(手动指定兼容版本) -->
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-buffer</artifactId>
    <version>${netty.version}</version>
</dependency>
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-transport</artifactId>
    <version>${netty.version}</version>
</dependency>
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-handler</artifactId>
    <version>${netty.version}</version>
</dependency>
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-codec-http</artifactId>
    <version>${netty.version}</version>
</dependency>

netty的版本这里采用了4.1.115.Final

服务端方法代码主要有如下几个部分:

  • 服务配置,比如host, port,以及跨域设置。
  • 增加服务namespace。这个是比较重要的一个概念,服务会有一个默认的namespace,导致没有和自定义的关联起来,导致请求没有正确处理。
  • 增加监听器。为了理清请求情况,增加了连接监听,数据监听,以及探针监听,分别是ConnectListener,DataListener,PingListener,PongListener。此外还增加了一个事件拦截器interceptor
  • 最后是启动服务端。

完整代码:

java 复制代码
import com.corundumstudio.socketio.*;
import com.corundumstudio.socketio.listener.*;
import com.corundumstudio.socketio.transport.NamespaceClient;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.concurrent.ExecutionException;

@Test
public void start() throws InterruptedException, ExecutionException {
    Configuration configuration = new Configuration();
    configuration.setAckMode(AckMode.AUTO);
    configuration.setContext("/realtime");
    configuration.setHostname("0.0.0.0");
    configuration.setPort(8000);
    configuration.setAllowHeaders("*");
    configuration.setOrigin("*");

    SocketIOServer server = new SocketIOServer(configuration);
    SocketIONamespace namespace = server.addNamespace("/realtime");

    namespace.addConnectListener(new ConnectListener() {
        @Override
        public void onConnect(SocketIOClient client) {
            log.info("Connected ns:{}, remote:{} ", client.getNamespace().getName(), client.getRemoteAddress());
        }
    });
    namespace.addEventListener("update", Object.class, new DataListener<Object>() {

        @Override
        public void onData(SocketIOClient client, Object data, AckRequest ackSender) throws Exception {
            log.info("Data:{}", data);
            ackSender.sendAckData("接受到客户端消息,类型:"+data.getClass().getSimpleName());
        }
    });

    namespace.addPingListener(new PingListener() {
        @Override
        public void onPing(SocketIOClient client) {
            log.info("Ping ns:{}, remote:{}", client.getNamespace().getName(),client.getRemoteAddress());
        }
    });
    namespace.addPongListener(new PongListener() {
        @Override
        public void onPong(SocketIOClient client) {
            log.info("Pong ns:{}, remote:{}", client.getNamespace(),client.getRemoteAddress());
        }
    });

    namespace.addEventInterceptor(new EventInterceptor() {
        @Override
        public void onEvent(NamespaceClient client, String eventName, List<Object> args, AckRequest ackRequest) {
            log.info("Event interceptor:{}", eventName);
        }
    });
    //start
    server.start();
    Thread.currentThread().join();
}

客户端开发

客户端代码功能主要有连接服务端,发送自定义事件消息,以及断开连接功能,基于AI生成和修改。实际的运行效果如下图,完整代码如下:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Socket.IO 验证 Demo</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: "Microsoft YaHei", sans-serif;
        }
        .container {
            max-width: 800px;
            margin: 20px auto;
            padding: 20px;
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .title {
            text-align: center;
            margin-bottom: 20px;
            color: #333;
        }
        .config {
            margin-bottom: 20px;
            padding: 15px;
            background: #f5f5f5;
            border-radius: 6px;
        }
        .config label {
            display: inline-block;
            width: 120px;
            margin-bottom: 10px;
            color: #555;
        }
        .config input {
            padding: 8px;
            width: calc(100% - 130px);
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .btn-group {
            margin-bottom: 20px;
            display: flex;
            gap: 10px;
        }
        button {
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            color: white;
            font-size: 14px;
            transition: background 0.3s;
        }
        #connectBtn { background: #2196F3; }
        #connectBtn:hover { background: #1976D2; }
        #sendBtn { background: #4CAF50; }
        #sendBtn:hover { background: #388E3C; }
        #disconnectBtn { background: #F44336; }
        #disconnectBtn:hover { background: #D32F2F; }
        button:disabled {
            background: #9E9E9E;
            cursor: not-allowed;
        }
        .message-input {
            margin-bottom: 20px;
        }
        .message-input textarea {
            width: 100%;
            height: 100px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            resize: none;
            font-size: 14px;
        }
        .log-area {
            height: 300px;
            border: 1px solid #ddd;
            border-radius: 4px;
            padding: 10px;
            overflow-y: auto;
            background: #fafafa;
            font-size: 13px;
            line-height: 1.5;
        }
        .log-info { color: #2196F3; }
        .log-success { color: #4CAF50; }
        .log-error { color: #F44336; }
        .log-time { color: #999; }
    </style>
</head>
<body>
    <div class="container">
        <h1 class="title">Socket.IO 验证 Demo</h1>
        
        <!-- 连接配置 -->
        <div class="config">
            <label>服务端地址:</label>
            <input type="text" id="serverUrl" value="http://localhost:8000/realtime" placeholder="如:http://localhost:8000/realtime">
            <br>
            <label>消息内容:</label>
            <input type="text" id="msgContent" value="测试消息内容" placeholder="消息内容">
            <br>
            <label>时间戳:</label>
            <input type="text" id="msgTimestamp" value="" placeholder="自动生成">
        </div>

        <!-- 操作按钮 -->
        <div class="btn-group">
            <button id="connectBtn">连接服务端</button>
            <button id="sendBtn" disabled>发送 update 事件</button>
            <button id="disconnectBtn" disabled>断开连接</button>
        </div>

        <!-- 自定义消息输入框(可选) -->
        <div class="message-input">
            <textarea id="customMsg" placeholder="自定义JSON格式消息(可选,优先级高于上方表单)">
{
    "content": "自定义测试消息",
    "timestamp": 1719000000000
}
            </textarea>
        </div>

        <!-- 日志输出 -->
        <div class="log-area" id="logArea"></div>
    </div>

    <!-- 引入Socket.IO客户端(CDN) -->
    <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
    <script>
        // 全局变量
        let socket = null;
        const serverUrlInput = document.getElementById('serverUrl');
        const msgContentInput = document.getElementById('msgContent');
        const msgTimestampInput = document.getElementById('msgTimestamp');
        const customMsgInput = document.getElementById('customMsg');
        const connectBtn = document.getElementById('connectBtn');
        const sendBtn = document.getElementById('sendBtn');
        const disconnectBtn = document.getElementById('disconnectBtn');
        const logArea = document.getElementById('logArea');

        // 初始化时间戳
        msgTimestampInput.value = Date.now();

        // 日志输出函数
        function log(message, type = 'info') {
            const time = new Date().toLocaleTimeString();
            const className = `log-${type}`;
            const logItem = document.createElement('div');
            logItem.innerHTML = `<span class="log-time">[${time}]</span> <span class="${className}">${message}</span>`;
            logArea.appendChild(logItem);
            // 自动滚动到底部
            logArea.scrollTop = logArea.scrollHeight;
        }

        // 连接按钮点击事件
        connectBtn.addEventListener('click', () => {
            const serverUrl = serverUrlInput.value.trim();
            if (!serverUrl) {
                log('请输入服务端地址!', 'error');
                return;
            }

            // 断开已有连接(防止重复连接)
            if (socket) {
                socket.disconnect();
                socket = null;
            }

            try {
                // 连接Socket.IO服务端
                log(`正在连接:${serverUrl}`, 'info');
                socket = io(serverUrl, {
                    path: '/realtime',
                    reconnection: false, // 关闭自动重连(便于测试)
                    timeout: 10000 // 连接超时10秒
                });

                // 连接成功回调
                socket.on('connect', () => {
                    log('✅ 连接服务端成功!', 'success');
                    connectBtn.disabled = true;
                    sendBtn.disabled = false;
                    disconnectBtn.disabled = false;
                });

                // 连接失败回调
                socket.on('connect_error', (error) => {
                    log(`❌ 连接失败:${error.message}`, 'error');
                    resetState();
                });

                // 断开连接回调
                socket.on('disconnect', (reason) => {
                    log(`🔌 连接断开:${reason}`, 'info');
                    resetState();
                });

                // 接收服务端消息(可选,若服务端主动推送事件)
                socket.onAny((event, ...args) => {
                    log(`📥 接收服务端事件 [${event}]:${JSON.stringify(args)}`, 'info');
                });

            } catch (error) {
                log(`❌ 连接异常:${error.message}`, 'error');
                resetState();
            }
        });

        // 发送消息按钮点击事件
        sendBtn.addEventListener('click', () => {
            if (!socket || !socket.connected) {
                log('❌ 未连接到服务端!', 'error');
                return;
            }

            // 构建消息数据(优先使用自定义JSON,否则用表单数据)
            let messageData;
            try {
                // 尝试解析自定义消息
                const customMsg = customMsgInput.value.trim();
                if (customMsg) {
                    messageData = JSON.parse(customMsg);
                } else {
                    // 表单构建消息
                    messageData = {
                        content: msgContentInput.value.trim() || '默认测试消息',
                        timestamp: msgTimestampInput.value.trim() ? Number(msgTimestampInput.value) : Date.now()
                    };
                }
            } catch (error) {
                log(`❌ 消息格式错误:${error.message}`, 'error');
                return;
            }

            // 发送update事件,并等待ACK响应
            log(`📤 发送 update 事件:${JSON.stringify(messageData)}`, 'info');
            socket.emit('update', messageData, (ackData) => {
                // 接收服务端的ACK响应
                log(`✅ 收到服务端ACK:${JSON.stringify(ackData)}`, 'success');
            });

            // 更新时间戳(便于下次发送)
            msgTimestampInput.value = Date.now();
        });

        // 断开连接按钮点击事件
        disconnectBtn.addEventListener('click', () => {
            if (socket) {
                socket.disconnect();
                log('🔌 主动断开连接', 'info');
            } else {
                log('❌ 无有效连接', 'error');
            }
        });

        // 重置按钮状态
        function resetState() {
            connectBtn.disabled = false;
            sendBtn.disabled = true;
            disconnectBtn.disabled = true;
            socket = null;
        }

        // 页面关闭时断开连接
        window.addEventListener('beforeunload', () => {
            if (socket) {
                socket.disconnect();
            }
        });
    </script>
</body>
</html>

效果演示

参考资料

  1. socket.io/zh-CN/
相关推荐
止语Lab1 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活1 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困1 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn1 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户298698530142 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908402 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金2 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
人间凡尔赛2 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构
IT_陈寒2 小时前
Vue的响应式让我加班到凌晨3点,原来问题出在这
前端·人工智能·后端
卷无止境2 小时前
提升 Python 代码健壮性的方法大盘点
后端·python