万字详解WebSocket 用法
一、简介
1.1 什么是WebSocket
WebSocket是一种基于TCP协议 的全双工通信协议,由W3C开发并于2011年成为正式标准,用于在Web应用程序和服务器之间建立实时、双向、持久化的通信连接。
它解决了传统HTTP请求-响应模型的核心痛点:HTTP协议是单向通信,服务器只能被动响应客户端请求,无法主动向客户端推送数据;而WebSocket通过一次HTTP握手建立起持久的TCP连接后,客户端和服务器可在该连接上随时双向传输数据,无需重复建立连接,实现真正的实时通信。
WebSocket的通信链路是单TCP连接复用,握手阶段基于HTTP协议,握手完成后脱离HTTP协议,使用独立的帧格式进行数据传输,适用于在线聊天、实时监控、多人游戏、大屏数据推送等需要实时交互的场景。
1.2 WebSocket的优势和劣势
核心优势
- 实时性极强:持久化连接让数据传输无需等待客户端请求,服务器可主动、即时地向客户端推送数据,响应延迟远低于HTTP轮询/长轮询;
- 双向通信:客户端和服务器均可在任意时间向对方发送数据,打破HTTP的单向通信限制;
- 减少网络负载:仅需一次握手建立连接,后续通信无需重复发送HTTP请求头,大幅降低数据包体积和网络请求次数;
- 兼容性良好:基于TCP/IP协议,兼容主流浏览器(Chrome、Firefox、Edge等)和服务器中间件(Tomcat、Jetty、Nginx等),且可通过降级方案适配老旧浏览器;
- 支持多种数据类型:可传输文本、二进制数据(如图片、音频、视频),满足不同业务的数据传输需求。
核心劣势
- 环境支持要求:需要浏览器和服务器均支持WebSocket协议,部分老旧浏览器(如IE10以下)和低版本服务器中间件(如Tomcat7及以下)不原生支持;
- 服务器额外开销:服务器需要维护大量长时间的持久化连接,会占用更多的内存、CPU资源,对服务器的资源配置和连接管理能力要求较高;
- 安全风险:由于服务器可主动推送数据,若未做严格的身份认证、权限校验和数据过滤,可能存在非法连接、数据泄露、恶意推送等安全问题;
- 网络环境敏感:持久化连接易受网络波动(如断网、网络超时、防火墙拦截)影响,可能导致连接异常断开,需要额外处理重连、心跳检测等逻辑;
- 跨域配置复杂: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)。
协议核心特点
- 握手基于HTTP:客户端通过发送一个特殊的HTTP请求发起握手,服务器响应后完成协议升级,从HTTP协议切换为WebSocket协议,握手过程完全兼容HTTP协议,可通过常规的HTTP端口(80/443)通信,避免被防火墙拦截;
- 全双工通信:TCP连接建立后,客户端和服务器的读写通道相互独立,双方可同时发送和接收数据,无先后顺序限制;
- 独立的帧格式:数据传输采用自定义的帧格式,而非HTTP的请求/响应格式,帧结构简洁,包含消息头和消息体,大幅降低传输开销;
- 协议协商机制:握手阶段客户端和服务器会协商协议版本、支持的子协议(如自定义的业务协议)、扩展选项(如压缩、心跳)等,保证通信的兼容性;
- 状态保持:连接建立后始终保持活跃状态,直至客户端或服务器主动关闭,无需像HTTP那样每次请求都重新建立连接。
协议通信流程
- 客户端向服务器发送WebSocket握手请求(HTTP GET请求,包含特殊的请求头);
- 服务器验证握手请求头,若合法则返回WebSocket握手响应(HTTP 101状态码,表示协议升级);
- 握手成功后,HTTP连接正式升级为WebSocket的TCP持久化连接,双方进入数据传输阶段;
- 客户端和服务器通过WebSocket帧格式双向传输数据;
- 一方发起关闭请求,双方完成连接关闭流程,释放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连接建立,具体流程:
- 客户端创建WebSocket对象,向服务器发送符合规范的HTTP握手请求;
- 服务器接收请求后,验证请求头的合法性(如协议版本、密钥、跨域源等);
- 验证通过后,服务器返回HTTP 101状态码的握手响应,完成协议升级;
- 客户端验证服务器的响应头(如Sec-WebSocket-Accept),验证通过则建立TCP持久化连接,进入下一阶段;若验证失败,直接关闭连接,生命周期结束。
关键标识 :客户端触发onopen事件,服务器触发连接建立回调(如Java的@OnOpen)。
阶段2:连接开放阶段(Connection Open)
这是WebSocket的核心通信阶段 ,连接处于活跃状态,客户端和服务器可自由进行双向数据传输,具体特点:
- TCP连接保持打开状态,双方的读写通道均处于可用状态;
- 支持文本、二进制等多种数据类型的传输,数据以WebSocket帧的形式发送和接收;
- 可通过扩展协议实现数据压缩、心跳检测等功能;
- 连接在此阶段会持续保持,直至收到关闭帧或发生异常。
关键标识 :客户端可调用send()方法发送数据,收到数据时触发onmessage事件;服务器可接收客户端消息并主动推送数据,触发消息接收回调(如Java的@OnMessage)。
阶段3:连接关闭阶段(Connection Closing)
此阶段为连接关闭的准备阶段 ,由客户端或服务器主动发起,核心完成关闭帧的交互和资源准备释放,具体流程:
- 发起方发送关闭帧(包含关闭状态码和关闭原因),请求关闭连接;
- 接收方收到关闭帧后,返回确认关闭帧,表示已收到关闭请求;
- 双方开始释放与该连接相关的资源(如缓冲区、线程、会话信息等),停止发送新数据,仅处理未完成的数据包传输。
关键特点:此阶段连接并未完全关闭,仍可处理未传输完成的数据,直至双方完成关闭帧交互。
阶段4:连接关闭完成阶段(Connection Closed)
这是WebSocket生命周期的结束阶段,核心完成TCP连接的释放和资源清理,具体流程:
- 双方完成所有未完成的数据传输,确认无残留数据后,主动关闭TCP连接;
- 双方彻底释放与该连接相关的所有资源(如会话对象、连接容器、缓冲区等);
- 连接状态变为关闭,任何后续的读写操作都会失败。
关键标识 :客户端触发onclose事件,服务器触发连接关闭回调(如Java的@OnClose);若连接因异常关闭,客户端会先触发onerror事件,再触发onclose事件,服务器触发异常回调(如Java的@OnError)。
生命周期核心注意点
- 连接的关闭建议由双方协商完成,即发起方发送关闭帧后,接收方确认并返回关闭帧,避免单方面关闭导致数据丢失;
- 网络异常(如断网、超时)会导致连接被动关闭,此时不会触发正常的关闭帧交互,需要通过心跳检测机制识别连接状态;
- 每个WebSocket连接对应一个独立的TCP连接,生命周期与TCP连接绑定,TCP连接断开则WebSocket连接同步断开;
- 客户端在连接关闭后,若需要重新通信,需重新创建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帧无消息体),核心特点:
- 数据类型:由Opcode标识,文本帧为UTF-8编码的文本,二进制帧为任意二进制数据;
- 掩码加密:客户端发送的帧的消息体会通过掩码密钥进行简单的异或加密,服务器需解密后处理;服务器发送的帧的消息体不加密,客户端可直接解析;
- 分片传输:单条大消息可拆分为多个帧传输(FIN=0的继续帧+FIN=1的最后一帧),接收方会将多个帧的消息体拼接为完整的消息后再触发回调;
- 无边界限制:消息体的长度仅受服务器和客户端的缓冲区限制,协议本身无最大长度限制。
三、核心帧类型的完整格式示例
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连接看似存在,但实际已无法通信)。心跳机制的核心作用是:
- 检测连接状态:定期发送心跳帧,验证连接是否真实可用,避免"假死连接"占用服务器资源;
- 维持连接存活:部分防火墙/代理服务器会主动关闭长时间无数据传输的连接,心跳帧可触发数据交互,避免连接被强制关闭;
- 及时发现断连:若心跳响应超时,可立即触发重连逻辑,保证通信的连续性;
- 资源清理:服务器可通过心跳超时识别无效连接,主动释放资源。
2.5.2 心跳机制的实现原理
基于WebSocket的Ping/Pong帧实现(Opcode=0x09/Ping、Opcode=0x0A/Pong),核心流程:
- 发起方 (客户端/服务器)定期发送Ping帧(无消息体或携带少量标识数据);
- 接收方 收到Ping帧后,必须立即返回Pong帧(与Ping帧的消息体一致);
- 发起方若在指定超时时间内未收到Pong帧,判定连接失效,触发重连/关闭逻辑;
- 若收到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的会话、连接、配置等核心信息,核心类如下:
Session:会话对象,每个客户端连接对应一个独立的Session实例,封装了连接的所有信息,核心方法:getId():获取会话唯一ID;isOpen():判断连接是否处于开放状态;getBasicRemote():获取同步消息发送器,发送消息为阻塞式;getAsyncRemote():获取异步消息发送器,发送消息为非阻塞式(推荐生产环境使用);close():主动关闭当前会话;getUserProperties():获取会话的自定义属性容器,用于存储连接相关的业务数据(如用户ID、权限)。
CloseReason:关闭原因对象,封装了关闭状态码和关闭原因,核心方法:getCloseCode():获取关闭状态码(CloseReason.CloseCodes枚举);getReasonPhrase():获取关闭原因描述。
Remote.Async:异步消息发送器,由session.getAsyncRemote()获取,核心方法sendText(String)、sendBinary(ByteBuffer),非阻塞发送,不会阻塞当前线程。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 部署与访问说明
- 将编写好的服务端类放入Java Web项目的
src/main/java对应包下,无需额外配置; - 将项目打包为WAR包,部署到支持WebSocket的服务器(如Tomcat8+);
- 启动服务器后,客户端可通过
ws://服务器IP:端口/项目名/echo/客户端ID连接(如ws://localhost:8080/websocket-demo/echo/1001); - 客户端连接成功后,发送任意文本消息,即可收到服务器的回显消息;发送心跳消息(
{"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有两种方式:
- 注解式 :基于JSR 356的
@ServerEndpoint注解,与Java原生实现类似,仅需添加一个配置类开启WebSocket支持,即可直接使用,开发简洁,适合简单场景; - 编程式 :基于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 连接建立失败
常见原因
- 服务器未开启WebSocket支持(如Tomcat7及以下不支持);
- 跨域配置错误(未配置
setAllowedOrigins或域名不匹配); - 握手拦截器返回
false(权限校验失败); - 连接地址错误(协议/端口/路径错误,如用
http代替ws)。
解决方案
- 确认服务器版本(Tomcat8+/Spring Boot 2.x+);
- 跨域配置:生产环境指定具体域名(如
setAllowedOrigins("https://your-domain.com")); - 调试握手拦截器,检查权限校验逻辑;
- 连接地址格式:
ws://IP:端口/路径?参数(明文)或wss://IP:端口/路径?参数(加密)。
4.2 心跳机制失效
常见原因
- 心跳间隔设置过长(超过防火墙超时时间);
- 未处理心跳响应超时逻辑;
- 服务器未返回Pong帧/自定义心跳响应。
解决方案
- 心跳间隔建议5-10秒,超时时间3-5秒;
- 客户端添加心跳超时计数,连续3次超时则主动重连;
- 服务器确保收到Ping帧后立即返回Pong帧/自定义心跳响应。
4.3 大量连接导致服务器性能下降
常见原因
- 未及时清理失效连接(如网络异常断开的连接);
- 同步发送消息导致线程阻塞;
- 无连接数限制,导致服务器资源耗尽。
解决方案
- 通过心跳超时主动关闭失效连接;
- 始终使用
getAsyncRemote()异步发送消息; - 限制单服务器最大连接数(如Tomcat配置
maxConnections); - 分布式场景使用消息队列(如RabbitMQ)实现集群推送。
4.4 数据传输乱码/解析失败
常见原因
- 文本消息非UTF-8编码;
- 二进制数据未指定格式;
- 大消息未分片传输导致缓冲区溢出。
解决方案
- 强制文本消息使用UTF-8编码;
- 二进制数据传输前约定格式(如Protobuf);
- 大消息分片传输(客户端拆分、服务器拼接)。
五、总结
核心知识点回顾
- WebSocket核心特性:基于TCP的全双工、持久化通信协议,握手阶段依赖HTTP,数据传输无请求头开销,适用于实时通信场景;
- 与HTTP的核心区别:HTTP是半双工短连接,服务器被动响应;WebSocket是全双工长连接,服务器可主动推送;
- 心跳机制:通过Ping/Pong帧或自定义心跳消息检测连接状态,避免"假死连接",核心是"定期发送-超时重连";
- Java实现方式 :
- 注解式(
@ServerEndpoint):简单场景首选,开发快捷; - 编程式(
WebSocketHandler):复杂场景首选,支持拦截器、跨域、降级等扩展;
- 注解式(
- 实战关键:异步发送消息、及时清理失效连接、合理配置心跳间隔、做好权限校验和跨域配置。
最佳实践建议
- 生产环境使用
wss加密连接,避免数据泄露; - 客户端添加重连机制,提升用户体验;
- 服务器添加连接数限制和资源监控;
- 复杂业务场景优先选择Spring Boot编程式实现,便于扩展和维护。