WebWorker與WebSocket實現前端訊息匯流排

_楊瀚博發表於2018-08-10

Web Worker讓JS有了多執行緒的能力,可以將複雜耗時的操作都交付給Worker執行緒處理。WebSocket讓web端與服務端維持一個有效的長連線,實現服務端主動推送資料。將二者一結合,業務系統資訊流轉通知功能完全就可以剝離出來。

架構圖

WebWorker與WebSocket實現前端訊息匯流排
JS Worker

Worker工作在一個專用的作用域DedicatedWorkerGlobalScope,在這個作用域中,不能直接操作DOM節點,不能使用Window物件的預設方法和屬性。不過對於網路的訪問是完全沒有問題的。具體能使用那些物件和方法請點選這裡檢視

從上圖中可明顯的看出,Worker在當前架構中實現一個橋樑的左右,上連線socket端中的資料,下負責分發socket中的資料。此處我們先了解下Worker本身的功能實現。

  1. 主執行緒與Worker執行緒通過方法postMessage相互傳遞資訊
  2. 主執行緒與Worker執行緒通過事件onmessage接收相互傳遞的訊息
  3. Worker中引入第三方js使用方法importScripts([url,])
  4. 主執行緒呼叫worker.terminate()結束執行緒
  5. Worker執行緒通過呼叫this.close()結束自身執行緒

新建一個webworker.js檔案,並在其中編寫如下程式碼

//author:herbert qq:464884492
onmessage = function (event) {
    if (event.data.code) {
        var code = event.data.code.toLowerCase();
        switch (code) {
            case "init":
                var userId = event.data.loggedUserId;
                var sessionId = event.data.sessionid;
                if (!sessionId) {
                    this.close();
                    return;
                }
                postMessage({ code: "codeone", msg: "你好,元件1" });
                postMessage({ code: "codetwo", msg: "你好,元件2" });
                break;
            default:
                break;
        }
    }
}
複製程式碼

注意:在 onmessage 前不能加var否則在IE下會接收不了訊息。IE真是讓人充滿挫敗感的瀏覽器

新建一個index.html頁面,在script塊中編寫以下程式碼,實現與webworker.js通訊

//author:herbert qq:464884492
var work = new Worker('webworker.js')
    , textone = document.querySelector("#textone")
    , textTwo = document.querySelector("#texttwo")
      textAll = document.querySelector("#textAll");

work.onmessage = function (event) {
    var data = event.data;
    if (!!data.code) {
        switch (data.code) {
            case "close":
                work.terminate();
            case "codeone":
                textone.value = textone.value + JSON.stringify(data) + "\r\n";
                textAll.value = textAll.value + JSON.stringify(data) + "\r\n";
                break;
            case "codetwo":
                textTwo.value = textTwo.value + JSON.stringify(data) + "\r\n";
                textAll.value = textAll.value + JSON.stringify(data) + "\r\n";
                break;
            default:
                textAll.value = textAll.value + JSON.stringify(data) + "\r\n";
        }
    }
};
work.postMessage({
    code: "init",
    loggedUserId: 'demo',
    sessionid: 'demo'
});
複製程式碼

JS WebSocket

WebSocket和Http一樣都是基於Tcp協議。不同是WebSocket實現了服務端與客戶端的全雙工通訊。在Websocket未出現之前,要是實現一個資訊推送的功能,通過http來實現唯一方案就是輪訓,輪訓分長短,各有弊端。現在WebSocket一出現,一切都好辦了。

接下來我們開始建立一個WebSocket連線

方法中的root表示當前作用域,在主執行緒是root=window,在WebWorker執行緒root=DedicatedWorkerGlobalScope

    //author:herbert qq:464884492
    var root = this,socket =null;
    function connect(wsurl) {
        if ('WebSocket' in root) {
            socket = new WebSocket(wsurl);
        } else if ('MozWebSocket' in root) {
            socket = new MozWebSocket(wsurl);
        } else {
            alert("您的瀏覽器版本過低,將不能接收系統訊息");
        }
    }
複製程式碼

wsurl格式為ws:\\ 或者 wss:\\,後者表示SSL加密傳輸。實際地址如: ws://localhost:8090/demo/demowebsocket 接下來,我們需要為socket處理事件,負責接收服務端推送的訊息

   //author:herbert qq:464884492
   function onOpen() {
        postMessage({ code: "openConnect" });
    }
    function onClose() {
        postMessage({ code: "closewsconnect" });
    }
    function onMessaage(event) {
        postMessage(JSON.parse(event.data));
    }
    function onError(event) {
        socket = null;
        if (event.target.readyState == 3) {
            //斷線重連
            setTimeout(function () {
                connect(event.target.url);
                initMessageEvent();
            }, 1000);
        }
    }
    function sendMessage(msg) {
        if (socket == null) return;
        socket.send(msg);
    }
 function initMessageEvent() {
        socket.onopen = onOpen; //socket連線成功處理事件
        socket.onclose = onClose; //socket連線關閉處理事件
        socket.onmessage = onMessaage; //socket接收到新訊息
        socket.onerror = onError; //soket錯誤處理事件
    }
複製程式碼

JAVA WebSocket

