
作者:逆境不可逃
技术永无止境
希望我的内容可以帮助到你!!!!
大家吼 ! 我是 逆境不可逃 今天给大家带来文章
《【WebSocket 01】 入门原理剖析,手写群发消息、私聊会话功能 》
本文章属于栏目 WebSocket
A. 第一部分 入门+群发
一、先搞懂:为什么要有 WebSocket?
1. 传统两种实时方案缺点
1)短轮询 setInterval 前端每隔 1~3s 发一次 HTTP 请求查数据,没人发消息也不停请求:
- 浪费 http 握手、头部带宽、服务器 IO;消息延迟最高 3s 适用:低频非实时(普通列表刷新),不适合聊天、实时推送
2)长轮询 前端发 HTTP,服务端无数据时 hold 住连接,有数据立即返回,客户端收到立刻再发起新请求:
- 每次消息都要新建 HTTP 连接,频繁握手,高并发性能差
2. WebSocket 核心优势
- 一次 HTTP 握手 → 升级为 TCP 长连接,全双工:客户端、服务端随时主动发消息
- 开销极小:ws 消息头远小于 HTTP 头
- 协议:
ws://ip:端口/路径(明文)、wss://(SSL 加密,生产)
3. 关键:HTTP1.1 协议升级握手(必背请求头)
客户端先发普通 HTTP 请求,携带三个关键 Header 申请升级协议:
GET /ws HTTP/1.1
Host: localhost:8080
Upgrade: websocket // 申请升级到ws协议
Connection: Upgrade // 标识连接需要升级
Sec-WebSocket-Key: xxx // 浏览器随机密钥,服务端加密校验
Sec-WebSocket-Version: 13
服务端返回101 Switching Protocols,握手成功,TCP 通道改成 WebSocket 长连接。
101 状态码 = 协议切换成功,此后不再遵循 HTTP,走 WS 自定义报文。
二、第一步:搭建 SpringBoot 原生 WS 环境(JSR356 @ServerEndpoint)
提醒:可以把代码直接复制给cursor它会帮你配置
1. pom.xml 依赖
<!-- SpringBoot Web基础 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- WebSocket JSR356依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. 开启 WebSocket 注册配置(必须配置,否则注解不生效)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
// 自动扫描@ServerEndpoint注解的类,注册ws端点
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
3. WebSocket 服务端点核心代码(核心)
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
// ws访问地址:ws://localhost:8080/ws/demo
@ServerEndpoint("/ws/demo")
@Component
public class DemoWebSocket {
// 存储所有在线客户端session(线程安全集合)
private static final CopyOnWriteArraySet<Session> SESSION_SET = new CopyOnWriteArraySet<>();
private Session session;
// 1. 客户端连接成功回调
@OnOpen
public void onOpen(Session session){
this.session = session;
SESSION_SET.add(session);
System.out.println("新客户端接入,当前在线:"+SESSION_SET.size());
}
// 2. 收到客户端发来的消息
@OnMessage
public void onMessage(String msg, Session session) throws IOException {
System.out.println("收到客户端消息:"+msg);
// 群发:把消息推送给所有在线用户
for(Session s : SESSION_SET){
s.getBasicRemote().sendText("服务端已收到:" + msg);
}
}
// 3. 连接关闭
@OnClose
public void onClose(){
SESSION_SET.remove(this.session);
System.out.println("客户端断开,剩余在线:"+SESSION_SET.size());
}
// 4. 连接异常
@OnError
public void onError(Session session, Throwable error){
error.printStackTrace();
}
}
4. 启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WsApplication {
public static void main(String[] args) {
SpringApplication.run(WsApplication.class,args);
}
}
启动项目,默认端口 8080
三、第二步:前端原生 JS WebSocket 页面(新建 ws.html,直接浏览器打开)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>原生WebSocket测试</title>
</head>
<body>
<div>
<h3>消息接收区</h3>
<div id="msgBox" style="border:1px solid #ccc;height:300px;overflow-y:auto;padding:10px;"></div>
<br>
<input type="text" id="inputMsg" placeholder="输入消息">
<button onclick="sendMsg()">发送</button>
</div>
<script>
// 1. 创建ws实例,连接后端地址
let ws = new WebSocket("ws://localhost:8080/ws/demo");
let msgBox = document.getElementById("msgBox");
let inputMsg = document.getElementById("inputMsg");
// 连接成功回调 onopen
ws.onopen = function(){
appendMsg("✅ 与服务端连接成功");
}
// 收到服务端消息 onmessage
ws.onmessage = function(e){
appendMsg("📩 服务端:"+e.data);
}
// 连接关闭 onclose
ws.onclose = function(){
appendMsg("❌ 连接已关闭");
}
// 连接报错 onerror
ws.onerror = function(err){
appendMsg("⚠️ 连接异常:"+err);
}
// 发送消息方法
function sendMsg(){
let val = inputMsg.value.trim();
if(!val) return;
ws.send(val);
appendMsg("👤 我:"+val);
inputMsg.value = "";
}
// 页面追加消息
function appendMsg(text){
msgBox.innerHTML += text + "<br>";
// 滚动到最底部
msgBox.scrollTop = msgBox.scrollHeight;
}
</script>
</body>
</html>
四、运行测试步骤
- 启动 SpringBoot 项目,控制台无报错
- 打开多个浏览器标签 / 多个浏览器,同时打开 ws.html
- 在输入框发消息:
- A 页面发消息 → 所有打开页面都收到服务端回传消息(群发效果)
- 关闭其中一个页面,后端控制台打印在线人数减少
打开一个标签页,服务端显示 新客户端接入 当前在线:1

又打开一个标签页,服务端显示 新客户端接入 当前在线:2

