为啥 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 用于检索rlistwlistxlist

select.select(rlist, wlist, xlist[, timeout]) 这是一个 Unix select() 系统调用的直接接口。 第一个 三个参数是“可等待对象”的可迭代对象:整数 用无参数方法表示文件描述符或对象 命名为fileno() 返回这样一个整数:

rlist: 等到准备好阅读

wlist: 等到准备好写入

xlist:等待“异常情况”(参见手册页 你的系统认为这种情况是什么)

看到这个特性是基于Unix系统调用的,我意识到这段代码可能不支持Windows,这是我的环境。我检查了rlistwlistxlist 的值,在第一次迭代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

这意味着在您的情况下,客户端和服务器之间的连接已经建立,并且一些数据已经双向传输。之后,套接字关闭。查看服务器代码,我认为服务器没有理由停止连接。所以我认为你使用的客户端应该受到责备。

要准确找出问题所在,请尝试使用 tcpdumpwireshark 拦截网络流量,并运行以下 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 服务器实现中返回一个空字节文字?的主要内容,如果未能解决你的问题,请参考以下文章

http服务器

python 网络编程篇

Pythonsocket解析模板

Web静态服务器-1-显示固定的页面

Web静态服务器-2-显示需要的页面

Python学习之路:socket简单实例