python之WebSocket協議

1、WebSocket理論部分

一、websocket是什麼javascript

Websocket是html5提出的一個協議規範,參考rfc6455。php

websocket約定了一個通訊的規範,經過一個握手的機制,客戶端(瀏覽器)和服務器(webserver)之間能創建一個相似tcp的鏈接,從而方便c-s之間的通訊。在websocket出現以前,web交互通常是基於http協議的短鏈接或者長鏈接。html

WebSocket是爲解決客戶端與服務端實時通訊而產生的技術。websocket協議本質上是一個基於tcp的協議,是先經過HTTP/HTTPS協議發起一條特殊的http請求進行握手後建立一個用於交換數據的TCP鏈接,此後服務端與客戶端經過此TCP鏈接進行實時通訊。前端

注意:此時再也不須要原HTTP協議的參與了html5

二、websocket的優勢java

之前web server實現推送技術或者即時通信,用的都是輪詢(polling),在特色的時間間隔(好比1秒鐘)由瀏覽器自動發出請求,將服務器的消息主動的拉回來,在這種狀況下,咱們須要不斷的向服務器發送請求,然而HTTP request 的header是很是長的,裏面包含的數據可能只是一個很小的值,這樣會佔用不少的帶寬和服務器資源。jquery

而最比較新的技術去作輪詢的效果是Comet – 用了AJAX。但這種技術雖然可達到全雙工通訊,但依然須要發出請求(reuqest)。web

WebSocket API最偉大之處在於服務器和客戶端能夠在給定的時間範圍內的任意時刻,相互推送信息。 瀏覽器和服務器只須要要作一個握手的動做,在創建鏈接以後,服務器能夠主動傳送數據給客戶端,客戶端也能夠隨時向服務器發送數據。 此外,服務器與客戶端之間交換的標頭信息很小。算法

WebSocket並不限於以Ajax(或XHR)方式通訊,由於Ajax技術須要客戶端發起請求,而WebSocket服務器和客戶端能夠彼此相互推送信息;json

所以從服務器角度來講,websocket有如下好處:

    1. 節省每次請求的header
      http的header通常有幾十字節
    2. Server Push
      服務器能夠主動傳送數據給客戶端

三、websocket的協議規範

1.基於flash的握手協議

使用場景是IE的多數版本,由於IE的多數版本不都不支持WebSocket協議,以及FF、CHROME等瀏覽器的低版本,尚未原生的支持WebSocket。此處,server惟一要作的,就是準備一個WebSocket-Location域給client,沒有加密,可靠性不好。

2.基於md5加密方式的握手協議

其中 Sec-WebSocket-Key1,Sec-WebSocket-Key2 和 [8-byte security key] 這幾個頭信息是web server用來生成應答信息的來源,依據 draft-hixie-thewebsocketprotocol-76 草案的定義。
web server基於如下的算法來產生正確的應答信息:

1. 逐個字符讀取 Sec-WebSocket-Key1 頭信息中的值,將數值型字符鏈接到一塊兒放到一個臨時字符串裏,同時統計全部空格的數量;
2. 將在第(1)步裏生成的數字字符串轉換成一個整型數字,而後除以第(1)步裏統計出來的空格數量,將獲得的浮點數轉換成整數型;
3. 將第(2)步裏生成的整型值轉換爲符合網絡傳輸的網絡字節數組;
4. 對 Sec-WebSocket-Key2 頭信息一樣進行第(1)到第(3)步的操做,獲得另一個網絡字節數組;
5. 將 [8-byte security key] 和在第(3)、(4)步裏生成的網絡字節數組合併成一個16字節的數組;
6. 對第(5)步生成的字節數組使用MD5算法生成一個哈希值,這個哈希值就做爲安全密鑰返回給客戶端,以代表服務器端獲取了客戶端的請求,贊成建立websocket鏈接

3.基於sha加密方式的握手協議

也是目前見的最多的一種方式,這裏的版本號目前是須要13以上的版本。

客戶端請求:

GET /ls HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: www.qixing318.com
Sec-WebSocket-Origin: http://www.qixing318.com
Sec-WebSocket-Key: 2SCVXUeP9cTjV+0mWB8J6A==
Sec-WebSocket-Version: 13

服務器返回:

HTTP/1.1 101 Switching Protocols 
Upgrade: websocket Connection:
Upgrade Sec-WebSocket-Accept: mLDKNeBNWz6T9SxU+o0Fy/HgeSw=

