WebSocket的實現與應用
前言
說到websocket,就不得不提http協議的連接特點特點與交互模型。
首先,http協議的特點是無狀態連接。即http的前一次連接與后一次連接是相互獨立的。
其次,http的交互模型是請求/應答模型。即交互是通過C/B端向S端發送一個請求,S端根據請求,返回一個響應。
那么這里就有一個問題了--S端無法主動向C/B端發送消息。而交互是雙方的事情,怎么能限定一方發數據,另一方接數據呢。
傳統解決方案:
傳統的解決方案就倆字:輪詢。
長短連接輪詢就不詳細說了,就說說輪詢。大概的場景是這樣的:
客戶端(Request):有消息不?
服務端(Response):No
客戶端(Request):有消息不?
服務端(Response):No
客戶端(Request):有消息不?
服務端(Response):No
客戶端(Request):有消息不?
服務端(Response):有了。你媽叫你回家吃飯。
客戶端(Request):有消息不?
服務端(Response):No
==================================> loop
看着都累,資源消耗那就更不必說了。尤其有些對實時性要求高的數據,那可能就是1s請求一次。目測服務器已經淚奔。
websocket解決方案:
那么websocket的解決方案,總結一下,就是:建立固定連接
說白了,就是C/B端與S端就一個websocket服務建立一個固定的連接,不斷開。
大概的場景是這樣的:
服務端:我建立了一個chat的websocket,歡迎大家連接。
客戶端:我要和你的chat的websocket連接,我的sid(唯一標識)是No.1
服務端:好的,我已經記住你了。如果有發往chat下No.1的消息,我會告訴你的。
客戶端:嗯。謝謝了哈。
==================================> 過了一段時間
(有一個請求調用了chat的websocket,並且指名是給No.1的消息)
服務端(發送消息給No.1):No.1,有你的消息。你媽媽叫你回家做作業。
客戶端(No.1):好的。我收到了。謝謝。
由於這次只是簡單說一下websocket,所以就不深入解讀網絡相關知識了。
應用場景
既然http無法滿足用戶的所有需求,那么為之誕生的websocket必然有其諸多應用場景。如:
- 實時顯示網站在線人數
- 賬戶余額等數據的實時更新
- 多玩家網絡游戲
- 多媒體聊天,如聊天室
- 。。。
其實總結一下,websocket的應用場景就倆字:實時
無論是多玩家網絡游戲,網站在線人數等都是由於實時性的需求,才用上了websocket(后面用縮寫ws)。
談幾個在我項目中用到的情景:
- 在線教育項目中的課件系統,通過ws實現學生端課件與教師端課件的實時交互
- 物聯網項目中的報警系統,通過ws實現報警信息的實時推送
- 大數據項目中的數據展示,通過ws實現數據的實時更新
- 物聯網項目中的硬件交互系統,通過ws實現硬件異步響應的展示
當你的項目中存在需要S端向C/B端發送數據的情形,那就可以考慮上一個websocket了。
實現
服務端開發:
引入依賴:
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
添加配置:
忍不住想要吐槽,為什么不可以如eureka等組件那樣,直接在啟動類寫一個注解就Ok了呢。看來還得以后自己動手,豐衣足食啊。
package com.renewable.center.warning.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* Websocket的配置
* 說白了就是引入Websocekt至spring容器
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
代碼實現:
WebSocketServer的實現:
package com.renewable.center.warning.controller.websocket;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @Description:
* @Author: jarry
*/
@Component
@Slf4j
@ServerEndpoint("/websocket/warning/{sid}")
public class WarningWebSocketServer {
// JUC包的線程安全Set,用來存放每個客戶端對應的WarningWebSocketServer對象。
// 用ConcurrentHashMap也是可以的。說白了就是類似線程池中的BlockingQueue那樣作為一個容器
private static CopyOnWriteArraySet<WarningWebSocketServer> warningWebSocketSet = new CopyOnWriteArraySet<WarningWebSocketServer>();
// 與某個客戶端的連接會話,需要通過它來給客戶端發送數據
private Session session;
// 接收sid
private String sid="";
/**
* 建立websocket連接
* 看起來很像JSONP的回調,因為前端那里是Socket.onOpne()
* @param session
* @param sid
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid){
this.session = session;
this.sid = sid;
warningWebSocketSet.add(this);
sendMessage("websocket connection has created.");
}
/**
* 關閉websocket連接
*/
@OnClose
public void onClose(){
warningWebSocketSet.remove(this);
log.info("there is an wsConnect has close .");
}
/**
* websocket連接出現問題時的處理
*/
@OnError
public void onError(Session session, Throwable error){
log.error("there is an error has happen ! error:{}",error);
}
/**
* websocket的server端用於接收消息的(目測是用於接收前端通過Socket.onMessage發送的消息)
* @param message
*/
@OnMessage
public void onMessage(String message){
log.info("webSocketServer has received a message:{} from {}", message, this.sid);
// 調用消息處理方法(此時針對的WarningWebSocektServer對象,只是一個實例。這里進行消息的單發)
// 目前這里還沒有處理邏輯。故為了便於前端調試,這里直接返回消息
this.sendMessage(message);
}
/**
* 服務器主動推送消息的方法
*/
public void sendMessage(String message){
try {
this.session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.warn("there is an IOException:{}!",e.toString());
}
}
public static void sendInfo(String sid, String message){
for (WarningWebSocketServer warningWebSocketServerItem : warningWebSocketSet) {
if (StringUtils.isBlank(sid)){
// 如果sid為空,即群發消息
warningWebSocketServerItem.sendMessage(message);
log.info("Mass messaging. the message({}) has sended to sid:{}.", message,warningWebSocketServerItem.sid);
}
if (StringUtils.isNotBlank(sid)){
if (warningWebSocketServerItem.sid.equals(sid)){
warningWebSocketServerItem.sendMessage(message);
log.info("single messaging. message({}) has sended to sid:{}.", message, warningWebSocketServerItem.sid);
}
}
}
}
}
WesocketController
為了便於調試與展示效果,寫一個控制層,用於推送消息
package com.renewable.center.warning.controller.websocket;
import com.renewable.terminal.terminal.common.ServerResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.IOException;
/**
* @Description: 用於測試WebsocketServer
* @Author: jarry
*/
@Controller
@RequestMapping("/websocket/test/")
public class WarningWebsocketController {
@GetMapping("link.do")
@ResponseBody
public ServerResponse link(@RequestParam(name = "sid") int sid){
return ServerResponse.createBySuccessMessage("link : "+sid);
}
/**
* 調用WarningWebsocketServer的消息推送方法,從而進行消息推送
* @param sid 連接WarningWebsocketServer的前端的唯一標識。如果sid為空,即表示向所有連接WarningWebsocketServer的前端發送相關消息
* @param message 需要發送的內容主體
* @return
*/
@ResponseBody
@RequestMapping("push.do")
public ServerResponse pushToWeb(@RequestParam(name = "sid", defaultValue = "") String sid, @RequestParam(name = "message") String message) {
WarningWebSocketServer.sendInfo(sid, message);
return ServerResponse.createBySuccessMessage(message+"@"+sid+" has send to target.");
}
}
WesocketTestIndex
這里建立了一個B端頁面,用於與S端進行交互,演示。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebsocketTestIndex</title>
</head>
<body>
<h1>Websocket Test</h1>
<script>
var socket;
if(typeof(WebSocket) == "undefined") {
console.log("Your browser not support WebSocket !");
}else{
console.log("Your browser support WebSocket");
// 實例化WebSocket對象
// 指定要連接的服務器地址與端口
// 建立連接
socket = new WebSocket("ws://localhost:10706/websocket/warning/2");
// 打開事件
socket.onopen = function() {
console.log("You has connect to WebSocketServer");
};
// 獲得消息事件
socket.onmessage = function(msg) {
// 打印接收到的消息
console.log(msg.data);
};
// 關閉事件
socket.onclose = function() {
console.log("Socket has closed");
};
// 發生了錯誤事件
socket.onerror = function() {
alert("Socket happen an error !");
}
}
</script>
</body>
</html>
效果展示
再次強調,圖片很大很清晰。如果看不清楚,請單獨打開圖片。
B端網頁初始化:
調用S端WarningWebsocketController下pushToWeb()接口,對sid=2的B端發送消息:
B端網頁接收到專門發給sid=2的消息后的效果:
調用S端WarningWebsocketController下pushToWeb()接口,所有連接該websocket的B端群發消息:
B端網頁接收到群發消息后的效果:
S端接收到消息后的日志打印:
S端在B端關閉連接后的日志打印:
總結
至此,websocket的應用就算入門了。至於實際的使用,其實就是服務端自己調用WebSocket的sendInfo接口。當然也可以自己擴展更為細致的邏輯,方法等。
另外,需要注意的是,別忘了及時關閉webocket的連接。尤其在負載較大的情況下,更需要注意即使關閉不必要的連接。
架構的技術選型,需要的不是最好的,而是最適合的。
擴展:
如果想要了解更多概念上的細節,可以看看這篇文章: