WebSocket API 是基于 TCP 协议的全双工、低延迟实时通信标准,它通过单一持久连接实现服务器与客户端的双向主动通信,彻底解决了传统 HTTP 请求-响应模型的高延迟问题。本文系统介绍了 WebSocket 的核心特性、原生 API 用法、浏览器兼容性及 Node.js 最新支持,并深入讲解了 Socket.IO 库的增强功能(如自动重连、房间机制)。通过一个完整的 Java Netty 服务器与 HTML5 客户端实践示例,展示了实时时间服务器的构建过程,同时总结了聊天、金融看板等典型应用场景及 WSS 加密、心跳检测等最佳实践,为开发现代实时 Web 应用提供全面指导。

博主博客

一、什么是 WebSocket API?

WebSocket API 是下一代客户端-服务器异步通信的标准。它通过单个 TCP 套接字建立持久连接,并使用 ws(非加密)或 wss(加密)协议进行通信,适用于任意客户端与服务器程序。WebSocket 目前由 W3C 维护并标准化。

核心特点与对比

相较于传统的 Ajax(基于 HTTP 请求-响应模型),WebSocket 具有根本性优势:

特性 WebSocket 传统 Ajax (HTTP)
通信模式 全双工、双向实时。连接建立后,服务器和客户端均可随时主动推送消息。 半双工、单向请求。必须由客户端发起请求,服务器才能响应。
连接开销 一次握手,持久连接。后续通信头信息开销极小,延迟低。 每次请求都需携带完整的 HTTP 头,重复建立连接开销大,延迟高。
跨域支持 默认支持跨域通信(通过 WebSocket 握手)。 受同源策略严格限制,需通过 CORS 等技术解决。
适用场景 聊天、实时游戏、金融报价、协同编辑等需要持续、即时数据流的应用。 数据获取、表单提交等离散、非实时的交互。

二、WebSocket 的基本用法

以下示例展示客户端如何使用原生 WebSocket API 建立连接、收发消息及处理事件。

// 创建 WebSocket 连接,连接到本地 8080 端口
const socket = new WebSocket('wss://localhost:8080'); // 生产环境推荐使用 wss

// 连接成功建立事件
socket.addEventListener('open', (event) => {
    console.log('连接已建立');
    socket.send('客户端已就绪!'); // 向服务器发送消息
});

// 接收服务器消息事件
socket.addEventListener('message', (event) => {
    console.log('收到服务器消息:', event.data);
});

// 连接关闭事件
socket.addEventListener('close', (event) => {
    console.log(`连接关闭,代码: ${event.code}, 原因: ${event.reason}`);
});

// 错误处理事件
socket.addEventListener('error', (error) => {
    console.error('WebSocket 错误:', error);
});

核心 API 解析:

  • new WebSocket(url):构造函数,创建连接实例。URL 需以 ws://wss:// 开头。
  • readyState 属性:表示连接状态(0-连接中,1-已打开,2-关闭中,3-已关闭)。
  • send(data) 方法:发送数据,支持字符串、Blob、ArrayBuffer 类型。
  • close([code[, reason]]) 方法:主动关闭连接。

三、浏览器支持与服务器端实现

浏览器支持

WebSocket 已被所有现代浏览器广泛支持多年,包括 Chrome、Firefox、Safari、Edge 等。对于极少数旧环境(如 IE 10以下),通常需要使用兼容库(如 Socket.IO)进行降级处理。

服务器端实现与 Node.js 重要更新

在服务器端,几乎所有主流语言都有成熟的 WebSocket 库。值得注意的是,Node.js 从 v22.4.0 开始,提供了稳定的原生 WebSocket 客户端支持

// Node.js 作为客户端连接其他 WebSocket 服务器
const socket = new WebSocket('ws://some-real-time-service.com');

这意味着 Node.js 应用现在可以不依赖 ws 等第三方库来充当客户端。但请注意,Node.js 原生模块仍未提供服务器实现,创建 WebSocket 服务器仍需使用 wssocket.io 等库。

四、使用 Socket.IO 简化开发

Socket.IO 是一个强大的实时通信库,它封装了 WebSocket 并提供了额外特性:自动降级(在不支持 WebSocket 时改用 HTTP 长轮询)、自动重连房间命名空间等。

客户端使用示例

<script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>
<script>
    // 建立连接
    const socket = io('https://server-domain.com');
    
    // 监听自定义事件
    socket.on('serverMessage', (data) => {
        console.log('收到消息:', data);
    });
    
    // 发送消息
    function sendMessage(msg) {
        socket.emit('clientMessage', { text: msg });
    }
</script>

服务端示例 (Node.js + Socket.IO)

const { Server } = require('socket.io');
const http = require('http');

const server = http.createServer();
const io = new Server(server, {
    cors: { origin: "*" } // 处理跨域
});

