基于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/
相关推荐
喵个咪3 分钟前
go-wind-cms 微服务架构设计:为什么基于 Kratos?
后端·微服务·cms
神奇小汤圆9 分钟前
百度面试官:Redis 内存满了怎么办?你有想过吗?
后端
喵个咪10 分钟前
Headless 架构优势:内容与展示解耦,一套 API 打通全端生态
前端·后端·cms
开心就好202512 分钟前
HTTPS超文本传输安全协议全面解析与工作原理
后端·ios
小江的记录本14 分钟前
【JEECG Boot】 JEECG Boot——数据字典管理 系统性知识体系全解析
java·前端·spring boot·后端·spring·spring cloud·mybatis
神奇小汤圆15 分钟前
Spring Batch实战
后端
喵个咪17 分钟前
传统 CMS 太笨重?试试 Headless 架构的 GoWind,轻量又强大
前端·后端·cms
程序员木圭19 分钟前
07-数组入门必看!Java数组的内存分析02
java·后端
喵个咪31 分钟前
Go 语言 CMS 横评:风行 GoWind 对比传统 PHP/Java CMS 核心优势
前端·后端·cms
面向Google编程34 分钟前
从零学习Kafka:位移与高水位
大数据·后端·kafka