5.Websocket实现消息推送
1.目的
项目需要一个在线协同办公功能来进行消息实时推送,我采用SpringBoot结合Websocket来实现该功能。WebSocket 是一种在单个TCP连接上进行全双工通信的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据,可以在客户端和服务端之间建立持久的连接,实现实时的双向通信。
相对于传统的HTTP请求,WebSocket具有以下优势:
- 实时性:Websocket提供实时的双向通信能力,服务器可以主动推送消息给客户端,而不需要客户端主动发送请求。这使得Websocket适用于需要及时更新的实时场景。
- 低延迟:Websocket通过建立长连接,可以减少每个消息的传输开销,从而降低通信的延迟。
- 较少的带宽占用:相比于HTTP请求,Websocket使用更少的带宽,因为Websocket在建立连接后只需要较小的额外开销
- 跨域支持:ebsocket可以轻松支持跨域通信,因为它不受浏览器同源策略的限制
WebSocket使用场景:
- 实时聊天应用:Websocket能够提供实时的双向通信,使得实时聊天系统能够实时更新消息,并且可以实现在线用户状态的实时更新。
- 实时协作编辑:Websocket使得多个用户能够实时协作编辑同一个文档,每个用户的修改可以广播给其他用户(腾讯文档),实现实时的协同编辑功能。
- 实时推送服务:Websocket可以与服务器建立持久化连接,服务器可以主动推送实时的更新给客户端,例如实时股票行情推送、实时新闻推送等
消息推送其实还可以通过消息队列来完成,后续将通过消息队列完成该功能。
2.使用
导入依赖<!--websocket实现消息推送--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> <version>2.3.7.RELEASE</version> </dependency>
代码实现:
@Configuration public class WebSocketConfig { //通过@Bean注解将实例注入到Spring容器中生成Bean对象 @Bean public ServerEndpointExporter serverEndpointExporter(){ //注册该Bean对象到Spring容器后,将会自动注册所有@ServerEndPoint注解声明的websocket endpoint(端点) return new ServerEndpointExporter(); } }
@Data public class Message { //发送者 private String from; //接收者 private String to; //消息 private String text; //工单编号 private String woCode; //发送时间:规定为这种格式 @JSONField(format = "yyyy-MM-dd HH:mm:ss") private Date date; }
@ServerEndpoint("/webSocket/{username}") @Component public class WebSocketServer { //静态变量,记录当前在线连接数,将其设置为线程安全 private static AtomicInteger onlineNum = new AtomicInteger(); //concurrent包的线程安全Set,存放每个客户端对应的WebSocketServer对象 private static ConcurrentHashMap<String, Session>sessionPools = new ConcurrentHashMap<>(); private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class); //发送消息 public void sendMessage(Session session, String message) throws IOException { if(session != null){ synchronized (session){ System.out.println("发送数据:"+message); session.getBasicRemote().sendText(message); } } } //给指定权限用户发送消息 public void sendInfo(String username, String message){ Session session = sessionPools.get(username); try{ sendMessage(session, message); }catch(Exception e){ e.printStackTrace(); } } //给具有调度员群发消息 public void broadcast(String message){ for (Session session : sessionPools.values()) { try{ sendMessage(session, message); }catch(Exception e){ e.printStackTrace(); continue; } } } //建立连接成功调用 @OnOpen public void onOpen(Session session, @PathParam(value = "username") String username){ sessionPools.put(username, session); addOnlineCount(); System.out.println(username+"加入WebSocket!当前人数为:"+ onlineNum); //广播上线信息 Message msg = new Message(); msg.setDate(new Date()); msg.setTo("0"); msg.setText(username); broadcast(JSON.toJSONString(msg, true)); } //关闭连接时调用 @OnClose public void onClose(@PathParam(value = "username") String username){ sessionPools.remove(username); subOnlineCount(); System.out.println(username + "断开webSocket连接!当前人数为" + onlineNum); // 广播下线消息 Message msg = new Message(); msg.setDate(new Date()); msg.setTo("-2");//下线时设置为-2 msg.setText(username); broadcast(JSON.toJSONString(msg,true)); } //收到客户端信息后,根据接收人的username把消息推下去或者群发 // to=-1群发消息 @OnMessage public void onMessage(String message) throws IOException{ System.out.println("server get" + message); Message msg= JSON.parseObject(message, Message.class); msg.setDate(new Date()); UserInfoMapper userInfoMapper = SpringBeanUtil.getBean(UserInfoMapper.class); UserInfo from = userInfoMapper.getUserByUserId(msg.getFrom()); UserInfo to = userInfoMapper.getUserByUserId(msg.getTo()); WorkOrderMapper workOrderMapper = SpringBeanUtil.getBean(WorkOrderMapper.class); if (msg.getTo().equals("-1")) {//当to为-1时就群发 broadcast(JSON.toJSONString(msg,true)); } else {//指定发送 if (from.getRoleId().equals(Integer.valueOf(4))){//发布工单给指挥员 //获取工单ID,然后查询工单,将工单转换为JSONString的格式 WorkOrder workOrder = workOrderMapper.getDetailsByCode(Integer.valueOf(msg.getWoCode())); String text = JSON.toJSONString(workOrder); msg.setText(text); sendInfo(msg.getTo(), JSON.toJSONString(msg,true)); //记录工单发布的人和时间 DispatchTask task = new DispatchTask(); task.setDispatcher(from.getUserId()); task.setWoCode(Integer.valueOf(msg.getWoCode())); task.setStatus(1); task.setCreateUser(from.getUserId()); task.setCreateTime(new Date()); IDispatchTaskService dispatchTaskService = SpringBeanUtil.getBean(IDispatchTaskService.class); boolean save = dispatchTaskService.save(task); if (save == false){ log.error("插入发布工单任务失败"); } }else if (from.getRoleId().equals(Integer.valueOf(5))){//审批工单 sendInfo(msg.getTo(), JSON.toJSONString(msg,true)); //记录工单审批的人和时间 DirectTask directTask = new DirectTask(); directTask.setDirector(from.getUserId()); directTask.setWoCode(Integer.valueOf(msg.getWoCode())); if (!msg.getText().equals("批准")){ directTask.setStatus(0); } directTask.setStatus(1);//批准 directTask.setCreateUser(from.getUserId()); directTask.setCreateTime(new Date()); IDirectTaskService directTaskService = SpringBeanUtil.getBean(IDirectTaskService.class); boolean save = directTaskService.save(directTask); if (save == false){ log.error("插入审批工单任务失败"); } }else{ onClose(from.getUserId()); } } } //错误时调用 @OnError public void onError(Session session, Throwable throwable){ System.out.println("发生错误"); throwable.printStackTrace(); } public static void addOnlineCount(){ onlineNum.incrementAndGet(); } public static void subOnlineCount(){ onlineNum.decrementAndGet(); } public static AtomicInteger getOnlineNumber(){ return onlineNum; } public static ConcurrentHashMap<String, Session>getSessionPools(){ return sessionPools; } }
注意事项:
- 定义为WebSocket的服务端点类需要通过@ServerPoint注解标识,并且需要配合@Component注解注入到Spring容器中生成Bean实例;
- 为将标有@ServerEndPoint的WebSocket服务注册到WebSocket服务中,需要通过@Bean注解标注一个返回值为ServerEndpointExporter的方法,这样就能解决WebSocket服务器注入问题。
WebSocket存在的问题:
- 较高的带宽消耗:虽然相对于HTTP,WebSocket拥有较小的宽带消耗,但是WebSocket在建立连接后会一直保持开启状态,导致持续的数据传输,还是可能会占用更多的带宽资源。
- 连接状态管理:由于WebSocket连接的持久性,需要在服务器端管理大量的连接状态,这可能对服务器产生一定的负担。
- 网络代理限制:某些网络环境或代理服务器可能会阻止或限制WebSocket连接,从而导致无法正常建立连接或通信受限。
- 旧版本兼容性:相对于传统的HTTP协议,WebSocket是一个较新的技术,因此在某些旧版本的浏览器和服务器可能不被完全支持。
- 安全性问题:WebSocket需要实时的双向通信,可能会引入潜在的安全风险,如跨站脚本攻击(XSS)或服务器资源过度利用等。
运行结果:
工单发布
工单审批
3.原理
HTTP与WebSocket的区别:
下面贴出参考链接的一张HTTP与WebSocket对比图:
- WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息,而HTTP是单向的;
- WebSocket是需要浏览器和服务器握手进行建立连接的,而http是浏览器发起向服务器的连接。
注意:虽然HTTP/2也具备服务器推送功能,但HTTP/2 只能推送静态资源,无法推送指定的信息。
实现原理:
与http协议一样,WebSocket协议也需要通过已建立的TCP连接来传输数据。具体实现上是通过http协议建立通道,然后在此基础上用真正的WebSocket协议进行通信,所以WebSocket协议和http协议是有一定的交叉关系。
1.WebSocket基于HTTP完成了一部分握手
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com
相对于HTTP协议握手请求,WebSocket多了Upgrade: websocket、Connection: Upgrade。这些就是 WebSocket 的核心了,告诉 Apache 、 Nginx 等服务器:我发起的请求要用 WebSocket 协议。
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
- Sec-WebSocket-Key 是一个 Base64 encode 的值,是浏览器随机生成的,用于验证WebSocket。
- Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同 URL 下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~
- Sec-WebSocket-Version 是告诉服务器所使用的 WebSocket Draft (协议版本);
然后服务器会返回下列东西,表示已经接受到请求, 成功建立 WebSocket 啦!
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
- Upgrade: websocket、Connection: Upgrade告诉客户端使用的WebSocket协议;
- Sec-WebSocket-Accept 这个是经过服务器确认,并且加密过后的 Sec-WebSocket-Key;
- Sec-WebSocket-Protocol 则是表示最终使用的协议;
WebSocket连接的过程:
- 首先,客户端发起http请求,经过3次握手后,建立起TCP连接;http请求里存放WebSocket支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version等;
- 然后,服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据;
- 最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。