Spring Boot中的WebSocket
Spring Boot中的WebSocket
很多網(wǎng)站為了實(shí)現(xiàn)推送技術(shù),所用的技術(shù)都是輪詢。輪詢是在特定的時(shí)間間隔(如每1秒),由瀏覽器對(duì)服務(wù)器發(fā)出HTTP請(qǐng)求,然后由服務(wù)器返回最新的數(shù)據(jù)給客戶端的瀏覽器。這種傳統(tǒng)的模式帶來(lái)很明顯的缺點(diǎn),即瀏覽器需要不斷的向服務(wù)器發(fā)出請(qǐng)求,然而HTTP請(qǐng)求可能包含較長(zhǎng)的頭部,其中真正有效的數(shù)據(jù)可能只是很小的一部分,顯然這樣會(huì)浪費(fèi)很多的帶寬等資源。
在這種情況下,HTML5定義了WebSocket協(xié)議,能更好的節(jié)省服務(wù)器資源和帶寬,并且能夠更實(shí)時(shí)地進(jìn)行通訊。
簡(jiǎn)介
WebSocket是一種在單個(gè)TCP連接上進(jìn)行全雙工通信的協(xié)議。
WebSocket使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡(jiǎn)單,允許服務(wù)端主動(dòng)向客戶端推送數(shù)據(jù)。在WebSocket API中,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進(jìn)行雙向數(shù)據(jù)傳輸。
特點(diǎn)
? 較少的控制開(kāi)銷。相對(duì)于HTTP請(qǐng)求每次都要攜帶完整的頭部,開(kāi)銷顯著減少了。
? 更強(qiáng)的實(shí)時(shí)性。由于協(xié)議是全雙工的,所以服務(wù)器可以隨時(shí)主動(dòng)給客戶端下發(fā)數(shù)據(jù)。
? 保持連接狀態(tài)。Websocket需要先創(chuàng)建連接,是一種有狀態(tài)的協(xié)議,之后通信時(shí)可以省略部分狀態(tài)信息。而HTTP請(qǐng)求可能需要在每個(gè)請(qǐng)求都攜帶狀態(tài)信息(如身份認(rèn)證等)。
? 更好的二進(jìn)制支持。Websocket定義了二進(jìn)制幀,相對(duì)HTTP,可以更輕松地處理二進(jìn)制內(nèi)容。
? 可以支持?jǐn)U展。Websocket定義了擴(kuò)展,用戶可以擴(kuò)展協(xié)議、實(shí)現(xiàn)部分自定義的子協(xié)議。如部分瀏覽器支持壓縮等。
? 更好的壓縮效果。相對(duì)于HTTP壓縮,Websocket在適當(dāng)?shù)臄U(kuò)展支持下,可以沿用之前內(nèi)容的上下文,在傳遞類似的數(shù)據(jù)時(shí),可以顯著地提高壓縮率。
在SpringBoot項(xiàng)目中創(chuàng)建WebSocket Server
項(xiàng)目依賴(Maven)
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.jeremysong</groupId>
<artifactId>demo</artifactId>
<version>1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.3</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>3.0.3</version>
</dependency>
</dependencies>
</project>WebSocket服務(wù)
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* ServerEndpointExporter 作用
*
* 這個(gè)Bean會(huì)自動(dòng)注冊(cè)使用@ServerEndpoint注解聲明的websocket endpoint
*
*/
@Component
public class WebSocketConfig {
@Bean
public ServerEndPointExporter serverEndPointExporter() {
return new ServerEndPointExporter();
}
}import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
@ServerEndpoint("/websocket/{name}")
public class WebSocket {
private Session session;
private String name;
private static ConcurrentHashMap<String, WebSocket> webSocketSet = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam(value = "name") String name) {
this.session = session;
this.name = name;
webSocketSet.put(name, this);
}
@OnClose
public void onClose() {
webSocketSet.remove(this.name);
}
@OnMessage
public void onMessage(String message) {
log.info("{} send {}", this.name, message);
}
/**
* 群發(fā)
* @param message 消息內(nèi)容
*/
public void groupSending(String message) {
for (String name : webSocketSet.keySet()) {
try {
webSocketSet.get(name).session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 指定發(fā)動(dòng)消息
* @param name 指定的客戶端名
* @param message 消息內(nèi)容
*/
public void appointSending(String name, String message) {
try {
webSocketSet.get(name).session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}使用JavaScript創(chuàng)建WebSocket Client
WebSocket客戶端
let websocket = null;
if ('WebSocket' in window) {
websocket = new WebSocket('ws://localhost:8888/websocket/cli-1');
websocket.onopen = function () {
console.log('連接成功');
};
websocket.onclose = function () {
console.log('退出連接');
};
websocket.onmessage = function (event) {
console.log('收到消息:' + event.data);
};
websocket.onerror = function () {
console.log('連接出錯(cuò)');
};
// MDN Example
websocket.addEventListener('open', function (event) {
websocket.send('Hello Server!');
});
websocket.addEventListener('message', function (event) {
console.log('Message from server ', event.data);
});
websocket.addEventListener('error', function (event) {
console.log('WebScoket error: ', event);
});
}
window.onbeforeunload = function () {
// 頁(yè)面關(guān)閉時(shí)關(guān)閉WebSocket連接
websocket.close(1000);
};在Chrome console中測(cè)試
執(zhí)行如下命令時(shí)可以在Server端添加日志輸出和debug觀察交互現(xiàn)象。
> ws1 = new WebSocket('ws://localhost:8888/websocket/name1');
> ws1.send('Send message to server! I am name1');
> ws2 = new WebSocket('ws://localhost:8888/websocket/name2');
> ws2.send('Send message to server! I am name2');
> ws1.close(1000);
> ws2.close(1000);參考
? WebSocket - MDN:
https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket? CloseEvent - MDN:
https://developer.mozilla.org/zh-CN/docs/Web/API/CloseEvent#status_codes
歡迎關(guān)注我的公眾號(hào)“須彌零一”,原創(chuàng)技術(shù)文章第一時(shí)間推送。