io.on('connection', (socket) => {
    console.log('客户端已连接');
    
    socket.emit('serverMessage', '欢迎!'); // 发送给当前客户端
    
    socket.on('clientMessage', (data) => { // 监听客户端消息
        console.log('客户端说:', data.text);
        io.emit('serverMessage', data.text); // 广播给所有客户端
    });
    
    socket.on('disconnect', () => {
        console.log('客户端断开');
    });
});

server.listen(3000, () => console.log('服务器运行在 3000 端口'));

五、完整示例:基于 Netty 的 WebSocket 时间服务器

下面通过一个完整的客户端-服务器示例,展示 WebSocket 的实际应用。这个示例包含一个 HTML5 客户端和一个使用 Netty 框架的 Java 服务器端,实现了一个简单的时间服务器。

客户端 (HTML + JavaScript)

以下是现代 WebSocket 客户端代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Netty WebSocket 时间服务器</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .status { padding: 10px; margin: 10px 0; border-radius: 5px; }
        .connected { background-color: #d4edda; color: #155724; }
        .disconnected { background-color: #f8d7da; color: #721c24; }
        textarea { width: 100%; height: 200px; font-family: monospace; }
    </style>
</head>
<body>
    <h2>Netty WebSocket 时间服务器测试</h2>
    <div id="status" class="status"></div>
    
    <div>
        <input type="text" id="messageInput" value="Hello WebSocket" style="width: 300px; padding: 5px;">
        <button onclick="sendMessage()">发送消息</button>
        <button onclick="clearLog()">清空日志</button>
    </div>
    
    <h3>消息日志</h3>
    <textarea id="responseText" readonly></textarea>
    
    <script>
        let socket = null;
        const statusElement = document.getElementById('status');
        const responseText = document.getElementById('responseText');
        const messageInput = document.getElementById('messageInput');
        
        function updateStatus(message, isConnected) {
            statusElement.textContent = message;
            statusElement.className = `status ${isConnected ? 'connected' : 'disconnected'}`;
        }
        
        function logMessage(message) {
            const timestamp = new Date().toLocaleTimeString();
            responseText.value += `[${timestamp}] ${message}\n`;
            responseText.scrollTop = responseText.scrollHeight;
        }
        
        function connectWebSocket() {
            if (!window.WebSocket) {
                updateStatus("抱歉,您的浏览器不支持 WebSocket 协议!", false);
                return;
            }
            
            // 创建 WebSocket 连接
            socket = new WebSocket("ws://localhost:8080/websocket");
            
            socket.onopen = function(event) {
                updateStatus("✅ WebSocket 连接已建立,可以发送消息!", true);
                logMessage("系统:连接服务器成功");
            };
            
            socket.onmessage = function(event) {
                logMessage(`服务器:${event.data}`);
            };
            
            socket.onclose = function(event) {
                updateStatus("❌ WebSocket 连接已关闭", false);
                logMessage("系统:连接已断开");
                // 3秒后尝试重连
                setTimeout(connectWebSocket, 3000);
            };
            
            socket.onerror = function(error) {
                updateStatus("⚠️ WebSocket 连接错误", false);
                logMessage("系统:连接错误,请检查服务器是否运行");
            };
        }
        
        function sendMessage() {
            if (!socket || socket.readyState !== WebSocket.OPEN) {
                alert("WebSocket 连接尚未建立!");
                return;
            }
            
            const message = messageInput.value.trim();
            if (message) {
                socket.send(message);
                logMessage(`客户端:${message}`);
                messageInput.value = "";
            }
        }
        
        function clearLog() {
            responseText.value = "";
        }
        
        // 页面加载时自动连接
        window.onload = connectWebSocket;
        
        // 添加键盘支持
        messageInput.addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                sendMessage();
            }
        });
    </script>
</body>
</html>

服务器端 (Java + Netty)

以下是 Netty WebSocket 服务器代码:

WebSocketServer.java - 服务器启动类

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

public class WebSocketServer {
    public void run(int port) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ChannelPipeline pipeline = ch.pipeline();
                        // HTTP 编解码器
                        pipeline.addLast(new HttpServerCodec());
                        // HTTP 消息聚合器
                        pipeline.addLast(new HttpObjectAggregator(65536));
                        // 支持大文件传输
                        pipeline.addLast(new ChunkedWriteHandler());
                        // WebSocket 协议处理器
                        pipeline.addLast(new WebSocketServerProtocolHandler("/websocket"));
                        // 自定义业务处理器
                        pipeline.addLast(new WebSocketServerHandler());
                    }
                });
            
            Channel channel = bootstrap.bind(port).sync().channel();
            System.out.println("=========================================");
            System.out.println("WebSocket 服务器启动成功!");
            System.out.println("监听端口: " + port);
            System.out.println("WebSocket 路径: ws://localhost:" + port + "/websocket");
            System.out.println("=========================================");
            
            channel.closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
    
    public static void main(String[] args) throws Exception {
        int port = 8080;
        if (args.length > 0) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (NumberFormatException e) {
                System.err.println("端口参数错误,使用默认端口 8080");
            }
        }
        new WebSocketServer().run(port);
    }
}

