万字详解WebSocket的用法

万字详解WebSocket 用法

一、简介

1.1 什么是WebSocket

WebSocket是一种基于TCP协议 的全双工通信协议,由W3C开发并于2011年成为正式标准,用于在Web应用程序和服务器之间建立实时、双向、持久化的通信连接。

它解决了传统HTTP请求-响应模型的核心痛点:HTTP协议是单向通信,服务器只能被动响应客户端请求,无法主动向客户端推送数据;而WebSocket通过一次HTTP握手建立起持久的TCP连接后,客户端和服务器可在该连接上随时双向传输数据,无需重复建立连接,实现真正的实时通信。

WebSocket的通信链路是单TCP连接复用,握手阶段基于HTTP协议,握手完成后脱离HTTP协议,使用独立的帧格式进行数据传输,适用于在线聊天、实时监控、多人游戏、大屏数据推送等需要实时交互的场景。

1.2 WebSocket的优势和劣势

核心优势
  1. 实时性极强:持久化连接让数据传输无需等待客户端请求,服务器可主动、即时地向客户端推送数据,响应延迟远低于HTTP轮询/长轮询;
  2. 双向通信:客户端和服务器均可在任意时间向对方发送数据,打破HTTP的单向通信限制;
  3. 减少网络负载:仅需一次握手建立连接,后续通信无需重复发送HTTP请求头,大幅降低数据包体积和网络请求次数;
  4. 兼容性良好:基于TCP/IP协议,兼容主流浏览器(Chrome、Firefox、Edge等)和服务器中间件(Tomcat、Jetty、Nginx等),且可通过降级方案适配老旧浏览器;
  5. 支持多种数据类型:可传输文本、二进制数据(如图片、音频、视频),满足不同业务的数据传输需求。
核心劣势
  1. 环境支持要求:需要浏览器和服务器均支持WebSocket协议,部分老旧浏览器(如IE10以下)和低版本服务器中间件(如Tomcat7及以下)不原生支持;
  2. 服务器额外开销:服务器需要维护大量长时间的持久化连接,会占用更多的内存、CPU资源,对服务器的资源配置和连接管理能力要求较高;
  3. 安全风险:由于服务器可主动推送数据,若未做严格的身份认证、权限校验和数据过滤,可能存在非法连接、数据泄露、恶意推送等安全问题;
  4. 网络环境敏感:持久化连接易受网络波动(如断网、网络超时、防火墙拦截)影响,可能导致连接异常断开,需要额外处理重连、心跳检测等逻辑;
  5. 跨域配置复杂:WebSocket的跨域配置与HTTP跨域不同,需要在服务器端单独配置允许的跨域源,若配置不当会导致连接失败。

1.3 WebSocket与HTTP的核心区别

WebSocket和HTTP均为基于TCP的应用层协议,但设计目标和通信模式完全不同,核心区别如下表所示:

对比维度 WebSocket HTTP
通信模式 全双工:客户端/服务器可同时收发数据,无方向限制 半双工:仅支持客户端发起请求、服务器响应,服务器无法主动推送
连接特性 持久化连接:一次握手后TCP连接持续保持,直至主动关闭 短连接/长连接 : 短连接:每次请求响应后关闭TCP连接; 长连接(Keep-Alive):复用连接但仍需客户端发起请求
协议类型 基于TCP的独立应用层协议,仅握手阶段依赖HTTP 基于TCP的应用层协议,核心为请求-响应模型
数据传输 无请求头开销,采用轻量级帧格式传输,效率高 每次请求/响应均携带完整HTTP头(如Cookie、Content-Type),开销大
触发方式 服务器可主动触发数据传输 仅客户端请求触发服务器响应
状态管理 连接保持状态,会话信息可持久化存储 无状态协议,需通过Cookie、Session、Token等维护状态
端口使用 可使用80/443端口(与HTTP/HTTPS一致),避免防火墙拦截 标准端口80(HTTP)/443(HTTPS)
数据格式 支持文本、二进制、Ping/Pong等多种帧类型 仅支持请求-响应格式,数据类型由Content-Type定义
关闭机制 需双方协商发送关闭帧,优雅释放连接 服务器响应完成后即可关闭,或通过Keep-Alive超时关闭
补充:WebSocket与HTTP长轮询/短轮询的对比
方案 实现方式 延迟 服务器开销 网络开销 适用场景
WebSocket 持久化TCP连接,双向通信 极低(毫秒级) 中(维护长连接) 极低(无重复请求头) 实时聊天、监控、游戏
HTTP长轮询 客户端发起请求,服务器挂起连接直至有数据/超时,客户端立即重连 低(秒级) 高(大量挂起的请求) 中(仍有HTTP头开销) 准实时场景(如消息通知)
HTTP短轮询 客户端定时(如1秒)发起请求,服务器即时响应 高(取决于轮询间隔) 极高(频繁创建/关闭连接) 高(重复请求头+频繁连接) 非实时场景(如普通数据查询)

二、WebSocket的基本概念

2.1 WebSocket的协议

WebSocket协议是一种基于TCP的应用层协议 ,专为Web端实时通信设计,分为客户端协议服务器协议 两部分,核心包含握手协议数据传输协议 两大模块,协议标识为ws(明文)和wss(加密,基于SSL/TLS,类似HTTP和HTTPS)。

协议核心特点
  1. 握手基于HTTP:客户端通过发送一个特殊的HTTP请求发起握手,服务器响应后完成协议升级,从HTTP协议切换为WebSocket协议,握手过程完全兼容HTTP协议,可通过常规的HTTP端口(80/443)通信,避免被防火墙拦截;
  2. 全双工通信:TCP连接建立后,客户端和服务器的读写通道相互独立,双方可同时发送和接收数据,无先后顺序限制;
  3. 独立的帧格式:数据传输采用自定义的帧格式,而非HTTP的请求/响应格式,帧结构简洁,包含消息头和消息体,大幅降低传输开销;
  4. 协议协商机制:握手阶段客户端和服务器会协商协议版本、支持的子协议(如自定义的业务协议)、扩展选项(如压缩、心跳)等,保证通信的兼容性;
  5. 状态保持:连接建立后始终保持活跃状态,直至客户端或服务器主动关闭,无需像HTTP那样每次请求都重新建立连接。
协议通信流程
  1. 客户端向服务器发送WebSocket握手请求(HTTP GET请求,包含特殊的请求头);
  2. 服务器验证握手请求头,若合法则返回WebSocket握手响应(HTTP 101状态码,表示协议升级);
  3. 握手成功后,HTTP连接正式升级为WebSocket的TCP持久化连接,双方进入数据传输阶段;
  4. 客户端和服务器通过WebSocket帧格式双向传输数据;
  5. 一方发起关闭请求,双方完成连接关闭流程,释放TCP连接。
核心握手请求/响应头
头部字段 客户端请求头 服务器响应头 作用
Connection Upgrade Upgrade 标识需要升级协议
Upgrade websocket websocket 标识升级为WebSocket协议
Sec-WebSocket-Version 13(主流版本) 13 标识WebSocket协议版本,主流为13版
Sec-WebSocket-Key 随机生成的Base64字符串 - 客户端生成的随机密钥,用于服务器验证
Sec-WebSocket-Accept - 加密后的密钥 服务器通过客户端的Key加密生成,用于客户端验证
Sec-WebSocket-Protocol 自定义子协议(如chat) 确认的子协议 协商双方使用的自定义业务协议
Sec-WebSocket-Extensions 支持的扩展(如permessage-deflate) 确认的扩展 协商数据压缩等扩展功能

2.2 WebSocket的生命周期

WebSocket的生命周期描述了连接从创建到完全关闭 的完整过程,核心分为四个阶段,整个生命周期由TCP连接状态和WebSocket帧交互共同控制,且连接在任意阶段都可能因网络异常、服务器/客户端主动操作而中断。

阶段1:连接建立阶段(Connection Establishment)

这是WebSocket生命周期的起始阶段,核心完成协议握手和TCP连接建立,具体流程:

  1. 客户端创建WebSocket对象,向服务器发送符合规范的HTTP握手请求;
  2. 服务器接收请求后,验证请求头的合法性(如协议版本、密钥、跨域源等);
  3. 验证通过后,服务器返回HTTP 101状态码的握手响应,完成协议升级;
  4. 客户端验证服务器的响应头(如Sec-WebSocket-Accept),验证通过则建立TCP持久化连接,进入下一阶段;若验证失败,直接关闭连接,生命周期结束。

