万字详解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事件 方法参数可注入Session、EndpointConfig、@PathParam(路径参数)
@OnMessage 方法 收到客户端消息时的回调方法 ,对应客户端的message事件 方法参数可注入String/ByteBuffer(消息内容)、Session、@PathParam
@OnClose 方法 连接完全关闭时的回调方法 ,对应客户端的close事件 方法参数可注入Session、CloseReason、@PathParam
@OnError 方法 连接发生异常时的回调方法 ,对应客户端的error事件 方法参数可注入Session、Throwable(异常信息)、@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编程式实现,便于扩展和维护。
相关推荐
驭渊的小故事19 分钟前
Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用
java·人工智能·spring
Elastic 中国社区官方博客43 分钟前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1881 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn1 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su2 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
g3voip2 小时前
工业防水电话到底怎么选?隧道、管廊与码头的需求差异
服务器·网络·信息与通信·ip
珠海西格电力3 小时前
零碳园区管理系统的性能评估体系的行业标准
大数据·网络·人工智能·物联网·机器学习·架构·能源
我命由我123453 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
.Bryce.3 小时前
从强化学习到Agentic RL:让Agent在“干活“中自己变聪明
服务器·网络·agent·rag·agentic rl
苜蓿muxu3 小时前
Java 字符串拼接:+、StringBuilder、StringBuffer的区别
java