作者:浦春城,單位:中國移動智慧家庭運營中心
WebSocket是一種雙向通信協(xié)議,主要應(yīng)用在Web瀏覽器,解決服務(wù)器端主動向瀏覽器推送消息的問題。WebSocket常見的應(yīng)用場景有頁游、視頻網(wǎng)站、在線文檔、股票網(wǎng)站等。
Part 01●??WebSocket是什么??●
WebSocket是一種在Web應(yīng)用程序中提供雙向通信的協(xié)議。它允許服務(wù)器主動向客戶端推送數(shù)據(jù),而不是像傳統(tǒng)的HTTP請求-響應(yīng)模式一樣,客戶端必須發(fā)送請求才能獲取數(shù)據(jù)。WebSocket 最早是在HTML5中引入的,建立在HTTP協(xié)議之上,使用握手階段來升級連接,然后通過保持連接的狀態(tài)來實現(xiàn)實時通信。
與傳統(tǒng)的HTTP協(xié)議相比,WebSocket具有以下優(yōu)勢:
⑴ 增強(qiáng)實時性
服務(wù)器可以隨時主動給客戶端下發(fā)數(shù)據(jù),相對于HTTP請求需要等待客戶端發(fā)起請求服務(wù)端才能響應(yīng),延遲明顯更少。和傳統(tǒng)的輪詢比較,WebSocket也可以在短時間內(nèi)更有效率地傳遞數(shù)據(jù);
⑵ 維持連接狀態(tài)
在一些需要身份認(rèn)證的場景下,HTTP請求可能需要在每個請求都攜帶狀態(tài)信息(服務(wù)器不記錄每次的請求和響應(yīng)信息),而WebSocket一次連接建立后就會保持住會話狀態(tài),這就使其成為一種有狀態(tài)的協(xié)議,后續(xù)通信時就可以省略部分狀態(tài)信息;
⑶ 更靈活的擴(kuò)展支持
開發(fā)者可以對WebSocket自定義二進(jìn)制幀,相對HTTP,可以更輕松地處理二進(jìn)制內(nèi)容,此外開發(fā)者也自行擴(kuò)展協(xié)議、實現(xiàn)部分自定義的子協(xié)議;
⑷ 更好的壓縮效果
WebSocket在適當(dāng)?shù)臄U(kuò)展支持下,可以沿用之前內(nèi)容的上下文,在傳遞類似的數(shù)據(jù)時,可以顯著地提高壓縮率。
Part 02●??Websocket報文?●
2.1 報文格式
- 第0個字節(jié)
0位(FIN):0表示報文沒有結(jié)束,1表示報文結(jié)束。
1-3位(RSV1、RSV2、RSV3):保留字段,一般全部為0。也可用于擴(kuò)展自己的協(xié)議。
4-7位(opcode):報文類型。0 代表一個繼續(xù)幀,1代表文本幀,2代表二進(jìn)制幀,8 代表連接關(guān)閉,9 代表ping,10代表pong。
- 第1個字節(jié)
8位(MASK):1表示需要掩碼操作,0表示不需要。如果為1,數(shù)據(jù)幀的masking-key屬性會存在一個值,接收方會利用這個值來進(jìn)行解掩碼操作,所有從客戶端傳輸?shù)椒?wù)器的數(shù)據(jù)幀的Mask都被設(shè)置為1。
9-15位(Payload len):表示Payload data的長度。如果值是0~125,則真實長度就是前7位表示;如果值是126,則真實長度就是后16位(Extended payload length 126);如果值是127,則真實長度就是后64位(Extended payload length 127)
2.2 報文樣例
- ping幀,Opcode=9,Mask=1,Payload len=0,Masking-Key有內(nèi)容
- pong幀,Opcode=10,Mask=0,Payload len=0,Masking-Key為空
- 文本幀,Opcode=1,Mask=0,Payload len=84,Masking-Key為空
Part 03●??代碼實現(xiàn)?●
接下來,一起動手編寫WebSocket服務(wù)端和客戶端,這里提供一個java版的服務(wù)端demo和一個html版的客戶端demo。
1、服務(wù)端demo是一個springboot項目
- 引用spring-boot-starter-websocket依賴
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- session管理
public class WsSessionManager {
public static ConcurrentHashMap<String, WebSocketSession> SESSION_POOL = new ConcurrentHashMap<>();
public static void add(String key, WebSocketSession session) {
SESSION_POOL.put(key, session);
}
public static WebSocketSession remove(String key) {
return SESSION_POOL.remove(key);
}
public static void removeAndClose(String key) {
WebSocketSession session = remove(key);
if (session != null) {
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static WebSocketSession get(String key) {
return SESSION_POOL.get(key);
}
}
- 消息處理
@Component
@Slf4j
public class MyWsHandler extends AbstractWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("建立ws連接,sessionId:{}", session.getId());
WsSessionManager.add(session.getId(), session);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 獲得客戶端傳來的消息
String payload = message.getPayload();
log.info("server 接收到消息, sessionId " + session.getId() + ", payload: " + payload);
session.sendMessage(new TextMessage("server 發(fā)送給的消息 " + payload + ",發(fā)送時間:" + LocalDateTime.now().toString()));
}
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
log.info("發(fā)送二進(jìn)制消息, sessionId " + session.getId());
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
log.error("異常處理, sessionId " + session.getId());
- WebSocket配置
@Configuration
@EnableWebSocket
public class WsServerConfig implements WebSocketConfigurer {
@Autowired
private MyWsHandler myWsHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myWsHandler, "myWs")
//允許跨域
.setAllowedOrigins("*");
}
}
2、客戶端demo是一個html網(wǎng)頁
<!doctype html>
<form name="publish">
<input type="text" name="message" maxlength="50"/>
<input type="submit" value="Send"/>
</form>
<div id="messages"></div>
<script>
let url='ws://127.0.0.1:8889/myWs';
let socket = new WebSocket(url);
// send message from the form
document.forms.publish.onsubmit = function() {
let outgoingMessage = this.message.value;
socket.send(outgoingMessage);
return false;
};
// handle incoming messages
socket.onmessage = function(event) {
let incomingMessage = event.data;
showMessage(incomingMessage);
};
socket.onclose = event => console.log(`Closed ${event.code}`);
// show message in div#messages
function showMessage(message) {
let messageElem = document.createElement('div');
messageElem.textContent = message;
document.getElementById('messages').prepend(messageElem);
Part 04●??結(jié)束語?●
WebSocket適用于服務(wù)器端需要快速向瀏覽器發(fā)送消息的場景,例如網(wǎng)頁游戲、視頻網(wǎng)站、在線文檔、運維工具等。瀏覽器可以看做是一個瘦客戶端,它不提供直接操作tcp長連接的編程接口,也無法簡單地集成消息組件客戶端。在使用瀏覽器作為客戶端的場景下,WebSocket是最常用的服務(wù)器端主動推送方案。
當(dāng)然,有很多種技術(shù)可以實現(xiàn)服務(wù)器端向客戶端主動推送消息,WebSocket只是其中一種,其他常見的方案還有tcp長連接、消息組件(如mqtt、kafka)等,不過各有優(yōu)缺點,這個可以在日后進(jìn)一步學(xué)習(xí)。