关键标识 :客户端触发onopen事件,服务器触发连接建立回调(如Java的@OnOpen)。

阶段2:连接开放阶段(Connection Open)

这是WebSocket的核心通信阶段 ,连接处于活跃状态,客户端和服务器可自由进行双向数据传输,具体特点:

  1. TCP连接保持打开状态,双方的读写通道均处于可用状态;
  2. 支持文本、二进制等多种数据类型的传输,数据以WebSocket帧的形式发送和接收;
  3. 可通过扩展协议实现数据压缩、心跳检测等功能;
  4. 连接在此阶段会持续保持,直至收到关闭帧或发生异常。

关键标识 :客户端可调用send()方法发送数据,收到数据时触发onmessage事件;服务器可接收客户端消息并主动推送数据,触发消息接收回调(如Java的@OnMessage)。

阶段3:连接关闭阶段(Connection Closing)

此阶段为连接关闭的准备阶段 ,由客户端或服务器主动发起,核心完成关闭帧的交互和资源准备释放,具体流程:

  1. 发起方发送关闭帧(包含关闭状态码和关闭原因),请求关闭连接;
  2. 接收方收到关闭帧后,返回确认关闭帧,表示已收到关闭请求;
  3. 双方开始释放与该连接相关的资源(如缓冲区、线程、会话信息等),停止发送新数据,仅处理未完成的数据包传输。

关键特点:此阶段连接并未完全关闭,仍可处理未传输完成的数据,直至双方完成关闭帧交互。

阶段4:连接关闭完成阶段(Connection Closed)

这是WebSocket生命周期的结束阶段,核心完成TCP连接的释放和资源清理,具体流程:

  1. 双方完成所有未完成的数据传输,确认无残留数据后,主动关闭TCP连接;
  2. 双方彻底释放与该连接相关的所有资源(如会话对象、连接容器、缓冲区等);
  3. 连接状态变为关闭,任何后续的读写操作都会失败。

关键标识 :客户端触发onclose事件,服务器触发连接关闭回调(如Java的@OnClose);若连接因异常关闭,客户端会先触发onerror事件,再触发onclose事件,服务器触发异常回调(如Java的@OnError)。

生命周期核心注意点
  1. 连接的关闭建议由双方协商完成,即发起方发送关闭帧后,接收方确认并返回关闭帧,避免单方面关闭导致数据丢失;
  2. 网络异常(如断网、超时)会导致连接被动关闭,此时不会触发正常的关闭帧交互,需要通过心跳检测机制识别连接状态;
  3. 每个WebSocket连接对应一个独立的TCP连接,生命周期与TCP连接绑定,TCP连接断开则WebSocket连接同步断开;
  4. 客户端在连接关闭后,若需要重新通信,需重新创建WebSocket对象,发起新的握手请求,建立新的连接。

2.3 WebSocket的消息格式

WebSocket的数据传输以帧(Frame) 为基本单位,所有数据(包括文本、二进制、关闭帧、心跳帧)都封装在帧中传输,单条消息可由单个帧多个帧组成(分片传输),消息格式与HTTP的请求/响应格式完全不同,结构更简洁,传输效率更高。

WebSocket的帧由消息头(Header)消息体(Payload) 两部分组成,其中消息头是定长/变长的二进制数据,包含帧的核心标识信息;消息体是实际传输的数据,可选(部分帧如心跳帧无消息体)。

一、消息头(Header)结构

消息头是WebSocket帧的核心,分为基础头(1-2字节)扩展头(可选,0-8字节),基础头为必选,包含帧的核心属性,扩展头根据帧的长度和功能动态添加,整体结构如下(按二进制位从高到低排列):

1. 第1字节(固定,8位)
位位置 标识 取值 作用
第1位 FIN 0/1 表示是否为消息的最后一帧:1=最后一帧(单帧消息/多帧消息的最后一帧),0=后续还有帧(多帧消息的中间帧)
第2-4位 RSV1/RSV2/RSV3 0/1 保留位,默认值为0,仅在使用扩展协议(如数据压缩)时由扩展协议定义,若未使用扩展则必须为0,否则接收方会拒绝连接
第5-8位 Opcode 0-15 帧的操作码,标识帧的类型,核心取值见下表
Opcode(操作码)核心取值
取值 帧类型 作用
0x00 继续帧(Continuation) 多帧消息的中间帧,标识此帧为上一帧的继续,无独立的帧类型
0x01 文本帧(Text) 消息体为UTF-8编码的文本数据,是最常用的帧类型
0x02 二进制帧(Binary) 消息体为二进制数据,用于传输图片、音频、视频等
0x08 关闭帧(Close) 发起连接关闭请求,消息体可选,包含关闭状态码和关闭原因
0x09 Ping帧(Ping) 心跳检测帧,由一方发送给另一方,要求对方返回Pong帧,无消息体或包含少量测试数据
0x0A Pong帧(Pong) 心跳响应帧,是对Ping帧的回应,与Ping帧的消息体一致
0x03-0x07/0x0B-0x0F 保留 暂未使用,为未来协议扩展预留
2. 第2字节(固定,8位)
位位置 标识 取值 作用
第1位 Mask 0/1 表示消息体是否被掩码加密:1=加密(客户端发送给服务器的帧必须为1),0=未加密(服务器发送给客户端的帧必须为0),这是WebSocket的强制规范,用于防止客户端的恶意数据注入
第2-8位 Payload length 0-127 消息体的长度 ,核心取值分三种情况: 1. 0-125:直接表示消息体的实际长度(字节); 2. 126:表示消息体长度为后续2个字节 的无符号整数; 3. 127:表示消息体长度为后续8个字节的无符号整数
3. 扩展头(可选,0-8字节)

根据第2字节的Payload length取值动态添加,仅在Payload length为126或127时存在,作用是表示消息体的实际长度

  • 若Payload length=126:后续2个字节为16位无符号整数,表示消息体长度(范围:126~65535);
  • 若Payload length=127:后续8个字节为64位无符号整数,表示消息体长度(范围:65536~2^64-1);
  • 若Payload length≤125:无扩展头,直接使用该值作为消息体长度。
4. 掩码密钥(可选,4字节)

仅在Mask=1 (客户端发送给服务器)时存在,是4个字节的随机二进制数据,用于对消息体进行掩码解密,服务器收到后需通过该密钥对消息体进行解密,才能获取原始数据。

二、消息体(Payload)结构

消息体是WebSocket帧的实际传输数据,为二进制字节流,长度由消息头中的Payload length指定,可选(如Ping/Pong帧无消息体),核心特点:

  1. 数据类型:由Opcode标识,文本帧为UTF-8编码的文本,二进制帧为任意二进制数据;
  2. 掩码加密:客户端发送的帧的消息体会通过掩码密钥进行简单的异或加密,服务器需解密后处理;服务器发送的帧的消息体不加密,客户端可直接解析;
  3. 分片传输:单条大消息可拆分为多个帧传输(FIN=0的继续帧+FIN=1的最后一帧),接收方会将多个帧的消息体拼接为完整的消息后再触发回调;
  4. 无边界限制:消息体的长度仅受服务器和客户端的缓冲区限制,协议本身无最大长度限制。
三、核心帧类型的完整格式示例
1. 客户端发送的文本帧(单帧、长度≤125)

FIN=1 + RSV1-3=0 + Opcode=0x01 + Mask=1 + Payload length=数据长度 + 4字节掩码密钥 + UTF-8文本数据

2. 服务器发送的二进制帧(单帧、长度≤125)

FIN=1 + RSV1-3=0 + Opcode=0x02 + Mask=0 + Payload length=数据长度 + 二进制数据

3. Ping帧(客户端发送给服务器)

FIN=1 + RSV1-3=0 + Opcode=0x09 + Mask=1 + Payload length=0 + 4字节掩码密钥(无消息体)

4. 关闭帧(服务器发送给客户端)

FIN=1 + RSV1-3=0 + Opcode=0x08 + Mask=0 + Payload length=状态码+原因长度 + 2字节关闭状态码+UTF-8关闭原因

2.4 WebSocket的API

WebSocket API是浏览器原生支持的前端接口集合,无需引入任何第三方JS库/框架,可直接在JavaScript中使用,用于创建、管理WebSocket连接,实现与服务器的双向通信。

