socketify.py

repository·main·Indexed 23 days ago

https://github.com/cirospaciari/socketify.py

A high-performance web framework for Python and PyPy3 providing fast HTTP/HTTPS and WebSocket support with pub/sub capabilities. Built on top of the uNetworking/uWebSockets C API and libuv, it supports CPython and PyPy3 across Windows, Linux, and macOS. Key features include an App class for routing and server management, Request and Response classes for HTTP handling, and a WebSocket class for managing connections and fragmented messaging.

Tokens
24.3K
Snippets
49
Records
117
Agent score
79%

What's inside socketify

  1. Mix WSGI/ASGI HTTP with socketify.py WebSockets

    main

    The CLI allows you to mix different application interfaces. For example, you can use a WSGI or ASGI application for standard HTTP requests while using socketify.py for high-performance WebSockets.

    Mixing Falcon WSGI + socketify WebSocket:

    python3 -m socketify falcon_wsgi:app --ws falcon_wsgi:ws --port 8080 --workers 2

    Mixing Falcon ASGI + socketify WebSocket:

    python3 -m socketify main:app --ws main:ws --port 8080 --workers 2
    python3 -m socketify falcon_wsgi:app --ws falcon_wsgi:ws --port 8080 --workers 2
  2. How middlewares work in socketify.py

    main

    Middlewares are executed in series. They can be synchronous or asynchronous. If a middleware is a coroutine (async), socketify.py automatically calls req.preserve() to ensure request data remains valid across async segments.

    Execution Flow and Data Passing:

    • Stopping Execution: If a middleware returns a Falsy value (e.g., False, None), the execution chain stops immediately, and subsequent middlewares are not called.
    • Passing Data: If a middleware returns a Truthy value, that value is passed as the third argument (data) to the next middleware in the chain.
    • Data Transformation: Middlewares can receive the data object from the previous middleware, modify it, and return it to pass the updated state down the chain.
  3. Extend request, response, and websocket objects

    main

    When writing an extension for socketify.py, you can augment the core objects using decorators and property methods:

    Adding Methods

    Use the @request.method, @response.method, or @ws.method decorators. The first argument of the decorated function must be self to allow access to the object's internal state (e.g., self.get_header(), self.publish(), or self.write_header()).

    Adding Properties

    Use request.property(name, default_value) to attach data to the request object that can be accessed later in the request lifecycle (e.g., in middlewares or route handlers).

    Example usage:

    def extension(request, response, ws):
        @request.method
        async def get_user(self):
            return {"name": "Test"}
    
        request.property("cart", [])
    @request.method
    async def get_user(self):
        token = self.get_header("token")
        return { "name": "Test" } if token else { "name": "Anonymous" }
    
    request.property("cart", [])
  4. How the corking mechanism works

    main

    Corking is a performance optimization mechanism that packs multiple send operations into a single syscall or SSL block. This prevents excessive networking and poor performance caused by many small, individual network writes.

    Automatic Corking

    • Default Behavior: Sockets are corked by default in most simple cases and within all registered business logic callbacks (e.g., when a socket opens or a message is received).
    • Limitations:
      • Third-party libraries: Callbacks registered to other libraries (like libhiredis) will not be corked automatically because the library cannot control their execution flow.
      • Async/Await Segments: In coroutines, automatic corking only applies to the first segment of the coroutine (the code before the first await). Once an await is encountered, the execution is handed over to the asyncio event loop, and the subsequent segments are no longer under the library's automatic corking control.

    Manual Corking

    To ensure efficiency when sending data after asynchronous operations, you must manually wrap your sending logic in a function or lambda using res.cork() or ws.cork_send().

    Important: You cannot use async inside a cork block. Instead, perform all your asynchronous work first, then use the cork block to execute the final synchronous sending operations.

    async def home(res, req):
        auth = req.get_header("authorization")
        user = await do_auth(auth)
        
        res.cork(lambda res: res.end(f"Hello {user.name}"))
  5. Understand route matching priority and pattern matching

    main

    Routes are matched based on specificity, not the order in which they are registered. The priority order is:

    1. Highest Priority: Static routes (e.g., /hello/this/is/static)
    2. Middle Priority: Parameter routes (e.g., /candy/:kind). Use req.get_parameter(index) to retrieve the value.
    3. Lowest Priority: Wildcard routes (e.g., /hello/*)

    Note: any routes (matching any HTTP method) have lower priority than specific method routes (like GET) if they are otherwise equally specific.

  6. Configure WebSocket idle timeouts and heartbeats

    main

    The idle_timeout setting (in seconds) defines the maximum inactivity period before a client is disconnected.

    • Automatic Pings: The library automatically sends pings to clients based on the idle_timeout. If idle_timeout is 120, a ping is sent a few seconds before the timeout expires.
    • Keep-alive: If the client responds to the ping, the connection stays open. If the client fails to respond, the socket is forcefully closed and the close event is triggered.
    • Manual Keep-alive: Clients can also send small messages periodically to prevent the timeout from triggering.
  7. How res.send_chunk works internally

    main

    The res.send_chunk method is a high-level abstraction for managing streaming backpressure. It works by:

    1. Creating a Future to track the chunk's status.
    2. Registering an on_writable callback that calls res.try_end with the remaining slice of the buffer once the socket is ready to write.
    3. Registering an on_aborted callback to set the aborted flag and resolve the Future with (False, True) if the client disconnects.
    4. Attempting an immediate res.try_end. If it fails (due to backpressure), it stores the current write offset via self.get_write_offset() so the next chunk can be sliced correctly.
    def send_chunk(self, buffer, total_size):
            self._chunkFuture = self.loop.create_future()
            self._lastChunkOffset = 0
    
            def is_aborted(self):
                self.aborted = True
                try:
                    if not self._chunkFuture.done():
                        self._chunkFuture.set_result(
                            (False, True)
                        )  # if aborted set to done True and ok False
                except:
                    pass
    
            def on_writeble(self, offset):
                # Here the timeout is off, we can spend as much time before calling try_end we want to
                (ok, done) = self.try_end(
                    buffer[offset - self._lastChunkOffset : :],
                    total_size
                )
                if ok:
                    self._chunkFuture.set_result((ok, done))
                return ok
    
            self.on_writable(on_writeble)
            self.on_aborted(is_aborted)
    
            if self.aborted:
                self._chunkFuture.set_result(
                    (False, True)
                )  # if aborted set to done True and ok False
                return self._chunkFuture
    
            (ok, done) = self.try_end(buffer, total_size)
            if ok:
                self._chunkFuture.set_result((ok, done))
                return self._chunkFuture
            # failed to send chunk
            self._lastChunkOffset = self.get_write_offset()
    
            return self._chunkFuture
  8. Implement a custom Template Engine extension

    main

    To add support for a template engine in socketify.py, you must create a class that implements a render method. This method should accept a templatename (string) and arbitrary keyword arguments (**kwargs), returning the rendered string.

    Commonly used engines like Jinja2 and Mako can be wrapped in this way to integrate with the socketify.py application lifecycle.

  9. Handle WebSocket backpressure with the drain handler

    main

    Sending data via ws.send() can cause backpressure if the client is slow. To manage this:

    1. Check return values: ws.send() returns an enum: BACKPRESSURE, SUCCESS, or DROPPED.
    2. Handle BACKPRESSURE: If ws.send() returns BACKPRESSURE, stop sending data.
    3. Use the drain handler: Register a drain callback in your app.ws settings. This event is a hint that the buffer has changed. Inside on_drain, check ws.get_buffered_amount() to determine if it is safe to resume sending.
    4. Prevent buffer overflow: If you use max_backpressure (set during WebSocketContext creation), an attempt to send data that exceeds this limit will return DROPPED and the message will not be queued.

    Always check ws.get_buffered_amount() before attempting to send large amounts of data.

  10. Configure Security, HTTPS, and Server Settings

    main

    Advanced server configuration options include:

    • https.py: Setting up an HTTPS server with SSL/TLS.
    • listen_options.py: Configuring server listening options.
    • graceful_shutdown.py: Implementing graceful server shutdowns.
    • forks.py: Setting up a multi-process server using forks.
  11. Install socketify

    main

    Install the socketify package via pip. The package supports CPython and PyPy3 on Windows, Linux, and macOS (x64 and Silicon).

    Note for Linux and macOS users: You may need to install libuv and zlib system dependencies before installing the package.

    macOS:

    brew install libuv
    brew install zlib

    Linux (Ubuntu/Debian):

    apt install libuv1 zlib1g

    Linux (RHEL/OEL):

    yum install cmake zlib-devel libuv-devel
    pip install socketify
    #or specify PyPy3
    pypy3 -m pip install socketify
  12. Stream large data using res.send_chunk

    main

    To avoid backpressure spikes and memory issues when sending large payloads, do not use res.end(huge_buffer). Instead, use res.send_chunk to stream data in parts.

    res.send_chunk(buffer, total_size) returns a Future. It internally manages res.on_writable and res.on_aborted callbacks to handle flow control.

    When iterating through chunks, check the returned tuple (ok, done):

    • ok: Indicates if the chunk was successfully accepted for sending.
    • done: Indicates if the entire response has been completed.

    If ok is False or done is True, you should stop reading from your data source (e.g., break the loop) as the connection may have been aborted.

    async def home(res, req):
        res.write_header("Content-Type", "audio/mpeg")
       
        filename = "./file_example_MP3_5MG.mp3"
        total = os.stat(filename).st_size
        
        async with aiofiles.open(filename, "rb") as fd:
            while not res.aborted:
                buffer = await fd.read(16384)  #16kb buffer
                (ok, done) = await res.send_chunk(buffer, total)
                if not ok or done: #if cannot send probably aborted
                    break