WebSocket即时通讯技术+springboot

1、关于web端即时通讯

web端即时通讯技术简单的说就是实现这样一种功能:服务器端可以即时地将数据的更新或变化反应到客户端,例如消息即时推送等功能都是通过这种技术实现的。

但是在Web中,由于浏览器的限制,实现即时通讯需要借助一些方法。这种限制出现的主要原因是,一般的Web通信都是浏览器先发送请求到服务器,服务器再进行响应完成数据的现实更新。

实现即时通讯主要有四种方式,它们分别是:短轮询、长轮询(comet)、长连接(SSE)、WebSocket。

  • 它们大体可以分为两类,一种是在HTTP基础上实现的,包括短轮询、comet和SSE;另一种不是在HTTP
  • 基础上实现是,即WebSocket。下面分别介绍一下这四种轮询方式,以及它们各自的优缺点。

1、ajax短轮询

短轮询的基本思路就是浏览器每隔一段时间向服务器发送http请求,服务器端在收到请求后,不论是否有数据更新,都直接进行响应。

2、ajax comet -长轮询

comet 指的是,当服务器收到客户端发来的请求后,不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制(服务器端设置)后关闭连接。明显减少了很多不必要的http请求次数,相比之下节约了资源。长轮询的缺点在于,连接挂起也会导致资源的浪费

3、SSE

SSE是HTML5新增的功能,全称为Server-SentEvents。它可以允许服务推送数据到客户端。SSE在本质上就与之前的长轮询、短轮询不同,虽然都是基于http协议的。而SSE最大的特点就是不需要客户端发送请求,可以实现只要服务器端数据有更新,就可以马上发送到客户端。

SSE的优势很明显,它不需要建立或保持大量的客户端发往服务器端的请求,节约了很多资源,提升应用性能SSE的实现非常简单,并且不需要依赖其他插件。(不支持IE浏览器,单向通道)

4、WebSocket

HTML5 定义的 WebSocket 协议,WebSocket 是独立的、创建在 TCP 上的协议,与传统的http协议不同,该协议可以实现服务器与客户端之间全双工通信。

简单来说,首先需要在客户端和服务器端建立起一个连接,这部分需要http。连接一旦建立,客户端和服务器端就处于平等的地位,可以相互发送数据,不存在请求和响应的区别。它能更好的节省服务器资源和带宽,并且能够更实时地进行通讯




请求细节分析:

浏览器先向服务器发送个url以ws://开头的http的GET请求,服务器根据请求头中

Connection :Upgrade 我要升级

Upgrade:websocket把客户端的请求升级websocket协议。

响应头中也包含了内容Upgrade:websocket,表示升级成WebSocket协议。

响应101: 握手成功,http协议切换成websocket协议了,连接建立成功,浏览器和服务器可以随时主动发送消息给对方了。

这里 Sec-WebSocket-Accept 和 Sec-WebSocket-Key 是配套的,主要作用在于提供基础的防护,减少恶意连接、意外连接。

Sec-WebSocket-Accept 的计算方法是:

计算公式为:

将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。
 
通过 SHA1 计算出摘要,并转成 base64 字符串。

伪代码:

toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 )  )
复制代码

对比:

从兼容性角度考虑,短轮询>长轮询>长连接SSE>WebSocket

从性能方面考虑,WebSocket>长连接SSE>长轮询>短轮询

2、各浏览器对webscoket的支持情况

3、websocket应用场景

决定手头的工作是否需要使用WebSocket技术的方法很简单:

  • 你的应用提供多个用户相互交流吗?
  • 你的应用是展示服务器端经常变动的数据吗?

以下是一些典型的应用场景:

  1. 协同办公 / 编辑

我们生活在分散式办公的时代,时常需要在不同地点同时编辑同一份文档,比如 腾讯在线office文档、编程文件等。

  1. 社交 / 订阅

比如微信朋友圈的实时更新提醒、点赞或评论的红点通知,比如qq的特别关注人的动态提醒,比如聊天信息的实时同步,比如新闻客户端的订阅通知等等。

  1. 多玩家游戏

对于在线实时的多人游戏,互动效率是非常重要的,你可不想在扣动扳机之后,你的对手却已经在10秒钟之前移动了位置。

  1. 股市基金报价

金融界瞬息万变——几乎是每毫秒都在变化。过时的信息也只能导致损失,我们人类的大脑不能持续以那样的速度处理那么多的数据,需要一些算法来帮我们处理这些事情。当你有一个显示盘来跟踪你感兴趣的公司时,你肯定想要随时知道他们的价值,而不是10秒前的数据。使用WebSocket可以流式更新这些数据变化而不需要等待。

  1. 体育实况播放

在体育播报的体验中,减低时延是最重要的一点。

  1. 音视频聊天 / 视频会议 / 在线教育