该API以**WebSocket构造函数为核心,创建的WebSocket实例包含属性、方法、事件**三部分,封装了连接建立、数据发送、消息接收、连接关闭、异常处理等所有核心功能,兼容所有支持WebSocket的主流浏览器。

一、WebSocket构造函数

用于创建WebSocket实例,发起与服务器的连接握手,语法如下:

javascript 复制代码
// 明文连接:ws://服务器地址:端口/接口路径
const ws = new WebSocket('ws://localhost:8080/ws/connect');
// 加密连接:wss://服务器地址:端口/接口路径(推荐生产环境使用)
const ws = new WebSocket('wss://localhost:443/ws/connect');

参数 :唯一参数为WebSocket服务器的连接地址,协议标识为ws(明文)或wss(加密),格式与URL一致,可携带路径参数(如ws://localhost:8080/ws/connect?type=chat)。

返回值:一个WebSocket实例对象,包含连接的所有状态和操作方法。

二、WebSocket实例属性

所有属性均为只读,用于获取连接的当前状态、相关信息,核心属性如下:

属性名 类型 取值/说明 作用
readyState 数字 0/1/2/3 表示连接的当前状态 ,核心取值: 0 - CONNECTING:正在建立连接,尚未完成握手; 1 - OPEN:连接已建立,可正常发送/接收数据; 2 - CLOSING:正在关闭连接,处于关闭阶段; 3 - CLOSED:连接已完全关闭,生命周期结束
bufferedAmount 数字 非负整数 表示已发送但未被服务器接收的字节数,用于检测发送缓冲区的状态,避免数据发送过快导致缓冲区溢出
url 字符串 连接地址 表示创建实例时传入的WebSocket服务器连接地址(含ws/wss协议)
protocol 字符串 子协议名称/空字符串 表示握手阶段协商的自定义子协议,若未协商则为空字符串
extensions 字符串 扩展名称/空字符串 表示握手阶段协商的扩展协议(如permessage-deflate),若未协商则为空字符串
三、WebSocket实例方法

用于主动执行连接相关操作(发送数据、关闭连接),核心方法如下:

1. send(data) - 发送数据给服务器

语法ws.send(data);
参数data为要发送的数据,支持四种类型:

  • 字符串(String):最常用,适用于文本、JSON数据传输;
  • 二进制数组(ArrayBuffer):适用于二进制数据传输;
  • 二进制数组视图(TypedArray):如Uint8Array,基于ArrayBuffer,适用于结构化二进制数据;
  • Blob对象:适用于大文件(图片、音频、视频)传输。
    注意事项
  • 仅当readyState=1(连接开放)时可调用,否则会抛出异常;
  • 发送大量数据时,需通过bufferedAmount检测缓冲区状态,做节流处理;
  • 发送JSON数据时,需先通过JSON.stringify()转为字符串。

示例

javascript 复制代码
// 发送字符串
ws.send('Hello, WebSocket Server!');
// 发送JSON数据
const data = { type: 'chat', content: '你好' };
ws.send(JSON.stringify(data));
// 发送二进制数据
const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);
view[0] = 1;
ws.send(buffer);
2. close(code, reason) - 主动关闭连接

语法ws.close(code, reason);
参数

  • code(可选):关闭状态码,为16位无符号整数,需符合WebSocket规范的合法状态码(见下文),默认值为1000;
  • reason(可选):关闭原因,为UTF-8编码的字符串,长度不超过123字节,默认值为空字符串。
    注意事项
  • 调用后连接状态变为2(CLOSING),进入关闭阶段,直至完成关闭帧交互后变为3(CLOSED);
  • 若连接已处于CLOSING/CLOSED状态,调用此方法无效果;
  • 状态码必须使用规范的合法值,自定义状态码会导致连接异常关闭。

示例

javascript 复制代码
// 正常关闭连接
ws.close(1000, '客户端主动正常关闭');
// 因业务结束关闭连接
ws.close(1001, '业务处理完成,关闭连接');
合法关闭状态码核心取值
状态码 含义 适用场景
1000 正常关闭 客户端/服务器主动正常关闭连接,无异常
1001 端点离开 端点正在离开(如客户端页面关闭、服务器服务停止)
1002 协议错误 接收到不符合协议规范的帧,导致连接关闭
1003 不支持的数据类型 接收到不支持的帧类型(如保留的Opcode)
1004 暂未使用 为未来协议扩展预留
1005 无状态码 关闭帧中未包含状态码(默认值)
1006 连接异常关闭 连接因网络异常、超时等原因被动关闭,无关闭帧
1007 数据格式错误 文本帧的数据不是UTF-8编码,导致解析失败
1008 策略违反 接收到违反服务器/客户端策略的数据(如非法内容、超出权限)
1009 消息过大 接收到的消息体积超过缓冲区限制,无法处理
1010 缺少扩展 客户端需要的扩展协议服务器不支持,导致连接关闭
1011 服务器内部错误 服务器处理数据时发生内部异常,无法继续通信
四、WebSocket实例事件

WebSocket采用事件驱动 模型,所有通信状态变化和数据接收均通过事件触发,需为实例绑定事件监听器来处理,核心事件均为原生DOM事件 ,可通过on+事件名addEventListener绑定,核心事件如下:

1. open 事件 - 连接建立成功触发

触发时机 :握手成功,连接状态变为1(OPEN)时触发,仅触发一次。
作用 :处理连接建立后的初始化逻辑(如发送登录信息、请求初始化数据)。
绑定方式

javascript 复制代码
// 方式1:直接绑定onopen属性
ws.onopen = function() {
  console.log('WebSocket连接建立成功!');
  // 发送初始化数据
  ws.send(JSON.stringify({ type: 'init', data: '请求初始化数据' }));
};

// 方式2:addEventListener绑定(支持多个监听器)
ws.addEventListener('open', function() {
  console.log('WebSocket连接建立成功!');
});
2. message 事件 - 收到服务器数据触发

触发时机 :客户端接收到服务器发送的任意帧(文本、二进制)时触发,每次收到数据都会触发。
作用 :处理服务器推送的数据,解析并渲染到页面。
事件对象 :事件参数event包含一个核心属性data,表示接收到的数据,类型与服务器发送的帧类型一致:

  • 服务器发送文本帧:event.data为字符串;
  • 服务器发送二进制帧:event.data为Blob对象或ArrayBuffer对象(由浏览器决定)。
    绑定方式
javascript 复制代码
ws.onmessage = function(event) {
  // 处理文本数据
  if (typeof event.data === 'string') {
    const data = JSON.parse(event.data);
    console.log('收到服务器文本数据:', data);
    // 业务处理逻辑
  }
  // 处理二进制数据
  else {
    console.log('收到服务器二进制数据:', event.data);
    // 如处理图片、音频
  }
};
3. error 事件 - 连接发生异常触发

触发时机 :连接在任意阶段发生异常(如握手失败、网络中断、数据解析错误、服务器异常)时触发,触发后会紧接着触发close事件。
作用 :捕获连接异常信息,记录日志,做异常提示。
事件对象 :事件参数event包含异常相关信息(不同浏览器实现不同),可通过console.error打印详细信息。
绑定方式

javascript 复制代码
ws.onerror = function(event) {
  console.error('WebSocket连接发生异常:', event);
  alert('连接异常,请稍后重试!');
};
4. close 事件 - 连接完全关闭触发

触发时机 :连接状态变为3(CLOSED)时触发,无论正常关闭还是异常关闭,都会触发,仅触发一次。
作用 :处理连接关闭后的逻辑(如清理资源、提示用户、触发重连机制)。
事件对象 :事件参数event包含两个核心属性:

  • code:关闭状态码,与close()方法的code参数一致;
  • reason:关闭原因,与close()方法的reason参数一致。
    绑定方式
javascript 复制代码
ws.onclose = function(event) {
  console.log(`WebSocket连接关闭,状态码:${event.code},原因:${event.reason}`);
  // 触发重连逻辑
  // reconnect();
};
五、API使用完整示例
javascript 复制代码
// 1. 创建WebSocket实例(加密连接)
const ws = new WebSocket('wss://localhost:8080/ws/chat');

// 2. 绑定连接建立事件
ws.onopen = function() {
  console.log('连接建立成功!');
  // 发送登录信息
  ws.send(JSON.stringify({ type: 'login', userId: '1001', username: '张三' }));
};

