• SpringBoot WebSocket 消息交互


    1. Websocket原理

    • Websocket协议本质上是一个基于TCP的独立协议,能够在浏览器和服务器之间建立双向连接,以基于消息的机制,赋予浏览器和服务器间实时通信能力。
    • WebSocket资源URI采用了自定义模式:ws表示纯文本通信,其连接地址写法为“ws://**”,占用与http相同的80端口;wss表示使用加密信道通信(TCP+TLS),基于SSL的安全传输,占用与TLS相同的443端口。

    2. Websocket与HTTP比较

    WebSocket 和 HTTP 都是基于 TCP 协议;
    TCP是传输层协议,WebSocket 和 HTTP 是应用层协议

    HTTP是用于文档传输、简单同步请求的响应式协议,本质上是无状态的应用层协议,半双工的连接特性。Websocket与 HTTP 之间的唯一关系就是它的握手请求可以作为一个升级请求(Upgrade request)经由 HTTP 服务器解释(也就是可以使用Nginx反向代理一个WebSocket)。

    联系:

    客户端建立WebSocket连接时发送一个header,标记了Upgrade的HTTP请求,表示请求协议升级;
    服务器直接在现有的HTTP服务器软件和端口上实现WebSocket,重用现有代码(比如解析和认证这个HTTP请求),然后再回一个状态码为101(协议转换)的HTTP响应完成握手,之后发送数据就跟HTTP没关系了。

    区别:

    • 持久性:

    HTTP协议:HTTP是非持久的协议(长连接、循环连接除外)
    WebSocket协议:Websocket是持久化的协议

    • 生命周期:

    HTTP的生命周期通过Request来界定,也就是一个Request 一个Response
    HTTP1.0中,这次HTTP请求就结束了;
    HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,并接收多个Respouse;
    在HTTP中永远都是一个Request只有一个Respouse,而且这个Respouse是被动的,不能主动发起。

    3. SpringWeb项目搭建

    3.1.1 pom.xml

    该项目基于maven搭建,使用SpringBoot2.0版本,引入Spring Websocket所需的jar包,以及对传输的消息体进行JSON序列化所需的jar包。

    <dependencies>
            <!-- Compile -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-websocket</artifactId>
            </dependency>
            <!-- Test -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <scope>provided</scope>
                <optional>true</optional>
            </dependency>
        </dependencies>
    pom.xml

    3.1.2 WebSocketHandler接口实现

      实现WebSocketHandler接口并重写接口中的方法,为消息的处理实现定制化。Spring WebSocket通过WebSocketSession建立会话,发送消息或关闭会话。Websocket可发送两类消息体,分别为文本消息TextMessage和二进制消息BinaryMessage,两类消息都实现了WebSocketMessage接口(A message that can be handled or sent on a WebSocket connection.)
    /**
     * Echo messages by implementing a Spring {@link WebSocketHandler} abstraction.
     */
    @Slf4j
        public class EchoWebSocketHandler extends TextWebSocketHandler {
    
            /**
             * Map 来存储 WebSocketSession,key 用 USER_ID 即在线用户列表
             */
            private static final Map<String, WebSocketSession> users = new HashMap<String, WebSocketSession>();
    
            /**
             * 用户唯一标识【WebSocketSession 中 getAttributes() 方法获取到的 Map 集合是不同的,因为不同用户 WebSocketSession 不同,
             * 所以不同用户可以使用相同的 key = WEBSOCKET_IDCARD,因为 Map 不同,互不影响】
             */
            private static final String IDCARD = "WEBSOCKET_IDCARD";
    
            private final EchoService echoService;
    
            public EchoWebSocketHandler(EchoService echoService) {
                this.echoService = echoService;
            }
    
            /**
             * 连接成功时候,会触发页面上onopen方法
             */
            @Override
            public void afterConnectionEstablished(WebSocketSession session) {
                Map<String, Object> attributes = session.getAttributes();
                log.info("EchoWebSocketHandler = {}, session = {}, attributes = {}", this, session, attributes);
    
                String idcard = (String) session.getAttributes().get(IDCARD);
                log.info("idcard = {}用户成功建立 websocket 连接", idcard);
                users.put(idcard, session);
            }
    
            /**
             * js 调用 websocket.send 时候,会调用该方法
             */
            @Override
            public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
                String echoMessage = this.echoService.getMessage(message.getPayload());
                log.info("前端发送消息,echoMessage = {}", echoMessage);
                session.sendMessage(new TextMessage(echoMessage));
            }
    
            @Override
            public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
                session.close(CloseStatus.SERVER_ERROR);
            }
    
            /**
             * 关闭连接时触发
             */
            public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
                log.info("关闭websocket连接");
                String userId = (String) session.getAttributes().get(IDCARD);
                log.info("用户 userId = {} 已退出!", userId);
                users.remove(userId);
            }
    
            /**
             * 给某个用户发送消息
             */
            public void sendMessageToUser(String idcard, TextMessage message) {
                try {
                    if (users.containsKey(idcard)) {
                        WebSocketSession session = users.get(idcard);
                        if (session.isOpen()) {
                            session.sendMessage(message);
                        }
                    }
                } catch (IOException e) {
                    log.error("发送消息异常, errerMsg = {}", e.getMessage());
                }
            }
    
            /**
             * 给所有在线用户发送消息
             */
            public void sendMessageToUsers(TextMessage message) {
                for (String userId : users.keySet()) {
                    try {
                        if (users.get(userId).isOpen()) {
                            users.get(userId).sendMessage(message);
                        }
                    } catch (IOException e) {
                        log.error("给所有在线用户发送消息异常, errorMsg = {}", e.getMessage());
                    }
                }
            }
    
    }
    EchoWebSocketHandler

    3.1.3 HttpSession存储属性值【key-value】

    @RestController
    @Slf4j
    public class WebSocketController {
    
        @Autowired
        EchoWebSocketHandler echoWebSocketHandler;
    
        @RequestMapping("/websocket/login")
        public String login(HttpServletRequest request) {
            String idcard = request.getParameter("idcard");
            log.info("idcard = {} 登录 ", idcard);
            HttpSession session = request.getSession();
            session.setAttribute("WEBSOCKET_IDCARD", idcard);
            return "登录成功";
        }
    
        @RequestMapping("/websocket/send")
        @ResponseBody
        public void send(HttpServletRequest request) {
            String username = request.getParameter("idcard");
            echoWebSocketHandler.sendMessageToUser(username, new TextMessage("你好,给您推送消息啦!"));
        }
    
    }
    WebSocketController

      HttpSession可以记录当前访问的会话用户,但WebSocketSession不能记录当前用户会话,必须要从HttpSession中存储当前用户相关信息idcard,在建立通信握手之前,需要将HttpSession中用户相关属性值存储到WebSocketSession中,服务器才能知道通话的当前用户;【目前怎么将HttpSession中的key-value的值存储到Map集合,并将Map集合同步到WebsocketSession的attributes属性中,这点我没有跟踪到源码,但是我敢确认,肯定设置进去了】

    3.1.4 WebSocket激活

    @Configuration(proxyBeanMethods = false)
    @EnableAutoConfiguration
    @EnableWebSocket
    @Slf4j
    public class SampleTomcatWebSocketApplication extends SpringBootServletInitializer implements WebSocketConfigurer {
    
        @Override
        public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
            //设置自定义的 WebSocket 握手拦截器
            registry.addHandler(echoWebSocketHandler(), "/echo").addInterceptors(webSocketHandlerIntereptor()).withSockJS();
        }
    
        @Override
        protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
            return application.sources(SampleTomcatWebSocketApplication.class);
        }
    
        @Bean
        public EchoService echoService() {
            return new DefaultEchoService("Did you say "%s"?");
        }
    
        @Bean
        public GreetingService greetingService() {
            return new SimpleGreetingService();
        }
    
        @Bean
        public WebSocketHandler echoWebSocketHandler() {
            return new EchoWebSocketHandler(echoService());
        }
    
        @Bean
        public WebSocketController webSocketController() {
            return new WebSocketController();
        }
    
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
            return new ServerEndpointExporter();
        }
    
        @Bean
        public WebSocketHandlerIntereptor webSocketHandlerIntereptor() {
            return new WebSocketHandlerIntereptor();
        }
    
        public static void main(String[] args) {
            SpringApplication.run(SampleTomcatWebSocketApplication.class, args);
        }
    
    }
    WebSocketConfig

    Websocket拦截器类:  

      HttpSession和WebSocketSession是不同的会话对象,如果想记录当前用户的Session对象的属性值,必须要在建立通信握手之前,将HttpSession的值copy到WebSocketSession中,否则获取不到;

      该拦截器实现了HandshakeInterceptor接口,HandshakeInterceptor可拦截Websocket的握手请求(通过HTTP协议)并可设置与Websocket session建立连接的HTTP握手连接的属性值。实例中配置重写了beforeHandshake方法,将HttpSession中对象放入WebSocketSession中,实现后续通信;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.server.ServerHttpRequest;
    import org.springframework.http.server.ServerHttpResponse;
    import org.springframework.http.server.ServletServerHttpRequest;
    import org.springframework.web.socket.WebSocketHandler;
    import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
    
    import javax.servlet.http.HttpSession;
    import java.util.Map;
    
    @Slf4j
    public class WebSocketHandlerIntereptor extends HttpSessionHandshakeInterceptor {
    
        @Override
        public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
                                       Map<String, Object> attributes) throws Exception {
            log.info("Before Handshake");
            if (request instanceof ServletServerHttpRequest) {
                ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
                HttpSession session = servletRequest.getServletRequest().getSession();
                if (session != null) {
                    //使用 idcard 区分 WebSocketHandler,以便定向发送消息【一般直接保存 user 实体】
                    String idcard = (String) session.getAttribute("idcard");
                    if (idcard != null) {
                        attributes.put("WEBSOCKET_IDCARD", idcard);
                    }
    
                }
            }
            return super.beforeHandshake(request, response, wsHandler, attributes);
        }
    
        @Override
        public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
                                   Exception ex) {
            super.afterHandshake(request, response, wsHandler, ex);
        }
    
    }
    WebSocketHandlerIntereptor

    3.2 WebSocket前端 

    <!DOCTYPE html>
    <html>
    <head>
        <title>Apache Tomcat WebSocket Examples: Echo</title>
        <style type="text/css">
            #connect-container {
                float: left;
                 400px
            }
    
            #connect-container div {
                padding: 5px;
            }
    
            #console-container {
                float: left;
                margin-left: 15px;
                 400px;
            }
    
            #console {
                border: 1px solid #CCCCCC;
                border-right-color: #999999;
                border-bottom-color: #999999;
                height: 170px;
                overflow-y: scroll;
                padding: 5px;
                 100%;
            }
    
            #console p {
                padding: 0;
                margin: 0;
            }
        </style>
        <script src="https://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js"></script>
        <script type="text/javascript">
            var ws = null;
    
            function setConnected(connected) {
                document.getElementById('connect').disabled = connected;
                document.getElementById('disconnect').disabled = !connected;
                document.getElementById('echo').disabled = !connected;
            }
    
            function connect() {
                var target = document.getElementById('target').value;
                ws = new SockJS(target);
                ws.onopen = function () {
                    setConnected(true);
                    log('Info: WebSocket connection opened.');
                };
                ws.onmessage = function (event) {
                    log('Received: ' + event.data);
                };
                ws.onclose = function () {
                    setConnected(false);
                    log('Info: WebSocket connection closed.');
                };
            }
    
            function disconnect() {
                if (ws != null) {
                    ws.close();
                    ws = null;
                }
                setConnected(false);
            }
    
            function echo() {
                if (ws != null) {
                    var message = document.getElementById('message').value;
                    log('Sent: ' + message);
                    ws.send(message);
                } else {
                    alert('WebSocket connection not established, please connect.');
                }
            }
    
            function log(message) {
                var console = document.getElementById('console');
                var p = document.createElement('p');
                p.style.wordWrap = 'break-word';
                p.appendChild(document.createTextNode(message));
                console.appendChild(p);
                while (console.childNodes.length > 25) {
                    console.removeChild(console.firstChild);
                }
                console.scrollTop = console.scrollHeight;
            }
        </script>
    </head>
    <body>
    <noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websockets rely on Javascript being enabled. Please enable
        Javascript and reload this page!</h2></noscript>
    <div>
        <div id="connect-container">
            <div>
                <input id="target" type="text" size="40" style=" 350px" value="/echo"/>
            </div>
            <div>
                <button id="connect" onclick="connect();">Connect</button>
                <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
            </div>
            <div>
                <textarea id="message" style=" 350px">Here is a message!</textarea>
            </div>
            <div>
                <button id="echo" onclick="echo();" disabled="disabled">Echo message</button>
            </div>
        </div>
        <div id="console-container">
            <div id="console"></div>
        </div>
    </div>
    </body>
    </html>
    WebSocket前端

    4. 总结

      当然,上述展示的只是一个小小的Demo,但按照上述思路即可将Websocket运用于其它项目中,为项目锦上添花。可,不知大家有没有注意到一个,上述Websocket协议我们使用的都是ws协议,那什么时候会用到wss协议呢?当我们的通信协议为HTTPS协议的时候,此时需要在服务端应用服务器中安装SSL证书,不然服务端是没法解析wss协议的。
      前后端通信,使用SpringBoot内置的WebSocket通信,如果更加深刻理解WebSocket通信,Debug走一下具体流程,才能理解的更加透彻,在同事的帮助下,我也理解了SpringBoot中WebSocket的通信机制;
  • 相关阅读:
    Cookie基本使用
    Chartlet简单易用的图表控件
    JQuery 基础:6.Each的用法
    图的基本算法
    Head First Design Patterns Strategy Pattern
    个人整理的面试题
    Android从SIM卡中获取联系人
    Android 覆盖安装
    Head First Design Patterns Adapter Pattern
    android 获取sim卡运营商信息(转)
  • 原文地址:https://www.cnblogs.com/blogtech/p/11739083.html
Copyright © 2020-2023  润新知