Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Asyncio BufferedProtocol with SSL is significantly slower than asyncio sockets with SSL #133112

Open
@NoahStapp

Description

@NoahStapp

Bug report

Bug description:

With SSL enabled, asyncio.BufferedProtocol is significantly slower than using sockets:

$ python clients.pyPython: 3.13.0 (main, Oct 14 2024, 11:12:17) [Clang 15.0.0 (clang-1500.3.9.4)]Running 100 trials with message size 100,000,000 bytesSockets: 10.36 secondsProtocols: 17.50 seconds

Reproducible example:

shared.py:

MESSAGE_SIZE = 1_000_000 * 100MESSAGE = b"a" * MESSAGE_SIZE HOST = "127.0.0.1"PORT = 1234

server.py:

import socketimport sslfrom shared import MESSAGE, MESSAGE_SIZE, HOST, PORTcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)context.verify_mode = ssl.CERT_NONEcontext.load_cert_chain(certfile="cert.pem", keyfile="cert.pem")s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s = context.wrap_socket(s, server_side=True)s.bind((HOST, PORT))s.listen()while True:    conn, _= s.accept()    bytes_read = 0    mv = memoryview(bytearray(MESSAGE_SIZE))    while bytes_read < MESSAGE_SIZE:        read = conn.recv_into(mv[bytes_read:])        if read == 0:            raise OSError("Closed by peer")        bytes_read += read    conn.sendall(MESSAGE)    conn.close()

clients.py:

import socketimport sysimport asyncioimport sslimport timeitfrom shared import MESSAGE, MESSAGE_SIZE, HOST, PORTTRIALS=100context = ssl.SSLContext()context.verify_mode = ssl.CERT_NONEcontext.check_hostname = Falseclass Protocol(asyncio.BufferedProtocol):    def __init__(self):        super().__init__()        self._buffer = memoryview(bytearray(MESSAGE_SIZE))        self._offset = 0        self._done = None        self._loop = asyncio.get_running_loop()    def connection_made(self, transport):        self.transport = transport        self.transport.set_write_buffer_limits(MESSAGE_SIZE, MESSAGE_SIZE)    async def write(self, message: bytes):        self.transport.write(message)    async def read(self):        self._done = self._loop.create_future()        await self._done    def get_buffer(self, sizehint: int):        return self._buffer[self._offset:]    def buffer_updated(self, nbytes: int):        if self._done and not self._done.done():            self._offset += nbytes            if self._offset == MESSAGE_SIZE:                self._done.set_result(True)    def data(self):        return self._bufferasync def _async_socket_sendall_ssl(    sock: ssl.SSLSocket, buf: bytes, loop: asyncio.AbstractEventLoop) -> None:    view = memoryview(buf)    sent = 0    def _is_ready(fut: asyncio.Future) -> None:        if fut.done():            return        fut.set_result(None)    while sent < len(buf):        try:            sent += sock.send(view[sent:])        except (ssl.SSLWantReadError, ssl.SSLWantWriteError) as exc:            fd = sock.fileno()            # Check for closed socket.            if fd == -1:                raise ssl.SSLError("Underlying socket has been closed") from None            if isinstance(exc, ssl.SSLWantReadError):                fut = loop.create_future()                loop.add_reader(fd, _is_ready, fut)                try:                    await fut                finally:                    loop.remove_reader(fd)            if isinstance(exc, ssl.SSLWantWriteError):                fut = loop.create_future()                loop.add_writer(fd, _is_ready, fut)                try:                    await fut                finally:                    loop.remove_writer(fd)async def _async_socket_receive_ssl(    conn: ssl.SSLSocket, length: int, loop: asyncio.AbstractEventLoop) -> memoryview:    mv = memoryview(bytearray(length))    total_read = 0    def _is_ready(fut: asyncio.Future) -> None:        if fut.done():            return        fut.set_result(None)    while total_read < length:        try:            read = conn.recv_into(mv[total_read:])            if read == 0:                raise OSError("connection closed")            total_read += read        except (ssl.SSLWantReadError, ssl.SSLWantWriteError) as exc:            fd = conn.fileno()            # Check for closed socket.            if fd == -1:                raise ssl.SSLError("Underlying socket has been closed") from None            if isinstance(exc, ssl.SSLWantReadError):                fut = loop.create_future()                loop.add_reader(fd, _is_ready, fut)                try:                    await fut                finally:                    loop.remove_reader(fd)            if isinstance(exc, ssl.SSLWantWriteError):                fut = loop.create_future()                loop.add_writer(fd, _is_ready, fut)                try:                    await fut                finally:                    loop.remove_writer(fd)    return mvdef socket_client():    async def inner():        loop = asyncio.get_running_loop()        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)        s = context.wrap_socket(s)        s.connect((HOST, PORT))        s.setblocking(False)        await _async_socket_sendall_ssl(s, MESSAGE, loop)        data = await _async_socket_receive_ssl(s, MESSAGE_SIZE, loop)        assert len(data) == MESSAGE_SIZE and data[0]        s.close()        asyncio.run(inner())def protocols_client():    async def inner():        loop = asyncio.get_running_loop()        transport, protocol = await loop.create_connection(            lambda: Protocol(),            HOST, PORT, ssl=context)                await asyncio.wait_for(protocol.write(MESSAGE), timeout=None)        await asyncio.wait_for(protocol.read(), timeout=None)        data = protocol.data()        assert len(data) == MESSAGE_SIZE and data[0] == ord("a")        transport.close()    asyncio.run(inner())def run_test(title, func):    result = timeit.timeit(f"{func}()", setup=f"from __main__ import {func}", number=TRIALS)    print(f"{title}: {result:.2f} seconds")if __name__ == '__main__':    print(f"Python: {sys.version}")    print(f"Running {TRIALS} trials with message size {format(MESSAGE_SIZE, ',')} bytes")    run_test("Sockets", "socket_client")    run_test("Protocols", "protocols_client")

Profiling with cProfile + snakeviz shows that the protocol is callingssl.write, while the socket is callingssl.send, but that seems like an unlikely cause by itself.

Protocol:
Image

Socket:
Image

CPython versions tested on:

3.13

Operating systems tested on:

macOS, Linux

Metadata

Metadata

Assignees

No one assigned

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions


      [8]ページ先頭

      ©2009-2025 Movatter.jp