背景
之前在项目里用过WebSocket做过"用户下单实时通知"的功能,但最近回过头来看,竟然完全没有一点印象。仔细一想,原因也很简单:当初只是看别人写的代码,自己真正动手实践较少。结果就是,学了很久,看似懂了,真正落到代码上却无从下手。
于是决定重新审视自己,将学习的脚印发布在网上(也是犹豫了很久才决定的,因为之前总觉得发博客必须是技术大佬才行,自己什么都不懂的菜鸡发出来的东西会污染了大伙的眼睛),现在虽然也会这么想,但是更多的是挑战和记录自己了------记录学习的过程,未来回头看也有迹可循,而不是一片模糊的回忆。人生嘛,不就是不断体验吗?既然想写,就从现在开始😋
所以,就从一个简单的WebSocket Demo开始,正式开启我的博客之旅。
一、简单理解
WebSocket是一种网络协议。它实现了服务端与客户端全双工通信,使得服务端可以主动向客户端推送消息,这适用于需要实时更新的场景,比如用户下单提醒、医疗看板数据实时刷新等。
二、实现步骤
1、导入依赖
创建一个Spring Boot项目,并在pom.xml中导入相关依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、定义配置类
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
说明:关于为什么要定义配置类,我查找了网上一些资料,简单来说就是通过这个配置spring boot才能去扫描关于websocket的注解。
由于WebSocket的
@ServerEndpoint注解属于Java EE标准,并不是Spring本身的组件,Spring Boot不会自动识别。为了让Spring Boot能够扫描并注册这些WebSocket端点,我们需要声明
ServerEndpointExporter这个 Bean。它的作用就是负责把所有
@ServerEndpoint类注册到容器中,否则WebSocket服务将无法被访问。
3、开发服务端组件
java
import jakarta.websocket.*;
import org.springframework.stereotype.Component;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* WebSocket服务
*/
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {
//存放会话对象
private static Map<String, Session> sessionMap = new HashMap<>();
/**
* 连接建立成功调用的方法,并存储当前会话对象
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
@OnError
public void onError(Session session, Throwable error, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "发生错误");
error.printStackTrace();
}
/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
@ServerEndpoint:表示此类是一个WebSocket端点,value属性是必须的,用于设置路由。可以类比@RequestMapping这样就好理解多了。
@OnOpen:连接建立时触发,用于监听客户端的连接事件。可在这里保存session。
@OnMessage:收到消息时触发,用于监听客户端消息事件。类似Controller的"处理请求方法",但它处理的是WebSocket消息。
@OnClose:连接关闭时触发,用于处理连接断开事件。可在此移除session、释放资源。
@OnError:发生异常时触发,用于处理异常事件。该方法必须要有一个Throwable类型的参数,表示发生的异常。在这个方法中可以打印错误日志,清除错误会话。
4、前端页面(可选)
html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Demo</title>
</head>
<body>
<input id="text" type="text" />
<button onclick="send()">发送消息</button>
<button onclick="closeWebSocket()">关闭连接</button>
<div id="message">
</div>
</body>
<script type="text/javascript">
var websocket = null;
var clientId = Math.random().toString(36).substr(2);
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
//连接WebSocket节点
websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
}
else{
alert('Not support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};
//连接成功建立的回调方法
websocket.onopen = function(){
setMessageInnerHTML("连接成功");
}
//接收到消息的回调方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
//关闭连接
function closeWebSocket() {
websocket.close();
}
</script>
</html>
注意:这里主要注意new WebSocket("ws://localhost:8080/ws/"+clientId),这是WebSocket对象的初始化,指定要连接服务器的地址。
三、测试结果
1️⃣启动服务器,并打开websocket.html进行双端连接

2️⃣客户端向服务器发送消息 
3️⃣服务器向客户端发送消息
定义定时任务类,定时向客户端推送数据
java
import com.sky.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Component
public class WebSocketTask {
@Autowired
private WebSocketServer webSocketServer;
/**
* 通过WebSocket每隔5秒向客户端发送消息
*/
@Scheduled(cron = "0/5 * * * * ?")
public void sendMessageToClient() {
webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
}
}
注意要在启动类添加注解@EnableScheduling开启任务调度

此外还可以在其他接口中使用webSocketServer.sendToAllClient(),这样就是可以实时推送。比如在订单下单功能中调用此方法,就可以在用户下单后实时通知商家。
四、问题发现
上述的代码演示中,部分都是以往项目中使用到的。现在来看,确实存在一些问题。
问题描述
在一个商城交易系统中,假设有两个商家同时登录了浏览器,这时每个商家都会与服务器建立一个WebSocket连接,用于接收实时消息。当用户访问商家A的店铺并进行下单时,前端会调用下单接口完成订单创建。为了实现实时提醒功能,服务端希望通过WebSocket将"新订单"消息发送给商家A。然而,如果直接使用群发的方式,系统会把消息发送给所有在线商家,包括商家 B,这显然是业务上的错误。
因此,需要改成单发,即只发送给商家A。但问题来了:在用户下单的接口中,如何获取商家A的WebSocket session ID,从而精确地将消息发送给对应商家?
思考
单发需要WebSocket session ID,在下单功能中我如何获取到这个id标识?先思考在用户下单功能中我们能获得什么,能否从现有的资源中解决这个问题。我们可以得到商家的id,那么这个商家id是否可以作为商家与服务器进行WebSocket连接的session ID呢?
答案明了,在上述前端页面中,我们使用随机字符串clientId来作为session ID。那么我们可以进行业务规定,把clientId换成商户ID(merchantId),就能让WebSocket连接与业务绑定,这样在下单接口就能找到对应的WebSocket Session,然后只给那个商户推送消息。
伪代码
前端:
js
//规定如下
var merchantId = "商户登录成功后的真实ID";
websocket = new WebSocket("ws://localhost:8080/ws/" + merchantId);
后端:
java
/**
* WebSocket服务
*/
@ServerEndpoint("/ws/{merchantId}")
public class WebSocketServer {
//存放商户与服务器会话对象
private static Map<String, Session> merchantSessions = new ConcurrentHashMap<>();
/**
* 连接建立成功调用的方法,并存储当前会话对象
*/
@OnOpen
public void onOpen(Session session, @PathParam("merchantId") String merchantId) {
merchantSessions.put(merchantId, session);
}
//其他方法...
//单发
public void sendToMerchant(String merchantId, String message) {
Session session = merchantSessions.get(merchantId);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
java
/**
* 下单接口调用示例
*/
@PostMapping("/order/create")
public void createOrder(@RequestBody Order order) {
// 创建订单....
WebSocketServer.sendToMerchant(order.getMerchantId(), "您有新订单!");
}
这样在用户下单接口中就能根据商户ID找到对应的WebSocket会话,从而实现定向推送。
五、总结
整篇文章写下来,感觉又回到了当初一开始写项目的时候。磕磕碰碰,碰到困难时心烦意乱,解决问题时又上蹿下跳🥲,温故而知新,通过对WebSocket的回顾,进一步理解了如何实现用户下单实时通知功能。同时也极大的鼓舞了自信,原来认真投入写一篇博客可以带来这么多的正反馈,当然也有可能是三分钟热度哈😁。总之敢于记录自己,敢于分享自己,敢于尝试不同的体验。
最后: 本文仅作为学习记录分享使用,内容基于作者当前技术认知整理,若有不准确或描述不清之处,欢迎评论区指正,我会在能力范围内修正完善,谢谢阅读!🙏