socket-netty 0.3.1__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 (61) hide show
  1. socket_netty-0.4.0/PKG-INFO +650 -0
  2. socket_netty-0.4.0/README.md +630 -0
  3. {socket_netty-0.3.1 → socket_netty-0.4.0}/examples/02_game_protocol_client.py +1 -1
  4. {socket_netty-0.3.1 → socket_netty-0.4.0}/pyproject.toml +1 -1
  5. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/__init__.py +70 -1
  6. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/bootstrap/bootstrap.py +38 -1
  7. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/buffer/allocator.py +32 -0
  8. socket_netty-0.4.0/socket_netty/buffer/bytebuf.py +597 -0
  9. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/channel/channel.py +79 -4
  10. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/channel/channel_future.py +9 -1
  11. socket_netty-0.4.0/socket_netty/channel/channel_pipeline.py +301 -0
  12. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/channel/datagram_channel.py +35 -5
  13. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/channel/flow_control.py +8 -0
  14. socket_netty-0.4.0/socket_netty/channel/protocol_adapter.py +132 -0
  15. socket_netty-0.4.0/socket_netty/handler/__init__.py +117 -0
  16. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/handler/channel_handler.py +23 -0
  17. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/handler/channel_handler_context.py +45 -1
  18. socket_netty-0.4.0/socket_netty/handler/http/__init__.py +80 -0
  19. socket_netty-0.4.0/socket_netty/handler/http/http_aggregator.py +128 -0
  20. socket_netty-0.4.0/socket_netty/handler/http/http_codec.py +392 -0
  21. socket_netty-0.4.0/socket_netty/handler/http/http_message.py +177 -0
  22. socket_netty-0.4.0/socket_netty/handler/http/http_types.py +254 -0
  23. socket_netty-0.4.0/socket_netty/handler/http/websocket_codec.py +220 -0
  24. socket_netty-0.4.0/socket_netty/handler/http/websocket_frame.py +66 -0
  25. socket_netty-0.4.0/socket_netty/handler/http/websocket_handshake.py +138 -0
  26. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/handler/timeout.py +21 -6
  27. socket_netty-0.4.0/socket_netty.egg-info/PKG-INFO +650 -0
  28. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty.egg-info/SOURCES.txt +8 -0
  29. {socket_netty-0.3.1 → socket_netty-0.4.0}/tests/test_bytebuf.py +3 -3
  30. {socket_netty-0.3.1 → socket_netty-0.4.0}/tests/test_codec.py +4 -1
  31. {socket_netty-0.3.1 → socket_netty-0.4.0}/tests/test_concurrency.py +1 -1
  32. {socket_netty-0.3.1 → socket_netty-0.4.0}/tests/test_core_extras.py +1 -1
  33. {socket_netty-0.3.1 → socket_netty-0.4.0}/tests/test_echo.py +3 -3
  34. {socket_netty-0.3.1 → socket_netty-0.4.0}/tests/test_flow_and_allocator.py +1 -1
  35. {socket_netty-0.3.1 → socket_netty-0.4.0}/tests/test_network_extras.py +1 -1
  36. {socket_netty-0.3.1 → socket_netty-0.4.0}/tests/test_pipeline.py +33 -40
  37. {socket_netty-0.3.1 → socket_netty-0.4.0}/tests/test_tls.py +1 -1
  38. socket_netty-0.3.1/PKG-INFO +0 -399
  39. socket_netty-0.3.1/README.md +0 -379
  40. socket_netty-0.3.1/socket_netty/buffer/bytebuf.py +0 -256
  41. socket_netty-0.3.1/socket_netty/channel/channel_pipeline.py +0 -173
  42. socket_netty-0.3.1/socket_netty/channel/protocol_adapter.py +0 -59
  43. socket_netty-0.3.1/socket_netty/handler/__init__.py +0 -47
  44. socket_netty-0.3.1/socket_netty.egg-info/PKG-INFO +0 -399
  45. {socket_netty-0.3.1 → socket_netty-0.4.0}/MANIFEST.in +0 -0
  46. {socket_netty-0.3.1 → socket_netty-0.4.0}/examples/01_echo_client.py +0 -0
  47. {socket_netty-0.3.1 → socket_netty-0.4.0}/examples/01_echo_server.py +0 -0
  48. {socket_netty-0.3.1 → socket_netty-0.4.0}/examples/02_game_protocol_server.py +0 -0
  49. {socket_netty-0.3.1 → socket_netty-0.4.0}/setup.cfg +0 -0
  50. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/bootstrap/__init__.py +0 -0
  51. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/buffer/__init__.py +0 -0
  52. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/channel/__init__.py +0 -0
  53. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/channel/channel_option.py +0 -0
  54. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/channel/event_loop.py +0 -0
  55. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/exceptions.py +0 -0
  56. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/handler/codec.py +0 -0
  57. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/handler/protolib_codec.py +0 -0
  58. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty/handler/ssl_context.py +0 -0
  59. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty.egg-info/dependency_links.txt +0 -0
  60. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty.egg-info/requires.txt +0 -0
  61. {socket_netty-0.3.1 → socket_netty-0.4.0}/socket_netty.egg-info/top_level.txt +0 -0