用WebSockets getUserMedia API和HTML5音视频元素明显是个不错的选择。WebRTC的出现顺理成章的成为我刚才概括的组合体,它看起来很有希望,但其缺乏目前浏览器的支持。

  1. 基于位置的应用

越来越多的开发者借用移动设备的GPS功能来实现他们基于位置的网络应用。比如共享单车、共享汽车、百度天眼,地图GPS服务、疫情监控目标人的实时运动轨迹、运动员的轨迹分析。借用WebSocket TCP链接可以让数据飞起来。

4、WebSocket、SockJs、STOMP三者关系

SockJs

SockJS是一个JavaScript库,为了应对许多浏览器不支持WebSocket协议的问题,设计了备选SockJs。SockJS 是 WebSocket 技术的一种模拟。SockJS会尽可能对应 WebSocket API,会优先选择WebSocket进行连接,但是当服务器或客户端不支持WebSocket时,会自动在 XHR流、XDR流、iFrame事件源、iFrame HTML文件、XHR轮询、XDR轮询、iFrame XHR轮询、JSONP轮询 这几个方案中择优进行连接。

Stompjs

STOMP—— Simple Text Oriented Message Protocol——面向消息的简单文本协议。

SockJS 为 WebSocket 提供了 备选方案。但无论哪种场景,对于实际应用来说,这种通信形式层级过低。 STOMP协议:来为浏览器 和 server 间的 通信增加适当的消息语义。

WebSocket协议定义了两种类型的消息(文本和二进制),但是没有定义消息语义。协议定义了一种机制,供客户端和服务器协商子协议(即更高级别的消息传递协议),以便在WebSocket上使用它来定义每个消息可以发送哪些类型、格式是什么、每个消息的内容等等。子协议的使用是可选的,但无论如何,客户端和服务器都需要就一些定义消息内容的协议达成一致。

\

简而言之,WebSocket 是底层协议,SockJS 是WebSocket 的备选方案,也是底层协议,而 STOMP 是基于 WebSocket(SockJS)的上层协议。

1、HTTP协议解决了 web 浏览器发起请求以及 web 服务器响应请求的细节,假设 HTTP 协议 并不存在,只能使用 TCP 套接字来 编写 web 应用。

2、直接使用 WebSocket(SockJS) 就很类似于 使用 TCP 套接字来编写 web 应用,因为没有高层协议,就需要我们定义应用间所发送消息的语义,还需要确保连接的两端都能遵循这些语义;

3、同HTTP在TCP 套接字上添加请求-响应模型层一样,STOMP在WebSocket 之上提供了一个基于帧的线路格式层,用来定义消息语义;

消息格式:

连接

[CONNECT
 accept-version:1.1,1.0
 heart-beat:10000,10000]
复制代码

发送消息

[SEND
destination:/app/chat.addUser
content-length:36
{"sender":"tony","type":"JOIN"}]
复制代码

订阅消息

