異步通信----WebSocket
什麽是WebSocket?
WebSocket API是下一代客戶端-服務器的異步通信方法。該通信取代了單個的TCP套接字,使用ws或wss協議,可用於任意的客戶端和服務器程序。WebSocket目前由W3C進行標準化。WebSocket已經受到Firefox 4、Chrome 4、Opera 10.70以及Safari 5等瀏覽器的支持。
WebSocket API最偉大之處在於服務器和客戶端可以在給定的時間範圍內的任意時刻,相互推送信息。WebSocket並不限於以Ajax(或XHR)方式通信,因為Ajax技術需要客戶端發起請求,而WebSocket服務器和客戶端可以彼此相互推送信息;XHR受到域的限制,而WebSocket允許跨域通信。
Ajax技術很聰明的一點是沒有設計要使用的方式。WebSocket為指定目標創建,用於雙向推送消息。
WebSocket通信原理
- 服務端(socket服務端) 1. 服務端開啟socket,監聽IP和端口 3. 允許連接 * 5. 服務端接收到特殊值【加密sha1,特殊值,migic string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"】 * 6. 加密後的值發送給客戶端 - 客戶端(瀏覽器) 2. 客戶端發起連接請求(IP和端口) * 4. 客戶端生成一個xxx,【加密sha1,特殊值,migic string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"】,向服務端發送一段特殊值 * 7. 客戶端接收到加密的值
基於代碼實現:
1. 啟動服務端
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. 客戶端連接
<script type="text/javascript"> var socket = new WebSocket("ws://127.0.0.1:8002/xxoo"); ... </script>
當客戶端向服務端發送連接請求時,不僅連接還會發送【握手】信息,並等待服務端響應,至此連接才創建成功!
3. 建立連接【握手】
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 #這個是API隨機生成的
- 利用magic_string 和 Sec-WebSocket-Key 進行hmac1加密,再進行base64加密
- 將加密結果響應給客戶端
註:magic string為(亙古不變):258EAFA5-E914-47DA-95CA-C5AB0DC85B11
請求【握手】信息為:
GET /chatsocket HTTP/1.1 Host: 127.0.0.1:8002 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Origin: http://localhost:63342 Sec-WebSocket-Version: 13 Sec-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits ... ...
提取Sec-WebSocket-Key值並加密:
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‘)) ... ... ...
4.客戶端和服務端收發數據
客戶端和服務端傳輸數據時,需要對數據進行【封包】和【解包】。客戶端的JavaScript類庫已經封裝【封包】和【解包】過程,但Socket服務端需要手動實現。
第一步:獲取客戶端發送的數據【解包】
1 info = conn.recv(8096) 2 3 payload_len = info[1] & 127 4 if payload_len == 126: 5 extend_payload_len = info[2:4] 6 mask = info[4:8] 7 decoded = info[8:] 8 elif payload_len == 127: 9 extend_payload_len = info[2:10] 10 mask = info[10:14] 11 decoded = info[14:] 12 else: 13 extend_payload_len = None 14 mask = info[2:6] 15 decoded = info[6:] 16 17 bytes_list = bytearray() 18 for i in range(len(decoded)): 19 chunk = decoded[i] ^ mask[i % 4] 20 bytes_list.append(chunk) 21 body = str(bytes_list, encoding=‘utf-8‘) 22 print(body)基於Python實現解包過程(未實現長內容)
數據交互協議:
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 ... | +---------------------------------------------------------------+
協議解讀:
第一個字節 最高位用於描述消息是否結束,如果為1則該消息為消息尾部,如果為零則還有後續數據包;後面3位是用於擴展定義的,如果沒有擴展約定的情況則必須為0.可以通過以下c#代碼方式得到相應值 mDataPackage.IsEof = (data[start] >> 7) > 0; 最低4位用於描述消息類型,消息類型暫定有15種,其中有幾種是預留設置.c#代碼可以這樣得到消息類型: int type = data[start] & 0xF; mDataPackage.Type = (PackageType)type; 第二個字節 消息的第二個字節主要用一描述掩碼和消息長度,最高位用0或1來描述是否有掩碼處理,可以通過以下c#代碼方式得到相應值 bool hasMask = (data[start] >>7) > 0; 剩下的後面7位用來描述消息長度,由於7位最多只能描述127所以這個值會代表三種情況,一種是消息內容少於126存儲消息長度,如果消息長度少於UINT16的情況此值為126,當消息長度大於UINT16的情況下此值為127;這兩種情況的消息長度存儲到緊隨後面的byte[],分別是UINT16(2位byte)和UINT64(4位byte).可以通過以下c#代碼方式得到相應值 mPackageLength = (uint)(data[start] & 0x7F); start++; if (mPackageLength == 126) { mPackageLength = BitConverter.ToUInt16(data, start); start = start + 2; } else if (mPackageLength == 127) { mPackageLength = BitConverter.ToUInt64(data, start); start = start + 8; } 如果存在掩碼的情況下獲取4位掩碼值: if (hasMask) { mDataPackage.Masking_key = new byte[4]; Buffer.BlockCopy(data, start, mDataPackage.Masking_key, 0, 4); start = start + 4; count = count - 4; } 獲取消息體 當得到消息體長度後就可以獲取對應長度的byte[],有些消息類型是沒有長度的如%x8 denotes a connection close.對於Text類型的消息對應的byte[]是相應字符的UTF8編碼.獲取消息體還有一個需要註意的地方就是掩碼,如果存在掩碼的情況下接收的byte[]要做如下轉換處理: if (mDataPackage.Masking_key != null) { int length = mDataPackage.Data.Count; for (var i = 0; i < length; i++) mDataPackage.Data.Array[i] = (byte)(mDataPackage.Data.Array[i] ^ mDataPackage.Masking_key[i % 4]); }
第二步:向客戶端發送數據【封包】
1 def send_msg(conn, msg_bytes): 2 """ 3 WebSocket服務端向客戶端發送消息 4 :param conn: 客戶端連接到服務器端的socket對象,即: conn,address = socket.accept() 5 :param msg_bytes: 向客戶端發送的字節 6 :return: 7 """ 8 import struct 9 10 token = b"\x81" #用於描述數據交互協議中數據傳輸是否完成 11 length = len(msg_bytes) 12 if length < 126: 13 token += struct.pack("B", length) 14 elif length <= 0xFFFF: 15 token += struct.pack("!BH", 126, length) 16 else: 17 token += struct.pack("!BQ", 127, length) 18 19 msg = token + msg_bytes 20 conn.send(msg) 21 return TrueView Code
基於Python實現簡單示例
a. 基於Python socket實現的WebSocket服務端:
1 import socket 2 import base64 3 import hashlib 4 5 6 def get_headers(data): 7 """ 8 將請求頭格式化成字典 9 :param data: 10 :return: 11 """ 12 header_dict = {} 13 data = str(data, encoding=‘utf-8‘) 14 15 header, body = data.split(‘\r\n\r\n‘, 1) 16 header_list = header.split(‘\r\n‘) 17 for i in range(0, len(header_list)): 18 if i == 0: 19 if len(header_list[i].split(‘ ‘)) == 3: 20 header_dict[‘method‘], header_dict[‘url‘], header_dict[‘protocol‘] = header_list[i].split(‘ ‘) 21 else: 22 k, v = header_list[i].split(‘:‘, 1) 23 header_dict[k] = v.strip() 24 return header_dict 25 26 27 def send_msg(conn, msg_bytes): 28 """ 29 WebSocket服務端向客戶端發送消息 30 :param conn: 客戶端連接到服務器端的socket對象,即: conn,address = socket.accept() 31 :param msg_bytes: 向客戶端發送的字節 32 :return: 33 """ 34 import struct 35 36 token = b"\x81" 37 length = len(msg_bytes) 38 if length < 126: 39 token += struct.pack("B", length) 40 elif length <= 0xFFFF: 41 token += struct.pack("!BH", 126, length) 42 else: 43 token += struct.pack("!BQ", 127, length) 44 45 msg = token + msg_bytes 46 conn.send(msg) 47 return True 48 49 50 def run(): 51 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 52 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 53 sock.bind((‘127.0.0.1‘, 8003)) 54 sock.listen(5) 55 56 conn, address = sock.accept() 57 data = conn.recv(1024) 58 headers = get_headers(data) 59 response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" 60 "Upgrade:websocket\r\n" 61 "Connection:Upgrade\r\n" 62 "Sec-WebSocket-Accept:%s\r\n" 63 "WebSocket-Location:ws://%s%s\r\n\r\n" 64 65 value = headers[‘Sec-WebSocket-Key‘] + ‘258EAFA5-E914-47DA-95CA-C5AB0DC85B11‘ 66 ac = base64.b64encode(hashlib.sha1(value.encode(‘utf-8‘)).digest()) 67 response_str = response_tpl % (ac.decode(‘utf-8‘), headers[‘Host‘], headers[‘url‘]) 68 conn.send(bytes(response_str, encoding=‘utf-8‘)) 69 70 while True: 71 try: 72 info = conn.recv(8096) 73 except Exception as e: 74 info = None 75 if not info: 76 break 77 payload_len = info[1] & 127 78 if payload_len == 126: 79 extend_payload_len = info[2:4] 80 mask = info[4:8] 81 decoded = info[8:] 82 elif payload_len == 127: 83 extend_payload_len = info[2:10] 84 mask = info[10:14] 85 decoded = info[14:] 86 else: 87 extend_payload_len = None 88 mask = info[2:6] 89 decoded = info[6:] 90 91 bytes_list = bytearray() 92 for i in range(len(decoded)): 93 chunk = decoded[i] ^ mask[i % 4] 94 bytes_list.append(chunk) 95 body = str(bytes_list, encoding=‘utf-8‘) 96 send_msg(conn,body.encode(‘utf-8‘)) 97 98 sock.close() 99 100 if __name__ == ‘__main__‘: 101 run()View Code
b. 利用JavaScript類庫實現客戶端
1 <!DOCTYPE html> 2 <html> 3 <head lang="en"> 4 <meta charset="UTF-8"> 5 <title></title> 6 </head> 7 <body> 8 <div> 9 <input type="text" id="txt"/> 10 <input type="button" id="btn" value="提交" onclick="sendMsg();"/> 11 <input type="button" id="close" value="關閉連接" onclick="closeConn();"/> 12 </div> 13 <div id="content"></div> 14 15 <script type="text/javascript"> 16 var socket = new WebSocket("ws://127.0.0.1:8003/chatsocket"); 17 18 socket.onopen = function () { 19 /* 與服務器端連接成功後,自動執行 */ 20 21 var newTag = document.createElement(‘div‘); 22 newTag.innerHTML = "【連接成功】"; 23 document.getElementById(‘content‘).appendChild(newTag); 24 }; 25 26 socket.onmessage = function (event) { 27 /* 服務器端向客戶端發送數據時,自動執行 */ 28 var response = event.data; 29 var newTag = document.createElement(‘div‘); 30 newTag.innerHTML = response; 31 document.getElementById(‘content‘).appendChild(newTag); 32 }; 33 34 socket.onclose = function (event) { 35 /* 服務器端主動斷開連接時,自動執行 */ 36 var newTag = document.createElement(‘div‘); 37 newTag.innerHTML = "【關閉連接】"; 38 document.getElementById(‘content‘).appendChild(newTag); 39 }; 40 41 function sendMsg() { 42 var txt = document.getElementById(‘txt‘); 43 socket.send(txt.value); 44 txt.value = ""; 45 } 46 function closeConn() { 47 socket.close(); 48 var newTag = document.createElement(‘div‘); 49 newTag.innerHTML = "【關閉連接】"; 50 document.getElementById(‘content‘).appendChild(newTag); 51 } 52 53 </script> 54 </body> 55 </html>View Code
基於Tornado框架實現Web聊天室
Tornado是一個支持WebSocket的優秀框架,其內部原理正如1~5步驟描述,當然Tornado內部封裝功能更加完整。
源碼見鏈接:點我下載
異步通信----WebSocket