socket-netty 0.3.2__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {socket_netty-0.3.2/socket_netty.egg-info → socket_netty-0.4.0}/PKG-INFO +268 -14
  2. {socket_netty-0.3.2 → socket_netty-0.4.0}/README.md +267 -13
  3. {socket_netty-0.3.2 → socket_netty-0.4.0}/pyproject.toml +1 -1
  4. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/__init__.py +70 -1
  5. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/bootstrap/bootstrap.py +38 -1
  6. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/buffer/allocator.py +32 -0
  7. socket_netty-0.4.0/socket_netty/buffer/bytebuf.py +597 -0
  8. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/channel/channel.py +79 -4
  9. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/channel/channel_future.py +9 -1
  10. socket_netty-0.4.0/socket_netty/channel/channel_pipeline.py +301 -0
  11. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/channel/datagram_channel.py +35 -5
  12. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/channel/flow_control.py +8 -0
  13. socket_netty-0.4.0/socket_netty/channel/protocol_adapter.py +132 -0
  14. socket_netty-0.4.0/socket_netty/handler/__init__.py +117 -0
  15. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/handler/channel_handler.py +23 -0
  16. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/handler/channel_handler_context.py +45 -1
  17. socket_netty-0.4.0/socket_netty/handler/http/__init__.py +80 -0
  18. socket_netty-0.4.0/socket_netty/handler/http/http_aggregator.py +128 -0
  19. socket_netty-0.4.0/socket_netty/handler/http/http_codec.py +392 -0
  20. socket_netty-0.4.0/socket_netty/handler/http/http_message.py +177 -0
  21. socket_netty-0.4.0/socket_netty/handler/http/http_types.py +254 -0
  22. socket_netty-0.4.0/socket_netty/handler/http/websocket_codec.py +220 -0
  23. socket_netty-0.4.0/socket_netty/handler/http/websocket_frame.py +66 -0
  24. socket_netty-0.4.0/socket_netty/handler/http/websocket_handshake.py +138 -0
  25. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/handler/timeout.py +21 -6
  26. {socket_netty-0.3.2 → socket_netty-0.4.0/socket_netty.egg-info}/PKG-INFO +268 -14
  27. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty.egg-info/SOURCES.txt +8 -0
  28. {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_codec.py +3 -0
  29. {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_pipeline.py +32 -39
  30. socket_netty-0.3.2/socket_netty/buffer/bytebuf.py +0 -256
  31. socket_netty-0.3.2/socket_netty/channel/channel_pipeline.py +0 -173
  32. socket_netty-0.3.2/socket_netty/channel/protocol_adapter.py +0 -59
  33. socket_netty-0.3.2/socket_netty/handler/__init__.py +0 -47
  34. {socket_netty-0.3.2 → socket_netty-0.4.0}/MANIFEST.in +0 -0
  35. {socket_netty-0.3.2 → socket_netty-0.4.0}/examples/01_echo_client.py +0 -0
  36. {socket_netty-0.3.2 → socket_netty-0.4.0}/examples/01_echo_server.py +0 -0
  37. {socket_netty-0.3.2 → socket_netty-0.4.0}/examples/02_game_protocol_client.py +0 -0
  38. {socket_netty-0.3.2 → socket_netty-0.4.0}/examples/02_game_protocol_server.py +0 -0
  39. {socket_netty-0.3.2 → socket_netty-0.4.0}/setup.cfg +0 -0
  40. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/bootstrap/__init__.py +0 -0
  41. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/buffer/__init__.py +0 -0
  42. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/channel/__init__.py +0 -0
  43. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/channel/channel_option.py +0 -0
  44. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/channel/event_loop.py +0 -0
  45. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/exceptions.py +0 -0
  46. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/handler/codec.py +0 -0
  47. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/handler/protolib_codec.py +0 -0
  48. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/handler/ssl_context.py +0 -0
  49. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty.egg-info/dependency_links.txt +0 -0
  50. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty.egg-info/requires.txt +0 -0
  51. {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty.egg-info/top_level.txt +0 -0
  52. {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_bytebuf.py +0 -0
  53. {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_concurrency.py +0 -0
  54. {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_core_extras.py +0 -0
  55. {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_echo.py +0 -0
  56. {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_flow_and_allocator.py +0 -0
  57. {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_network_extras.py +0 -0
  58. {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_tls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socket-netty
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: Asynchronous networking library for Python, inspired by Netty
5
5
  Author: Button
6
6
  Keywords: networking,asyncio,netty,tcp,udp,game-server,protocol
@@ -28,23 +28,26 @@ external dependencies.
28
28
 
29
29
  | Netty | Python (socket-netty) | Notes |
30
30
  |---|---|---|
31
- | Bootstrap | Yes | |
31
+ | Bootstrap | Yes | + `child_option` for per-connection socket options |
32
32
  | ServerBootstrap | Yes | |
33
- | Channel | Yes | TCP |
34
- | ChannelPipeline | Yes | |
35
- | ChannelHandler | Yes | inbound + outbound |
36
- | ChannelHandlerContext | Yes | |
33
+ | Channel | Yes | TCP, `write`/`queue_write`/`flush` |
34
+ | ChannelPipeline | Yes | `add_first/last/before/after`, `replace`, `remove_first/last`, `first`/`last` |
35
+ | ChannelHandler | Yes | inbound + outbound, `channel_writability_changed`, `user_event_triggered` |
36
+ | ChannelHandlerContext | Yes | `is_removed()` |
37
37
  | EventLoop | Yes | |
38
38
  | EventLoopGroup | Yes | |
39
39
  | ChannelFuture | Yes | + ChannelPromise |
40
- | ByteBuf | Yes | |
40
+ | ByteBuf | Yes | `get_*`/`set_*`, little-endian, `slice`/`copy`/`discard_read_bytes` |
41
41
  | Allocator | Yes | Unpooled + Pooled |
42
42
  | TCP | Yes | |
43
43
  | UDP | Yes | DatagramChannel |
44
- | Socket options | Yes | ChannelOption |
45
- | Backpressure | Yes | WriteBufferWaterMark |
44
+ | Socket options | Yes | ChannelOption, `option`/`child_option` |
45
+ | Backpressure | Yes | WriteBufferWaterMark, `channel_writability_changed` |
46
46
  | Concurrency | Yes | ChannelExecutor |
47
47
  | TLS/SSL | Yes | SslContextBuilder |
48
+ | HTTP/1.1 | Yes | HttpServerCodec/HttpClientCodec, chunked encoding, keep-alive, HttpObjectAggregator |
49
+ | HTTPS | Yes | Same HTTP pipeline + `.ssl(...)`, TLS handled transparently by the transport |
50
+ | WebSocket | Yes | RFC 6455 server-side upgrade, all frame types, WSS (WebSocket over TLS) |
48
51
  | Encoders / Decoders | Yes | length-based framing |
49
52
  | Idle handlers | Yes | IdleStateHandler |
50
53
  | Timeouts | Yes | Read/WriteTimeoutHandler |
@@ -77,6 +80,8 @@ socket_netty/
77
80
  buffer/ -> ByteBuf, ByteBufAllocator (Unpooled/Pooled)
78
81
  handler/ -> ChannelHandler, codecs, protolib bridge, SSL,
79
82
  timeouts/idle
83
+ handler/http/ -> HTTP/1.1 (HttpServerCodec, HttpObjectAggregator) and
84
+ WebSocket (RFC 6455 handshake + frame codec)
80
85
  channel/ -> Channel, DatagramChannel, ChannelPipeline,
81
86
  EventLoop/Group, ChannelFuture, ChannelOption,
82
87
  flow control (backpressure + concurrency)
@@ -146,6 +151,31 @@ wrap any unexpected error raised inside `decode()` into a
146
151
  `EncoderException` — matching Netty's own behavior of never letting a
147
152
  raw internal error escape a codec unwrapped.
148
153
 
154
+ ## Pipeline management
155
+
156
+ Beyond `add_first`/`add_last`/`remove`/`get`, the pipeline supports
157
+ inserting or swapping handlers at any position:
158
+
159
+ ```python
160
+ pipeline.add_before("game", "auth", AuthHandler()) # right before "game"
161
+ pipeline.add_after("auth", "logging", LoggingHandler()) # right after "auth"
162
+
163
+ pipeline.first() # handler closest to the head, or None
164
+ pipeline.last() # handler closest to the tail, or None
165
+ pipeline.names() # names in actual pipeline order
166
+
167
+ pipeline.remove_first()
168
+ pipeline.remove_last()
169
+
170
+ # Common pattern: swap a login handler for the real game handler once
171
+ # a player authenticates.
172
+ pipeline.replace("auth", "game", GameHandler())
173
+ ```
174
+
175
+ A handler can check `ctx.is_removed()` to see whether it's still part
176
+ of the pipeline before touching `ctx` again from an async task started
177
+ earlier (e.g. one kicked off from `channel_read`).
178
+
149
179
  ## Socket options
150
180
 
151
181
  ```python
@@ -154,13 +184,19 @@ from socket_netty import ServerBootstrap, ChannelOption
154
184
  server = (
155
185
  ServerBootstrap()
156
186
  .child_handler(init_channel)
157
- .option(ChannelOption.SO_REUSEADDR, True)
158
- .option(ChannelOption.SO_BACKLOG, 128)
159
- .option(ChannelOption.TCP_NODELAY, True) # low latency, typical for games
187
+ .option(ChannelOption.SO_REUSEADDR, True) # applies to the listening socket
188
+ .option(ChannelOption.SO_BACKLOG, 128) # applies to the listening socket
189
+ .child_option(ChannelOption.TCP_NODELAY, True) # applies to each ACCEPTED connection
160
190
  )
161
191
  await server.bind("0.0.0.0", 9000)
162
192
  ```
163
193
 
194
+ `option()` configures the listening socket itself. `child_option()`
195
+ configures each accepted connection instead — this is what you want
196
+ for per-connection tuning like `TCP_NODELAY` (disabling Nagle's
197
+ algorithm for lower latency), since the listening socket never
198
+ carries application traffic.
199
+
164
200
  ## TLS/SSL
165
201
 
166
202
  ```python
@@ -176,6 +212,117 @@ client_ctx = SslContextBuilder.for_client().trust_manager("cert.pem").build()
176
212
  channel = await Bootstrap().handler(init_client).ssl(client_ctx).connect("myserver.com", 9443)
177
213
  ```
178
214
 
215
+ ## HTTP / HTTPS
216
+
217
+ `HttpServerCodec` decodes incoming requests and encodes outgoing
218
+ responses through a single pipeline handler. Pair it with
219
+ `HttpObjectAggregator` if you don't need streaming and just want a
220
+ complete `FullHttpRequest` per request:
221
+
222
+ ```python
223
+ from socket_netty import (
224
+ ServerBootstrap, ChannelInboundHandler,
225
+ HttpServerCodec, HttpObjectAggregator,
226
+ FullHttpRequest, FullHttpResponse, HttpResponseStatus, HttpVersion, HttpUtil,
227
+ LastHttpContent,
228
+ )
229
+
230
+ class HelloHandler(ChannelInboundHandler):
231
+ async def channel_read(self, ctx, msg):
232
+ if isinstance(msg, FullHttpRequest):
233
+ body = f"Hello, {msg.uri}".encode()
234
+ resp = FullHttpResponse(HttpResponseStatus.OK, body, HttpVersion.HTTP_1_1)
235
+ resp.headers.set("Content-Type", "text/plain")
236
+ HttpUtil.set_content_length(resp, len(body))
237
+ HttpUtil.set_keep_alive(resp, HttpUtil.is_keep_alive(msg))
238
+ await ctx.write_no_flush(resp)
239
+ await ctx.write(LastHttpContent(b""))
240
+
241
+ def init_channel(channel):
242
+ channel.pipeline.add_last("http", HttpServerCodec())
243
+ channel.pipeline.add_last("aggregator", HttpObjectAggregator(1024 * 1024)) # max body size
244
+ channel.pipeline.add_last("hello", HelloHandler())
245
+
246
+ server = await ServerBootstrap().child_handler(init_channel).bind("0.0.0.0", 8080)
247
+ ```
248
+
249
+ **HTTPS is the exact same pipeline** with `.ssl(...)` added — TLS is
250
+ handled transparently by the underlying transport, the HTTP handlers
251
+ above don't change at all:
252
+
253
+ ```python
254
+ server_ctx = SslContextBuilder.for_server("cert.pem", "key.pem").build()
255
+ server = ServerBootstrap().child_handler(init_channel).ssl(server_ctx)
256
+ await server.bind("0.0.0.0", 8443)
257
+ ```
258
+
259
+ Chunked request/response bodies are decoded/encoded transparently -
260
+ `HttpObjectAggregator` reassembles a chunked body into a normal
261
+ `FullHttpRequest`/`FullHttpResponse` (with `Content-Length` set and
262
+ `Transfer-Encoding` removed) before your handler ever sees it. If you
263
+ want to stream a large response instead of buffering it all, skip the
264
+ aggregator and write `HttpResponse` + one or more `HttpContent` +
265
+ `LastHttpContent` yourself.
266
+
267
+ Keep-alive follows the real HTTP/1.0 vs 1.1 rules via `HttpUtil`:
268
+ HTTP/1.1 connections stay open by default (`Connection: close` closes
269
+ them), HTTP/1.0 connections close by default (`Connection: keep-alive`
270
+ keeps them open).
271
+
272
+ Not implemented (documented gap): the `Expect: 100-continue`
273
+ request/response flow.
274
+
275
+ ## WebSocket
276
+
277
+ `WebSocketServerProtocolHandler` sits after the HTTP codec/aggregator
278
+ and handles the RFC 6455 upgrade handshake automatically - once a
279
+ client connects, it swaps the pipeline from HTTP handling to
280
+ WebSocket frames for that connection:
281
+
282
+ ```python
283
+ from socket_netty import (
284
+ ServerBootstrap, ChannelInboundHandler,
285
+ HttpServerCodec, HttpObjectAggregator,
286
+ TextWebSocketFrame, CloseWebSocketFrame,
287
+ WebSocketServerProtocolHandler,
288
+ )
289
+
290
+ class EchoHandler(ChannelInboundHandler):
291
+ async def channel_read(self, ctx, msg):
292
+ if isinstance(msg, TextWebSocketFrame):
293
+ await ctx.write(TextWebSocketFrame(text=f"echo: {msg.text}"))
294
+ elif isinstance(msg, CloseWebSocketFrame):
295
+ await ctx.write(CloseWebSocketFrame(1000, "bye"))
296
+ await ctx.close()
297
+
298
+ def init_channel(channel):
299
+ channel.pipeline.add_last("http", HttpServerCodec())
300
+ channel.pipeline.add_last("aggregator", HttpObjectAggregator(1024 * 1024))
301
+ channel.pipeline.add_last(
302
+ "upgrade",
303
+ WebSocketServerProtocolHandler(path="/ws", http_handler_names=["http", "aggregator"]),
304
+ )
305
+ channel.pipeline.add_last("echo", EchoHandler())
306
+
307
+ server = await ServerBootstrap().child_handler(init_channel).bind("0.0.0.0", 8080)
308
+ ```
309
+
310
+ `path` restricts the upgrade to that specific request path, so the
311
+ same server can serve normal HTTP on other paths. `http_handler_names`
312
+ tells the handshake handler which HTTP-side handlers to remove from
313
+ the pipeline once the connection becomes a WebSocket (there's no
314
+ reliable way to infer this automatically, since you choose those
315
+ names yourself). **WSS (WebSocket over TLS)** is, again, the same
316
+ pipeline with `.ssl(...)` on the bootstrap — nothing else changes.
317
+
318
+ Frame types: `TextWebSocketFrame`, `BinaryWebSocketFrame`,
319
+ `ContinuationWebSocketFrame`, `PingWebSocketFrame`,
320
+ `PongWebSocketFrame`, `CloseWebSocketFrame`. Not implemented
321
+ (documented gap): WebSocket extensions (permessage-deflate
322
+ compression) and the client-side handshaker (`WebSocketClientHandshaker`)
323
+ - the server side above covers the common "game server exposes a
324
+ WebSocket API/panel" case.
325
+
179
326
  ## UDP
180
327
 
181
328
  ```python
@@ -208,6 +355,23 @@ def init_channel(channel):
208
355
  channel.pipeline.add_last("my_handler", MyHandler())
209
356
  ```
210
357
 
358
+ ## Custom (non-I/O) events
359
+
360
+ `ChannelInboundHandler.user_event_triggered(ctx, event)` lets an
361
+ application signal something through the pipeline that isn't a
362
+ network read - e.g. "player finished login" triggered from
363
+ application code rather than `channel_read`:
364
+
365
+ ```python
366
+ class GameHandler(ChannelInboundHandler):
367
+ async def user_event_triggered(self, ctx, event):
368
+ if event == "login_complete":
369
+ print("Player is ready:", ctx.channel.remote_address())
370
+
371
+ # From anywhere with access to the channel/ctx:
372
+ await ctx.fire_user_event_triggered("login_complete")
373
+ ```
374
+
211
375
  ## EventLoopGroup (real multi-threaded concurrency)
212
376
 
213
377
  ```python
@@ -250,6 +414,37 @@ if not ctx.channel.is_writable():
250
414
  ...
251
415
  ```
252
416
 
417
+ `ChannelInboundHandler.channel_writability_changed(ctx)` is fired
418
+ automatically through the pipeline whenever writability flips, so you
419
+ don't have to poll `is_writable()` to react to backpressure:
420
+
421
+ ```python
422
+ class MyHandler(ChannelInboundHandler):
423
+ async def channel_writability_changed(self, ctx):
424
+ if not ctx.channel.is_writable():
425
+ print("Backpressure: pausing outgoing updates")
426
+ else:
427
+ print("Writable again: resuming")
428
+ ```
429
+
430
+ ## Batched writes (write / queue_write / flush)
431
+
432
+ `write()` keeps its historical behavior: it queues a message through
433
+ the outbound handlers and immediately flushes it (equivalent to
434
+ Netty's `writeAndFlush()`). For a game server updating many entities
435
+ per tick, `queue_write()` + a single `flush()` sends everything
436
+ queued as one combined write, paying the pipeline traversal cost once
437
+ instead of once per message:
438
+
439
+ ```python
440
+ for entity in updated_entities:
441
+ await channel.queue_write(encode(entity)) # queued, not sent yet
442
+ await channel.flush() # sends everything queued, combined
443
+ ```
444
+
445
+ The same pair (`write_no_flush()`/`flush()`) is available on
446
+ `ChannelHandlerContext` for handlers that want the same control.
447
+
253
448
  ## Concurrency (ChannelExecutor)
254
449
 
255
450
  Guarantees a channel's messages are processed one at a time, in
@@ -365,7 +560,55 @@ i = read_buf.read_int()
365
560
 
366
561
  Supports: `byte`, `unsigned_byte`, `short`, `unsigned_short`, `int`,
367
562
  `unsigned_int`, `long`, `float`, `double`, `boolean`, `varint`,
368
- `varlong` (Minecraft/protobuf style), `string`, and raw bytes.
563
+ `varlong` (Minecraft/protobuf style), `string`, and raw bytes — in
564
+ both big-endian (default) and little-endian (`*_le` suffix, e.g.
565
+ `write_int_le`/`read_int_le`) for every fixed-size numeric type.
566
+
567
+ ### Absolute get/set (don't move reader_index/writer_index)
568
+
569
+ ```python
570
+ buf = ByteBuf()
571
+ buf.write_int(0) # placeholder for a length prefix
572
+ start = buf.writer_index
573
+ buf.write_string("hello world")
574
+ buf.set_int(0, buf.writer_index - start) # patch the length in afterwards
575
+ ```
576
+
577
+ Every fixed-size type has a `get_*`/`set_*` pair (`get_int`/`set_int`,
578
+ `get_short`/`set_short`, etc., plus `_le` variants) for reading/writing
579
+ at an absolute index without touching `reader_index`/`writer_index` —
580
+ the classic "write payload, go back and patch the length prefix"
581
+ pattern.
582
+
583
+ ### Views: slice / duplicate / copy
584
+
585
+ ```python
586
+ buf = ByteBuf(b"hello world")
587
+ buf.read_bytes(6) # consume "hello "
588
+ view = buf.slice() # shares memory with buf - "world"
589
+ independent = buf.copy() # independent deep copy
590
+ ```
591
+
592
+ `slice()`/`duplicate()`/`retained_slice()` share backing storage with
593
+ the original buffer (writes through one are visible through the
594
+ other) and can't grow past their fixed window. `copy()` is a fully
595
+ independent deep copy. Note a CPython-specific limitation: while a
596
+ slice is alive, the parent buffer can't grow, `clear()`, or
597
+ `discard_read_bytes()` — doing so raises `IndexOutOfBoundsError` with
598
+ a clear message; drop every reference to a slice once you're done
599
+ with it.
600
+
601
+ ### Other utilities
602
+
603
+ - `discard_read_bytes()` — compacts the buffer, dropping the
604
+ already-consumed prefix. Useful for long-lived accumulation buffers
605
+ (frame decoders) so they don't grow indefinitely in memory.
606
+ - `mark_writer_index()` / `reset_writer_index()` — writer-side
607
+ counterpart to `mark_reader_index()`/`reset_to_mark()`.
608
+ - `bytes_before(value)` — position of the first occurrence of a byte
609
+ ahead of `reader_index`, without moving it (handy for
610
+ null-terminated C-style strings).
611
+ - `ensure_writable(length)` — pre-reserves capacity.
369
612
 
370
613
  ## Tests
371
614
 
@@ -386,9 +629,20 @@ python3 tests/test_concurrency.py # ChannelExecutor
386
629
  single loop — it's the honest way to replicate Netty's model
387
630
  (thread pool) in Python.
388
631
  - `ByteBuf` is a simple implementation backed by `bytearray`, with no
389
- manual refcounting (Python already has GC).
632
+ manual refcounting (Python already has GC). `slice()`/`duplicate()`
633
+ share memory via a `memoryview` instead of copying, but inherit a
634
+ CPython-specific limitation from it: a `bytearray` can't be resized
635
+ while a `memoryview` into it is alive, so the parent buffer can't
636
+ grow/`clear()`/`discard_read_bytes()` while a slice of it still is.
390
637
  - `PooledByteBufAllocator` recycles buffers by size "bucket" (powers
391
638
  of 2), useful in high-traffic game servers to reduce GC pressure.
639
+ - Per-connection events (`data_received`, `connection_made`,
640
+ `connection_lost`, writability changes) are processed one at a time,
641
+ in the exact order asyncio delivered them, via a small internal
642
+ queue — this preserves TCP's in-order delivery guarantee at the
643
+ application level even when a handler's processing of one message
644
+ takes an `await` long enough for a later message to otherwise finish
645
+ first.
392
646
  - Exceptions mirror Netty's own `io.netty.*` hierarchy (see
393
647
  [Exceptions](#exceptions) above), so error messages and logs read
394
648
  the same as a real Netty stack trace.