1:什么是 WebSocket?
WebSocket 是一种网络通信协议,它在单个 TCP 连接上提供全双工 (Full-Duplex)的通信信道。这意味着客户端(如浏览器)和服务器可以同时、独立地发送和接收数据,实现了真正的实时、双向对话。
2:为什么需要 WebSocket?
在 WebSocket 出现之前,实现实时数据更新(如聊天、实时游戏、股票行情)主要靠以下"笨办法":
轮询(Polling):客户端每隔几秒就向服务器发送一个 HTTP 请求问:"有新数据吗?"。即使没有数据,也会频繁请求,造成巨大的网络和服务器资源浪费。
长轮询(Long-Polling):客户端发送一个请求,服务器hold住这个连接,直到有数据更新或超时才返回响应。客户端收到响应后立即再发起一个新的请求。这种方式比普通轮询好,但每次请求仍然包含冗长的 HTTP 头信息,效率不高。
WebSocket 的优势:
低延迟:连接建立后,数据可以瞬间到达,没有 HTTP 请求的延迟。
高效:一旦连接建立,后续通信的数据包包头很小(通常只有 2-10 字节),远小于 HTTP 每次请求都携带的冗长头部。
全双工通信:服务器可以在任何需要的时候主动"推送"数据给客户端,而不需要客户端先请求。
3:应用场景
WebSocket 非常适合需要高实时性的应用:
即时通讯(Chat):微信网页版、Slack、Discord。
多人在线游戏:实时同步玩家位置和状态。
实时数据推送:股票行情、体育赛事实时比分、实时监控系统。
协同工具:多人同时在线编辑文档(如 Google Docs)。
物联网(IoT):设备状态的实时控制和监控。
4:实现
1)加载依赖
XML
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
2)配置WebSocket
java
package ***.***;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WEebSocketConfig {
/**
* ServerEndpointExporter 会自动注册使用ServerEndpoint注解的websocket endpoint
*
* @param
* @return org.springframework.web.socket.server.standard.ServerEndpointExporter
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3)创建抽象基类
java
package ***.***;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.websocket.Session;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public abstract class BaseWebsocket {
static Logger logger = LoggerFactory.getLogger(BaseWebsocket.class);
protected static void _sendMessage(String msg, Set<Session> set) {
try {
Iterator<Session> it = set.iterator();
while (it.hasNext()) {
Session s = it.next();
if (s.isOpen()) {
synchronized (s) {
s.getBasicRemote().sendText(msg);
}
} else {
it.remove();
}
}
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
// 发送消息
public static void sendMessage(String msg, Set<Session> set) {
_sendMessage(msg, set);
}
// 连接建立时触发
public abstract void onOpen(Session session);
// 连接关闭时触发
public abstract void onCLose(String num, Session session);
// 接收数据时触发
public abstract void onMessage(String msg, Session session);
// 通信发生错误时触发
public abstract void onError(Throwable t);
// 获取所用连接的session信息,不区分key时用此方法
public abstract Set<Session> getSet();
// 获取所有连接的key,session信息,需要根据key区分时使用此方法
public abstract Map<String, Set<Session>> getMap();
// 指定key连接
protected void _onOpen(String num, Session session) {
Map<String, Set<Session>> map = getMap();
Set<Session> set = map.get(num);
if (null == set) {
set = new HashSet<>();
map.put(num, set);
}
set.add(session);
}
// 不指定key连接
protected void _onOpen(Session session) {
getSet().add(session);
}
// 指定key关闭
protected void _onClose(String num, Session session) {
Map<String, Set<Session>> map = getMap();
Set<Session> set = map.get(num);
if (set != null) {
set.remove(session);
}
}
// 不指定key关闭
protected void _onClose(Session session) {
getSet().remove(session);
}
// 抛出错误信息
protected void _onError(Throwable t) {
logger.error(t.getMessage());
}
}
4)创建websocket实例
java
package com.ruoyi.lxsjgl.ws;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.lxsjgl.domain.JzfzxnxtLxsjglRwsxjl;
import com.ruoyi.lxsjgl.domain.JzfzxnxtLxsjglSbxx;
import com.ruoyi.lxsjgl.domain.LxsjglConstants;
import com.ruoyi.lxsjgl.service.IJzfzxnxtLxsjglSbxxService;
import com.ruoyi.lxsjgl.service.impl.JzfzxnxtLxsjglRwsxjlServiceImpl;
import com.ruoyi.lxsjgl.service.impl.JzfzxnxtLxsjglSbxxServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* 测试 WS
* 我写了两种方法,指定连接目标/针对所有连接
* 如无需指定目标通信,将 @OnOpen @OnClose @OnMessage @OnError 与注释的交换即可
*/
@Component
@ServerEndpoint("/websocket/testws/{key}")
// @ServerEndpoint("/websocket/testws")
public class TestWS extends BaseWebsocket {
private static JzfzxnxtLxsjglRwsxjlServiceImpl rwmrczjlService;
private static IJzfzxnxtLxsjglSbxxService sbxxService;
@Resource
public void setRwmrczjlService(JzfzxnxtLxsjglRwsxjlServiceImpl rwmrczjlService) {
TestWS.rwmrczjlService = rwmrczjlService;
}
@Resource
public void setSbxxService(JzfzxnxtLxsjglSbxxServiceImpl sbxxService) {
TestWS.sbxxService = sbxxService;
}
private static final Set<Session> set = new CopyOnWriteArraySet<>();
private static final Map<String, Set<Session>> map = new ConcurrentHashMap<>();
public static Logger logger = LoggerFactory.getLogger(TestWS.class);
public static void sendMessage2All(String msg) {
_sendMessage(msg, set);
}
public static void sendMessage(String key, String msg) {
Set<Session> sessions = map.get(key);
_sendMessage(msg, sessions);
logger.info(System.currentTimeMillis() + " --> " + key + " --> " + "/websocket/testws 发送消息:" + msg);
}
// @OnOpen
public void onOpen(Session session) {
_onOpen(session);
logger.info(System.currentTimeMillis() + " --> 建立websocket连接:/websocket/testws");
}
@OnOpen
public void onOpen(@PathParam("key") String key, Session session) {
_onOpen(key, session);
logger.info(System.currentTimeMillis() + " --> " + key + " --> " + "建立websocket连接:/websocket/testws");
}
// @OnClose
public void onCLose(Session session) {
_onClose(session);
logger.info(System.currentTimeMillis() + " --> 关闭websocket连接:/websocket/testws");
}
@OnClose
public void onCLose(@PathParam("key") String key, Session session) {
_onClose(key, session);
logger.info(System.currentTimeMillis() + " --> " + key + " --> " + "关闭websocket连接:/websocket/testws");
}
// @OnMessage
public void onMessage(String msg, Session session) {
if (msg.equals("ping")) {
sendMessage2All("pong");
} else {
logger.info(System.currentTimeMillis() + " --> " + "/websocket/testws收到消息:" + msg);
}
}
@OnMessage
public void onMessage(@PathParam("key") String key, String msg, Session session) {
if (msg.equals("ping")) { // 心跳消息
sendMessage(key, "pong");
} else {
// 此处可以写收到消息后的具体实现
logger.info(System.currentTimeMillis() + " --> " + key + " --> " + "/websocket/testws 收到消息:" + msg);
}
}
@OnError
public void onError(Throwable t) {
_onError(t);
}
@Override
public Set<Session> getSet() {
return set;
}
@Override
public Map<String, Set<Session>> getMap() {
return map;
}
}
5)测试截图