// 3. 绑定消息接收事件
ws.onmessage = function(event) {
  const data = JSON.parse(event.data);
  switch (data.type) {
    case 'chat':
      // 处理聊天消息
      console.log(`收到${data.username}的消息:${data.content}`);
      break;
    case 'system':
      // 处理系统消息
      console.log(`系统消息:${data.content}`);
      break;
    default:
      console.log('收到未知类型数据:', data);
  }
};

// 4. 绑定异常事件
ws.onerror = function(event) {
  console.error('连接异常:', event);
  alert('网络异常,连接中断!');
};

// 5. 绑定连接关闭事件
ws.onclose = function(event) {
  console.log(`连接关闭:${event.code} - ${event.reason}`);
  // 3秒后尝试重连
  setTimeout(() => {
    location.reload();
  }, 3000);
};

// 6. 主动发送聊天消息
function sendChatMessage(content) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({
      type: 'chat',
      userId: '1001',
      username: '张三',
      content: content
    }));
  } else {
    alert('连接未建立,无法发送消息!');
  }
}

// 7. 页面关闭时主动关闭连接
window.onunload = function() {
  ws.close(1000, '页面关闭,主动关闭连接');
};

2.5 WebSocket的心跳机制

2.5.1 心跳机制的核心作用

WebSocket的持久化连接易受网络波动、防火墙超时、服务器空闲检测 等因素影响,导致连接假死(TCP连接看似存在,但实际已无法通信)。心跳机制的核心作用是:

  1. 检测连接状态:定期发送心跳帧,验证连接是否真实可用,避免"假死连接"占用服务器资源;
  2. 维持连接存活:部分防火墙/代理服务器会主动关闭长时间无数据传输的连接,心跳帧可触发数据交互,避免连接被强制关闭;
  3. 及时发现断连:若心跳响应超时,可立即触发重连逻辑,保证通信的连续性;
  4. 资源清理:服务器可通过心跳超时识别无效连接,主动释放资源。
2.5.2 心跳机制的实现原理

基于WebSocket的Ping/Pong帧实现(Opcode=0x09/Ping、Opcode=0x0A/Pong),核心流程:

  1. 发起方 (客户端/服务器)定期发送Ping帧(无消息体或携带少量标识数据);
  2. 接收方 收到Ping帧后,必须立即返回Pong帧(与Ping帧的消息体一致);
  3. 发起方若在指定超时时间内未收到Pong帧,判定连接失效,触发重连/关闭逻辑;
  4. 若收到Pong帧,重置超时计时器,继续维持连接。

注意:浏览器原生WebSocket API不支持直接发送Ping帧(出于安全考虑),前端需通过自定义文本/二进制帧模拟心跳,服务器端可原生支持Ping/Pong帧。

2.5.3 客户端心跳实现(前端JS)
javascript 复制代码
class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.ws = null;
    // 心跳配置
    this.heartbeatInterval = 10000; // 心跳发送间隔(10秒)
    this.heartbeatTimeout = 5000;   // 心跳超时时间(5秒)
    this.heartbeatTimer = null;     // 心跳发送计时器
    this.reconnectTimer = null;     // 重连计时器
    this.reconnectInterval = 3000;  // 重连间隔(3秒)
    this.maxReconnectTimes = 10;    // 最大重连次数
    this.currentReconnectTimes = 0; // 当前重连次数

    // 初始化连接
    this.connect();
  }

  // 建立连接
  connect() {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      return;
    }
    this.ws = new WebSocket(this.url);

    // 连接成功
    this.ws.onopen = () => {
      console.log('WebSocket连接成功!');
      this.currentReconnectTimes = 0; // 重置重连次数
      this.startHeartbeat(); // 启动心跳
    };

    // 接收消息(包含心跳响应)
    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      // 处理心跳响应
      if (data.type === 'pong') {
        console.log('收到心跳响应,连接正常');
        // 重置心跳超时计时器
        this.resetHeartbeatTimeout();
        return;
      }
      // 处理业务消息
      console.log('收到业务消息:', data);
    };

    // 连接异常
    this.ws.onerror = (event) => {
      console.error('WebSocket连接异常:', event);
    };

    // 连接关闭
    this.ws.onclose = (event) => {
      console.log(`WebSocket连接关闭,状态码:${event.code},原因:${event.reason}`);
      this.stopHeartbeat(); // 停止心跳
      // 触发重连(未达到最大重连次数)
      if (this.currentReconnectTimes < this.maxReconnectTimes) {
        this.reconnect();
      } else {
        console.log('达到最大重连次数,停止重连');
      }
    };
  }

  // 启动心跳
  startHeartbeat() {
    // 清除原有计时器
    this.stopHeartbeat();
    // 定期发送心跳
    this.heartbeatTimer = setInterval(() => {
      this.sendHeartbeat();
    }, this.heartbeatInterval);
  }

  // 发送心跳
  sendHeartbeat() {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      return;
    }
    console.log('发送心跳请求');
    // 发送自定义心跳帧(文本格式)
    this.ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
    // 设置心跳超时检测
    this.heartbeatTimeoutTimer = setTimeout(() => {
      console.log('心跳超时,判定连接失效');
      // 主动关闭连接
      this.ws.close(1006, '心跳超时');
    }, this.heartbeatTimeout);
  }

  // 重置心跳超时
  resetHeartbeatTimeout() {
    if (this.heartbeatTimeoutTimer) {
      clearTimeout(this.heartbeatTimeoutTimer);
      this.heartbeatTimeoutTimer = null;
    }
  }

  // 停止心跳
  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
    this.resetHeartbeatTimeout();
  }

  // 重连逻辑
  reconnect() {
    this.reconnectTimer = setTimeout(() => {
      this.currentReconnectTimes++;
      console.log(`第${this.currentReconnectTimes}次重连...`);
      this.connect();
    }, this.reconnectInterval);
  }

  // 发送业务消息
  sendMessage(data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    } else {
      console.error('连接未建立,无法发送消息');
    }
  }

  // 主动关闭连接
  close() {
    this.stopHeartbeat();
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
    }
    if (this.ws) {
      this.ws.close(1000, '客户端主动关闭');
    }
  }
}

// 使用示例
const client = new WebSocketClient('wss://localhost:8080/ws/chat');
// 发送业务消息
client.sendMessage({ type: 'chat', content: 'Hello World' });
// 页面关闭时清理
window.onunload = () => {
  client.close();
};
2.5.4 服务端心跳实现(Java原生)
java 复制代码
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@ServerEndpoint("/ws/chat/{userId}")
public class HeartbeatWebSocketServer {

    // 存储在线会话
    private static final Map<String, Session> ONLINE_SESSIONS = new ConcurrentHashMap<>();
    // 心跳线程池(定时发送Ping帧)
    private static final ScheduledExecutorService HEARTBEAT_EXECUTOR = Executors.newScheduledThreadPool(1);
    // 服务端心跳间隔(10秒)
    private static final int SERVER_HEARTBEAT_INTERVAL = 10;

    private Session session;
    private String userId;
    // 心跳超时计数器(连续3次未收到Pong则关闭连接)
    private int heartbeatTimeoutCount = 0;

