WebSocket 理論+實踐

1. 理論

1.1 Http和WebSocket

1.1.1 HTTP

http協議在通訊過程當中存在一個巨大的缺陷,通訊只能由客戶端發起,服務器只能根據響應返回響應的結果。也就說,服務器端沒法主動給客戶端發送消息。html

對於服務器端連續的狀態變化,http協議就顯得有些力不從心了,固然也能夠經過其餘的方式實現。好比:java

  1. 輪詢(每隔一段時候,就發出一個詢問,瞭解服務器有沒有新的信息)
  2. long poll(採用阻塞模式,客戶端發起鏈接後,若是沒消息,就一直不返回Response給客戶端。直到有消息才返回,返回完以後,客戶端再次創建鏈接,周而復始)。

雖然這樣也能夠實現咱們的要求,可是資源就在輪詢的過程當中被大量浪費。web

1.1.1 WebSocket

WebSocket協議,2008年誕生,2011年稱爲國際標準,其最大的特色就是服務器端能夠主動向客戶端發送消息,實現真正的雙向平等對話。主要特色:redis

  1. 創建在tcp協議之上,服務器端的實現比較容易
  2. 與http協議有很好的兼容性,握手階段採用http協議
  3. 數據格式比較輕量,性能開銷小,通訊高效
  4. 能夠發送文本,也能夠發送二進制
  5. 沒有同源策略(htpp的同源策略主要是出於安全考慮)
  6. 協議標識是ws,如ws://127.0.0.1:8080/myHandler/{Id}"

理論沒看懂的能夠戳這 故事描述型spring

1.2 WebSocket工做方式

1.2.1 WebSocket 客戶端

建立WebSocketapi

var Socket = new WebSocket(url, [protocol] );//協議能夠爲空

屬性緩存

Socket.readyState//鏈接狀態
Socket.bufferedAmount //隊列中等待傳輸,可是尚未發出的 UTF-8 文本字節數。

事件,編寫的時候要加上on 好比onOpen安全

open //鏈接創建時觸發
message //客戶端接收服務端數據時觸發
error //通訊發生錯誤時觸發
close //鏈接關閉時觸發

方法服務器

Socket.send() //使用鏈接發送數據
Socket.close() //關閉鏈接

實例:websocket

// 初始化一個 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 代碼注意問題

  1. 這裏的session必定是須要回調的那個客戶端的session,因此第一次請求是須要保存客戶端的session,公司通常放在redis中緩存。