其中 server就是把客戶端上報的key拼上一段GUID( 「258EAFA5-E914-47DA-95CA-C5AB0DC85B11″),拿這個字符串作SHA-1 hash計算,而後再把獲得的結果經過base64加密,最後再返回給客戶端。

-格式:\r\n

-建立連接以後默認不斷開

四、基於sha加密的Opening Handshake(握手環節)

客戶端發起鏈接Handshake請求

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

服務器端響應:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
    • Upgrade:WebSocket
      表示這是一個特殊的 HTTP 請求,請求的目的就是要將客戶端和服務器端的通信協議從 HTTP 協議升級到 WebSocket 協議。
    • Sec-WebSocket-Key
      是一段瀏覽器base64加密的密鑰,server端收到後須要提取Sec-WebSocket-Key 信息,而後加密。
    • Sec-WebSocket-Accept
      服務器端在接收到的Sec-WebSocket-Key密鑰後追加一段神奇字符串「258EAFA5-E914-47DA-95CA-C5AB0DC85B11」,並將結果進行sha-1哈希,而後再進行base64加密返回給客戶端(就是Sec-WebSocket-Key)。 好比:

      function encry($req) { $key = $this->getKey($req); $mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; # 將 SHA-1 加密後的字符串再進行一次 base64 加密 return base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)); }
      若是加密算法錯誤,客戶端在進行校檢的時候會直接報錯。若是握手成功,則客戶端側會出發onopen事件。
    • Sec-WebSocket-Protocol
      表示客戶端請求提供的可供選擇的子協議,及服務器端選中的支持的子協議,「Origin」服務器端用於區分未受權的websocket瀏覽器
    • Sec-WebSocket-Version: 13
      客戶端在握手時的請求中攜帶,這樣的版本標識,表示這個是一個升級版本,如今的瀏覽器都是使用的這個版本。
    • HTTP/1.1 101 Switching Protocols
      101爲服務器返回的狀態碼,全部非101的狀態碼都表示handshake並未完成。

Data Framing

Websocket協議經過序列化的數據幀傳輸數據。數據封包協議中定義了opcode、payload length、Payload data等字段。其中要求:

  1. 客戶端向服務器傳輸的數據幀必須進行掩碼處理:服務器若接收到未通過掩碼處理的數據幀,則必須主動關閉鏈接。
  2. 服務器向客戶端傳輸的數據幀必定不能進行掩碼處理。客戶端若接收到通過掩碼處理的數據幀,則必須主動關閉鏈接。

針對上狀況,發現錯誤的一方可向對方發送close幀(狀態碼是1002,表示協議錯誤),以關閉鏈接。
具體數據幀格式以下圖所示:

  • FIN
    標識是否爲此消息的最後一個數據包,佔 1 bit
  • RSV1, RSV2, RSV3: 用於擴展協議,通常爲0,各佔1bit
  • Opcode
    數據包類型(frame type),佔4bits
    0x0:標識一箇中間數據包
    0x1:標識一個text類型數據包
    0x2:標識一個binary類型數據包
    0x3-7:保留
    0x8:標識一個斷開鏈接類型數據包
    0x9:標識一個ping類型數據包
    0xA:表示一個pong類型數據包
    0xB-F:保留
  • MASK:佔1bits
    用於標識PayloadData是否通過掩碼處理。若是是1,Masking-key域的數據便是掩碼密鑰,用於解碼PayloadData。客戶端發出的數據幀須要進行掩碼處理,因此此位是1。
  • Payload length
    Payload data的長度,佔7bits,7+16bits,7+64bits:
    • 若是其值在0-125,則是payload的真實長度。
    • 若是值是126,則後面2個字節造成的16bits無符號整型數的值是payload的真實長度。注意,網絡字節序,須要轉換。
    • 若是值是127,則後面8個字節造成的64bits無符號整型數的值是payload的真實長度。注意,網絡字節序,須要轉換。

這裏的長度表示遵循一個原則,用最少的字節表示長度(儘可能減小沒必要要的傳輸)。舉例說,payload真實長度是124,在0-125之間,必須用前7位表示;不容許長度1是126或127,而後長度2是124,這樣違反原則。

    • Payload data
      應用層數據

      server解析client端的數據

      接收到客戶端數據後的解析規則以下:

    • 1byte
      • 1bit: frame-fin,x0表示該message後續還有frame;x1表示是message的最後一個frame
      • 3bit: 分別是frame-rsv一、frame-rsv2和frame-rsv3,一般都是x0
      • 4bit: frame-opcode,x0表示是延續frame;x1表示文本frame;x2表示二進制frame;x3-7保留給非控制frame;x8表示關 閉鏈接;x9表示ping;xA表示pong;xB-F保留給控制frame
    • 2byte
      • 1bit: Mask,1表示該frame包含掩碼;0表示無掩碼
      • 7bit、7bit+2byte、7bit+8byte: 7bit取整數值,若在0-125之間,則是負載數據長度;如果126表示,後兩個byte取無符號16位整數值,是負載長度;127表示後8個 byte,取64位無符號整數值,是負載長度
      • 3-6byte: 這裏假定負載長度在0-125之間,而且Mask爲1,則這4個byte是掩碼
      • 7-end byte: 長度是上面取出的負載長度,包括擴展數據和應用數據兩部分,一般沒有擴展數據;若Mask爲1,則此數據須要解碼,解碼規則爲- 1-4byte掩碼循環和數據byte作異或操做。

示例代碼:

while True:
    # 對數據進行解密
    # send_msg(conn, bytes('alex', encoding='utf-8'))
    # send_msg(conn, bytes('SB', encoding='utf-8'))
    # info = conn.recv(8096)
    # print(info)

    info = conn.recv(8096)
    payload_len = info[1] & 127
    if payload_len == 126:
        extend_payload_len = info[2:4]
        mask = info[4:8]
        decoded = info[8:]
    elif payload_len == 127:
        extend_payload_len = info[2:10]
        mask = info[10:14]
        decoded = info[14:]
    else:
        extend_payload_len = None
        mask = info[2:6]
        decoded = info[6:]

    bytes_list = bytearray()
    for i in range(len(decoded)):
        chunk = decoded[i] ^ mask[i % 4]
        bytes_list.append(chunk)
    msg = str(bytes_list, encoding='utf-8')

    rep = msg + 'sb'
    send_msg(conn,bytes(rep,encoding='utf-8'))

五、原理代碼:

import socket
import hashlib
import base64


def get_headers(data):
    """
    將請求頭格式化成字典
    :param data:
    :return:
    """
    header_dict = {}
    data = str(data, encoding='utf-8')

    header, body = data.split('\r\n\r\n', 1)
    header_list = header.split('\r\n')
    for i in range(0, len(header_list)):
        if i == 0:
            if len(header_list[i].split(' ')) == 3:
                header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')
        else:
            k, v = header_list[i].split(':', 1)
            header_dict[k] = v.strip()
    return header_dict

def send_msg(conn, msg_bytes):
    """
    WebSocket服務端向客戶端發送消息
    :param conn: 客戶端鏈接到服務器端的socket對象,即: conn,address = socket.accept()
    :param msg_bytes: 向客戶端發送的字節
    :return:
    """
    import struct

    token = b"\x81"
    length = len(msg_bytes)
    if length < 126:
        token += struct.pack("B", length)
    elif length <= 0xFFFF:
        token += struct.pack("!BH", 126, length)
    else:
        token += struct.pack("!BQ", 127, length)

    msg = token + msg_bytes
    conn.send(msg)
    return True

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('127.0.0.1', 8002))
sock.listen(5)

