1. 程式人生 > >WebSocket 理論+實踐

WebSocket 理論+實踐

1. 理論

1.1 Http和WebSocket

1.1.1 HTTP

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

對於伺服器端連續的狀態變化,http協議就顯得有些力不從心了,當然也可以通過其他的方式實現。比如:

  1. 輪詢(每隔一段時候,就發出一個詢問,瞭解伺服器有沒有新的資訊)
  2. long poll(採用阻塞模式,客戶端發起連線後,如果沒訊息,就一直不返回Response給客戶端。直到有訊息才返回,返回完之後,客戶端再次建立連線,周而復始)。

雖然這樣也可以實現我們的要求,但是資源就在輪詢的過程中被大量浪費。

1.1.1 WebSocket

WebSocket協議,2008年誕生,2011年稱為國際標準,其最大的特點就是伺服器端可以主動向客戶端傳送訊息,實現真正的雙向平等對話。主要特點:

  1. 建立在tcp協議之上,伺服器端的實現比較容易
  2. 與http協議有很好的相容性,握手階段採用http協議
  3. 資料格式比較輕量,效能開銷小,通訊高效
  4. 可以傳送文字,也可以傳送二進位制
  5. 沒有同源策略(htpp的同源策略主要是出於安全考慮)
  6. 協議標識是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 程式碼注意問題

  1. 這裡的session一定是需要回調的那個客戶端的session,所以第一次請求是需要儲存客戶端的session,公司一般放在redis中快取。