尝试为 Python3 支持的应用程序创建 Web 前端。该应用程序将需要双向流,这听起来是研究 websocket 的好机会。
我的第一个倾向是使用已经存在的东西,并且 mod-pywebsocket 中的示例应用程序已被证明很有值(value)。不幸的是,他们的 API 似乎不太容易进行扩展,而且它是 Python2。
环顾博客圈,很多人都为早期版本的 websocket 协议(protocol)编写了自己的 websocket 服务器,大多数人没有实现安全 key 哈希,因此不起作用。
阅读RFC 6455我决定自己尝试一下,并得出以下结论:
#!/usr/bin/env python3
"""
A partial implementation of RFC 6455
http://tools.ietf.org/pdf/rfc6455.pdf
Brian Thorne 2012
"""
import socket
import threading
import time
import base64
import hashlib
def calculate_websocket_hash(key):
magic_websocket_string = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
result_string = key + magic_websocket_string
sha1_digest = hashlib.sha1(result_string).digest()
response_data = base64.encodestring(sha1_digest)
response_string = response_data.decode('utf8')
return response_string
def is_bit_set(int_type, offset):
mask = 1 << offset
return not 0 == (int_type & mask)
def set_bit(int_type, offset):
return int_type | (1 << offset)
def bytes_to_int(data):
# note big-endian is the standard network byte order
return int.from_bytes(data, byteorder='big')
def pack(data):
"""pack bytes for sending to client"""
frame_head = bytearray(2)
# set final fragment
frame_head[0] = set_bit(frame_head[0], 7)
# set opcode 1 = text
frame_head[0] = set_bit(frame_head[0], 0)
# payload length
assert len(data) < 126, "haven't implemented that yet"
frame_head[1] = len(data)
# add data
frame = frame_head + data.encode('utf-8')
print(list(hex(b) for b in frame))
return frame
def receive(s):
"""receive data from client"""
# read the first two bytes
frame_head = s.recv(2)
# very first bit indicates if this is the final fragment
print("final fragment: ", is_bit_set(frame_head[0], 7))
# bits 4-7 are the opcode (0x01 -> text)
print("opcode: ", frame_head[0] & 0x0f)
# mask bit, from client will ALWAYS be 1
assert is_bit_set(frame_head[1], 7)
# length of payload
# 7 bits, or 7 bits + 16 bits, or 7 bits + 64 bits
payload_length = frame_head[1] & 0x7F
if payload_length == 126:
raw = s.recv(2)
payload_length = bytes_to_int(raw)
elif payload_length == 127:
raw = s.recv(8)
payload_length = bytes_to_int(raw)
print('Payload is {} bytes'.format(payload_length))
"""masking key
All frames sent from the client to the server are masked by a
32-bit nounce value that is contained within the frame
"""
masking_key = s.recv(4)
print("mask: ", masking_key, bytes_to_int(masking_key))
# finally get the payload data:
masked_data_in = s.recv(payload_length)
data = bytearray(payload_length)
# The ith byte is the XOR of byte i of the data with
# masking_key[i % 4]
for i, b in enumerate(masked_data_in):
data[i] = b ^ masking_key[i%4]
return data
def handle(s):
client_request = s.recv(4096)
# get to the key
for line in client_request.splitlines():
if b'Sec-WebSocket-Key:' in line:
key = line.split(b': ')[1]
break
response_string = calculate_websocket_hash(key)
header = '''HTTP/1.1 101 Switching Protocols\r
Upgrade: websocket\r
Connection: Upgrade\r
Sec-WebSocket-Accept: {}\r
\r
'''.format(response_string)
s.send(header.encode())
# this works
print(receive(s))
# this doesn't
s.send(pack('Hello'))
s.close()
s = socket.socket( socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('', 9876))
s.listen(1)
while True:
t,_ = s.accept()
threading.Thread(target=handle, args = (t,)).start()
使用这个基本测试页面(与 mod-pywebsocket 一起使用):
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Web Socket Example</title>
<meta charset="UTF-8">
</head>
<body>
<div id="serveroutput"></div>
<form id="form">
<input type="text" value="Hello World!" id="msg" />
<input type="submit" value="Send" onclick="sendMsg()" />
</form>
<script>
var form = document.getElementById('form');
var msg = document.getElementById('msg');
var output = document.getElementById('serveroutput');
var s = new WebSocket("ws://"+window.location.hostname+":9876");
s.onopen = function(e) {
console.log("opened");
out('Connected.');
}
s.onclose = function(e) {
console.log("closed");
out('Connection closed.');
}
s.onmessage = function(e) {
console.log("got: " + e.data);
out(e.data);
}
form.onsubmit = function(e) {
e.preventDefault();
msg.value = '';
window.scrollTop = window.scrollHeight;
}
function sendMsg() {
s.send(msg.value);
}
function out(text) {
var el = document.createElement('p');
el.innerHTML = text;
output.appendChild(el);
}
msg.focus();
</script>
</body>
</html>
这会接收数据并正确解除屏蔽,但我无法使传输路径正常工作。
作为向套接字写入“Hello”的测试,上面的程序计算要写入套接字的字节数:
['0x81', '0x5', '0x48', '0x65', '0x6c', '0x6c', '0x6f']
与 section 5.7 中给出的十六进制值匹配RFC 的。不幸的是,该框架从未出现在 Chrome 的开发者工具中。
知道我错过了什么吗?或者当前正在运行的 Python3 websocket 示例?
最佳答案
当我尝试在 Lion 上的 Safari 6.0.1 中与你的 python 代码对话时,我得到了
Unexpected LF in Value at ...
在 Javascript 控制台中。我还从 Python 代码中收到 IndexError
异常。
当我在 Lion 上的 Chrome 版本 24.0.1290.1 开发版中与您的 python 代码交谈时,我没有收到任何 Javascript 错误。在您的 javascript 中,会调用 onopen()
和 onclose()
方法,但不会调用 onmessage()
。 python 代码不会抛出任何异常,并且似乎已接收消息并发送了响应,即正是您所看到的行为。
由于 Safari 不喜欢 header 中的尾随 LF,我尝试将其删除,即
header = '''HTTP/1.1 101 Switching Protocols\r
Upgrade: websocket\r
Connection: Upgrade\r
Sec-WebSocket-Accept: {}\r
'''.format(response_string)
当我进行此更改时,Chrome 可以看到您的响应消息,即
got: Hello
显示在 JavaScript 控制台中。
Safari 仍然无法工作。现在,当我尝试发送消息时,它提出了一个不同的问题。
websocket.html:36 INVALID_STATE_ERR: DOM Exception 11: An attempt was made to use an object that is not, or is no longer, usable.
没有任何 javascript websocket 事件处理程序触发,而且我仍然看到来自 python 的 IndexError
异常。
总而言之。由于 header 响应中存在额外的 LF,您的 Python 代码无法在 Chrome 上运行。还有其他问题正在发生,因为适用于 Chrome 的代码不适用于 Safari。
更新
我已经解决了根本问题,现在示例可以在 Safari 和 Chrome 中运行。
base64.encodestring()
始终在其返回值中添加尾随 \n
。这就是 Safari 提示的 LF 的来源。
对 calculate_websocket_hash
的返回值调用 .strip()
并使用原始 header 模板可以在 Safari 和 Chrome 上正常工作。
关于sockets - Python 3 中的 Websocket 实现,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12879274/