第一个标签页 发送你好 服务端接受并广播到两个标签页


关闭标签页

B. 第二部分 增加私聊
一、前端 WebSocket 四大状态 readyState
ws.readyState 数字标识当前连接状态:
| 值 | 常量名 | 含义 |
|---|---|---|
| 0 | CONNECTING | 正在建立连接(new WebSocket 之后、onopen 之前) |
| 1 | OPEN | 连接成功,可以正常 send () |
| 2 | CLOSING | 正在关闭(调用 close () 后) |
| 3 | CLOSED | 连接已彻底关闭,无法发消息 |
关键坑:连接关闭后不能 send ()
如果在状态≠1 时调用ws.send()会直接报错,发消息前要做状态判断。 改造前端发送函数:
function sendMsg(){
let val = inputMsg.value.trim();
if(!val) return;
// 校验连接是否可用
if(ws.readyState !== 1){
appendMsg("⚠️ 当前未连接,发送失败");
return;
}
ws.send(val);
appendMsg("👤 我:"+val);
inputMsg.value = "";
}
手动关闭连接:ws.close (code, reason)
-
ws.close():正常关闭 -
可选参数:
close(1000,"用户主动退出")前端新增一个关闭按钮:function closeConn(){
if(ws.readyState === 1){
ws.close(1000,"用户手动关闭");
}
}
关闭码 1000:正常退出;非 1000 代表异常断开。
二、后端改造:区分群发 / 单点私聊
需求:
- 普通文本 = 全员群发
- 约定格式:
@用户编号:消息内容= 点对点发给指定客户端
问题:原先CopyOnWriteArraySet<Session>只存连接,分不清哪个 session 对应哪个用户,改用 Map 存储:key:用户ID,value:Session
修改后端 DemoWebSocket.java
@ServerEndpoint("/ws/demo")
@Component
public class DemoWebSocket {
// 用户id -> 连接会话
private static final Map<String,Session> USER_MAP = new ConcurrentHashMap<>();
private Session session;
// 给当前连接随机分配一个简易用户ID(实际业务从url参数/token拿)
private String uid;
@OnOpen
public void onOpen(Session session){
this.session = session;
// 简易生成用户id,正式环境从请求参数获取:ws://localhost:8080/ws/demo?uid=1001
uid = String.valueOf(System.currentTimeMillis() % 10000);
USER_MAP.put(uid,session);
System.out.println("用户【"+uid+"】上线,在线总数:"+USER_MAP.size());
try {
// 上线通知自己的用户编号
session.getBasicRemote().sendText("✅ 你的用户ID:"+uid);
} catch (IOException e) {e.printStackTrace();}
}
@OnMessage
public void onMessage(String msg, Session session) throws IOException {
System.out.println("【"+uid+"】发来:"+msg);
// 私聊规则:@目标uid:内容
if(msg.startsWith("@")){
// @123:你好
String[] arr = msg.split(":",2);
String targetUid = arr[0].replace("@","");
String content = arr[1];
Session targetSession = USER_MAP.get(targetUid);
if(targetSession != null && targetSession.isOpen()){
targetSession.getBasicRemote().sendText("【私聊来自"+uid+"】:"+content);
}else{
// 对方不在线,回复发送人
session.getBasicRemote().sendText("用户"+targetUid+"不在线");
}
}else{
// 全员群发
for(Session s : USER_MAP.values()){
if(s.isOpen()){
s.getBasicRemote().sendText("【"+uid+"群发】:"+msg);
}
}
}
}
@OnClose
public void onClose(){
USER_MAP.remove(uid);
System.out.println("用户【"+uid+"】下线,剩余在线:"+USER_MAP.size());
}
@OnError
public void onError(Session session, Throwable error){
USER_MAP.remove(uid);
error.printStackTrace();
}
}
三、前端页面不用改动,测试规则
- 新开页面,每个页面弹窗收到自己用户 ID(4 位数字)
- 直接发文字:所有页面都收到群发消息
- 输入
@xxxx:晚上吃饭吗(xxxx 替换成另一个页面的用户 ID),只有目标用户收到私聊 - 由于后端写的比较简陋。如果不按照规则,则会异常,这个自行处理
示例: A 页面 ID:4281,B 页面 ID:1803 A 发送:@1803:内容 → 只有 B 收到私聊消息。


四、拓展:URL 参数携带 uid(正式业务用法)
上面是后端随机生成,真实项目前端传参:
// 前端连接时带上用户id
let myUid = prompt("输入你的用户ID");
let ws = new WebSocket(`ws://localhost:8080/ws/demo?uid=${myUid}`);
后端在 onOpen 里获取 url 参数:
@OnOpen
public void onOpen(Session session){
// 获取url参数
String query = session.getQueryString();
// 解析uid=xxx,这里先记住用法,下节课详细解析握手参数、token鉴权
}
总结
本文介绍了WebSocket技术的基础知识和实践应用,主要包括以下内容:
WebSocket的优势
- 相比传统短轮询和长轮询方案,WebSocket通过一次HTTP握手建立TCP长连接,实现全双工通信
- 具有开销小、实时性强等特点,适合聊天、实时推送等场景
SpringBoot实现WebSocket服务端
- 通过@ServerEndpoint注解创建端点
- 使用ServerEndpointExporter注册端点
- 实现onOpen、onMessage等回调方法处理连接和消息
前端WebSocket实现
- 使用原生WebSocket API创建连接
- 实现消息收发和状态管理
- 增加连接状态检查和错误处理
功能扩展
- 从简单群发到增加私聊功能
- 使用Map存储用户会话实现点对点通信
- 介绍了URL参数传递用户ID的方法
文章提供了完整的代码示例,包括服务端和前端实现,帮助开发者快速掌握WebSocket的基础应用。
