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>


目录结构



具体实现


  1. WebSocket需要一个配置文件,需要配置一个ServerEndpointExporter的Bean,ServerEndpointExporter的作用是自动扫描@ServerEndpoint所标记类,把该类注册成一个WebSocket连接类
/**
* WebSocket配置类
*/
@Configuration
public class WebSocketConfig {
   @Bean
   public ServerEndpointExporter serverEndpointExporter(){
       return new ServerEndpointExporter();
  }
}
  1. 具体的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);
  }
}
  1. 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;
}
  1. 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>


实测


  1. 启动后端项目,然后打开上述的html文件,我们可以在控制台看到

前端的控制台也可以看到

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

  1. 去调用OrderController里面的payOrder方法模拟一次支付,可见请求成功
  2. 查看前端控制台,发现后端给前端发送的消息已经打印在控制台了

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

  1. 这样就说明前后端的连接已经建立成功,并且能实时进行通讯了


总结


WebSocket的使用总结起来就一个词,方便,我可以在上述demo的基础上实现多种场景的开发,比如一开始提到的核验之后进行页面刷新,订单超时之后跳转页面等,然后后端其实还有更加强大的网络编程框架Netty,虽然我没用过,但是应该也有类似的功能来实现这种前后端长连接的建立


0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP