1. 理論
1.1 Http和WebSocket
1.1.1 HTTP
http協議在通信過程中存在一個巨大的缺陷,通信只能由客戶端發起,服務器只能根據響應返回響應的結果。也就說,服務器端無法主動給客戶端發送消息。
對於服務器端連續的狀態變化,http協議就顯得有些力不從心了,當然也可以通過其他的方式實現。比如:
- 輪詢(每隔一段時候,就發出一個詢問,瞭解服務器有沒有新的信息)
- long poll(採用阻塞模式,客戶端發起連接後,如果沒消息,就一直不返回Response給客戶端。直到有消息才返回,返回完之後,客戶端再次建立連接,周而復始)。
雖然這樣也可以實現我們的要求,但是資源就在輪詢的過程中被大量浪費。
1.1.1 WebSocket
WebSocket協議,2008年誕生,2011年稱爲國際標準,其最大的特點就是服務器端可以主動向客戶端發送消息,實現真正的雙向平等對話。主要特點:
- 建立在tcp協議之上,服務器端的實現比較容易
- 與http協議有很好的兼容性,握手階段採用http協議
- 數據格式比較輕量,性能開銷小,通信高效
- 可以發送文本,也可以發送二進制
- 沒有同源策略(htpp的同源策略主要是出於安全考慮)
- 協議標識是ws,如ws://127.0.0.1:8080/myHandler/{Id}"
理論沒看懂的可以戳這 故事描述型
1.2 WebSocket工作方式
1.2.1 WebSocket 客戶端
創建WebSocket
var Socket = new WebSocket(url, [protocol] );//協議可以爲空
屬性
Socket.readyState//連接狀態
Socket.bufferedAmount //隊列中等待傳輸,但是還沒有發出的 UTF-8 文本字節數。
事件,編寫的時候要加上on 比如onOpen
open //連接建立時觸發
message //客戶端接收服務端數據時觸發
error //通信發生錯誤時觸發
close //連接關閉時觸發
方法
Socket.send() //使用連接發送數據
Socket.close() //關閉連接
實例:
// 初始化一個 WebSocket 對象
var ws = new WebSocket("ws://localhost:9998/echo");
// 建立 web socket 連接成功觸發事件
ws.onopen = function () {
// 使用 send() 方法發送數據
ws.send("發送數據");
alert("數據發送中...");
};
// 接收服務端數據時觸發事件
ws.onmessage = function (evt) {
var received_msg = evt.data;
alert("數據已接收...");
};
// 斷開 web socket 連接成功觸發事件
ws.onclose = function () {
alert("連接已關閉...");
};
1.2.2 WebSocket 服務器端
服務器端的就主要用代碼來實現吧
2. 實踐篇
源碼地址 密碼:f28e
服務端獲取消息很簡單,主要是向服務器端發送消息。需要向客戶端發送消息,那麼我們需要知道客戶端的某個唯一標識,那麼這個標識用什麼來表示呢,那就是session。
2.1 普通javaEE方式
直接貼碼,裏面註釋很清晰 需要的依賴
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
java源碼
package me.gacl.websocket;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
/**
* @ServerEndpoint 註解是一個類層次的註解,它的功能主要是將目前的類定義成一個websocket服務器端,
* 註解的值將被用於監聽用戶連接的終端訪問URL地址,客戶端可以通過這個URL來連接到WebSocket服務器端
*/
@ServerEndpoint("/websocket")
public class WebSocketTest {
//靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。
private static int onlineCount = 0;
//concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。若要實現服務端與單一客戶端通信的話,可以使用Map來存放,其中Key可以爲用戶標識
private static CopyOnWriteArraySet<WebSocketTest> webSocketSet = new CopyOnWriteArraySet<WebSocketTest>();
//與某個客戶端的連接會話,需要通過它來給客戶端發送數據
private Session session;
/**
* 連接建立成功調用的方法
* @param session 可選的參數。session爲與某個客戶端的連接會話,需要通過它來給客戶端發送數據
*/
@OnOpen
public void onOpen(Session session){
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在線數加1
System.out.println("有新連接加入!當前在線人數爲" + getOnlineCount());
}
/**
* 連接關閉調用的方法
*/
@OnClose
public void onClose(){
webSocketSet.remove(this); //從set中刪除
subOnlineCount(); //在線數減1
System.out.println("有一連接關閉!當前在線人數爲" + getOnlineCount());
}
/**
* 收到客戶端消息後調用的方法
* @param message 客戶端發送過來的消息
* @param session 可選的參數
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("來自客戶端的消息:" + message);
//羣發消息
for(WebSocketTest item: webSocketSet){
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
continue;
}
}
}
/**
* 發生錯誤時調用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error){
System.out.println("發生錯誤");
error.printStackTrace();
}
/**
* 這個方法與上面幾個方法不一樣。沒有用註解,是根據自己需要添加的方法。
* @param message
* @throws IOException
*/
public void sendMessage(String message) throws IOException{
this.session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketTest.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketTest.onlineCount--;
}
}
2.2 spring boot集成
這裏通過重新寫一個controller,直接給客戶端發送消息,先貼這裏的代碼,應該大部分人都是想實現這個功能。這裏的目的是,在處理一個其他請求之後,需要給原來的客戶端發送消息,告訴它我已經處理完了,收到消息之後再處理後續的邏輯(掃碼場景比較普遍)。
@GetMapping("/")
public WebsocketResponse sendSuccess(){
MyHandler send = new MyHandler();
TextMessage msg = new TextMessage("發給客戶端");
send.sendMessageToUser("888",msg);
return new WebsocketResponse(1);
}
有需要的直到源碼中拉取代碼吧,服務器端的原理都類似
2.2.1 代碼注意問題
- 這裏的session一定是需要回調的那個客戶端的session,所以第一次請求是需要保存客戶端的session,公司一般放在redis中緩存。