一、前言
近期項目里需做一個在線聊天功能,就想要在對話的時候建立socket鏈接。又因為聊天只是其中一個部分,在它外面還有一些全局的消息通知需要接收,因此也需要建立socket鏈接。在該項目里不僅一處用到了socket,就想着封裝一個socket的,可以在項目里調用。
之前也用過一次websocket,但那次是直接用的socke.io,我也忘了這次為啥沒有繼續使用,對這個也一知半解,似懂非懂,先一點一點記起來。具體是介紹和解釋就不寫了,主要寫幾個幫助理解的部分。
二、HTML5 WebSocket
WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。
2.1、創建WebSocket 實例
WebSocket 對象作為一個構造函數,用於新建 WebSocket 實例。
var Socket = new WebSocket(url,[protocol]);
以上代碼用於創建 WebSocket 對象,其中的第一個參數 url, 即為指定連接的 URL。第二個參數 protocol 是可選的,指定了可接受的子協議。一般來說,大多沒有具體要求的,就只用寫url即可。
2.2 websocket屬性
例如:var ws = new WebSocket('ws://1xx.xxx.xxx.xxx:8080/ws');
打印出創建的socket對象,內容如下,這些也正是wensocket的屬性。
binaryType: "blob" //返回websocket連接所傳輸二進制數據的類型,如果傳輸的是Blob類型的數據,則為"blob",如果傳輸的是Arraybuffer類型的數據,則為"arraybuffer"
bufferedAmount: 0 //為只讀屬性,用於返回已經被send()方法放入隊列中但還沒有被發送到網絡中的數據的字節數。
extensions: ""
onclose: ƒ () //連接關閉時觸發
onerror: ƒ () //通信發生錯誤時觸發
onmessage: ƒ (e) //客戶端接收服務端數據時觸發,e為接受的數據對象
onopen: ƒ () //連接建立時觸發
protocol: "" //用於返回服務器端選中的子協議的名字;這是一個在創建websocket對象時,在參數protocols
中指定的字符串。
readyState: 1 //返回值為當前websocket的鏈接狀態
url: "ws://1xx.xxx.xxx.xxx:8080/ws" //返回值為當構造函數創建WebSocket實例對象時URL的絕對路徑。
所有屬性講解清單:https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
2.3 websocket屬性之readyState
readyState返回當前websocket的鏈接狀態,共有4種。可根據具體項目的需求來利用此狀態,寫對應的需求。
CONNECTING:值為0,表示正在連接。
OPEN: 值為1,表示連接成功,可以通信了。
CLOSING: 值為2,表示連接正在關閉。
CLOSED: 值為3,表示連接已經關閉,或者打開連接失敗。
2.4 websocket的方法
假定我們使用了上述代碼創建的websocket對象,下面兩個方法可以在任意socket事件中調用,根據自己項目需求而定。
ws.send() :使用連接發送數據,可以發送你想要發送的各種類型數據,如Blob對象、ArrayBuffer 對象、基本或復雜的數據類型等;
ws.send('消息'); //發送對象需要格式轉換,接受數據同理 ws.send(JSON.stringify(data));
ws.close() : 關閉連接,用戶可以主動調取此方法,來關閉連接。
三、封裝websocket
可在項目中定義一個socket.js文件,在需要建立socket的頁面引入此js文件,即可在一個項目中創建多個socket連接。
var webSocket = null; var globalCallback = null;//定義外部接收數據的回調函數 //初始化websocket function initWebSocket(url) { if ("WebSocket" in window) { webSocket = new WebSocket(url);//創建socket對象 console.log(webSocket) } else { alert("該瀏覽器不支持websocket!"); } //打開 webSocket.onopen = function() { webSocketOpen(); }; //收信 webSocket.onmessage = function(e) { webSocketOnMessage(e); }; //關閉 webSocket.onclose = function() { webSocketClose(); }; //連接發生錯誤的回調方法 webSocket.onerror = function() { console.log("WebSocket連接發生錯誤"); }; } //連接socket建立時觸發 function webSocketOpen() { if (e === "LOGIN") { const data = { type: "CONNECT", token: sessionStorage.getItem("token") || "" }; sendSock(data, function() {}); } console.log("WebSocket連接成功"); } //客戶端接收服務端數據時觸發,e為接受的數據對象 function webSocketOnMessage(e) { const data = JSON.parse(e.data);//根據自己的需要對接收到的數據進行格式化 globalCallback(data);//將data傳給在外定義的接收數據的函數,至關重要。 /*在此函數中還可以繼續根據項目需求來寫其他東西。 比如我的項目里需要根據接收的數據來判斷用戶登錄是否失效了,此時需要關閉此連接,跳轉到登錄頁去。*/ } //發送數據 function webSocketSend(data) { webSocket.send(JSON.stringify(data));//在這里根據自己的需要轉換數據格式 } //關閉socket function webSocketClose() { //因為我建立了多個socket,所以我需要知道我關閉的是哪一個socket,就做了一些判斷。 if ( webSocket.readyState === 1 && webSocket.url === "ws://1xx.xx.xx.xxx:8088/ws" ) { webSocket.close();//這句話是關鍵,之前我忘了寫,一直沒有真正的關閉socket console.log("對話連接已關閉"); } } //在其他需要socket地方調用的函數,用來發送數據及接受數據 function sendSock(agentData, callback) { globalCallback = callback;//此callback為在其他地方調用時定義的接收socket數據的函數,此關重要。 //下面的判斷主要是考慮到socket連接可能中斷或者其他的因素,可以重新發送此條消息。 switch (webSocket.readyState) { //CONNECTING:值為0,表示正在連接。 case webSocket.CONNECTING: setTimeout(function() { webSocketSend(agentData, callback); }, 1000); break; //OPEN:值為1,表示連接成功,可以通信了。 case webSocket.OPEN: webSocketSend(agentData); break; //CLOSING:值為2,表示連接正在關閉。 case webSocket.CLOSING: setTimeout(function() { webSocketSend(agentData, callback); }, 1000); break; //CLOSED:值為3,表示連接已經關閉,或者打開連接失敗。 case webSocket.CLOSED: // do something break; default: // this never happens break; } } //將初始化socket函數、發送(接收)數據的函數、關閉連接的函數export出去 export default { initWebSocket, webSocketClose, sendSock };
四、使用此socketJs文件(主要在vue項目中使用封裝的socket)
因為我項目里涉及多出頁面需要使用socket,所以我將此文件在main.js中,將其定義在原型上,使其在每個 Vue 的實例中均可使用。不用單獨在每個頁面都引入一次。
1、部分main.js代碼
import socketApi from "./tool/socket";//找到封裝的socket.js文件
Vue.prototype.socketApi = socketApi;//將其掛在原型上,這樣 $socketApi就在所有的 Vue 實例中可用了。
2、某一vue頁面
<template> <div>在此頁面使用封裝的socket</div> </template> <script> export default { name: "Message", data() { return { wsUrl: " ws://172.16.10.140:8088/ws",//定義socket連接地址 wsType: "CONNECT" }; }, methods: { // 接收socket回調函數返回數據的方法 getConfigResult(res) { console.log(res);//服務端返回的數據 }, websocketSend(data) { //data為要發送的數據,this.getConfigResult為回調函數,用於在此頁面接收socket返回的數據。 //至關重要!我一開始沒寫這個,就蒙了,咋才能到拿到回來的數據呢。 this.socketApi.sendSock(data, this.getConfigResult); }, }, beforeRouteLeave(to, from, next) { //在離開此頁面的時候主動關閉socket this.socketApi.webSocketClose(); next(); }, created() { //建立socket連接 this.socketApi.initWebSocket(this.wsUrl); //data為和后端商量好的數據格式 const data = { type:this.wsType, msg: "說的話", }; this.websocketSend(data); } }; </script> <style lang="scss" scoped> </style>
五、遇到的問題
1、websocket Failed to execute 'send' on 'WebSocket': Still in CONNECTING state
一開始我只是初始化了socket,並沒有發送消息過去。於是websocket 實例化后(我以為的建立成功了),就立馬發送數據,就報了這個錯誤,說“正在連接”。其實我以為的建立成功是我看到了我在連接成功后的回調函數里打印的一句話:“WebSocket連接成功”,到底是什么正在連接還是連接成功我也不知道,所以需要在發送數據前,利用socket.readyState先判斷此時的連接狀態。
function sendSock(agentData, callback) { globalCallback = callback;//此callback為在其他地方調用時定義的接收socket數據的函數,此關重要。 //此處先判斷socket連接狀態 switch (webSocket.readyState) { //CONNECTING:值為0,表示正在連接。 case webSocket.CONNECTING: setTimeout(function() { webSocketSend(agentData, callback); }, 1000); break; //OPEN:值為1,表示連接成功,可以通信了。 case webSocket.OPEN: webSocketSend(agentData); break; //CLOSING:值為2,表示連接正在關閉。 case webSocket.CLOSING: setTimeout(function() { webSocketSend(agentData, callback); }, 1000); break; //CLOSED:值為3,表示連接已經關閉,或者打開連接失敗。 case webSocket.CLOSED: // do something break; default: // this never happens break; } }
2、后端要求在建立連接的時候先發送一條數據,用於確定當前連接的狀態
在此項目里,因為建立的socket需要有不同的連接用途,所以后端要在建立連接的時候給它發消息,確定建立socket,等此條消息發送完以后,再發送一條數據確定開始對話的狀態。當時我在疑惑,我都socket還沒建立,怎么給你發消息呢,我在什么時候發給你呢?
解決辦法是在open函數里,發送一條消息過去。
function webSocketOpen() { //在此次定義好需要傳過去的數據,先發送一個數據過去,data為與后端協議的數據類型 const data = { type: "CONNECT", token: sessionStorage.getItem("token") || "" }; sendSock(data, function() {});//調用發送數據的函數 console.log("WebSocket連接成功"); }
六、其他未考慮的地方
1、因為socket連接也會因為各種因素中斷,所以有人寫了“保活”:保活的原理-->心跳,前端每隔一段時間發送一段約定好的message給后端,后端收到后返回一段約定好的message給前端,如果多久沒收到前端就調用重連方法進行重連。詳見:https://www.jianshu.com/p/a4eacaf8de17
2、關於socket.io使用介紹,https://www.cnblogs.com/dreamsqin/p/12018866.html