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.
- {socket_netty-0.3.2/socket_netty.egg-info → socket_netty-0.4.0}/PKG-INFO +268 -14
- {socket_netty-0.3.2 → socket_netty-0.4.0}/README.md +267 -13
- {socket_netty-0.3.2 → socket_netty-0.4.0}/pyproject.toml +1 -1
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/__init__.py +70 -1
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/bootstrap/bootstrap.py +38 -1
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/buffer/allocator.py +32 -0
- socket_netty-0.4.0/socket_netty/buffer/bytebuf.py +597 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/channel/channel.py +79 -4
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/channel/channel_future.py +9 -1
- socket_netty-0.4.0/socket_netty/channel/channel_pipeline.py +301 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/channel/datagram_channel.py +35 -5
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/channel/flow_control.py +8 -0
- socket_netty-0.4.0/socket_netty/channel/protocol_adapter.py +132 -0
- socket_netty-0.4.0/socket_netty/handler/__init__.py +117 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/handler/channel_handler.py +23 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/handler/channel_handler_context.py +45 -1
- socket_netty-0.4.0/socket_netty/handler/http/__init__.py +80 -0
- socket_netty-0.4.0/socket_netty/handler/http/http_aggregator.py +128 -0
- socket_netty-0.4.0/socket_netty/handler/http/http_codec.py +392 -0
- socket_netty-0.4.0/socket_netty/handler/http/http_message.py +177 -0
- socket_netty-0.4.0/socket_netty/handler/http/http_types.py +254 -0
- socket_netty-0.4.0/socket_netty/handler/http/websocket_codec.py +220 -0
- socket_netty-0.4.0/socket_netty/handler/http/websocket_frame.py +66 -0
- socket_netty-0.4.0/socket_netty/handler/http/websocket_handshake.py +138 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/handler/timeout.py +21 -6
- {socket_netty-0.3.2 → socket_netty-0.4.0/socket_netty.egg-info}/PKG-INFO +268 -14
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty.egg-info/SOURCES.txt +8 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_codec.py +3 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_pipeline.py +32 -39
- socket_netty-0.3.2/socket_netty/buffer/bytebuf.py +0 -256
- socket_netty-0.3.2/socket_netty/channel/channel_pipeline.py +0 -173
- socket_netty-0.3.2/socket_netty/channel/protocol_adapter.py +0 -59
- socket_netty-0.3.2/socket_netty/handler/__init__.py +0 -47
- {socket_netty-0.3.2 → socket_netty-0.4.0}/MANIFEST.in +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/examples/01_echo_client.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/examples/01_echo_server.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/examples/02_game_protocol_client.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/examples/02_game_protocol_server.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/setup.cfg +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/bootstrap/__init__.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/buffer/__init__.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/channel/__init__.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/channel/channel_option.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/channel/event_loop.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/exceptions.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/handler/codec.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/handler/protolib_codec.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty/handler/ssl_context.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty.egg-info/dependency_links.txt +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty.egg-info/requires.txt +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/socket_netty.egg-info/top_level.txt +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_bytebuf.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_concurrency.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_core_extras.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_echo.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_flow_and_allocator.py +0 -0
- {socket_netty-0.3.2 → socket_netty-0.4.0}/tests/test_network_extras.py +0 -0
- {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
|
+
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
|
-
.
|
|
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.
|