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>
