補習系列(20)-大話 WebSocket 與 "尬聊"的實現
目錄
- 一、聊聊 WebSocket
- 二、Stomp 是個什麽鬼
- 三、SpringBoot 整合 WebSocket
- A. 引入依賴
- B. WebSocket 配置
- C. 控制器
- D. 前端實現
- 四、參考文檔
一、聊聊 WebSocket
從HTML5技術流行至今,WebSocket已經有非常廣泛的應用:
- 在線遊戲,提供實時的操作交互體驗
- 社交平臺,與好友實時的私信對話
- 新聞動態,獲得感興趣的主題信息推送
...
這些場景,都需要服務器能主動實時的給瀏覽器或客戶端推送消息,註意關鍵詞是主動,還有實時!
而在HTML5一統江湖之前,由於HTTP在推送場景下的"薄弱",我們需要借助一些復雜或者非標準的手段來實現。
這些方式包括有:
- Ajax輪詢,比如每隔5秒鐘,由瀏覽器對服務器主動請求數據後返回。
在這種方案下,瀏覽器需要不斷的向服務器發出請求,問題是比較明顯的,包括:
- HTTP請求頭部會浪費一些帶寬;
- 頻繁重建連接會造成很大的開銷。
- Comet,這個詞好像翻譯為"彗星"? 這個是采用 streaming 或 long-pulling 的長連接技術:
Comet 效率提升了不少,它解決了Ajax輪詢的部分問題,利用HTTP長連接的特性盡可能的避免了連接、帶寬資源的浪費等等,於是在很長一段時間 Comet 成為了Web推送技術的主流。
But ,.. Comet 的實現技術比較復雜,不同框架下的實現方式差異很大,在靈活性、性能上也有些欠缺。
關於服務端Comet的技術可以參考下面這篇經典文章:
https://www.ibm.com/developerworks/cn/web/wa-lo-comet/
- Flash,通過Flash 插件代碼實現Socket通訊,本質上是基於TCP的通訊模式,由於Flash 需要安裝插件以及瀏覽器的兼容性問題,目前已經逐漸廢棄。
WebSocket 出場
WebSocket 出現的目的沒有別的,就是幹掉前面的東西,Both!
最開始WebSocket 協議由 RFC6455 定義,其API標準包含於HTML5 範疇之中。
目前各大主流瀏覽器已經能完全支持該技術。然後可以看看下面這個圖:
如上圖,WebSocket 協議中, 瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。
那麽相比以往的方式,這種方案更加節省資源了,它的實時性、靈活性都要強大不少。
當然,有HTML5標準給它站臺,後臺杠杠的~
那麽一個 WebSocket 的請求響應長成怎麽樣呢?
看下面這個圖:
二、Stomp 是個什麽鬼
一開始我一直認為 Stomp是暴風雨(誤看為 Storm),然後覺得說這個技術挺犀利的。
然後在看了 Stomp 的協議介紹後發現,它是如此的簡單..
Stomp 的 全稱叫 Simple Text Orientated Messaging Protocol,就是一個簡單的文本定向消息協議,
除了設計為簡單易用之外,它的支持者也非常多。就比如目前主流的消息隊列服務器如RabbitMQ、ActiveMQ都支持Stomp 協議。
開源地址:
http://stomp.github.io/
Stomp 定義了一些簡單的指令,如下:
命令 | 說明 |
---|---|
CONNECT | 建立連接 |
SEND | 發送消息 |
SUBSCRIBE | 訂閱主題 |
UNSUBSCRIBE | 取消訂閱 |
BEGIN | 開啟事務 |
COMMIT | 提交事務 |
ABORT | 回滾事務 |
ACK | 確認消費 |
NACK | 消息丟棄 |
DISCONNECT | 斷開連接 |
一個簡單的STOMP消息大致如下:
CONNECT
accept-version:1.1,1.0
heart-beat:10000,10000\n\n\u0000
SEND
destination:/app/message\ncontent-length:6
發送內容\u0000
好的,你現在應該了解 Stomp是個什麽了,那麽為什麽要介紹這個?
WebSocket 為我們提供了Web 雙向通信的通道,但對於消息的交互協議還需要我們來自己實現(WebSocket 果然不夠意思)
借助Stomp 協議,可以很方便的實現一種"訂閱-發布"的通用機制,這個就是非常具有競爭力的一個特性了。
三、SpringBoot 整合 WebSocket
在介紹完WebSocket 之後,接下來幹什麽呢?
可能你看完前面的東西會覺得 WebSocket 是如此之強大,以至於很多場景都應該使用這個技術來實現。
那麽如何做? 在此前我所介紹的 SpringBoot 也是如此之強大,那麽能不能通過SpringBoot 輕松整合WebSocket 呢?這當然可以!
思索了很久,我決定做一個最簡單的應用展示: 尬聊!
為什麽是"尬聊”,而不是聊天室...
那麽,下面開始講這個案例,在該樣例中會包含一個Controller類、一個HTML頁面以及一個JS腳本。
步驟如下:
A. 引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>${springboot.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<version>${springboot.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
<version>0.32</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>org.foo.springboot</groupId>
<artifactId>base</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- jackson version -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.8.3</version>
</dependency>
添加spring-boot-starter-websocket 會自動引入spring-websocket的依賴,而後者就實現了WebSocket 操作的高級封裝。
還有一個好消息,就是spring-websocket 也默認支持了 Stomp協議(看吧,Stomp支持者太多了)。
而除此之外,還內置了一個叫 SocketJS 的東西。
SocketJS是一個流行的JS庫,主要是在WebSocket之上封裝了一層API,用於支持瀏覽器不兼容WebSocket的情況。
其項目地址:
https://github.com/sockjs/sockjs-client
其他組件的說明
- webjars 主要是將一些前端的框架打包到Jar包中以方便我們使用,這裏我們添加了socketJS、stompWebSocket相關的一些包;
- jackson 用於支持WebSocket消息的編解碼,是必須添加的。
B. WebSocket 配置
參考下面的代碼,添加一個JavaConfig風格的配置類:
WebSocketConfig.java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private static final Logger logger = LoggerFactory.getLogger(WebSocketConfig.class);
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
//設置訂閱通道(客戶端可訂閱)
config.enableSimpleBroker("/topic");
//接收APP(客戶端)消息的路由前綴,可通過@MessageMapping 映射到方法
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//websocket 連接端點
registry.addEndpoint("/backend").withSockJS();
}
@Override
public void configureWebSocketTransport(final WebSocketTransportRegistration registration) {
//配置攔截器
registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
@Override
public WebSocketHandler decorate(final WebSocketHandler handler) {
return new WebSocketHandlerDecorator(handler) {
@Override
public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
String username = session.getPrincipal() != null? session.getPrincipal().getName(): "GUEST";
logger.info("{} connect.", username);
super.afterConnectionEstablished(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
String username = session.getPrincipal() != null? session.getPrincipal().getName(): "GUEST";
logger.info("{} disconnect.", username);
super.afterConnectionClosed(session, closeStatus);
}
};
}
});
super.configureWebSocketTransport(registration);
}
}
在WebSocketConfig的配置中,有兩點需要關註:
- registerStompEndpoints 用於添加端點,即瀏覽器通過 ws://xxx 能訪問到的路徑
- configureMessageBroker 用於做消息路由配置,包括訂閱主題、方法映射路徑
C. 控制器
控制層除了支持頁面的渲染,還需要對WebSocket消息進行處理,實現如下:
ConsoleController
@Controller
public class ConsoleController {
//輸出數據頻道
public static final String CHANNEL_CONSOLE = "/topic/console";
@Autowired
private SimpMessagingTemplate template;
/**
* 控制臺頁面
*
* @return
*/
@GetMapping("/console")
public String console() {
return "console";
}
/**
* 接收WebSocket消息方法
* @param message
*/
@MessageMapping("/message")
public void onMessage(String message) {
template.convertAndSend(CHANNEL_CONSOLE, "我收到了你的消息:" + message);
}
}
D. 前端實現
先做一個HTML頁面,編輯templates/console.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"></meta>
<title>Web控制臺</title>
<script th:src="@{/webjars/sockjs-client/sockjs.min.js}"></script>
<script th:src="@{/webjars/stomp-websocket/stomp.min.js}"></script>
<script th:src="@{/webjars/jquery/jquery.min.js}"></script>
<script type="text/javascript" th:src="@{/static/console.js}"></script>
<style type="text/css">
body { font-family: "Microsoft YaHei" ;}
.span-tv{padding-right:12px}
#console p {padding: 0px; margin: 0px;}
</style>
</head>
<body>
<div style="background-color:#AAA; padding: 5px; border-bottom: 1px solid #333">
<input type="text" id="word" style="width:100px"></input>
<button onclick="sendMessage()">發送消息</button>
<button onclick="reconnect()">重新連接</button>
<button onclick="clearConsole()">清空內容</button>
</div>
<div id="console" style="padding:5px; font-size:10px"></div>
</body>
</html>
然後是實現 JS 腳本,編輯public/static/console.js
$(document).ready(function(){
//首次打開頁面自動連接
connect();
})
//執行連接
function connect() {
//接入端點/backend
var socket = new SockJS('/backend');
window.stompClient = Stomp.over(socket);
window.stompClient.connect({}, function (frame) {
log('Connected: ' + frame);
//訂閱服務端輸出的 Topic
stompClient.subscribe('/topic/console', function (message) {
log("[服務器說]:" + message.body);
});
});
}
//斷開連接
function disconnect() {
if (stompClient !== null) {
stompClient.disconnect();
}
log("Disconnected");
}
//重新連接
function reconnect(){
clearConsole();
disconnect();
connect();
}
//發送消息
function sendMessage(){
var content = $("#word").val();
if(!content){
alert("請輸入消息!")
return;
}
//向應用Topic發送消息
stompClient.send("/app/message", {}, content);
log("[你說]:" + content);
}
//記錄控制臺消息
function log(message){
$("<p></p>").text(message).appendTo($("#console"));
}
//清空控制臺
function clearConsole(){
$("#console").empty();
}
這樣,Web控制臺已經制作好了,運行主程序後,打開地址
http://localhost:8080/console
進行體驗,如下:
好了,這個案例的確很尷尬..
但是我認為,在這上面做一做改造,應該可以實現一個諸如"美女聊天室" 的功能的,或者,你可以動手試試。
碼雲同步代碼
四、參考文檔
https://spring.io/guides/gs/messaging-stomp-websocket/
https://blog.coding.net/blog/spring-static-resource-process
https://zh.wikipedia.org/wiki/WebSocket
https://halfrost.com/websocket/
歡迎繼續關註"美碼師的補習系列-springboot篇" ,期待更多精彩內容^-^
補習系列(20)-大話 WebSocket 與 "尬聊"的實現