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>
3.1.2 WebSocketHandler接口實現

/** * 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()); } } } }
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("你好,給您推送消息啦!")); } }
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); } }
Websocket攔截器類:
HttpSession和WebSocketSession是不同的會話對象,如果想記錄當前用戶的Session對象的屬性值,必須要在建立通信握手之前,將HttpSession的值copy到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); } }
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>