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;
            width: 400px
        }

        #connect-container div {
            padding: 5px;
        }

        #console-container {
            float: left;
            margin-left: 15px;
            width: 400px;
        }

        #console {
            border: 1px solid #CCCCCC;
            border-right-color: #999999;
            border-bottom-color: #999999;
            height: 170px;
            overflow-y: scroll;
            padding: 5px;
            width: 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="width: 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="width: 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的通信機制;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM