javascript - 为什么 client.recv(1024) 在这个简单的 WebSocket 服务器实现中返回一个空字节文字?

标签 javascript python websocket

我需要在隔离网络上的 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对于 select用于检索 rlist , wlist , xlist

select.select(rlist, wlist, xlist[, timeout]) This is a straightforward interface to the Unix select() system call. The first three arguments are iterables of ‘waitable objects’: either integers representing file descriptors or objects with a parameterless method named fileno() returning such an integer:

rlist: wait until ready for reading

wlist: wait until ready for writing

xlist: wait for an “exceptional condition” (see the manual page for what your system considers such a condition)


看到这个特性是基于 Unix 系统调用的,我意识到这段代码可能不支持 Windows,这是我的环境。我检查了 rlist 的值, wlist , xlist发现它们在第一次迭代时都是空列表rList = [784] (或其他数字,如 664)和其他两个为空,之后连接关闭。
该文档继续指出:

Note: File objects on Windows are not acceptable, but sockets are. On Windows, the underlying select() function is provided by the WinSock library, and does not handle file descriptors that don’t originate from 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应该包含多个整数,或者协议(protocol)是否按预期工作直到 recv谁能解释造成损坏的原因.recv打电话到这里?客户端 JavaScript WebSocket 协议(protocol)是否没有发送任何预期的数据?还是 WebSocket 服务器有问题,它有什么问题?

最佳答案

我尝试运行您的示例,它似乎按预期工作。至少服务器日志以以下行结尾:

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 确实指出

    On Windows, only sockets are supported


    但很可能操作系统无关紧要,因为服务器代码仅使用套接字作为 select.select论据。recv当套接字的读取端关闭时返回一个空字节字符串。来自 recv(3)男人:

    If no messages are available to be received and the peer has performed an orderly shutdown, recv() shall return 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
    

    关于javascript - 为什么 client.recv(1024) 在这个简单的 WebSocket 服务器实现中返回一个空字节文字?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63920237/

    相关文章:

    javascript - 尽管设置了 'No Store',iPad 仍缓存 AJAX 响应

    python - 依赖 apt 库构建 conda 包

    java - 使用 websocket 和 Spring Boot 广播消息

    python - 根据 pandas 数据框中的相邻列将 NaN 值替换为特定文本

    java - 如何在 AWS Elastic Beanstalk 部署的 Java Web 应用程序中启用 WebSocket 请求

    ios - 在 iOS 中保持应用程序空闲一段时间后,SRWebsocket 连接自动关闭

    javascript - 原生地从对象中提取属性

    javascript - 如何使用 Node.js 进行异步 api 调用?

    JavaScript:调用 init 函数与返回该函数调用

    python - Django 表单抛出 ValueError