WebSocket是HTML5新增的協議,它的目的是在瀏覽器和服務器之間創建一個不受限的雙向通訊的通道,好比說,服務器能夠在任意時刻發送消息給瀏覽器。html
爲何傳統的HTTP協議不能作到WebSocket實現的功能?這是由於HTTP協議是一個請求-響應協議,請求必須先由瀏覽器發給服務器,服務器才能響應這個請求,再把數據發送給瀏覽器。換句話說,瀏覽器不主動請求,服務器是無法主動發數據給瀏覽器的。前端
WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只須要完成一次握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸。web
也有人說,HTTP協議其實也能實現啊,好比用輪詢或者Comet。json
輪詢是指瀏覽器經過JavaScript啓動一個定時器,而後以固定的間隔給服務器發請求,詢問服務器有沒有新消息。flask
缺點:浪費客戶端資源後端
瀏覽器和服務器之間能夠創建無限制的全雙工通訊,任何一方均可以主動發消息給對方。瀏覽器
瀏覽器經過 JavaScript 向服務器發出創建 WebSocket 鏈接的請求,鏈接創建之後,客戶端和服務器端就能夠經過 TCP 鏈接直接交換數據。服務器
當你獲取 Web Socket 鏈接後,你能夠經過 send() 方法來向服務器發送數據,並經過 onmessage 事件來接收服務器返回的數據。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
輪詢:客戶端定時向服務器發送Ajax請求,服務器接到請求後立刻返回響應信息並關閉鏈接。
長輪詢:客戶端向服務器發送Ajax請求,服務器接到請求後hold住鏈接,直到有新消息才返回響應信息並關閉鏈接(或到了設定的超時時間關閉鏈接),客戶端處理完響應信息後再向服務器發送新的請求。
長鏈接:在頁面裏嵌入一個隱蔵iframe,將這個隱蔵iframe的src屬性設爲對一個長鏈接的請求,服務器端就能源源不斷地往客戶端輸入數據。
鏈接保持 - Http 發起請求 在請求中寫一個協議 - WebSocket - 服務器收到Websocket請求 ,自動保持此鏈接 - 永久不斷開,除非主動斷開 - 能夠經過此鏈接主動找到客戶端
如下是 WebSocket 對象的屬性。假定咱們使用了以上代碼建立了 Socket 對象:
類型 | 解釋 |
---|---|
Socket.readyState | 只讀屬性 readyState 表示鏈接狀態,能夠是如下值:0 - 表示鏈接還沒有創建。1 - 表示鏈接已創建,能夠進行通訊。2 - 表示鏈接正在進行關閉。3 - 表示鏈接已經關閉或者鏈接不能打開。 |
Socket.bufferedAmount | 只讀屬性 bufferedAmount 已被 send() 放入正在隊列中等待傳輸,可是尚未發出的 UTF-8 文本字節數。 |
如下是 WebSocket 對象的相關事件。假定咱們使用了以上代碼建立了 Socket 對象:
事件 | 事件處理程序 | 描述 |
---|---|---|
open | Socket.onopen | 鏈接創建時觸發 |
message | Socket.onmessage | 客戶端接收服務端數據時觸發 |
error | Socket.onerror | 通訊發生錯誤時觸發 |
close | Socket.onclose | 鏈接關閉時觸發 |
如下是 WebSocket 對象的相關方法。假定咱們使用了以上代碼建立了 Socket 對象:
方法 | 描述 |
---|---|
Socket.send() | 使用鏈接發送數據 |
Socket.close() | 關閉鏈接 |
既然學習了websocket的使用,如今咱們就要基於flask來實現即時通訊的簡易版,也就是羣聊和私聊的網頁版,對於頁面效果,你們就別吐槽了,僅用做練習學習。
因爲是基於flask來實現的websocket,咱們須要裝flask和gevent-websocket
pip3 install flask
pip3 install gevent-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進行羣聊,能夠開多個服務器模擬多用戶羣聊,查看效果。
私聊後端代碼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>