一、WebSocket理論部分
1、websocket是什么
Websocket是html5提出的一個協議規范,參考rfc6455。
websocket約定了一個通信的規范,通過一個握手的機制,客戶端(瀏覽器)和服務器(webserver)之間能建立一個類似tcp的連接,從而方便c-s之間的通信。在websocket出現之前,web交互一般是基於http協議的短連接或者長連接。
WebSocket是為解決客戶端與服務端實時通信而產生的技術。websocket協議本質上是一個基於tcp的協議,是先通過HTTP/HTTPS協議發起一條特殊的http請求進行握手后創建一個用於交換數據的TCP連接,此后服務端與客戶端通過此TCP連接進行實時通信。
注意:此時不再需要原HTTP協議的參與了。
2、websocket的優點
以前web server實現推送技術或者即時通訊,用的都是輪詢(polling),在特點的時間間隔(比如1秒鍾)由瀏覽器自動發出請求,將服務器的消息主動的拉回來,在這種情況下,我們需要不斷的向服務器發送請求,然而HTTP request 的header是非常長的,里面包含的數據可能只是一個很小的值,這樣會占用很多的帶寬和服務器資源。
而最比較新的技術去做輪詢的效果是Comet – 用了AJAX。但這種技術雖然可達到全雙工通信,但依然需要發出請求(reuqest)。
WebSocket API最偉大之處在於服務器和客戶端可以在給定的時間范圍內的任意時刻,相互推送信息。 瀏覽器和服務器只需要要做一個握手的動作,在建立連接之后,服務器可以主動傳送數據給客戶端,客戶端也可以隨時向服務器發送數據。 此外,服務器與客戶端之間交換的標頭信息很小。
WebSocket並不限於以Ajax(或XHR)方式通信,因為Ajax技術需要客戶端發起請求,而WebSocket服務器和客戶端可以彼此相互推送信息;
因此從服務器角度來說,websocket有以下好處:
- 節省每次請求的header
http的header一般有幾十字節 - Server Push
服務器可以主動傳送數據給客戶端
3、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
-創建鏈接之后默認不斷開
4、基於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)。 比如:
如果加密算法錯誤,客戶端在進行校檢的時候會直接報錯。如果握手成功,則客戶端側會出發onopen事件。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)); }
- 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等字段。其中要求:
- 客戶端向服務器傳輸的數據幀必須進行掩碼處理:服務器若接收到未經過掩碼處理的數據幀,則必須主動關閉連接。
- 服務器向客戶端傳輸的數據幀一定不能進行掩碼處理。客戶端若接收到經過掩碼處理的數據幀,則必須主動關閉連接。
針對上情況,發現錯誤的一方可向對方發送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-rsv1、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'))
5、原理代碼:

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>
二、應用:
1、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()

<!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>

<!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>
2、Django應用:channel
3、Tornado應用:自己有