# 等待用戶鏈接
conn, address = sock.accept()

# WebSocket發來的鏈接
# 1. 獲取握手數據
data = conn.recv(1024)
headers = get_headers(data)

# 2. 對握手信息進行加密:
magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
value = headers['Sec-WebSocket-Key'] + magic_string
ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())

# 3. 返回握手信息
response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \
      "Upgrade:websocket\r\n" \
      "Connection: Upgrade\r\n" \
      "Sec-WebSocket-Accept: %s\r\n" \
      "WebSocket-Location: ws://127.0.0.1:8002\r\n\r\n"

response_str = response_tpl % (ac.decode('utf-8'),)

conn.sendall(bytes(response_str, encoding='utf-8'))

# 以後,才能進行首發數據。

while True:
    # 對數據進行解密
    # send_msg(conn, bytes('alex', encoding='utf-8'))
    # send_msg(conn, bytes('SB', encoding='utf-8'))
    # info = conn.recv(8096)
    # print(info)

    info = conn.recv(8096)
    payload_len = info[1] & 127
    if payload_len == 126:
        extend_payload_len = info[2:4]
        mask = info[4:8]
        decoded = info[8:]
    elif payload_len == 127:
        extend_payload_len = info[2:10]
        mask = info[10:14]
        decoded = info[14:]
    else:
        extend_payload_len = None
        mask = info[2:6]
        decoded = info[6:]

    bytes_list = bytearray()
    for i in range(len(decoded)):
        chunk = decoded[i] ^ mask[i % 4]
        bytes_list.append(chunk)
    msg = str(bytes_list, encoding='utf-8')

    rep = msg + 'sb'
    send_msg(conn,bytes(rep,encoding='utf-8'))