@@ -0,0 +1,650 @@
1
+ Metadata-Version: 2.4
2
+ Name: socket-netty
3
+ Version: 0.4.0
4
+ Summary: Asynchronous networking library for Python, inspired by Netty
5
+ Author: Button
6
+ Keywords: networking,asyncio,netty,tcp,udp,game-server,protocol
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Topic :: System :: Networking
16
+ Classifier: Framework :: AsyncIO
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: protolib>=0.4.3
20
+
21
+ # socket-netty
22
+
23
+ Asynchronous networking library for Python, closely inspired by
24
+ [Netty](https://netty.io) (Java). Built on top of `asyncio`, with no
25
+ external dependencies.
26
+
27
+ ## Coverage relative to Netty
28
+
29
+ | Netty | Python (socket-netty) | Notes |
30
+ |---|---|---|
31
+ | Bootstrap | Yes | + `child_option` for per-connection socket options |
32
+ | ServerBootstrap | 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
+ | EventLoop | Yes | |
38
+ | EventLoopGroup | Yes | |
39
+ | ChannelFuture | Yes | + ChannelPromise |
40
+ | ByteBuf | Yes | `get_*`/`set_*`, little-endian, `slice`/`copy`/`discard_read_bytes` |
41
+ | Allocator | Yes | Unpooled + Pooled |
42
+ | TCP | Yes | |
43
+ | UDP | Yes | DatagramChannel |
44
+ | Socket options | Yes | ChannelOption, `option`/`child_option` |
45
+ | Backpressure | Yes | WriteBufferWaterMark, `channel_writability_changed` |
46
+ | Concurrency | Yes | ChannelExecutor |
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) |
51
+ | Encoders / Decoders | Yes | length-based framing |
52
+ | Idle handlers | Yes | IdleStateHandler |
53
+ | Timeouts | Yes | Read/WriteTimeoutHandler |
54
+ | Exception handling | Yes | `exception_caught` via the pipeline, `io.netty.*`-style exception classes |
55
+ | Declarative packet codec | Yes | ProtolibCodec, via the `protolib` dependency |
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ pip install socket-netty
61
+ ```
62
+
63
+ Or, for local development from a clone of this repo:
64
+
65
+ ```bash
66
+ pip install -e .
67
+ ```
68
+
69
+ `socket_netty` declares [`protolib`](https://pypi.org/project/protolib/) as
70
+ an install dependency, so either install method pulls it in
71
+ automatically — you don't need a separate install step to use
72
+ `ProtolibCodec` below.
73
+
74
+ ## Structure
75
+
76
+ ```
77
+ socket_netty/
78
+ exceptions.py -> Netty-style exception hierarchy (DecoderException,
79
+ EncoderException, CorruptedFrameException, etc.)
80
+ buffer/ -> ByteBuf, ByteBufAllocator (Unpooled/Pooled)
81
+ handler/ -> ChannelHandler, codecs, protolib bridge, SSL,
82
+ timeouts/idle
83
+ handler/http/ -> HTTP/1.1 (HttpServerCodec, HttpObjectAggregator) and
84
+ WebSocket (RFC 6455 handshake + frame codec)
85
+ channel/ -> Channel, DatagramChannel, ChannelPipeline,
86
+ EventLoop/Group, ChannelFuture, ChannelOption,
87
+ flow control (backpressure + concurrency)
88
+ bootstrap/ -> ServerBootstrap (server), Bootstrap (client)
89
+ ```
90
+
91
+ ## Example: TCP echo server
92
+
93
+ ```python
94
+ import asyncio
95
+ from socket_netty import ServerBootstrap, ChannelInboundHandler
96
+
97
+ class EchoHandler(ChannelInboundHandler):
98
+ async def channel_active(self, ctx):
99
+ print("Client connected:", ctx.channel.remote_address())
100
+
101
+ async def channel_read(self, ctx, msg):
102
+ await ctx.write(msg) # echo
103
+
104
+ async def exception_caught(self, ctx, exc):
105
+ print("Error:", exc)
106
+ await ctx.close()
107
+
108
+ async def main():
109
+ def init_channel(channel):
110
+ channel.pipeline.add_last("echo", EchoHandler())
111
+
112
+ server = await ServerBootstrap().child_handler(init_channel).bind("0.0.0.0", 9000)
113
+ await server.serve_forever()
114
+
115
+ asyncio.run(main())
116
+ ```
117
+
118
+ ## Exceptions
119
+
120
+ socket_netty raises the same exception hierarchy Netty does, under
121
+ `io.netty.*`. Every exception's `str()` is prefixed with its
122
+ fully-qualified Java-style path, so logs and `exception_caught` output
123
+ look exactly like a real Netty stack:
124
+
125
+ ```python
126
+ from socket_netty import DecoderException
127
+
128
+ async def exception_caught(self, ctx, exc):
129
+ print(exc)
130
+ # io.netty.handler.codec.DecoderException: Failed to decode packet
131
+ ```
132
+
133
+ Available exceptions (all subclass `NettyException`):
134
+
135
+ | Class | Netty path |
136
+ |---|---|
137
+ | `ChannelException` | `io.netty.channel.ChannelException` |
138
+ | `DuplicateHandlerNameError` | `io.netty.channel.ChannelPipelineException` |
139
+ | `CodecException` | `io.netty.handler.codec.CodecException` |
140
+ | `DecoderException` | `io.netty.handler.codec.DecoderException` |
141
+ | `EncoderException` | `io.netty.handler.codec.EncoderException` |
142
+ | `CorruptedFrameException` | `io.netty.handler.codec.CorruptedFrameException` |
143
+ | `TooLongFrameException` | `io.netty.handler.codec.TooLongFrameException` |
144
+ | `ReadTimeoutError` | `io.netty.handler.timeout.ReadTimeoutException` |
145
+ | `WriteTimeoutError` | `io.netty.handler.timeout.WriteTimeoutException` |
146
+ | `IndexOutOfBoundsError` | `io.netty.buffer.IndexOutOfBoundsException` |
147
+
148
+ `LengthFieldBasedFrameDecoder` and `ByteToMessageCodec` automatically
149
+ wrap any unexpected error raised inside `decode()` into a
150
+ `DecoderException`, and `LengthFieldPrepender` does the same for
151
+ `EncoderException` — matching Netty's own behavior of never letting a
152
+ raw internal error escape a codec unwrapped.
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
+
179
+ ## Socket options
180
+
181
+ ```python
182
+ from socket_netty import ServerBootstrap, ChannelOption
183
+
184
+ server = (
185
+ ServerBootstrap()
186
+ .child_handler(init_channel)
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
190
+ )
191
+ await server.bind("0.0.0.0", 9000)
192
+ ```
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
+
200
+ ## TLS/SSL
201
+
202
+ ```python
203
+ from socket_netty import ServerBootstrap, Bootstrap, SslContextBuilder
204
+
205
+ # Server
206
+ server_ctx = SslContextBuilder.for_server("cert.pem", "key.pem").build()
207
+ server = ServerBootstrap().child_handler(init_channel).ssl(server_ctx)
208
+ await server.bind("0.0.0.0", 9443)
209
+
210
+ # Client
211
+ client_ctx = SslContextBuilder.for_client().trust_manager("cert.pem").build()
212
+ channel = await Bootstrap().handler(init_client).ssl(client_ctx).connect("myserver.com", 9443)
213
+ ```
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
+
326
+ ## UDP
327
+
328
+ ```python
329
+ from socket_netty import DatagramBootstrap, ChannelInboundHandler
330
+
331
+ class UdpHandler(ChannelInboundHandler):
332
+ async def channel_read(self, ctx, msg):
333
+ data, addr = msg # UDP delivers (bytes, (host, port))
334
+ await ctx.write((b"pong", addr))
335
+
336
+ channel = await DatagramBootstrap().handler(
337
+ lambda ch: ch.pipeline.add_last("udp", UdpHandler())
338
+ ).bind("0.0.0.0", 9001)
339
+ ```
340
+
341
+ ## Idle / Timeouts
342
+
343
+ ```python
344
+ from socket_netty import IdleStateHandler, ReadTimeoutHandler
345
+
346
+ async def on_idle(ctx, event):
347
+ print("Idle channel:", event.state)
348
+ await ctx.write(b"PING") # e.g. a game protocol heartbeat
349
+
350
+ def init_channel(channel):
351
+ idle = IdleStateHandler(reader_idle_seconds=30)
352
+ idle.on_idle = on_idle
353
+ channel.pipeline.add_last("idle", idle)
354
+ channel.pipeline.add_last("read_timeout", ReadTimeoutHandler(60)) # closes if no data in 60s
355
+ channel.pipeline.add_last("my_handler", MyHandler())
356
+ ```
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
+
375
+ ## EventLoopGroup (real multi-threaded concurrency)
376
+
377
+ ```python
378
+ from socket_netty import EventLoopGroup
379
+
380
+ worker_group = EventLoopGroup(num_threads=4)
381
+ worker_group.start()
382
+
383
+ loop = worker_group.next_loop() # round-robin
384
+ future = loop.submit(lambda: my_heavy_coroutine())
385
+ result = future.result(timeout=5) # blocking, from any thread
386
+ ```
387
+
388
+ ## ChannelFuture / ChannelPromise
389
+
390
+ ```python
391
+ from socket_netty import ChannelFuture
392
+
393
+ future = channel.new_future()
394
+ future.add_listener(lambda f: print("Finished:", f.is_success()))
395
+
396
+ # Still directly awaitable, Python-style:
397
+ result = await future
398
+ ```
399
+
400
+ ## Backpressure (WriteBufferWaterMark)
401
+
402
+ ```python
403
+ from socket_netty import ServerBootstrap, WriteBufferWaterMark
404
+
405
+ server = (
406
+ ServerBootstrap()
407
+ .child_handler(init_channel)
408
+ .water_mark(WriteBufferWaterMark(low=32*1024, high=64*1024))
409
+ )
410
+
411
+ # In the handler:
412
+ if not ctx.channel.is_writable():
413
+ # pause sending more data until it becomes writable again
414
+ ...
415
+ ```
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
+
448
+ ## Concurrency (ChannelExecutor)
449
+
450
+ Guarantees a channel's messages are processed one at a time, in
451
+ order, even across `await` points (avoids race conditions on shared
452
+ handler state):
453
+
454
+ ```python
455
+ from socket_netty import ChannelExecutor
456
+
457
+ executor = ChannelExecutor()
458
+ executor.start()
459
+
460
+ async def channel_read(self, ctx, msg):
461
+ await executor.submit(self._process(ctx, msg))
462
+ ```
463
+
464
+ ## Allocator (buffer pool)
465
+
466
+ ```python
467
+ from socket_netty import PooledByteBufAllocator
468
+
469
+ allocator = PooledByteBufAllocator()
470
+ buf = allocator.buffer(256)
471
+ buf.write_int(42)
472
+ # ... use the buffer ...
473
+ allocator.release(buf) # goes back to the pool, ready to be reused
474
+ ```
475
+
476
+ ## Protocol framing (length-prefixed packets)
477
+
478
+ Very common in game protocols (Minecraft, Free Fire, etc.):
479
+
480
+ ```python
481
+ from socket_netty import LengthFieldBasedFrameDecoder, LengthFieldPrepender
482
+
483
+ def init_channel(channel):
484
+ channel.pipeline.add_last("frame_decoder", LengthFieldBasedFrameDecoder(4))
485
+ channel.pipeline.add_last("frame_prepender", LengthFieldPrepender(4))
486
+ channel.pipeline.add_last("my_handler", MyHandler())
487
+ ```
488
+
489
+ ## Protocol decoding with protolib (declarative packets)
490
+
491
+ `LengthFieldBasedFrameDecoder`/`LengthFieldPrepender` above only solve
492
+ framing (where one packet ends and the next begins) — you still have
493
+ to hand-write the code that turns those raw bytes into a meaningful
494
+ packet. [`protolib`](https://pypi.org/project/protolib/) solves that
495
+ second half: you describe every packet's fields in a `.yml`/`.json`
496
+ file, and `ProtolibCodec` plugs that description directly into the
497
+ pipeline. `channel_read` then hands your handler a ready-made
498
+ `{"name": ..., "params": {...}}` dict instead of raw bytes, and
499
+ `write` accepts the same shape (or a `(name, params)` tuple) going
500
+ out.
501
+
502
+ `ProtolibCodec` has two framing modes, controlled by `framed`:
503
+
504
+ ```python
505
+ from socket_netty import ServerBootstrap, ChannelInboundHandler, ProtolibCodec
506
+
507
+ # framed=True (default): the codec frames the stream itself via
508
+ # protolib's own PacketFramer (varint length-prefix, Minecraft-style).
509
+ # Don't also add LengthFieldBasedFrameDecoder in this mode.
510
+ def init_channel(channel):
511
+ channel.pipeline.add_last(
512
+ "protolib", ProtolibCodec("my_protocol.yml", state="play",
513
+ direction_in="toServer", direction_out="toClient"),
514
+ )
515
+ channel.pipeline.add_last("game", GameHandler())
516
+
517
+ class GameHandler(ChannelInboundHandler):
518
+ async def channel_read(self, ctx, msg):
519
+ print(msg["name"], msg["params"]) # ready-made dict
520
+ await ctx.write(("keep_alive", {"id": 1})) # (name, params) out
521
+ ```
522
+
523
+ ```python
524
+ # framed=False: for fixed-size / non-varint protocols (e.g. Minecraft
525
+ # Classic/ClassiCube), keep your own LengthFieldBasedFrameDecoder in
526
+ # front and let ProtolibCodec just parse/serialize the complete frame
527
+ # it's handed.
528
+ def init_channel(channel):
529
+ channel.pipeline.add_last("frame_decoder", LengthFieldBasedFrameDecoder(1))
530
+ channel.pipeline.add_last(
531
+ "protolib", ProtolibCodec("classicube_protocol.yml", framed=False),
532
+ )
533
+ channel.pipeline.add_last("game", GameHandler())
534
+ ```
535
+
536
+ `protocol` accepts a `protolib.Protocol` instance you already built,
537
+ or anything `Protocol(...)` itself accepts (a `.yml`/`.json` path, an
538
+ in-memory string, or a parsed dict) — `ProtolibCodec` builds the
539
+ `Protocol` for you in that case. Any parsing/serialization error
540
+ protolib raises is wrapped into `DecoderException`/`EncoderException`,
541
+ same as the rest of the codecs in this library.
542
+
543
+ ## ByteBuf
544
+
545
+ ```python
546
+ from socket_netty import ByteBuf
547
+
548
+ buf = ByteBuf()
549
+ buf.write_varint(1000)
550
+ buf.write_string("hello")
551
+ buf.write_int(-42)
552
+
553
+ data = buf.to_bytes()
554
+
555
+ read_buf = ByteBuf.wrapped(data)
556
+ n = read_buf.read_varint()
557
+ s = read_buf.read_string()
558
+ i = read_buf.read_int()
559
+ ```
560
+
561
+ Supports: `byte`, `unsigned_byte`, `short`, `unsigned_short`, `int`,
562
+ `unsigned_int`, `long`, `float`, `double`, `boolean`, `varint`,
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.
612
+
613
+ ## Tests
614
+
615
+ ```bash
616
+ python3 tests/test_bytebuf.py
617
+ python3 tests/test_echo.py
618
+ python3 tests/test_core_extras.py # EventLoop, ChannelFuture, Allocator
619
+ python3 tests/test_network_extras.py # UDP, socket options, timeouts, idle
620
+ python3 tests/test_tls.py # end-to-end TLS (generates temporary certs)
621
+ python3 tests/test_concurrency.py # ChannelExecutor
622
+ ```
623
+
624
+ ## Design notes
625
+
626
+ - The entire pipeline is 100% `async`/`await`.
627
+ - `EventLoopGroup` uses real OS threads (each with its own asyncio
628
+ loop), unlike the rest of the library which normally runs on a
629
+ single loop — it's the honest way to replicate Netty's model
630
+ (thread pool) in Python.
631
+ - `ByteBuf` is a simple implementation backed by `bytearray`, with no
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.
637
+ - `PooledByteBufAllocator` recycles buffers by size "bucket" (powers
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.
646
+ - Exceptions mirror Netty's own `io.netty.*` hierarchy (see
647
+ [Exceptions](#exceptions) above), so error messages and logs read
648
+ the same as a real Netty stack trace.
649
+ - Designed for use both in game projects (custom servers, protocol
650
+ reversing) and as a general-purpose library.