基于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/
相关推荐
用户69371750013841 小时前
14.Kotlin 类:类的形态(一):抽象类 (Abstract Class)
android·后端·kotlin
组合缺一1 小时前
Spring Boot 国产化替代方案。Solon v3.7.2, v3.6.5, v3.5.9 发布(支持 LTS)
java·后端·spring·ai·web·solon·mcp
s***11701 小时前
常见的 Spring 项目目录结构
java·后端·spring
7***47712 小时前
在2023idea中如何创建SpringBoot
java·spring boot·后端
IT_陈寒2 小时前
React性能优化:这5个Hooks技巧让我减少了40%的重新渲染
前端·人工智能·后端
L***d6703 小时前
十七:Spring Boot依赖 (2)-- spring-boot-starter-web 依赖详解
前端·spring boot·后端
本妖精不是妖精3 小时前
基于 Rokid Max 与 JSAR 构建空间锚定型 AR 信息面板
后端·ar·restful
芳草萋萋鹦鹉洲哦3 小时前
【tauri+rust】App会加载白屏,有时显示在左上角显示一小块,如何优化
开发语言·后端·rust