WebSocket的使用Demo
新手博客人上路,如果星球里的md格式不好的话,可以来我的博客看一看
WebSocket的使用Demo | Adagio Blog (gitee.io)
WebSocket的使用Demo
起因
周日一个朋友问我,当用户扫描二维码进行核验之后,如何自动刷新前端页面呢,我说前端可以发请求等待回调啊,后来想想好像不对,扫码这个操作,是用户发起的,并不是前端发起的,那前端肯定是不能监听到核验这个操作的结果的,在这个过程里,似乎前端才是服务端,后端是客户端,后端确定核验之后向前端发请求,这似乎有悖于我之前的理解
后来上网查了一下,HTML5推出了WebSocket技术,最主要的功能是可以让浏览器有着双向通信的能力,这不就很符合“前端是服务端”的需求嘛,于是我就去学习了一下WebSocket的用法,并仿写了一个小Demo
实现
具体逻辑
前端与后端建立长连接,后端如果发生了订单状态变化,就向前端发起请求,前端进行页面刷新
ps:并没有实际订单和其他更严谨的逻辑,主打一个模拟
后端的实现
依赖
用的就是常见的SpringBoot框架和WebSocket的Starter
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>
<!--WebSocket依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>
目录结构

具体实现
- WebSocket需要一个配置文件,需要配置一个ServerEndpointExporter的Bean,ServerEndpointExporter的作用是自动扫描@ServerEndpoint所标记类,把该类注册成一个WebSocket连接类
/**
* WebSocket配置类
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
- 具体的WebSocket服务端
/**
* WebSocket服务端,用于和前端连接 ServerEndpoint注解表示这个类是一个WebSocket连接类,括号里面的值表示客户端用于访问当前服务端的地址
*/
@ServerEndpoint("/WebSocket/{userId}")
@Slf4j
@Component
public class WebSocket {
//记录连接的客户端
private static final Map<String, Session> clients = new ConcurrentHashMap<String, Session>();
//userId关联sid,解决一个userId连接多个服务端的问题
private static final Map<String, Set<String>> connections = new ConcurrentHashMap<String, Set<String>>();
//连接id
private String sid = null;
private String userId = null;
private static AtomicLong initialCount = new AtomicLong(0);
//判断是否连接
public static boolean judgeConnect() {
if (CollectionUtils.isEmpty(clients.values())) {
log.info("未连接");
return false;
} else {
log.info("已连接");
return true;
}
}
//给所有客户端发送消息
public static void sendMessage(Message message) {
String messageStr = JSONObject.toJSONString(message);
for (Session value : clients.values()) {
try {
value.getBasicRemote().sendText(messageStr);
} catch (Exception e) {
log.error("发送消息错误", e);
}
}
}
//给指定用户发送消息
public static void sendMessageByUserId(Message message, String userId) {
if (!StringUtils.hasText(userId)) {
return;
}
Set<String> clientSet = connections.get(userId);
if (CollectionUtils.isEmpty(clientSet)) {
return;
}
String messageStr = JSONObject.toJSONString(message);
for (String sid : clientSet) {
Session session = clients.get(sid);
Optional.ofNullable(session).ifPresent(one -> {
try {
one.getBasicRemote().sendText(messageStr);
} catch (Exception e) {
log.error("发送消息错误", e);
}
});
}
}
//连接成功
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.sid = UUID.randomUUID().toString();
clients.put(this.sid, session);
this.userId = userId;
Set<String> clientSet = connections.get(userId);
if (CollectionUtils.isEmpty(clientSet)) {
clientSet = new HashSet<String>();
connections.put(userId, clientSet);
}
clientSet.add(this.sid);
log.info(this.sid + "已开启连接");
}
//连接关闭
@OnClose
public void onClose() {
clients.remove(this.sid);
log.info(this.sid + "已断开连接");
}
//前端接受到消息的回调
@OnMessage
public void onMessage(String message) {
log.info("前端已收到消息,返回消息为:" + message);
if ("消息已确认收到".equals(message)) {
initialCount.incrementAndGet();
}
}
//前端发生错误的回调
@OnError
public void onError(Throwable error) {
log.error("发生了错误", error);
}
}
- Message类和他的子类OrderMessage,这里之所以抽象出一个Message父类,是为了保证扩展性,在WebSocket的SendMessage方法里可以接收任意的Message子类,达到发布任意的消息的功能
@Data
public abstract class Message {
private String title;
}
/**
* 模拟支付信息
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class OrderMessage extends Message{
private String status;
private BigDecimal cost;
}
- OrderController用来模拟支付,在前后端连接建立之后,给订单设定“已支付”状态,然后通过WebSocket把消息发送给前端,前端接收后再具体执行剩下的操作
@RestController
@RequestMapping("/order")
public class OrderController {
@GetMapping("/payOrder/{orderId}")
public String payOrder(@PathVariable("orderId")String orderId){
OrderMessage orderMessage = new OrderMessage();
//模拟支付成功
orderMessage.setTitle("订单id为"+orderId+"的订单已经被支付");
orderMessage.setCost(BigDecimal.ONE);
orderMessage.setStatus("已支付");
//发送消息
WebSocket.sendMessage(orderMessage);
return "success";
}
}
前端的实现
前端的实现就通过一个简单的html代码模拟
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SseEmitter</title>
</head>
<body>
<div id="message"></div>
</body>
<script>
var limitConnect = 0;
// 如果订单状态为未支付,就建立连接,如果订单超时或者已经支付,就不建立连接,现在默认订单未支付
// 初始化,建立连接
init();
function init() {
// 8080未默认端口,可自行替换,这里的路径和后端的@ServerEndpoint的路径需要对应上
var ws = new WebSocket('ws://localhost:8080/WebSocket/1');
// 获取连接状态
console.log('WebSocket连接状态:' + ws.readyState);
//监听是否连接成功
ws.onopen = function () {
console.log('WebSocket连接状态:' + ws.readyState);
limitConnect = 0;
//连接成功则发送一个数据
ws.send('我们建立连接啦');
}
// 接听服务器发回的信息并处理展示
ws.onmessage = function (data) {
console.log('接收到来自服务器的消息:');
console.log(data);
//发起消息回调,告诉后端,前端已经收到消息
ws.send("前端已接收到消息")
//收到消息之后,代表订单已被支付,可以刷新页面,或者跳转到支付成功的页面等其他操作
}
// 监听连接关闭事件
ws.onclose = function () {
// 监听整个过程中websocket的状态
console.log('WebSocket连接状态:' + ws.readyState);
reconnect();
}
// 监听并处理error事件
ws.onerror = function (error) {
console.log(error);
}
}
function reconnect() {
limitConnect++;
console.log("重连第" + limitConnect + "次");
setTimeout(function () {
init();
}, 2000);
}
</script>
</html>
实测
- 启动后端项目,然后打开上述的html文件,我们可以在控制台看到

前端的控制台也可以看到

有两个连接状态打印对应着前端的代码,未连接前会打印一次,连接之后又会打印一次,并且向后端发送消 息,说明前后端已经连接

- 去调用OrderController里面的payOrder方法模拟一次支付,可见请求成功

- 查看前端控制台,发现后端给前端发送的消息已经打印在控制台了

然后看后端控制台,发现后端也接收到前端确认消息的信息了

- 这样就说明前后端的连接已经建立成功,并且能实时进行通讯了
总结
WebSocket的使用总结起来就一个词,方便,我可以在上述demo的基础上实现多种场景的开发,比如一开始提到的核验之后进行页面刷新,订单超时之后跳转页面等,然后后端其实还有更加强大的网络编程框架Netty,虽然我没用过,但是应该也有类似的功能来实现这种前后端长连接的建立
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论