    static {
        // 服务端定时任务:给所有在线客户端发送Ping帧
        HEARTBEAT_EXECUTOR.scheduleAtFixedRate(() -> {
            ONLINE_SESSIONS.forEach((userId, session) -> {
                if (session.isOpen()) {
                    try {
                        // 发送原生Ping帧(推荐)
                        Remote.Async asyncRemote = session.getAsyncRemote();
                        asyncRemote.sendPing(null); // 无消息体的Ping帧
                        System.out.printf("给客户端[%s]发送Ping帧%n", userId);
                    } catch (Exception e) {
                        System.err.printf("给客户端[%s]发送Ping帧失败:%s%n", userId, e.getMessage());
                    }
                }
            });
        }, SERVER_HEARTBEAT_INTERVAL, SERVER_HEARTBEAT_INTERVAL, TimeUnit.SECONDS);
    }

    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        this.session = session;
        this.userId = userId;
        ONLINE_SESSIONS.put(userId, session);
        System.out.printf("客户端[%s]连接成功,当前在线数:%d%n", userId, ONLINE_SESSIONS.size());
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            // 解析客户端消息
            // (实际项目建议使用JSON解析库,如FastJSON/Jackson)
            if (message.contains("\"type\":\"ping\"")) {
                // 处理客户端心跳请求,返回Pong响应
                session.getAsyncRemote().sendText("{\"type\":\"pong\",\"timestamp\":" + System.currentTimeMillis() + "}");
                System.out.printf("收到客户端[%s]心跳,返回Pong响应%n", userId);
                // 重置超时计数器
                this.heartbeatTimeoutCount = 0;
                return;
            }
            // 处理业务消息
            System.out.printf("收到客户端[%s]业务消息:%s%n", userId, message);
            // 广播消息
            broadcastMessage(message);
        } catch (Exception e) {
            System.err.printf("处理客户端[%s]消息失败:%s%n", userId, e.getMessage());
        }
    }

    // 接收Pong帧的回调(原生Pong帧)
    @OnMessage
    public void onPong(PongMessage pongMessage, Session session) {
        System.out.printf("收到客户端[%s]的Pong帧响应%n", userId);
        // 重置超时计数器
        this.heartbeatTimeoutCount = 0;
    }

    @OnClose
    public void onClose(Session session, CloseReason closeReason) {
        ONLINE_SESSIONS.remove(userId);
        System.out.printf("客户端[%s]连接关闭,原因:%s,当前在线数:%d%n",
                userId, closeReason.getReasonPhrase(), ONLINE_SESSIONS.size());
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        System.err.printf("客户端[%s]连接异常:%s%n", userId, throwable.getMessage());
        this.heartbeatTimeoutCount++;
        // 连续3次心跳超时,主动关闭连接
        if (this.heartbeatTimeoutCount >= 3) {
            try {
                session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "心跳超时"));
                ONLINE_SESSIONS.remove(userId);
                System.out.printf("客户端[%s]心跳超时,主动关闭连接%n", userId);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // 广播消息
    private void broadcastMessage(String message) {
        ONLINE_SESSIONS.forEach((uid, s) -> {
            if (s.isOpen()) {
                s.getAsyncRemote().sendText(message);
            }
        });
    }
}

三、在Java中使用WebSocket

Java生态中使用WebSocket主要基于JSR 356规范 (Java API for WebSocket,JDK7+支持),该规范定义了Java端WebSocket的统一接口,主流服务器中间件(Tomcat8+、Jetty9+、GlassFish4+)均原生实现了该规范,同时Spring框架(Spring 4.0+)对JSR 356进行了封装,提供了更贴合Spring生态的使用方式,核心分为Java原生实现(JSR 356)Spring Boot封装实现两种方式,覆盖绝大部分Java后端开发场景。

3.1 基础环境准备

3.1.1 核心依赖(Maven)

无论哪种实现方式,均需引入JSR 356的标准API依赖,服务器中间件(如Tomcat)会提供该API的实现,因此依赖范围需设为provided(打包时不打入,由容器提供):

xml 复制代码
<!-- JSR 356 WebSocket 标准API -->
<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
    <scope>provided</scope>
</dependency>

注意:若使用Spring Boot开发,无需单独引入上述依赖,Spring Boot的websocket起步依赖会自动引入并管理。

3.1.2 服务器环境要求
  • Tomcat:8.0及以上版本(Tomcat7及以下不原生支持JSR 356);
  • Jetty:9.0及以上版本;
  • Spring Boot:1.3及以上版本(推荐2.0+);
  • JDK:1.7及以上版本(推荐1.8+)。

3.2 使用Java WebSocket API(JSR 356)编写WebSocket服务端

Java原生实现基于JSR 356规范,通过注解式 开发,无需依赖任何框架,仅需通过注解标识服务端端点、绑定事件回调,核心注解由javax.websocket包提供,开发简洁,兼容性好,适用于非Spring生态的Java Web项目(如纯Servlet项目)。

3.2.1 核心注解说明

JSR 356的核心注解均为类级方法级 注解,用于标识WebSocket服务端端点和事件回调,所有注解均位于javax.websocket包下,核心注解如下:

