异步通信----WebSocket
Posted amchen
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了异步通信----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)
数据交互协议:
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 True
基于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()
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>
基于Tornado框架实现Web聊天室
Tornado是一个支持WebSocket的优秀框架,其内部原理正如1~5步骤描述,当然Tornado内部封装功能更加完整。
源码见链接:点我下载
以上是关于异步通信----WebSocket的主要内容,如果未能解决你的问题,请参考以下文章
Python 多线程服务器和与 Android 客户端的异步 websocket 通信
swoolefy PHP的异步并行高性能网络通信引擎内置了Http/WebSocket服务器端/客户端
Spring Boot2 系列教程 (十六) | 整合 WebSocket 实现广播
把酒言欢话聊天,基于Vue3.0+Tornado6.1+Redis发布订阅(pubsub)模式打造异步非阻塞(aioredis)实时(websocket)通信聊天系统