为啥 client.recv(1024) 在这个简单的 WebSocket 服务器实现中返回一个空字节文字?
Posted
技术标签:
【中文标题】为啥 client.recv(1024) 在这个简单的 WebSocket 服务器实现中返回一个空字节文字?【英文标题】:Why does client.recv(1024) return an empty byte literal in this bare-bones WebSocket Server implementation?为什么 client.recv(1024) 在这个简单的 WebSocket 服务器实现中返回一个空字节文字? 【发布时间】:2020-09-16 12:36:12 【问题描述】:我需要在隔离网络上的 Python 和 javascript 之间进行 Web 套接字客户端服务器交换,所以我只能阅读和输入的内容(相信我我希望能够运行 pip install websockets
)。这是 Python 和 JavaScript 之间的基本 RFC 6455 WebSocket 客户端-服务器关系。在代码下方,我将指出 client.recv(1024)
返回空字节文字的具体问题,导致 WebSocket 服务器实现中止连接。
客户:
<script>
const message =
name: "ping",
data: 0
const socket = new WebSocket("ws://localhost:8000")
socket.addEventListener("open", (event) =>
console.log("socket connected to server")
socket.send(JSON.stringify(message))
)
socket.addEventListener("message", (event) =>
console.log("message from socket server:", JSON.parse(event))
)
</script>
服务器,found here (minimal implementation of RFC 6455):
import array
import time
import socket
import hashlib
import sys
from select import select
import re
import logging
from threading import Thread
import signal
from base64 import b64encode
class WebSocket(object):
handshake = (
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
"Upgrade: WebSocket\r\n"
"Connection: Upgrade\r\n"
"WebSocket-Origin: %(origin)s\r\n"
"WebSocket-Location: ws://%(bind)s:%(port)s/\r\n"
"Sec-Websocket-Accept: %(accept)s\r\n"
"Sec-Websocket-Origin: %(origin)s\r\n"
"Sec-Websocket-Location: ws://%(bind)s:%(port)s/\r\n"
"\r\n"
)
def __init__(self, client, server):
self.client = client
self.server = server
self.handshaken = False
self.header = ""
self.data = ""
def feed(self, data):
if not self.handshaken:
self.header += str(data)
if self.header.find('\\r\\n\\r\\n') != -1:
parts = self.header.split('\\r\\n\\r\\n', 1)
self.header = parts[0]
if self.dohandshake(self.header, parts[1]):
logging.info("Handshake successful")
self.handshaken = True
else:
self.data += data.decode("utf-8", "ignore")
playloadData = data[6:]
mask = data[2:6]
unmasked = array.array("B", playloadData)
for i in range(len(playloadData)):
unmasked[i] = unmasked[i] ^ mask[i % 4]
self.onmessage(bytes(unmasked).decode("utf-8", "ignore"))
def dohandshake(self, header, key=None):
logging.debug("Begin handshake: %s" % header)
digitRe = re.compile(r'[^0-9]')
spacesRe = re.compile(r'\s')
part = part_1 = part_2 = origin = None
for line in header.split('\\r\\n')[1:]:
name, value = line.split(': ', 1)
if name.lower() == "sec-websocket-key1":
key_number_1 = int(digitRe.sub('', value))
spaces_1 = len(spacesRe.findall(value))
if spaces_1 == 0:
return False
if key_number_1 % spaces_1 != 0:
return False
part_1 = key_number_1 / spaces_1
elif name.lower() == "sec-websocket-key2":
key_number_2 = int(digitRe.sub('', value))
spaces_2 = len(spacesRe.findall(value))
if spaces_2 == 0:
return False
if key_number_2 % spaces_2 != 0:
return False
part_2 = key_number_2 / spaces_2
elif name.lower() == "sec-websocket-key":
part = bytes(value, 'UTF-8')
elif name.lower() == "origin":
origin = value
if part:
sha1 = hashlib.sha1()
sha1.update(part)
sha1.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11".encode('utf-8'))
accept = (b64encode(sha1.digest())).decode("utf-8", "ignore")
handshake = WebSocket.handshake %
'accept': accept,
'origin': origin,
'port': self.server.port,
'bind': self.server.bind
#handshake += response
else:
logging.warning("Not using challenge + response")
handshake = WebSocket.handshake %
'origin': origin,
'port': self.server.port,
'bind': self.server.bind
logging.debug("Sending handshake %s" % handshake)
self.client.send(bytes(handshake, 'UTF-8'))
return True
def onmessage(self, data):
logging.info("Got message: %s" % data)
def send(self, data):
logging.info("Sent message: %s" % data)
self.client.send("\x00%s\xff" % data)
def close(self):
self.client.close()
class WebSocketServer(object):
def __init__(self, bind, port, cls):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind((bind, port))
self.bind = bind
self.port = port
self.cls = cls
self.connections =
self.listeners = [self.socket]
def listen(self, backlog=5):
self.socket.listen(backlog)
logging.info("Listening on %s" % self.port)
self.running = True
while self.running:
# upon first connection rList = [784] and the other two are empty
rList, wList, xList = select(self.listeners, [], self.listeners, 1)
for ready in rList:
if ready == self.socket:
logging.debug("New client connection")
client, address = self.socket.accept()
fileno = client.fileno()
self.listeners.append(fileno)
self.connections[fileno] = self.cls(client, self)
else:
logging.debug("Client ready for reading %s" % ready)
client = self.connections[ready].client
data = client.recv(1024) # currently, this results in: b''
fileno = client.fileno()
if data: # data = b''
self.connections[fileno].feed(data)
else:
logging.debug("Closing client %s" % ready)
self.connections[fileno].close()
del self.connections[fileno]
self.listeners.remove(ready)
for failed in xList:
if failed == self.socket:
logging.error("Socket broke")
for fileno, conn in self.connections:
conn.close()
self.running = False
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG,
format="%(asctime)s - %(levelname)s - %(message)s")
server = WebSocketServer("localhost", 8000, WebSocket)
server_thread = Thread(target=server.listen, args=[5])
server_thread.start()
# Add SIGINT handler for killing the threads
def signal_handler(signal, frame):
logging.info("Caught Ctrl+C, shutting down...")
server.running = False
sys.exit()
signal.signal(signal.SIGINT, signal_handler)
while True:
time.sleep(100)
服务器端日志:
INFO - Hanshake successful
DEBUG - Client ready for reading 664
DEBUG - Closing client 664
在客户端我得到
WebSocket connection to 'ws://localhost:8000' failed: Unknown Reason
问题追踪到这里:
if data:
self.connections[fileno].feed(data)
else: # this is being triggered on the server side
logging.debug("Closing client %s" % ready)
所以研究这个我发现了一个潜在的问题in the Python documentation for select
用于检索rlist
,wlist
,xlist
select.select(rlist, wlist, xlist[, timeout])
这是一个 Unixselect()
系统调用的直接接口。 第一个 三个参数是“可等待对象”的可迭代对象:整数 用无参数方法表示文件描述符或对象 命名为fileno()
返回这样一个整数:
rlist
: 等到准备好阅读
wlist
: 等到准备好写入
xlist
:等待“异常情况”(参见手册页 你的系统认为这种情况是什么)
看到这个特性是基于Unix系统调用的,我意识到这段代码可能不支持Windows,这是我的环境。我检查了rlist
、wlist
、xlist
的值,在第一次迭代rList = [784]
(或另一个数字,例如 664)和另一个两个是空的,然后关闭连接。
文档继续说明:
注意: Windows 上的文件对象是不可接受的,但套接字是可接受的。在 Windows 上,底层的 select() 函数由 WinSock 库,并且不处理不支持的文件描述符 源自 WinSock。
但我不清楚这个的确切含义。
所以在代码逻辑中,我做了一些日志记录并在此处追踪问题:
rList, wList, xList = select(self.listeners, [], self.listeners, 1)
for ready in rList: # rList = [836] or some other number
# and then we check if ready (so the 836 int) == self.socket
# but if we log self.socket we get this:
# <socket.socket fd=772, family=AddressFamily.AF_INET,
# type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8000)>
# so of course an integer isn't going to be equivalent to that
if ready == self.socket:
logging.debug("New client connection")
#so lets skip this code and see what the other condition does
else:
logging.debug("Client ready for reading %s" % ready)
client = self.connections[ready].client
data = client.recv(1024) # currently, this results in: b''
fileno = client.fileno()
if data: # data = b'', so this is handled as falsy
self.connections[fileno].feed(data)
else:
logging.debug("Closing client %s" % ready)
至于为什么client.recv(1024)
返回一个空的二进制字符串,我不知道。我不知道rList
是否应该包含多个整数,或者协议是否按预期工作直到recv
谁能在这里解释导致.recv
通话中断的原因?客户端 JavaScript WebSocket 协议是否没有发送任何预期的数据?还是 WebSocket 服务器有问题,有什么问题?
【问题讨论】:
【参考方案1】:我尝试运行您的示例,它似乎按预期工作。至少服务器日志以以下行结尾:
INFO - Got message: "name":"ping","data":0
我的环境:
操作系统:Arch Linux; WebSocket 客户端:Chromium/85.0.4183.121 运行您提供的 JS 代码; WebSocket 服务器:运行您提供的 Python 代码的 Python/3.8.5;select.select
docstring 确实声明了
在 Windows 上,仅支持套接字
但很可能与操作系统无关,因为服务器代码仅使用套接字作为select.select
参数。
recv
在套接字的读取端关闭时返回一个空字节字符串。来自recv(3)
男:
如果没有可接收的消息并且对等方已经执行了有序关闭,recv() 将返回 0。
一个有趣的事情是你在服务器日志中收到一条关于成功握手的消息:
INFO - Hanshake successful
这意味着在您的情况下,客户端和服务器之间的连接已经建立,并且一些数据已经双向传输。之后,套接字关闭。查看服务器代码,我认为服务器没有理由停止连接。所以我认为你使用的客户端应该受到责备。
要准确找出问题所在,请尝试使用 tcpdump
或 wireshark
拦截网络流量,并运行以下 Python WebSocket 客户端脚本,该脚本会重现我在测试时浏览器执行的操作:
import socket
SERVER = ("localhost", 8000)
HANDSHAKE = (
b"GET /chat HTTP/1.1\r\n"
b"Host: server.example.com\r\n"
b"Upgrade: websocket\r\n"
b"Connection: Upgrade\r\n"
b"Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n"
b"Sec-WebSocket-Protocol: chat, superchat\r\n"
b"Sec-WebSocket-Version: 13\r\n"
b"Origin: http://example.com\r\n"
b"\r\n\r\n"
)
# a frame with `"name":"ping","data":0` payload
MESSAGE = b"\x81\x983\x81\xde\x04H\xa3\xb0e^\xe4\xfc>\x11\xf1\xb7jT\xa3\xf2&W\xe0\xaae\x11\xbb\xeey"
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(SERVER)
n = s.send(HANDSHAKE)
assert n != 0
data = s.recv(1024)
print(data.decode())
n = s.send(MESSAGE)
assert n != 0
【讨论】:
以上是关于为啥 client.recv(1024) 在这个简单的 WebSocket 服务器实现中返回一个空字节文字?的主要内容,如果未能解决你的问题,请参考以下文章