MagicStack / MagicStack/uvloop

Missing asyncio feature regarding support of SCTP sockets

未关闭
#467 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

主要语言
Cython
星标
11.9k
派生
616
PR 合并指标
30 天内没有已合并 PR

描述

  • uvloop version: v0.16.0

  • Python version: 3.10.4

  • Platform: Linux
    Please note that SCTP works only if you have libsctp(-dev) installed on your Linux system. MacOS doesn't support SCTP without further effort and Windows I cannot say, since I don't use WIndows.

  • Can you reproduce the bug with PYTHONASYNCIODEBUG in env?: Please clarify if you expect further output from uvloop in this case. The error is an OS error as a result of uvloop OS socket configuration, so there is no additional output I can see other than the output below. I will edit my post accordingly if I am missing information or should try something else. Thanks!

  • Does uvloop behave differently from vanilla asyncio? How?: Yes, in vanilla asyncio, SCTP sockets can be used out of the box, behaving in the same way TCP stream sockets do.

I am using SCTP as the transport stream protocol, which works fine with Python vanilla asyncio. Asyncio shows the same behaviour as for TCP sockets, which is expected since both transport protocols are stream protocols. However, uvloop seems to only allow TCP sockets.

Server side SCTP socket issue observation
When using an SCTP listening server socket, starting the server works fine. As soon as a SCTP client tries to connect, no connection notification or client data ever reaches the Python application code, but also no errors or other messages are observed on application layer. Wireshark shows that the connection from the client is dropped by the server side right after the SCTP cookie ack handshake.

Client side SCTP socket issue observation
Using an SCTP socket for a client raises an OS Error. From the stack trace it looks like uvloop is trying to use TCP under the hood instead of the SCTP socket.

