5.Websocket实现消息推送

1.目的

  项目需要一个在线协同办公功能来进行消息实时推送,我采用SpringBoot结合Websocket来实现该功能。WebSocket 是一种在单个TCP连接上进行全双工通信的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据,可以在客户端和服务端之间建立持久的连接,实现实时的双向通信。

相对于传统的HTTP请求,WebSocket具有以下优势:

  • 实时性:Websocket提供实时的双向通信能力服务器可以主动推送消息给客户端,而不需要客户端主动发送请求。这使得Websocket适用于需要及时更新的实时场景
  • 低延迟:Websocket通过建立长连接,可以减少每个消息的传输开销,从而降低通信的延迟
  • 较少的带宽占用:相比于HTTP请求,Websocket使用更少的带宽,因为Websocket在建立连接后只需要较小的额外开销
  • 跨域支持:ebsocket可以轻松支持跨域通信,因为它不受浏览器同源策略的限制

WebSocket使用场景:

  1. 实时聊天应用:Websocket能够提供实时的双向通信,使得实时聊天系统能够实时更新消息,并且可以实现在线用户状态的实时更新。
  2. 实时协作编辑:Websocket使得多个用户能够实时协作编辑同一个文档每个用户的修改可以广播给其他用户(腾讯文档),实现实时的协同编辑功能。
  3. 实时推送服务: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();
    }
}
WebSocket配置类
@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;
    }
}
服务端点类

注意事项

  1. 定义为WebSocket的服务端点类需要通过@ServerPoint注解标识,并且需要配合@Component注解注入到Spring容器中生成Bean实例
  2. 为将标有@ServerEndPoint的WebSocket服务注册到WebSocket服务中,需要通过@Bean注解标注一个返回值为ServerEndpointExporter的方法,这样就能解决WebSocket服务器注入问题。

WebSocket存在的问题

  1. 较高的带宽消耗:虽然相对于HTTP,WebSocket拥有较小的宽带消耗,但是WebSocket在建立连接后会一直保持开启状态,导致持续的数据传输,还是可能会占用更多的带宽资源
  2. 连接状态管理由于WebSocket连接的持久性,需要在服务器端管理大量的连接状态,这可能对服务器产生一定的负担。
  3. 网络代理限制:某些网络环境或代理服务器可能会阻止或限制WebSocket连接,从而导致无法正常建立连接或通信受限。
  4. 旧版本兼容性:相对于传统的HTTP协议,WebSocket是一个较新的技术,因此在某些旧版本的浏览器和服务器可能不被完全支持
  5. 安全性问题:WebSocket需要实时的双向通信可能会引入潜在的安全风险,如跨站脚本攻击(XSS)服务器资源过度利用等。

运行结果

工单发布

工单审批

3.原理

HTTP与WebSocket的区别

  下面贴出参考链接的一张HTTP与WebSocket对比图:

  1. WebSocket是双向通信协议,模拟Socket协议可以双向发送或接受信息,而HTTP是单向的
  2. 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连接的过程:

  1. 首先,客户端发起http请求,经过3次握手后,建立起TCP连接http请求里存放WebSocket支持的版本号等信息,如:UpgradeConnectionWebSocket-Version等;
  2. 然后,服务器收到客户端的握手请求后同样采用HTTP协议回馈数据
  3. 最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信

参考链接

一文吃透 WebSocket 原理 刚面试完,趁热赶紧整理 - 掘金 (juejin.cn)

posted @ 2023-12-05 21:57  求知律己  阅读(114)  评论(0编辑  收藏  举报