WebSocket協議是基於TCP的一種新的協議。WebSocket最初在HTML5規範中被引用為TCP連線,作為基於TCP的套接字API的佔位符。它實現了瀏覽器與伺服器全雙工(full-duplex)通訊。其本質是保持TCP連線,在瀏覽器和服務端通過Socket進行通訊。
本文將使用Python編寫Socket服務端,一步一步分析請求過程!!!
1. 啟動服務端
1 2 3 4 5 6 7 8 9 10 |
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) # 等待使用者連線 conn, address = sock.accept() ... ... ... |
啟動Socket伺服器後,等待使用者【連線】,然後進行收發資料。
2. 客戶端連線
1 2 3 4 |
<script type="text/javascript"> var socket = new WebSocket("ws://127.0.0.1:8002/xxoo"); ... </script> |
當客戶端向服務端傳送連線請求時,不僅連線還會傳送【握手】資訊,並等待服務端響應,至此連線才建立成功!
3. 建立連線【握手】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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(1024) ... ... ... conn.send('響應【握手】資訊') |
請求和響應的【握手】資訊需要遵循規則:
- 從請求【握手】資訊中提取 Sec-WebSocket-Key
- 利用magic_string 和 Sec-WebSocket-Key 進行hmac1加密,再進行base64加密
- 將加密結果響應給客戶端
注:magic string為:258EAFA5-E914-47DA-95CA-C5AB0DC85B11
請求【握手】資訊為:
提取Sec-WebSocket-Key值並加密:
4.客戶端和服務端收發資料
客戶端和服務端傳輸資料時,需要對資料進行【封包】和【解包】。客戶端的JavaScript類庫已經封裝【封包】和【解包】過程,但Socket服務端需要手動實現。
第一步:獲取客戶端傳送的資料【解包】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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) 基於Python實現解包過程(未實現長內容) |
解包詳細過程:
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.
第二步:向客戶端傳送資料【封包】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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 |
5. 基於Python實現簡單示例
a. 基於Python socket實現的WebSocket服務端:
b. 利用JavaScript類庫實現客戶端
6. 基於Tornado框架實現Web聊天室
Tornado是一個支援WebSocket的優秀框架,其內部原理正如1~5步驟描述,當然Tornado內部封裝功能更加完整。 以下是基於Tornado實現的聊天室示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
#!/usr/bin/env python # -*- coding:utf-8 -*- import uuid import json import tornado.ioloop import tornado.web import tornado.websocket class IndexHandler(tornado.web.RequestHandler): def get(self): self.render('index.html') class ChatHandler(tornado.websocket.WebSocketHandler): # 使用者儲存當前聊天室使用者 waiters = set() # 用於儲存歷時訊息 messages = [] def open(self): """ 客戶端連線成功時,自動執行 :return: """ ChatHandler.waiters.add(self) uid = str(uuid.uuid4()) self.write_message(uid) for msg in ChatHandler.messages: content = self.render_string('message.html', **msg) self.write_message(content) def on_message(self, message): """ 客戶端連傳送訊息時,自動執行 :param message: :return: """ msg = json.loads(message) ChatHandler.messages.append(message) for client in ChatHandler.waiters: content = client.render_string('message.html', **msg) client.write_message(content) def on_close(self): """ 客戶端關閉連線時,,自動執行 :return: """ ChatHandler.waiters.remove(self) def run(): settings = { 'template_path': 'templates', 'static_path': 'static', } application = tornado.web.Application([ (r"/", IndexHandler), (r"/chat", ChatHandler), ], **settings) application.listen(8888) tornado.ioloop.IOLoop.instance().start() if __name__ == "__main__": run() app.py |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Python聊天室</title> </head> <body> <div> <input type="text" id="txt"/> <input type="button" id="btn" value="提交" onclick="sendMsg();"/> <input type="button" id="close" value="關閉連線" onclick="closeConn();"/> </div> <div id="container" style="border: 1px solid #dddddd;margin: 20px;min-height: 500px;"> </div> <script src="/static/jquery-2.1.4.min.js"></script> <script type="text/javascript"> $(function () { wsUpdater.start(); }); var wsUpdater = { socket: null, uid: null, start: function() { var url = "ws://127.0.0.1:8888/chat"; wsUpdater.socket = new WebSocket(url); wsUpdater.socket.onmessage = function(event) { console.log(event); if(wsUpdater.uid){ wsUpdater.showMessage(event.data); }else{ wsUpdater.uid = event.data; } } }, showMessage: function(content) { $('#container').append(content); } }; function sendMsg() { var msg = { uid: wsUpdater.uid, message: $("#txt").val() }; wsUpdater.socket.send(JSON.stringify(msg)); } </script> </body> </html> index.html |
參考文獻:https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers