WebSocket知識、輪詢、長輪詢、長鏈接

1、WebSocket理論知識

1.什麼是websocket

WebSocket是HTML5新增的協議,它的目的是在瀏覽器和服務器之間創建一個不受限的雙向通訊的通道,好比說,服務器能夠在任意時刻發送消息給瀏覽器。html

爲何傳統的HTTP協議不能作到WebSocket實現的功能?這是由於HTTP協議是一個請求-響應協議,請求必須先由瀏覽器發給服務器,服務器才能響應這個請求,再把數據發送給瀏覽器。換句話說,瀏覽器不主動請求,服務器是無法主動發數據給瀏覽器的。前端

WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只須要完成一次握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸。web

2.爲何使用Websocket

也有人說,HTTP協議其實也能實現啊,好比用輪詢或者Comet。json

輪詢

輪詢是指瀏覽器經過JavaScript啓動一個定時器,而後以固定的間隔給服務器發請求,詢問服務器有沒有新消息。flask

缺點:浪費客戶端資源後端

websocket

瀏覽器和服務器之間能夠創建無限制的全雙工通訊,任何一方均可以主動發消息給對方。瀏覽器

 

瀏覽器經過 JavaScript 向服務器發出創建 WebSocket 鏈接的請求,鏈接創建之後,客戶端和服務器端就能夠經過 TCP 鏈接直接交換數據。服務器

當你獲取 Web Socket 鏈接後,你能夠經過 send() 方法來向服務器發送數據,並經過 onmessage 事件來接收服務器返回的數據。websocket

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請求有幾點不一樣:

  1. GET請求的地址不是相似/path/,而是以ws://開頭的地址;

  2. 請求頭Upgrade: websocket和Connection: Upgrade表示這個鏈接將要被轉換爲WebSocket鏈接;

  3. Sec-WebSocket-Key是用於標識這個鏈接,並不是用於加密數據;

  4. 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聊天

2、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() 關閉鏈接

3、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>
相關文章
相關標籤/搜索