注解名 作用范围 核心作用 关键属性
@ServerEndpoint 标识当前类为WebSocket服务端端点,指定客户端访问的路径 value:访问路径(如/echo); encoders:消息编码器; decoders:消息解码器; subprotocols:支持的子协议
@OnOpen 方法 连接建立成功时的回调方法 ,对应客户端的open事件 方法参数可注入SessionEndpointConfig@PathParam(路径参数)
@OnMessage 方法 收到客户端消息时的回调方法 ,对应客户端的message事件 方法参数可注入String/ByteBuffer(消息内容)、Session@PathParam
@OnClose 方法 连接完全关闭时的回调方法 ,对应客户端的close事件 方法参数可注入SessionCloseReason@PathParam
@OnError 方法 连接发生异常时的回调方法 ,对应客户端的error事件 方法参数可注入SessionThrowable(异常信息)、@PathParam
@PathParam 方法参数 提取访问路径中的路径参数,类似RESTful的路径参数 唯一参数为路径参数名(如@PathParam("userId")
3.2.2 核心类说明

JSR 356的核心类均位于javax.websocket包下,封装了WebSocket的会话、连接、配置等核心信息,核心类如下:

  1. Session :会话对象,每个客户端连接对应一个独立的Session实例,封装了连接的所有信息,核心方法:
    • getId():获取会话唯一ID;
    • isOpen():判断连接是否处于开放状态;
    • getBasicRemote():获取同步消息发送器,发送消息为阻塞式;
    • getAsyncRemote():获取异步消息发送器,发送消息为非阻塞式(推荐生产环境使用);
    • close():主动关闭当前会话;
    • getUserProperties():获取会话的自定义属性容器,用于存储连接相关的业务数据(如用户ID、权限)。
  2. CloseReason :关闭原因对象,封装了关闭状态码和关闭原因,核心方法:
    • getCloseCode():获取关闭状态码(CloseReason.CloseCodes枚举);
    • getReasonPhrase():获取关闭原因描述。
  3. Remote.Async :异步消息发送器,由session.getAsyncRemote()获取,核心方法sendText(String)sendBinary(ByteBuffer),非阻塞发送,不会阻塞当前线程。
  4. Remote.Basic :同步消息发送器,由session.getBasicRemote()获取,核心方法sendText(String)sendBinary(ByteBuffer),阻塞发送,直至消息发送成功/失败。
3.2.3 完整服务端示例(消息回显+心跳)
java 复制代码
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Java原生WebSocket服务端端点
 * 功能:消息回显 + 心跳检测 + 连接管理
 */
@ServerEndpoint("/echo/{userId}")
public class EchoServer {

    // 线程安全的连接容器:key=userId,value=Session
    private static final Map<String, Session> ONLINE_SESSIONS = new ConcurrentHashMap<>();

    // 当前会话对象(每个客户端连接对应一个实例)
    private Session session;
    // 当前客户端ID
    private String userId;

    /**
     * 连接建立成功时的回调方法
     * @param session 会话对象
     * @param userId 路径参数(客户端ID)
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        this.session = session;
        this.userId = userId;
        // 存储会话(ConcurrentHashMap保证多线程安全)
        ONLINE_SESSIONS.put(userId, session);
        // 将用户ID存入会话属性,方便后续使用
        session.getUserProperties().put("userId", userId);
        
        System.out.printf("客户端[%s]连接建立成功,会话ID:%s,当前在线数:%d%n",
                userId, session.getId(), ONLINE_SESSIONS.size());
        
        // 主动推送欢迎消息
        sendMessageToClient(userId, "连接成功!当前在线人数:" + ONLINE_SESSIONS.size());
    }

    /**
     * 收到客户端文本消息时的回调方法
     * @param message 客户端发送的文本消息
     * @param session 发送消息的客户端会话
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.printf("收到客户端[%s]消息:%s%n", userId, message);
        
        // 区分心跳消息和业务消息
        if (message.contains("\"type\":\"ping\"")) {
            // 心跳响应
            sendMessageToClient(userId, "{\"type\":\"pong\",\"timestamp\":" + System.currentTimeMillis() + "}");
            return;
        }
        
        // 业务消息:回显给发送方
        sendMessageToClient(userId, "服务器回显:" + message);
        
        // 可选:广播消息给所有在线客户端
        // broadcastMessage("客户端[" + userId + "]:" + message);
    }

    /**
     * 接收Pong帧(原生心跳响应)
     */
    @OnMessage
    public void onPong(PongMessage pongMessage) {
        System.out.printf("收到客户端[%s]的Pong帧响应%n", userId);
    }

    /**
     * 连接关闭时的回调方法
     * @param session 关闭的会话
     * @param closeReason 关闭原因
     */
    @OnClose
    public void onClose(Session session, CloseReason closeReason) {
        // 移除会话
        ONLINE_SESSIONS.remove(userId);
        System.out.printf("客户端[%s]连接关闭,会话ID:%s,状态码:%s,原因:%s,当前在线数:%d%n",
                userId, session.getId(), closeReason.getCloseCode(), closeReason.getReasonPhrase(), ONLINE_SESSIONS.size());
    }

    /**
     * 连接发生异常时的回调方法
     * @param session 异常的会话
     * @param throwable 异常信息
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        System.err.printf("客户端[%s]连接发生异常,会话ID:%s,异常信息:%s%n",
                userId, session.getId(), throwable.getMessage());
        
        // 异常时清理会话
        ONLINE_SESSIONS.remove(userId);
        
        // 主动关闭异常连接
        try {
            if (session.isOpen()) {
                session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "服务器处理异常"));
            }
        } catch (IOException e) {
            System.err.printf("关闭客户端[%s]会话失败:%s%n", userId, e.getMessage());
        }
    }

    // ---------------------- 工具方法 ----------------------

    /**
     * 精准推送消息给指定客户端
     * @param userId 客户端ID
     * @param message 消息内容
     */
    private void sendMessageToClient(String userId, String message) {
        Session targetSession = ONLINE_SESSIONS.get(userId);
        if (targetSession == null || !targetSession.isOpen()) {
            System.err.printf("客户端[%s]连接已关闭,推送失败%n", userId);
            return;
        }
        
        try {
            // 异步发送消息(非阻塞,推荐生产环境)
            targetSession.getAsyncRemote().sendText(message);
        } catch (Exception e) {
            System.err.printf("推送消息给客户端[%s]失败:%s%n", userId, e.getMessage());
        }
    }

    /**
     * 广播消息给所有在线客户端
     * @param message 广播内容
     */
    private void broadcastMessage(String message) {
        ONLINE_SESSIONS.forEach((uid, session) -> {
            if (session.isOpen()) {
                try {
                    session.getAsyncRemote().sendText(message);
                } catch (Exception e) {
                    System.err.printf("广播消息给客户端[%s]失败:%s%n", uid, e.getMessage());
                }
            }
        });
    }
}
3.2.4 部署与访问说明
  1. 将编写好的服务端类放入Java Web项目的src/main/java对应包下,无需额外配置;
  2. 将项目打包为WAR包,部署到支持WebSocket的服务器(如Tomcat8+);
  3. 启动服务器后,客户端可通过ws://服务器IP:端口/项目名/echo/客户端ID连接(如ws://localhost:8080/websocket-demo/echo/1001);
  4. 客户端连接成功后,发送任意文本消息,即可收到服务器的回显消息;发送心跳消息({"type":"ping"}),可收到心跳响应。

3.3 使用Java WebSocket API(JSR 356)编写WebSocket客户端

JSR 356不仅定义了服务端接口,也提供了Java客户端接口 ,用于在Java程序中作为WebSocket客户端与服务器建立连接,实现双向通信,核心注解和类与服务端一致,仅需通过@ClientEndpoint标识客户端端点,适用于Java后端程序之间的WebSocket通信(如微服务之间的实时通信)。

3.3.1 核心注解与类说明
  • 核心注解:@ClientEndpoint(标识当前类为WebSocket客户端端点),其余@OnOpen/@OnMessage/@OnClose/@OnError与服务端完全一致;
  • 核心类:WebSocketContainer(客户端容器,用于创建和管理连接)、ContainerProvider(获取WebSocket容器的工厂类),其余Session/CloseReason与服务端一致。
3.3.2 完整Java客户端示例(带心跳)
java 复制代码
import javax.websocket.*;
import java.net.URI;
import java.util.Scanner;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Java原生WebSocket客户端(带心跳)
 */
@ClientEndpoint
public class EchoClient {

    // 客户端会话对象
    private Session session;
    // 心跳配置
    private final ScheduledExecutorService heartbeatExecutor = Executors.newScheduledThreadPool(1);
    private final int heartbeatInterval = 10; // 心跳间隔(秒)

    /**
     * 连接建立成功回调
     */
    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        System.out.println("客户端与服务器连接建立成功!会话ID:" + session.getId());
        
        // 启动心跳任务
        startHeartbeat();
    }

    /**
     * 接收服务器消息回调
     */
    @OnMessage
    public void onMessage(String message) {
        // 处理心跳响应
        if (message.contains("\"type\":\"pong\"")) {
            System.out.println("收到服务器心跳响应:" + message);
            return;
        }
        // 处理业务消息
        System.out.println("收到服务器消息:" + message);
    }

    /**
     * 接收Pong帧(原生心跳响应)
     */
    @OnMessage
    public void onPong(PongMessage pongMessage) {
        System.out.println("收到服务器原生Pong帧响应");
    }

    /**
     * 连接关闭回调
     */
    @OnClose
    public void onClose(CloseReason closeReason) {
        System.out.printf("客户端与服务器连接关闭,状态码:%s,原因:%s%n",
                closeReason.getCloseCode(), closeReason.getReasonPhrase());
        // 停止心跳
        stopHeartbeat();
    }

    /**
     * 异常回调
     */
    @OnError
    public void onError(Throwable throwable) {
        System.err.println("客户端连接发生异常:" + throwable.getMessage());
        stopHeartbeat();
    }

    /**
     * 启动心跳
     */
    private void startHeartbeat() {
        heartbeatExecutor.scheduleAtFixedRate(() -> {
            if (session != null && session.isOpen()) {
                try {
                    // 发送自定义心跳消息
                    String pingMsg = "{\"type\":\"ping\",\"timestamp\":" + System.currentTimeMillis() + "}";
                    session.getAsyncRemote().sendText(pingMsg);
                    System.out.println("发送心跳请求:" + pingMsg);
                } catch (Exception e) {
                    System.err.println("发送心跳失败:" + e.getMessage());
                }
            }
        }, 0, heartbeatInterval, TimeUnit.SECONDS);
    }

    /**
     * 停止心跳
     */
    private void stopHeartbeat() {
        if (!heartbeatExecutor.isShutdown()) {
            heartbeatExecutor.shutdown();
        }
    }

    /**
     * 连接服务器
     */
    public void connect(String url) throws Exception {
        WebSocketContainer container = ContainerProvider.getWebSocketContainer();
        container.connectToServer(this, URI.create(url));
    }

    /**
     * 发送消息
     */
    public void sendMessage(String message) {
        if (session != null && session.isOpen()) {
            session.getAsyncRemote().sendText(message);
            System.out.println("客户端发送消息:" + message);
        } else {
            System.err.println("连接未建立,无法发送消息!");
        }
    }

    /**
     * 关闭连接
     */
    public void close() throws Exception {
        stopHeartbeat();
        if (session != null && session.isOpen()) {
            session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "客户端主动关闭"));
        }
    }

    // 主方法测试
    public static void main(String[] args) throws Exception {
        EchoClient client = new EchoClient();
        // 连接服务器
        client.connect("ws://localhost:8080/websocket-demo/echo/1001");
        
        // 控制台输入消息
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入要发送的消息(输入exit退出):");
        while (true) {
            String message = scanner.nextLine();
            if ("exit".equals(message)) {
                client.close();
                scanner.close();
                break;
            }
            client.sendMessage(message);
        }
    }
}

3.4 使用Spring Boot编写WebSocket服务端

Spring Boot对JSR 356规范进行了深度封装 ,提供了spring-boot-starter-websocket起步依赖,简化了WebSocket的配置和使用,同时兼容Spring生态的所有特性(如依赖注入、AOP、注解驱动),是目前Java后端开发中最主流的WebSocket实现方式,适用于Spring Boot/Spring Cloud项目。

Spring Boot中实现WebSocket有两种方式

  1. 注解式 :基于JSR 356的@ServerEndpoint注解,与Java原生实现类似,仅需添加一个配置类开启WebSocket支持,即可直接使用,开发简洁,适合简单场景;
  2. 编程式 :基于Spring提供的WebSocketHandler接口,自定义处理器实现消息处理,支持拦截器、消息转换器等扩展,灵活性更高,适合复杂业务场景(如大屏数据推送、实时监控)。
