websocket通訊之握手,封包,解包
介紹
WebSocket協議是基於TCP的一種新的協議。WebSocket最初在HTML5規範中被引用為TCP連線,作為基於TCP的套接字API的佔位符。它實現了瀏覽器與伺服器全雙工(full-duplex)通訊。其本質是保持TCP連線,在瀏覽器和服務端通過Socket進行通訊。
上古時期的瀏覽器有些是不支援WebSocket的,下面來介紹如何在瀏覽器中建立一個websocket物件
var socket = new WebSocket("ws://127.0.0.1:8002/xxoo");
socket傳送接收資料
ws.send() # 傳送訊息 ws.onmessage = function (event) { console.log(event.data) # 收到資料執行的函式,event.data就是收到的資料 } ws.onclose = function (event) { // 服務端主動斷開了連線 }
Socket伺服器
import socket 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) # 獲取客戶端socket物件 conn, address = sock.accept() # 獲取客戶端的【握手】資訊 data = conn.recv(8096) print(str(data,encoding='utf-8'))
瀏覽器Socket
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> </head> <body> <script> ws = new WebSocket('ws://127.0.0.1:8002/') </script> </body> </html>
執行結果
# 瀏覽器列印 GET / HTTP/1.1 Host: 127.0.0.1:8002 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Origin: http://localhost:53512 Sec-WebSocket-Version: 13 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: _ga=GA1.1.414503566.1517490000; csrftoken=hm3ml70razspGU9n46ay8z7KaouRS1XrFjBgjqU2ANy8lZOKWPZJMHpNDz1QXNZ1 Sec-WebSocket-Key: Gm61RLyES5nWI3UxzOES0g== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits # 瀏覽器端 WebSocket connection to 'ws://127.0.0.1:8002/' failed: Connection closed before receiving a handshake response
當客戶端向服務端傳送連線請求時,不僅連線還會發送【握手】資訊,並等待服務端響應,至此連線才建立成功!
客戶端的響應內容
- 從請求【握手】資訊中提取 Sec-WebSocket-Key
- 利用magic_string 和 Sec-WebSocket-Key 進行hmac1加密,再進行base64加密
- 將加密結果響應給客戶端
import socket import base64 import hashlib def get_headers(data): """ 將請求頭格式化成字典 :param data: :return: """ header_dict = {} data = str(data, encoding='utf-8') for i in data.split('\r\n'): print(i) 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 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() data = conn.recv(1024) headers = get_headers(data) # 提取請求頭資訊 # 對請求頭中的sec-websocket-key進行加密 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://%s%s\r\n\r\n" magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' value = headers['Sec-WebSocket-Key'] + magic_string ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest()) response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url']) # 響應【握手】資訊 conn.send(bytes(response_str, encoding='utf-8'))
服務端響應後,連線就建成了,接下來就可以收發資料了.
客戶端和服務端傳輸資料時,需要對資料進行【封包】和【解包】。客戶端的JavaScript類庫已經封裝【封包】和【解包】過程,但Socket服務端需要手動實現。
解包詳解
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+ # 根據第二個位元組的後七位判斷頭部大小, # 127是向後64位,也就是8位元組,頭部共10位元組 # 127是向後16位,也就是2位元組,頭部共4位元組 # Masking-key在資料部分的前四位,後面才是真正資料的加密部分 # 解密過程就將Masking-key與加密的資料進行位運算
官方解釋
The MASK bit simply tells whether the message is encoded. Messages from the client must be masked, so your server should expect this to be 1. (In fact, section 5.1 of the spec says that your server must disconnect from a client if that client sends an unmasked message.) When sending a frame back to the client, do not mask it and do not set the mask bit. We'll explain masking later. Note: You have to mask messages even when using a secure socket.RSV1-3 can be ignored, they are for extensions.
The opcode field defines how to interpret the payload data: 0x0 for continuation,
0x1
for text (which is always encoded in UTF-8),0x2
for binary, and other so-called "control codes" that will be discussed later. In this version of WebSockets,0x3
to0x7
and0xB
to0xF
have no meaning.The FIN bit tells whether this is the last message in a series. If it's 0, then the server will keep listening for more parts of the message; otherwise, the server should consider the message delivered. More on this later.
Decoding Payload Length
To read the payload data, you must know when to stop reading. That's why the payload length is important to know. Unfortunately, this is somewhat complicated. To read it, follow these steps:
- Read bits 9-15 (inclusive) and interpret that as an unsigned integer. If it's 125 or less, then that's the length; you're done. If it's 126, go to step 2. If it's 127, go to step 3.
- Read the next 16 bits and interpret those as an unsigned integer. You're done.
- Read the next 64 bits and interpret those as an unsigned integer (The most significant bit MUST be 0). You're done.
Reading and Unmasking the Data
If the MASK bit was set (and it should be, for client-to-server messages), read the next 4 octets (32 bits); this is the masking key. Once the payload length and masking key is decoded, you can go ahead and read that number of bytes from the socket. Let's call the data ENCODED, and the key MASK. To get DECODED, loop through the octets (bytes a.k.a. characters for text data) of ENCODED and XOR the octet with the (i modulo 4)th octet of MASK. In pseudo-code (that happens to be valid JavaScript):
var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}
Now you can figure out what DECODED means depending on your application.
python的解包過程
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) body = str(bytes_list, encoding='utf-8') print(body)
封包
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
框架中使用
- django: channel
- flask: gevent-websocket
- tornado: 內建
Flask
安裝
pip3 install gevent-websocket
作用
- - 處理Http、Websocket協議的請求 -> socket
- - 封裝Http、Websocket相關資料 -> request
基本結構
from geventwebsocket.handler import WebSocketHandler from gevent.pywsgi import WSGIServer from flask import Flask,render_template,request app = Flask(__name__) @app.route('/test') def test(): ws = request.environ.get('wsgi.websocket') # 如果是websocket請求會得到socket物件,否則None if ws: while: ws.receive() # 收資料 ws.send(message) # 發資料 # ws.close() # 關閉 return render_template('index.html') if __name__ == '__main__': http_server = WSGIServer(('0.0.0.0', 5000,), app, handler_class=WebSocketHandler) http_server.serve_forever()
tornado
Tornado是一個輕量級的Web框架,非同步非阻塞+內建WebSocket功能
安裝
pip3 install tornado
示例
import tornado from tornado.web import Application from tornado.web import RequestHandler from tornado.websocket import WebSocketHandler # HTTP繼承RequestHandler類 class IndexHandler(RequestHandler): def get(self, *args, **kwargs): # self.write('Hello World') 響應字串 self.render('index.html') # 響應模板 def post(self, *args, **kwargs): user = self.get_argument('user') # 提交資料中取值 self.write('成功') WS_LIST = [] # WebSocket繼承WebSocketHandler類 class MessageHandler(WebSocketHandler): def open(self, *args, **kwargs): # 連線時執行的 WS_LIST.append(self) def on_message(self, message): # self.close() 服務端主動斷開連線, # 收到資料時執行的,message就是收到的資料 for ws in WS_LIST: ws.write_message(message) # 傳送資料 def on_close(self): # 客戶端關閉連線時執行的 WS_LIST.remove(self) settings = { 'template_path':'templates', 'static_path':'static', } # 寫配置 app = Application([ (r"/index", IndexHandler), (r"/message", MessageHandler), ],**settings) # 路由及配置 if __name__ == '__main__': app.listen(address='0.0.0.0',port=9999) tornado.ioloop.IOLoop.instance().start() # 啟動