WebSocketServerHandler.java - 业务处理器

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.*;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class WebSocketServerHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
    private static final DateTimeFormatter TIME_FORMATTER =
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // 处理关闭帧
        if (frame instanceof CloseWebSocketFrame) {
            ctx.close();
            System.out.println("客户端连接关闭: " + ctx.channel().remoteAddress());
            return;
        }
        
        // 处理 Ping 帧(回复 Pong)
        if (frame instanceof PingWebSocketFrame) {
            ctx.writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
            System.out.println("收到 Ping,回复 Pong");
            return;
        }
        
        // 只处理文本帧
        if (!(frame instanceof TextWebSocketFrame)) {
            throw new UnsupportedOperationException("不支持的帧类型: " + frame.getClass().getName());
        }
        
        // 处理文本消息
        String request = ((TextWebSocketFrame) frame).text();
        System.out.println("收到客户端消息: " + request + " (来自: " + ctx.channel().remoteAddress() + ")");
        
        // 构造响应
        String currentTime = LocalDateTime.now().format(TIME_FORMATTER);
        String response;
        
        if (request.equalsIgnoreCase("time") || request.equalsIgnoreCase("时间")) {
            response = "当前服务器时间: " + currentTime;
        } else if (request.equalsIgnoreCase("hello") || request.contains("你好")) {
            response = "你好!我是 Netty WebSocket 服务器。当前时间: " + currentTime;
        } else {
            response = "收到您的消息: \"" + request + "\"。当前时间: " + currentTime;
        }
        
        // 发送响应
        ctx.writeAndFlush(new TextWebSocketFrame(response));
        System.out.println("发送响应: " + response);
    }
    
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        System.out.println("新客户端连接: " + ctx.channel().remoteAddress());
    }
    
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        System.out.println("客户端断开: " + ctx.channel().remoteAddress());
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("WebSocket 处理异常:");
        cause.printStackTrace();
        ctx.close();
    }
}

运行步骤

  1. 准备环境

    • 安装 Java JDK 8+
    • 添加 Netty 依赖(Maven):
      <dependency>
          <groupId>io.netty</groupId>
          <artifactId>netty-all</artifactId>
          <version>4.1.100.Final</version>
      </dependency>
      
  2. 启动服务器

    javac -cp ".:netty-all-4.1.100.Final.jar" *.java
    java -cp ".:netty-all-4.1.100.Final.jar" WebSocketServer
    
  3. 运行客户端

    • 将 HTML 文件保存为 websocket-client.html
    • 用浏览器打开该文件
    • 确保服务器运行在 localhost:8080
  4. 功能测试

    • 页面会自动连接服务器
    • 在输入框中发送消息
    • 服务器会回复消息并附加当前时间
    • 尝试发送 “time”、“hello” 等关键词查看不同响应

示例特点

  1. 完整的双向通信:展示了 WebSocket 双向通信的完整流程
  2. 错误处理:包含连接失败、断开重连等处理
  3. 用户友好界面:清晰的状态显示和消息日志
  4. 生产级代码结构:服务器端使用线程池、资源清理等最佳实践
  5. 易扩展性:可轻松添加更多消息类型和业务逻辑

六、实际应用场景与最佳实践

典型应用场景

  • 实时聊天(一对一、群聊)
  • 实时数据仪表盘(金融报价、系统监控、体育比分)
  • 多人在线游戏
  • 协同编辑工具(如在线文档)
  • 即时通知系统

最佳实践建议

  1. 使用安全连接:生产环境务必使用 wss://,对数据进行加密。
  2. 实现心跳机制:定期发送 Ping/Pong 帧,检测并关闭死连接,防止资源浪费。
  3. 处理重连逻辑:在连接异常断开时,采用指数退避等策略实现自动重连。
  4. 注意连接数限制:单个服务器的并发连接数有限,大型应用需考虑分片、负载均衡。

七、学习资源

八、过时技术说明

dojox.Socket 是 Dojo 工具箱为提供统一通信接口而设计的模块。随着 WebSocket 标准的确立和普及,以及现代前端框架(如 React、Vue)的兴起,Dojo Toolkit 及其 dojox.socket 在现代 Web 开发中已极少使用。

总而言之,WebSocket 是实现现代 Web 实时功能的基石。对于新项目,可以直接使用浏览器原生 API;若需更强的兼容性和高级功能(如房间、广播),Socket.IO 是经过充分验证的优秀选择。