3.4.1 方式1:注解式实现(推荐简单场景)

基于JSR 356的注解,结合Spring Boot的自动配置,仅需三步即可实现,与Java原生实现的代码几乎一致,支持Spring的依赖注入。

步骤1:引入Spring Boot WebSocket起步依赖
xml 复制代码
<!-- Spring Boot WebSocket 起步依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
步骤2:编写WebSocket服务端端点(注解式+心跳)
java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * Spring Boot 注解式 WebSocket服务端(带心跳)
 */
@ServerEndpoint("/ws/echo/{userId}")
@Component
public class SpringBootEchoServer {

    // 在线会话容器
    private static final Map<String, Session> ONLINE_SESSIONS = new ConcurrentHashMap<>();
    // 心跳线程池
    private static final ScheduledExecutorService HEARTBEAT_EXECUTOR = Executors.newScheduledThreadPool(1);

    static {
        // 服务端定时发送Ping帧
        HEARTBEAT_EXECUTOR.scheduleAtFixedRate(() -> {
            ONLINE_SESSIONS.forEach((userId, session) -> {
                if (session.isOpen()) {
                    try {
                        session.getAsyncRemote().sendPing(null);
                        System.out.printf("给客户端[%s]发送Ping帧%n", userId);
                    } catch (Exception e) {
                        System.err.printf("发送Ping帧失败:%s%n", e.getMessage());
                    }
                }
            });
        }, 10, 10, TimeUnit.SECONDS);
    }

    // Spring依赖注入(原生实现不支持)
    @Autowired
    private UserService userService;

    private Session session;
    private String userId;

    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        this.session = session;
        this.userId = userId;
        ONLINE_SESSIONS.put(userId, session);
        
        // 调用Spring Bean
        String userName = userService.getUserNameById(userId);
        System.out.printf("客户端[%s(%s)]连接成功,当前在线数:%d%n", userId, userName, ONLINE_SESSIONS.size());
        
        sendMessageToUser(userId, "欢迎你," + userName + "!当前在线人数:" + ONLINE_SESSIONS.size());
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        // 处理心跳
        if (message.contains("\"type\":\"ping\"")) {
            sendMessageToUser(userId, "{\"type\":\"pong\",\"timestamp\":" + System.currentTimeMillis() + "}");
            return;
        }
        // 业务消息处理
        System.out.printf("收到客户端[%s]消息:%s%n", userId, message);
        sendMessageToUser(userId, "服务器回显:" + message);
    }

    @OnMessage
    public void onPong(PongMessage pongMessage) {
        System.out.printf("收到客户端[%s]Pong帧响应%n", userId);
    }

    @OnClose
    public void onClose(Session session) {
        ONLINE_SESSIONS.remove(userId);
        System.out.printf("客户端[%s]连接关闭,当前在线数:%d%n", userId, ONLINE_SESSIONS.size());
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        System.err.printf("客户端[%s]异常:%s%n", userId, throwable.getMessage());
        ONLINE_SESSIONS.remove(userId);
    }

    // 精准推送
    private void sendMessageToUser(String userId, String message) {
        Session targetSession = ONLINE_SESSIONS.get(userId);
        if (targetSession != null && targetSession.isOpen()) {
            targetSession.getAsyncRemote().sendText(message);
        }
    }

    // 自定义业务服务(示例)
    @Component
    static class UserService {
        public String getUserNameById(String userId) {
            // 模拟数据库查询
            return switch (userId) {
                case "1001" -> "张三";
                case "1002" -> "李四";
                default -> "未知用户";
            };
        }
    }
}
步骤3:添加WebSocket配置类
java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * Spring Boot WebSocket配置类
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig {

    /**
     * 注册ServerEndpointExporter,扫描@ServerEndpoint注解
     * Spring Boot 2.x需手动注册,3.x自动注册
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
步骤4:Spring Boot主类
java 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class WebSocketApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebSocketApplication.class, args);
    }
}

3.4.2 方式2:编程式实现(推荐复杂场景)

基于Spring提供的**WebSocketHandler接口**,自定义消息处理器,结合WebSocketConfigurer配置类注册处理器和访问路径,支持握手拦截器消息编码器/解码器跨域配置等扩展功能,灵活性更高,适用于大屏数据推送、实时监控、即时通讯等复杂业务场景。

步骤1:自定义WebSocket处理器
java 复制代码
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 编程式WebSocket处理器
 */
public class CustomWebSocketHandler extends TextWebSocketHandler {

    // 存储在线会话
    private static final Map<String, WebSocketSession> SESSION_MAP = new ConcurrentHashMap<>();

    /**
     * 连接建立成功
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 从握手属性中获取用户ID(由拦截器传入)
        String userId = (String) session.getAttributes().get("userId");
        SESSION_MAP.put(userId, session);
        System.out.printf("客户端[%s]连接成功,会话ID:%s,当前在线数:%d%n",
                userId, session.getId(), SESSION_MAP.size());
        // 发送欢迎消息
        session.sendMessage(new TextMessage("连接成功!当前在线人数:" + SESSION_MAP.size()));
    }

    /**
     * 处理文本消息
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String userId = (String) session.getAttributes().get("userId");
        String payload = message.getPayload();
        System.out.printf("收到客户端[%s]消息:%s%n", userId, payload);

        // 心跳处理
        if (payload.contains("\"type\":\"ping\"")) {
            session.sendMessage(new TextMessage("{\"type\":\"pong\",\"timestamp\":" + System.currentTimeMillis() + "}"));
            return;
        }

        // 回显消息
        session.sendMessage(new TextMessage("服务器回显:" + payload));
        // 广播消息
        broadcastMessage("客户端[" + userId + "]:" + payload);
    }

    /**
     * 连接关闭
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String userId = (String) session.getAttributes().get("userId");
        SESSION_MAP.remove(userId);
        System.out.printf("客户端[%s]连接关闭,状态:%s,当前在线数:%d%n",
                userId, status, SESSION_MAP.size());
    }

    /**
     * 处理传输异常
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        String userId = (String) session.getAttributes().get("userId");
        System.err.printf("客户端[%s]连接异常:%s%n", userId, exception.getMessage());
        if (session.isOpen()) {
            session.close(CloseStatus.SERVER_ERROR);
        }
        SESSION_MAP.remove(userId);
    }

    /**
     * 广播消息给所有在线客户端
     */
    private void broadcastMessage(String message) {
        SESSION_MAP.forEach((userId, session) -> {
            if (session.isOpen()) {
                try {
                    session.sendMessage(new TextMessage(message));
                } catch (Exception e) {
                    System.err.printf("广播消息给客户端[%s]失败:%s%n", userId, e.getMessage());
                }
            }
        });
    }

    /**
     * 精准推送消息给指定用户
     */
    public void sendMessageToUser(String userId, String message) {
        WebSocketSession session = SESSION_MAP.get(userId);
        if (session != null && session.isOpen()) {
            try {
                session.sendMessage(new TextMessage(message));
            } catch (Exception e) {
                System.err.printf("推送消息给客户端[%s]失败:%s%n", userId, e.getMessage());
            }
        }
    }
}
步骤2:自定义握手拦截器(可选,用于权限校验/参数传递)

握手拦截器可在连接建立前校验用户身份、传递自定义参数(如用户ID),是编程式实现中处理权限的核心方式:

java 复制代码
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
 * WebSocket握手拦截器
 * 作用:1. 权限校验 2. 传递自定义参数(如用户ID)
 */
public class CustomHandshakeInterceptor implements HandshakeInterceptor {

    /**
     * 握手前执行(返回true则继续握手,false则拒绝连接)
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                   WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        // 转换为Servlet请求,获取请求参数/Header
        ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
        HttpServletRequest httpRequest = servletRequest.getServletRequest();

        // 1. 获取用户ID(可从请求参数、Token、Session中获取)
        String userId = httpRequest.getParameter("userId");
        if (userId == null || userId.isEmpty()) {
            System.err.println("握手失败:未传入用户ID");
            return false; // 拒绝连接
        }

        // 2. 权限校验(示例:简单校验用户ID格式)
        if (!userId.matches("^\\d+$")) {
            System.err.printf("握手失败:用户ID[%s]格式非法%n", userId);
            return false;
        }

        // 3. 将用户ID存入握手属性,供处理器使用
        attributes.put("userId", userId);
        System.out.printf("用户[%s]握手校验通过%n", userId);
        return true;
    }

    /**
     * 握手后执行(仅记录日志,无实际拦截作用)
     */
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
                               WebSocketHandler wsHandler, Exception exception) {
        if (exception != null) {
            System.err.println("握手完成但发生异常:" + exception.getMessage());
        } else {
            System.out.println("握手成功完成");
        }
    }
}
步骤3:配置WebSocket(注册处理器+拦截器+跨域)