[SUBSCRIBE
 id:sub-0
 destination:/topic/public
 "]
复制代码

服务器广播消息

[MESSAGE
 destination:/topic/public
 content-type:application/json;charset=UTF-8
 subscription:sub-0
 message-id:axsspnqm-15
 content-length:67
 {"type":"JOIN","content":null,"sender":"tony","receiver":null}]
复制代码

5、实战:

1、服务端

spring-messaging和spring-websocket模块提供WebSocket支持的STOMP,一旦有了这些依赖项,就可以通过WebSocket使用SockJS Fallback公开STOMP端点

设置websocket的配置:

  1. @EnableWebSocketMessageBroker:用于启用我们的WebSocket服务器。
  2. 我们实现了WebSocketMessageBrokerConfigurer接口,并实现了其中的方法。
  1. endpointSang:我们注册一个websocket端点,客户端将使用它连接到我们的websocket服务器。
  2. withSockJS():是用来为不支持websocket的浏览器启用后备选项,使用了SockJS。
  1. StompEndpointRegistry:是来自Spring框架STOMP实现。STOMP代表简单文本导向的消息传递协议。它是一种消息传递协议,用于定义数据交换的格式和规则。为啥我们需要这个东西?因为WebSocket只是一种通信协议。它没有定义诸如以下内容:如何仅向订阅特定主题的用户发送消息,或者如何向特定用户发送消息。我们需要STOMP来实现这些功能。
  2. 在configureMessageBroker方法中,我们配置一个消息代理,用于将消息从一个客户端路由到另一个客户端。

以“/app”开头的消息应该路由到消息处理方法(之后会定义这个方法)。

以“/topic”开头的消息应该路由到消息代理。消息代理向订阅特定主题的所有连接客户端广播消息。

  1. 在下面的示例中,我们使用的是内存中的消息代理。
package com.springbootwebsocket.conf;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * @description: TODO
 * @param:
 * @return:
 * @author: sy
 * @date: 2021/9/24
 */
@Configuration
@EnableWebSocketMessageBroker  //用于启用我们的WebSocket服务器下的子协议stomp来传输基于代理(MessageBroker)的消息,这时控制器支持使用@MessageMapping
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 我们注册一个websocket端点,来接收客户端的连接
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 添加一个endpointSang  端点,客户端通过这个断点进行连接,允许使用socketJs方式访问,允许跨域
        // 在网页上我们就可以通过这个链接
        // http://localhost:8080/endpointSang
        // 来和服务器的WebSocket连接
        registry.addEndpoint("/endpointSang")
                // .addInterceptors(new HttpSessionHandshakeInterceptor())
                .setAllowedOrigins("*") // 允许跨域设置
                .withSockJS();  //用来为不支持websocket的浏览器启用备选项,使用了SockJS
    }

    /**
     * 定义消息代理,设置消息连接的各种规范请求信息
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 启动一个简单基于内存的消息代理,接受消息用户必须以这个开头的路径才能收到消息
        config.enableSimpleBroker("/user", "/topic", "/queue");
        // 全局使用的消息前缀(客户端主动发送消息前缀),以/app 开头的数据会被@MessageMapping拦截 进入方法体
        config.setApplicationDestinationPrefixes("/app");
        // 点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
        config.setUserDestinationPrefix("/user");
    }


}
复制代码

创建Controller来接收和发送消息

基于注解的@SendTo和@SendToUser虽然方便,但不太灵活

SimpMessagingTemplate有俩个推送的方法

convertAndSend(destination, payload); //将消息广播到特定订阅路径中,类似@SendTo

convertAndSendToUser(user, destination, payload);//将消息推送到固定的用户订阅路径中,类似@SendToUser

 @MessageMapping("/chat.sendMessage")
    @SendTo("/topic/public")
    public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
        return chatMessage;
    }
    @MessageMapping("/chat.sendMessageToSingle")
    public void point(@Payload ChatMessage chatMessage) {
        log.info(chatMessage.getSender() + "--to--" + chatMessage.getReceiver());
        messagingTemplate.convertAndSendToUser(chatMessage.getReceiver(), "/queue/points", chatMessage);
    }
复制代码

添加WebSocket事件监听,广播用户进来和出去等操作

public class WebSocketEventListener {
    @Autowired
    private SimpMessageSendingOperations messagingTemplate;

    @EventListener
    public void handleWebSocketConnectListener(SessionConnectedEvent event) {
        log.info("Received a new web socket connection");
    }

    /**
     * 用来从websocket会话中提取用户名,并向所有连接的客户端广播用户离开事件。
     */
    @EventListener
    public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());

        String username = (String) headerAccessor.getSessionAttributes().get("username");
        if(username != null) {
            log.info("User Disconnected : " + username);
            ChatMessage chatMessage = new ChatMessage();
            chatMessage.setType(ChatMessage.MessageType.LEAVE);
            chatMessage.setSender(username);
            messagingTemplate.convertAndSend("/topic/public", chatMessage);
        }
    }
复制代码

2、客户端

引入sockjs、stomp.js

    <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
    <script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
复制代码

主要代码片段:

function connect(event) {
    username = document.querySelector('#name').value.trim();

    if (username) {
        usernamePage.classList.add('hidden');
        chatPage.classList.remove('hidden');
        // receiverChatPage.classList.remove('hidden');
      
        var socket = new SockJS('/endpointSang');
        stompClient = Stomp.over(socket);
        stompClient.connect({}, onConnected, onError);
    }
    event.preventDefault();
}
复制代码
function onConnected() {
    // Subscribe to the Public Topic 回调方法,只要消息到达订阅主题,就会调用该方法
    stompClient.subscribe('/topic/public', onMessageReceived);

    stompClient.subscribe("/user/" + username + "/queue/points", onMessageSingleReceived);
    // Tell your username to the server
    stompClient.send("/app/chat.addUser",
        {},
        JSON.stringify({sender: username, type: 'JOIN'})
    )
    userElement.textContent = username;
    connectingElement.classList.add('hidden');
}
复制代码
function sendMessage(event) {
    var messageContent = messageInput.value.trim();
    if (messageContent && stompClient) {
        var chatMessage = {
            sender: username,
            content: messageInput.value,
            type: 'CHAT'
        };
        stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage));
        messageInput.value = '';
    }
    event.preventDefault();
}
复制代码

3、集群模式下的通讯实现

Websocket服务器需要共享信息(redis的发布订阅模式)

4、常见问题

粘包拆包问题

猜你喜欢

转载自juejin.im/post/7041181976635637790