後端
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>WebSocket協議學習</h1>

    <script type="text/javascript">
        // 向 127.0.0.1:8002 發送一個WebSocket請求
        var socket = new WebSocket("ws://127.0.0.1:8002");
        socket.onmessage = function (event) {
        /* 服務器端向客戶端發送數據時,自動執行 */
        var response = event.data;
        console.log(response);
    };
    </script>
</body>
</html>
前端

2、應用:

一、Flask中應用: pip3 install gevent-websocket

from flask import Flask,request,render_template,session,redirect
import uuid
import json
from geventwebsocket.handler import WebSocketHandler
from gevent.pywsgi import WSGIServer


app = Flask(__name__)
app.secret_key = 'asdfasdf'

GENTIEMAN = {
    '1':{'name':'鋼彈','count':0},
    '2':{'name':'鐵錘','count':0},
    '3':{'name':'閆帥','count':0},
}

WEBSOCKET_DICT = {

}

@app.before_request
def before_request():
    if request.path == '/login':
        return None
    user_info = session.get('user_info')
    if user_info:
        return None
    return redirect('/login')

@app.route('/login',methods=['GET','POST'])
def login():
    if request.method == "GET":
        return render_template('login.html')
    else:
        uid = str(uuid.uuid4())
        session['user_info'] = {'id':uid,'name':request.form.get('user')}
        return redirect('/index')

@app.route('/index')
def index():
    return render_template('index.html',users=GENTIEMAN)

@app.route('/message')
def message():
    # 1. 判斷究竟是否是websocket請求?
    ws = request.environ.get('wsgi.websocket')
    if not ws:
        return "請使用WebSocket協議"
    # ----- ws鏈接成功 -------
    current_user_id = session['user_info']['id']
    WEBSOCKET_DICT[current_user_id] = ws
    while True:
        # 2. 等待用戶發送消息,並接受
        message = ws.receive() # 帥哥ID
        # 關閉:message=None
        if not message:
            del WEBSOCKET_DICT[current_user_id]
            break

        # 3. 獲取用戶要投票的帥哥ID,並+1
        old = GENTIEMAN[message]['count']
        new = old + 1
        GENTIEMAN[message]['count'] = new

        data = {'user_id': message, 'count': new,'type':'vote'}
        # 4. 給全部客戶端推送消息
        for conn in WEBSOCKET_DICT.values():
            conn.send(json.dumps(data))
    return 'close'

@app.route('/notify')
def notify():
    data = {'data': "你的訂單已經生成,請及時處理;", 'type': 'alert'}
    print(WEBSOCKET_DICT)
    for conn in WEBSOCKET_DICT.values():
        conn.send(json.dumps(data))
    return '發送成功'

if __name__ == '__main__':
    http_server = WSGIServer(('192.168.11.143', 5000), app, handler_class=WebSocketHandler)
    http_server.serve_forever()
View Code
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form method="post">
    <input type="text" name="user">
    <input type="submit" value="提交">
</form>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>投票系統:參與投票的人</h1>
    <ul>
        {% for k,v in users.items() %}
            <li id="user_{{k}}" ondblclick="vote('{{k}}')">{{v.name}} <span>{{v.count}}</span> </li>
        {% endfor %}

    </ul>
    <script src="{{ url_for('static',filename='jquery-3.3.1.min.js')}}"></script>
    <script>
        var socket = new WebSocket("ws://192.168.11.143:5000/message");

        socket.onmessage = function (event) {
            /* 服務器端向客戶端發送數據時,自動執行 */
            var response = JSON.parse(event.data); // {'user':1,'count':new}
            if(response.type == 'vote'){
                var nid = '#user_' + response.user_id;
                $(nid).find('span').text(response.count)
            }else{
                alert(response.data);
            }

        };

        /*
        我要給某人投票
         */
         function vote(id) {
            socket.send(id);
        }

    </script>
</body>
</html>
index.html

二、Django應用:channel

三、Tornado應用:本身有

相關文章
相關標籤/搜索