通过WebSocketConfigurer配置类,注册自定义的处理器、拦截器,并配置访问路径和跨域规则:

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

/**
 * Spring Boot WebSocket编程式配置类
 */
@Configuration
@EnableWebSocket
public class CustomWebSocketConfig implements WebSocketConfigurer {

    /**
     * 注册WebSocket处理器和拦截器
     */
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry
                // 1. 注册自定义处理器,指定访问路径
                .addHandler(customWebSocketHandler(), "/ws/custom/{userId}")
                // 2. 添加自定义握手拦截器(权限校验)
                .addInterceptors(customHandshakeInterceptor())
                // 3. 添加默认拦截器(可选,用于共享HttpSession)
                .addInterceptors(new HttpSessionHandshakeInterceptor())
                // 4. 允许跨域(生产环境建议指定具体域名,如allowedOrigins("https://xxx.com"))
                .setAllowedOrigins("*")
                // 5. 支持SockJS降级(适配不支持WebSocket的老旧浏览器)
                .withSockJS();
    }

    /**
     * 注入自定义WebSocket处理器
     */
    @Bean
    public CustomWebSocketHandler customWebSocketHandler() {
        return new CustomWebSocketHandler();
    }

    /**
     * 注入自定义握手拦截器
     */
    @Bean
    public HandshakeInterceptor customHandshakeInterceptor() {
        return new CustomHandshakeInterceptor();
    }
}
步骤4:测试编程式WebSocket服务端

前端测试代码(兼容SockJS)

javascript 复制代码
// 引入SockJS(适配老旧浏览器,生产环境建议CDN引入)
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>

<script>
// 方式1:原生WebSocket连接(推荐现代浏览器)
const userId = "1001";
const ws = new WebSocket(`ws://localhost:8080/ws/custom/${userId}?userId=${userId}`);

// 方式2:SockJS降级连接(适配老旧浏览器)
// const sock = new SockJS(`http://localhost:8080/ws/custom/${userId}?userId=${userId}`);

// 连接成功
ws.onopen = function() {
    console.log("编程式WebSocket连接成功!");
    // 发送心跳
    setInterval(() => {
        ws.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
    }, 10000);
};

// 接收消息
ws.onmessage = function(event) {
    const data = JSON.parse(event.data);
    if (data.type === "pong") {
        console.log("收到心跳响应:", data);
    } else {
        console.log("收到服务器消息:", data);
    }
};

// 连接异常
ws.onerror = function(event) {
    console.error("连接异常:", event);
};

// 连接关闭
ws.onclose = function(event) {
    console.log(`连接关闭:状态码${event.code},原因${event.reason}`);
};

// 发送业务消息
function sendMessage(content) {
    if (ws.readyState === WebSocket.OPEN) {
        ws.send(content);
    } else {
        alert("连接未建立!");
    }
}
</script>

核心扩展:服务端主动推送消息(业务常用)

在Spring Boot中,可通过注入CustomWebSocketHandler实例,在任意业务逻辑中主动向客户端推送消息(如订单通知、实时数据更新):

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * 业务接口:主动推送消息给指定用户
 */
@RestController
public class PushController {

    // 注入WebSocket处理器
    @Autowired
    private CustomWebSocketHandler webSocketHandler;

    /**
     * 主动推送消息接口
     */
    @GetMapping("/push/{userId}/{message}")
    public String pushMessage(@PathVariable String userId, @PathVariable String message) {
        webSocketHandler.sendMessageToUser(userId, message);
        return "消息已推送至用户[" + userId + "]:" + message;
    }
}

四、WebSocket实战常见问题与解决方案

4.1 连接建立失败

常见原因
  1. 服务器未开启WebSocket支持(如Tomcat7及以下不支持);
  2. 跨域配置错误(未配置setAllowedOrigins或域名不匹配);
  3. 握手拦截器返回false(权限校验失败);
  4. 连接地址错误(协议/端口/路径错误,如用http代替ws)。
解决方案
  1. 确认服务器版本(Tomcat8+/Spring Boot 2.x+);
  2. 跨域配置:生产环境指定具体域名(如setAllowedOrigins("https://your-domain.com"));
  3. 调试握手拦截器,检查权限校验逻辑;
  4. 连接地址格式:ws://IP:端口/路径?参数(明文)或wss://IP:端口/路径?参数(加密)。

4.2 心跳机制失效

常见原因
  1. 心跳间隔设置过长(超过防火墙超时时间);
  2. 未处理心跳响应超时逻辑;
  3. 服务器未返回Pong帧/自定义心跳响应。
解决方案
  1. 心跳间隔建议5-10秒,超时时间3-5秒;
  2. 客户端添加心跳超时计数,连续3次超时则主动重连;
  3. 服务器确保收到Ping帧后立即返回Pong帧/自定义心跳响应。

4.3 大量连接导致服务器性能下降

常见原因
  1. 未及时清理失效连接(如网络异常断开的连接);
  2. 同步发送消息导致线程阻塞;
  3. 无连接数限制,导致服务器资源耗尽。
解决方案
  1. 通过心跳超时主动关闭失效连接;
  2. 始终使用getAsyncRemote()异步发送消息;
  3. 限制单服务器最大连接数(如Tomcat配置maxConnections);
  4. 分布式场景使用消息队列(如RabbitMQ)实现集群推送。

4.4 数据传输乱码/解析失败

常见原因
  1. 文本消息非UTF-8编码;
  2. 二进制数据未指定格式;
  3. 大消息未分片传输导致缓冲区溢出。
解决方案
  1. 强制文本消息使用UTF-8编码;
  2. 二进制数据传输前约定格式(如Protobuf);
  3. 大消息分片传输(客户端拆分、服务器拼接)。

五、总结

核心知识点回顾

  1. WebSocket核心特性:基于TCP的全双工、持久化通信协议,握手阶段依赖HTTP,数据传输无请求头开销,适用于实时通信场景;
  2. 与HTTP的核心区别:HTTP是半双工短连接,服务器被动响应;WebSocket是全双工长连接,服务器可主动推送;
  3. 心跳机制:通过Ping/Pong帧或自定义心跳消息检测连接状态,避免"假死连接",核心是"定期发送-超时重连";
  4. Java实现方式
    • 注解式(@ServerEndpoint):简单场景首选,开发快捷;
    • 编程式(WebSocketHandler):复杂场景首选,支持拦截器、跨域、降级等扩展;
  5. 实战关键:异步发送消息、及时清理失效连接、合理配置心跳间隔、做好权限校验和跨域配置。

最佳实践建议

  1. 生产环境使用wss加密连接,避免数据泄露;
  2. 客户端添加重连机制,提升用户体验;
  3. 服务器添加连接数限制和资源监控;
  4. 复杂业务场景优先选择Spring Boot编程式实现,便于扩展和维护。
相关推荐
瀚高PG实验室1 小时前
hghac8008漏洞扫描处理
linux·网络·windows·瀚高数据库
一只酸奶牛^_^1 小时前
java实现pdf添加水印
java·pdf
瘾大侠1 小时前
HTB 赛季10 - Pterodactyl - user
网络·安全·web安全·网络安全
会周易的程序员1 小时前
openplc runtime v4 安全
网络·c++·物联网·websocket·安全·https·ssl
加农炮手Jinx1 小时前
Flutter for OpenHarmony 实战:network_info_plus 网络扫描与隐私合规深度适配
网络·flutter·华为·harmonyos·鸿蒙
不绝1912 小时前
延迟函数/协同程序
java·开发语言
摇滚侠2 小时前
登录认证,验证码实现逻辑
java·intellij-idea
老毛肚2 小时前
java juc 01 进程与线程
java·开发语言