Tomcat7x已經實現了標準WebScoket介面,在專案中只需要編寫一個普通的實體bean配置註解就可以實現一個標準的WebSocket Api。開發中主要使用一些註解

  • @ServerEndpoint 設定WebSocket連線地址,以及url引數 如: @ServerEndpoint(value = "/demowebsocket/{userId}/{sessionId}"),其中{userId}、{sessionId} 為pathParam可以在onOpen函式中通過函式引數 @PathParam 獲取
  • @PathParam 獲取URL地址上對應的註解引數
  • @OnOpen 建立連線註解
  • @OnClose 關閉連線註解
  • @OnMessage 接收訊息註解
  • @OnError 錯誤註解

被註解約束的函式都可以任意選擇需要的引數,可選擇的引數有 Session、EndpointConfig 以及 @PathParam, 服務端Bean程式碼如下

//author:herbert qq:464884492
@ServerEndpoint(value = "/demowebsocket/{userId}/{sessionId}")
public class DemoWebSokcet {
	private static final Set<DemoWebSokcet> connections = new CopyOnWriteArraySet<DemoWebSokcet>();
	private Session session;
	public DemoWebSokcet() {
	}

	@OnOpen
	public void openConnection(Session session, EndpointConfig conf,
			@PathParam("userId") String userId,
			@PathParam("sessionId") String sessionId) {
		this.session = session;
		connections.add(this);
		JSONObject jo = new JSONObject();
		jo.put("code", "newuser");
		jo.put("userid", userId);
		jo.put("sessionid", sessionId);
		jo.put("msg", "server:新連線使用者");
		sendMessage(jo);

		// 測試 程式碼
		JSONObject jo1 = new JSONObject();
		jo1.put("code", "codeone");
		jo1.put("userid", userId);
		jo1.put("sessionid", sessionId);
		jo1.put("msg", "Server:元件1你好");
		sendMessage(jo1);

		JSONObject jo2 = new JSONObject();
		jo2.put("code", "codetwo");
		jo2.put("userid", userId);
		jo2.put("sessionid", sessionId);
		jo2.put("msg", "server:元件2你好");
		sendMessage(jo2);
	}

	@OnClose
	public void closeConnection(@PathParam("userId") String userId,
			@PathParam("sessionId") String sessionId) {
		connections.remove(this);
		JSONObject jo = new JSONObject();
		jo.put("code", "connectionClose");
		jo.put("userid", userId);
		jo.put("sessionid", sessionId);
		jo.put("msg", "server:連線關閉");
		sendMessage(jo);
	}

	// 處理文字訊息
	@OnMessage
	public void handleTextMsg(Session session, String message,
			@PathParam("userId") String userId,
			@PathParam("sessionId") String sessionId) {
		System.out.println("userId=>" + userId + " sessionId=>" + sessionId);
		// 原樣轉發客戶端訊息
		sendMessage(JSONObject.parseObject(message));
	}

	// 處理二進位制訊息
	@OnMessage
	public void handleBinaryMsg(Session session, ByteBuffer msg,
			@PathParam("userId") String userId,
			@PathParam("sessionId") String sessionId) {

	}

	// 處理pong訊息
	@OnMessage
	public void handlePongMsg(Session session, PongMessage msg,
			@PathParam("userId") String userId,
			@PathParam("sessionId") String sessionId) {
		JSONObject jo = new JSONObject();
		jo.put("code", "pong");
		jo.put("userid", userId);
		jo.put("sessionid", sessionId);
		jo.put("msg", msg.getApplicationData().toString());
		sendMessage(jo);
	}

	@OnError
	public void onError(Throwable t, @PathParam("userId") String userId,
			@PathParam("sessionId") String sessionId) throws Throwable {
		JSONObject jo = new JSONObject();
		jo.put("code", "servererror");
		jo.put("userid", userId);
		jo.put("sessionid", userId);
		jo.put("msg", t.getMessage());
		sendMessage(jo);
	}

	private static void sendMessage(JSONObject msg) {
		for (DemoWebSokcet client : connections) {
			try {
				synchronized (client) {
					client.session.getBasicRemote()
							.sendText(msg.toJSONString());
				}
			} catch (IOException e) {
				JSONObject jo = new JSONObject();
				jo.put("code", "servererror");
				jo.put("userid",
						client.session.getPathParameters().get("userid"));
				jo.put("sessionid",
						client.session.getPathParameters().get("sessionid"));
				connections.remove(client);
				try {
					client.session.close();
				} catch (IOException e1) {
				}

				jo.put("msg", "server:傳送訊息出現異常,連線已關閉" + e.getMessage());
				sendMessage(jo);
			}
		}
	}
}
複製程式碼

在測試程式碼編寫過程中,通過pom方式引入javax.websocket-api,啟動後始終出現 Error during WebSocket handshake: Unexpected response code: 404連線錯誤,後來通過直接件tomcat/bin下對應的tomcat實現的jar複製到webapp對應的bin資料夾下解決問題。

Demo預覽

WebWorker與WebSocket實現前端訊息匯流排

總結

篇幅比較長,讀到這裡也不容易!WebWorker和WebSocket我也是第一次將二者結合起來。感覺現在javascript功能真的是越來越豐富了。demo地址,還有一點感悟,對於開發中的新知識點,首先你得學會怎麼用,其次在通過閱讀原始碼,以及理論知識讓你使用的更順利,甚至改變它。

相關文章