Traceback (most recent call last):
  File "/home/lukas/uvloop-bug-report/client.py", line 69, in <module>
    asyncio.run(start_client(use_sctp=True))
  File "/usr/lib/python3.10/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "uvloop/loop.pyx", line 1501, in uvloop.loop.Loop.run_until_complete
  File "/home/lukas/uvloop-bug-report/client.py", line 55, in start_client
    transport, _ = await asyncio.get_event_loop().create_connection(
  File "uvloop/loop.pyx", line 2057, in create_connection
  File "uvloop/loop.pyx", line 2045, in uvloop.loop.Loop.create_connection
  File "uvloop/handles/tcp.pyx", line 177, in uvloop.loop.TCPTransport._open
  File "uvloop/handles/tcp.pyx", line 34, in uvloop.loop.__tcp_open
OSError: [Errno 92] Protocol not available

To reproduce the issues, the following client/server code can be used on a Linux machine or wrapped in a Linux based Docker container if you don't have a Linux OS (I am happy to help with this if needed).

Server code

"""
The server side of the asyncio/uvloop SCTP socket demo.

Author: Lukas Stermann <lukas@ng-voice.com>
"""
import asyncio
import socket
from typing import Optional
import uvloop


class ServerProtocol(asyncio.Protocol):
    """
    This is just a dummy server echo protocol for demo purposes.
    """
    def __init__(self):
        self.transport: Optional[asyncio.Transport] = None

    def connection_made(self, transport: asyncio.Transport) -> None:
        self.transport = transport
        print('New connection from client')

    def data_received(self, raw_data: bytes) -> None:
        print(f'Data received from client: <{raw_data.decode()}>')
        self.transport.write(b'pong')

    def connection_lost(self, exc: Optional[Exception]) -> None:
        print('Connection lost')
        self.transport.close()


async def start_server(use_sctp: bool = True) -> None:
    """
    This server starts a listening TCP/SCTP socket.

    :param use_sctp: Whether the server shall use SCTP.
                     If set to False it uses TCP.
    """
    socket_: socket.socket

    if use_sctp:
        socket_ = socket.socket(socket.AF_INET6,
                                socket.SOCK_STREAM,
                                socket.IPPROTO_SCTP)
    else:
        socket_ = socket.socket(socket.AF_INET6,
                                socket.SOCK_STREAM)

    socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    if socket.has_dualstack_ipv6():
        socket_.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
    else:
        print('This OS does not support IPv4/IPv6 dual stack. '
              'Falling back to IPv6 only.')

    socket_.setblocking(False)
    socket_.bind(('', 10002))
    socket_.listen()

    connection_settings: dict = {
        'protocol_factory': lambda: ServerProtocol(),
        'sock': socket_}

    server = await asyncio.get_event_loop().create_server(**connection_settings)
    await server.serve_forever()


# Comment in the following line to see the issue with using an SCTP socket in uvloop.
# None of the print statements in the protocol are executed and the socket is closed
# right after the SCTP cookie handshake (use Wireshark to see this behaviour or see attached pcap).
uvloop.install()

# Switch between TCP and SCTP to see that for uvloop TCP works well, while SCTP doesn't
asyncio.run(start_server(use_sctp=True))

Client code

"""
The client side of the asyncio/uvloop SCTP socket demo.

Author: Lukas Stermann <lukas@ng-voice.com>
"""
import asyncio
import socket
from typing import Optional
import uvloop


class ClientProtocol(asyncio.Protocol):
    """
    This is just a dummy client echo protocol for demo purposes.
    """
    def __init__(self):
        self.transport: Optional[asyncio.Transport] = None

    def connection_made(self, transport: asyncio.Transport) -> None:
        self.transport = transport
        print('Connection established to server')
        self.transport.write(b'ping')

    def data_received(self, raw_data: bytes) -> None:
        print(f'Answer received from server: <{raw_data.decode()}>')

    def connection_lost(self, exc: Optional[Exception]) -> None:
        print('Connection lost')
        self.transport.close()


async def start_client(use_sctp: bool = True) -> None:
    """
    This client sends a message over TCP/SCTP socket.

    :param use_sctp: Whether the server shall use SCTP.
                     If set to False it uses TCP.
    """
    connection_parameters: dict = {
        'protocol_factory': lambda: ClientProtocol()
    }

    if use_sctp:
        socket_ = socket.socket(socket.AF_INET, socket.SOCK_STREAM,
                                socket.IPPROTO_SCTP)
    else:
        socket_ = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    socket_.setblocking(False)
    connection_parameters['sock'] = socket_

    await asyncio.get_event_loop().sock_connect(
                socket_, ('127.0.0.1', 10002))

    transport, _ = await asyncio.get_event_loop().create_connection(
        **connection_parameters)

    await asyncio.sleep(2)
    transport.close()

# Comment in the following line to see the issue with using an SCTP socket in uvloop.
# The client will crash with OSError saying the protocol is not supported
uvloop.install()

# Switch between TCP and SCTP to see that for uvloop TCP works well, while SCTP doesn't
asyncio.run(start_client(use_sctp=True))

Please also find attached a zipped PCAP for Wireshark. The first part of the trace there is the vanilla asyncio version with the uvloop.install() command commented out in both client and server code. The second trace shows the behavior when the server is using uvloop but the client is using vanilla asyncio.

Would be great if you would comment on this, cheers for your work, really appreciate that you are bringing performance to Python :)

uvloop_sctp_issue.zip

贡献指南

这个仓库没有索引到贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

调研方向

在安装了 libsctp 的 Linux 上运行提供的 SCTP 客户端和服务器复现,并比较原生 asyncio 与 uvloop。然后检查 uvloop/handles/tcp.pyx 以及 uvloop/loop.pyx 中的 create_connection 路径。完成的标准是 SCTP 客户端和服务器连接能够在 uvloop 下工作,且不会出现报告中的协议错误或连接断开。

由索引模型根据 Issue 内容生成。

评估

技术栈
python
领域
networking
Issue 类型
功能
难度
4/5
预计耗时
3-5 天
活跃度
停滞
描述清晰度
基本清楚
新手友好度
35/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。