WebSocket是HTML5新增的協議,它的目的是在瀏覽器和服務器之間建立一個不受限的雙向通信的通道,比如說,服務器可以在任意時刻發送消息給瀏覽器。
為什么傳統的HTTP協議不能做到WebSocket實現的功能?這是因為HTTP協議是一個請求-響應協議,請求必須先由瀏覽器發給服務器,服務器才能響應這個請求,再把數據發送給瀏覽器。換句話說,瀏覽器不主動請求,服務器是沒法主動發數據給瀏覽器的。
WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。
2.為什么使用Websocket
也有人說,HTTP協議其實也能實現啊,比如用輪詢或者Comet。
輪詢
輪詢是指瀏覽器通過JavaScript啟動一個定時器,然后以固定的間隔給服務器發請求,詢問服務器有沒有新消息。
缺點:浪費客戶端資源
websocket
瀏覽器和服務器之間可以建立無限制的全雙工通信,任何一方都可以主動發消息給對方。
瀏覽器通過 JavaScript 向服務器發出建立 WebSocket 連接的請求,連接建立以后,客戶端和服務器端就可以通過 TCP 連接直接交換數據。
當你獲取 Web Socket 連接后,你可以通過 send() 方法來向服務器發送數據,並通過 onmessage 事件來接收服務器返回的數據。
3.websocket協議
WebSocket並不是全新的協議,而是利用了HTTP協議來建立連接。我們來看看WebSocket連接是如何創建的。
首先,WebSocket連接必須由瀏覽器發起,因為請求協議是一個標准的HTTP請求,格式如下:
GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13
該請求和普通的HTTP請求有幾點不同:
-
GET請求的地址不是類似/path/,而是以ws://開頭的地址;
-
請求頭Upgrade: websocket和Connection: Upgrade表示這個連接將要被轉換為WebSocket連接;
-
Sec-WebSocket-Key是用於標識這個連接,並非用於加密數據;
-
Sec-WebSocket-Version指定了WebSocket的協議版本。
隨后,服務器如果接受該請求,就會返回如下響應:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string
該響應代碼101表示本次連接的HTTP協議即將被更改,更改后的協議就是Upgrade: websocket指定的WebSocket協議。
版本號和子協議規定了雙方能理解的數據格式,以及是否支持壓縮等等。如果僅使用WebSocket的API,就不需要關心這些。
現在,一個WebSocket連接就建立成功,瀏覽器和服務器就可以隨時主動發送消息給對方。消息有兩種,一種是文本,一種是二進制數據。通常,我們可以發送JSON格式的文本,這樣,在瀏覽器處理起來就十分容易。
瀏覽器
很顯然,要支持WebSocket通信,瀏覽器得支持這個協議,這樣才能發出ws://xxx的請求。目前,支持WebSocket的主流瀏覽器如下:
-
Chrome
-
Firefox
-
IE >= 10
-
Sarafi >= 6
-
Android >= 4.4
-
iOS >= 8
4.輪詢、長輪詢、長連接概念
輪詢:客戶端定時向服務器發送Ajax請求,服務器接到請求后馬上返回響應信息並關閉連接。
- 優點:后端程序編寫比較容易。
- 缺點:請求中有大半是無用,浪費帶寬和服務器資源。(而每一次的 HTTP 請求和應答都帶有完整的 HTTP 頭信息,這就增加了每次傳輸的數據量)
- 實例:適於小型應用。
長輪詢:客戶端向服務器發送Ajax請求,服務器接到請求后hold住連接,直到有新消息才返回響應信息並關閉連接(或到了設定的超時時間關閉連接),客戶端處理完響應信息后再向服務器發送新的請求。
- 優點:在無消息的情況下不會頻繁的請求,節省了網絡流量,解決了服務端一直疲於接受請求的窘境
- 缺點:服務器hold連接會消耗資源,需要同時維護多個線程,服務器所能承載的TCP連接數是有上限的,這種輪詢很容易把連接數頂滿。
- 實例:WebQQ、Hi網頁版、Facebook IM。
長連接:在頁面里嵌入一個隱蔵iframe,將這個隱蔵iframe的src屬性設為對一個長連接的請求,服務器端就能源源不斷地往客戶端輸入數據。
連接保持 - Http 發起請求 在請求中寫一個協議 - WebSocket - 服務器收到Websocket請求 ,自動保持此連接 - 永久不斷開,除非主動斷開 - 可以通過此連接主動找到客戶端
- 優點:消息即時到達,不發無用請求。
- 缺點:服務器維護一個長連接會增加開銷。
- 實例:Gmail聊天
二、WebSocket的相關方法
1.WebSocket 屬性
以下是 WebSocket 對象的屬性。假定我們使用了以上代碼創建了 Socket 對象:
類型 | 解釋 |
---|---|
Socket.readyState | 只讀屬性 readyState 表示連接狀態,可以是以下值:0 - 表示連接尚未建立。1 - 表示連接已建立,可以進行通信。2 - 表示連接正在進行關閉。3 - 表示連接已經關閉或者連接不能打開。 |
Socket.bufferedAmount | 只讀屬性 bufferedAmount 已被 send() 放入正在隊列中等待傳輸,但是還沒有發出的 UTF-8 文本字節數。 |
2.WebSocket事件
以下是 WebSocket 對象的相關事件。假定我們使用了以上代碼創建了 Socket 對象:
事件 | 事件處理程序 | 描述 |
---|---|---|
open | Socket.onopen | 連接建立時觸發 |
message | Socket.onmessage | 客戶端接收服務端數據時觸發 |
error | Socket.onerror | 通信發生錯誤時觸發 |
close | Socket.onclose | 連接關閉時觸發 |
3.WebSocket 方法
以下是 WebSocket 對象的相關方法。假定我們使用了以上代碼創建了 Socket 對象:
方法 | 描述 |
---|---|
Socket.send() | 使用連接發送數據 |
Socket.close() | 關閉連接 |
三、Websocket實戰練習
既然學習了websocket的使用,現在我們就要基於flask來實現即時通信的簡易版,也就是群聊和私聊的網頁版,對於頁面效果,大家就別吐槽了,僅用作練習學習。
環境包准備
由於是基於flask來實現的websocket,我們需要裝flask和gevent-websocket
pip3 install flask
pip3 install gevent-websocket
1.websocket實現群聊代碼
群聊后端代碼groupChat.py
from flask import Flask,render_template,request from geventwebsocket.handler import WebSocketHandler # 提供ws協議處理 from geventwebsocket.server import WSGIServer # 承載服務 from geventwebsocket.websocket import WebSocket # 提供語法提示 app = Flask(__name__) user_socket_dict = {} # 群發消息視圖 @app.route("/groupchat/<username>") def groupChat(username): # 獲取當前客戶端與服務器的socket連接 user_socket = request.environ.get("wsgi.websocket") # type: WebSocket if user_socket: # 保存鏈接到字典中,用戶名作為鍵 user_socket_dict[username] = user_socket print(len(user_socket_dict),user_socket_dict) while 1: msg = user_socket.receive() # 接受每個客戶端的消息 # 遍歷字典,給每一個客戶端發送消息 for name,socket in user_socket_dict.items(): try: socket.send(msg) except: pass # 獲取聊天頁面視圖 @app.route("/chat") def chat(): return render_template("groupChat.html") if __name__ == '__main__': # 通過WSGIServer來啟動web服務,並指定用WebSocketHandler來處理websocket的請求 server = WSGIServer(("0.0.0.0",9527),app,handler_class=WebSocketHandler) server.serve_forever()
前端頁面groupChat.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div style="text-align: center"> <p id="login-tag"> <input type="text" id="username"> <button id="login-btn" onclick="login()">登錄</button> </p> <p> <input type="text" id="content"> <button id="send-btn" onclick="sendMsg()">發送</button> </p> </div> <div id="chat_list"></div> </body> <script> var ws = null; // 登錄建立websocket鏈接函數 function login() { var username = document.getElementById("username").value; console.log(username); var tag = document.getElementById("login-tag"); tag.style.display = "none"; // 登錄后隱藏登錄框 ws = new WebSocket("ws://192.168.16.13:9527/groupchat/"+username); // 監聽電話 ws.onmessage = function (messageEvent) { // 獲取服務器發送過來的數據 var msg = messageEvent.data; // 對json字符串進行反序列化 msg_dic = JSON.parse(msg); var p = document.createElement("p"); p.innerText = msg_dic.from_user + ":" + msg_dic.info; // 添加聊天記錄到頁面中 document.getElementById("chat_list").appendChild(p) }; } // 發送消息的函數 function sendMsg() { // 原生js獲取數據 var username = document.getElementById("username").value; var content = document.getElementById("content").value; // 把數據封裝在自定義對象中 var msg = { from_user:username, info:content }; // 通過websocket鏈接發送數據 ws.send(JSON.stringify(msg)); } </script> </html>
啟動flask項目,訪問頁面192.168.16.13:9527/chat進行群聊,可以開多個服務器模擬多用戶群聊,查看效果。
2.websocket實現私聊實戰
私聊后端代碼privateChat.py
from flask import Flask,render_template,request from geventwebsocket.handler import WebSocketHandler from geventwebsocket.server import WSGIServer from geventwebsocket.websocket import WebSocket import json app = Flask(__name__) user_socket_dict = {} # 用戶私聊視圖 @app.route("/privateChat/<username>") def privateChat(username): # 獲取客戶端和服務器之間的鏈接 user_socket = request.environ.get("wsgi.websocket") # type: WebSocket if user_socket: # 保存鏈接到字典中,用戶名作為鍵 user_socket_dict[username] = user_socket print(len(user_socket_dict), user_socket_dict) while True: # 獲取客戶端發送的信息 msg = user_socket.receive() msg_dic = json.loads(msg) to_user = msg_dic.get("to_user") # 獲取目標用戶名 to_user_socket = user_socket_dict.get(to_user) # 根據用戶名獲取用戶的鏈接 to_user_socket.send(msg) # 給目標用戶發送信息 # 獲取聊天頁面視圖 @app.route("/chat") def chat(): return render_template("privateChat.html") if __name__ == '__main__': # 通過WSGIServer來啟動web服務,並指定用WebSocketHandler來處理websocket的請求 server = WSGIServer(("0.0.0.0",8520),app,handler_class=WebSocketHandler) server.serve_forever()
私聊前端代碼privateChat.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div style="text-align: center"> <p id="login-tag"> <input type="text" id="username"> <button id="login-btn" onclick="login()">登錄</button> </p> <p> <input type="text" id="content"> <button id="send-btn" onclick="sendMsg()">發送</button> </p> </div> <div id="chat_list"></div> </body> <script> var ws = null; // 登錄建立websocket鏈接函數 function login() { var username = document.getElementById("username").value; console.log(username); var tag = document.getElementById("login-tag"); tag.style.display = "none"; // 登錄后隱藏登錄框 ws = new WebSocket("ws://192.168.16.13:9527/groupchat/"+username); // 監聽電話 ws.onmessage = function (messageEvent) { // 獲取服務器發送過來的數據 var msg = messageEvent.data; // 對json字符串進行反序列化 msg_dic = JSON.parse(msg); var p = document.createElement("p"); p.innerText = msg_dic.from_user + ":" + msg_dic.info; // 添加聊天記錄到頁面中 document.getElementById("chat_list").appendChild(p) }; } // 發送消息的函數 function sendMsg() { // 原生js獲取數據 var username = document.getElementById("username").value; var content = document.getElementById("content").value; // 把數據封裝在自定義對象中 var msg = { from_user:username, info:content }; // 通過websocket鏈接發送數據 ws.send(JSON.stringify(msg)); } </script> </html>