socket-netty 0.3.0__py3-none-any.whl
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.
- pynetty/__init__.py +137 -0
- pynetty/bootstrap/__init__.py +3 -0
- pynetty/bootstrap/bootstrap.py +162 -0
- pynetty/buffer/__init__.py +18 -0
- pynetty/buffer/allocator.py +113 -0
- pynetty/buffer/bytebuf.py +256 -0
- pynetty/channel/__init__.py +24 -0
- pynetty/channel/channel.py +130 -0
- pynetty/channel/channel_future.py +143 -0
- pynetty/channel/channel_option.py +59 -0
- pynetty/channel/channel_pipeline.py +173 -0
- pynetty/channel/datagram_channel.py +164 -0
- pynetty/channel/event_loop.py +157 -0
- pynetty/channel/flow_control.py +99 -0
- pynetty/channel/protocol_adapter.py +59 -0
- pynetty/exceptions.py +94 -0
- pynetty/handler/__init__.py +47 -0
- pynetty/handler/channel_handler.py +83 -0
- pynetty/handler/channel_handler_context.py +104 -0
- pynetty/handler/codec.py +152 -0
- pynetty/handler/protolib_codec.py +152 -0
- pynetty/handler/ssl_context.py +62 -0
- pynetty/handler/timeout.py +159 -0
- socket_netty-0.3.0.dist-info/METADATA +402 -0
- socket_netty-0.3.0.dist-info/RECORD +27 -0
- socket_netty-0.3.0.dist-info/WHEEL +5 -0
- socket_netty-0.3.0.dist-info/top_level.txt +1 -0
pynetty/__init__.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pynetty: an event-driven asynchronous I/O library inspired by Netty
|
|
3
|
+
(Java), built on top of asyncio.
|
|
4
|
+
|
|
5
|
+
Typical usage (server):
|
|
6
|
+
|
|
7
|
+
from pynetty import ServerBootstrap, ChannelInboundHandler
|
|
8
|
+
|
|
9
|
+
class EchoHandler(ChannelInboundHandler):
|
|
10
|
+
async def channel_read(self, ctx, msg):
|
|
11
|
+
await ctx.write(msg) # echo
|
|
12
|
+
|
|
13
|
+
async def main():
|
|
14
|
+
def init_channel(channel):
|
|
15
|
+
channel.pipeline.add_last("echo", EchoHandler())
|
|
16
|
+
|
|
17
|
+
server = await ServerBootstrap().child_handler(init_channel).bind("0.0.0.0", 9000)
|
|
18
|
+
await server.serve_forever()
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from pynetty.buffer import (
|
|
22
|
+
ByteBuf,
|
|
23
|
+
IndexOutOfBoundsError,
|
|
24
|
+
ByteBufAllocator,
|
|
25
|
+
UnpooledByteBufAllocator,
|
|
26
|
+
PooledByteBufAllocator,
|
|
27
|
+
DEFAULT_UNPOOLED,
|
|
28
|
+
DEFAULT_POOLED,
|
|
29
|
+
)
|
|
30
|
+
from pynetty.channel import (
|
|
31
|
+
Channel,
|
|
32
|
+
ChannelPipeline,
|
|
33
|
+
DuplicateHandlerNameError,
|
|
34
|
+
ChannelFuture,
|
|
35
|
+
ChannelPromise,
|
|
36
|
+
EventLoop,
|
|
37
|
+
EventLoopGroup,
|
|
38
|
+
ChannelOption,
|
|
39
|
+
apply_socket_options,
|
|
40
|
+
WriteBufferWaterMark,
|
|
41
|
+
ChannelExecutor,
|
|
42
|
+
FlowControlInboundHandler,
|
|
43
|
+
DatagramChannel,
|
|
44
|
+
DatagramBootstrap,
|
|
45
|
+
)
|
|
46
|
+
from pynetty.handler import (
|
|
47
|
+
ChannelHandler,
|
|
48
|
+
ChannelInboundHandler,
|
|
49
|
+
ChannelOutboundHandler,
|
|
50
|
+
ChannelInboundHandlerAdapter,
|
|
51
|
+
ChannelOutboundHandlerAdapter,
|
|
52
|
+
SimpleChannelInboundHandler,
|
|
53
|
+
ChannelHandlerContext,
|
|
54
|
+
LengthFieldBasedFrameDecoder,
|
|
55
|
+
LengthFieldPrepender,
|
|
56
|
+
ByteToMessageCodec,
|
|
57
|
+
ProtolibCodec,
|
|
58
|
+
SslContextBuilder,
|
|
59
|
+
IdleState,
|
|
60
|
+
IdleStateEvent,
|
|
61
|
+
IdleStateHandler,
|
|
62
|
+
ReadTimeoutHandler,
|
|
63
|
+
ReadTimeoutError,
|
|
64
|
+
WriteTimeoutHandler,
|
|
65
|
+
WriteTimeoutError,
|
|
66
|
+
)
|
|
67
|
+
from pynetty.bootstrap import ServerBootstrap, Bootstrap
|
|
68
|
+
from pynetty.exceptions import (
|
|
69
|
+
NettyException,
|
|
70
|
+
ChannelException,
|
|
71
|
+
CodecException,
|
|
72
|
+
DecoderException,
|
|
73
|
+
EncoderException,
|
|
74
|
+
CorruptedFrameException,
|
|
75
|
+
TooLongFrameException,
|
|
76
|
+
TimeoutException,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
__version__ = "0.3.0"
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
# exceptions
|
|
83
|
+
"NettyException",
|
|
84
|
+
"ChannelException",
|
|
85
|
+
"CodecException",
|
|
86
|
+
"DecoderException",
|
|
87
|
+
"EncoderException",
|
|
88
|
+
"CorruptedFrameException",
|
|
89
|
+
"TooLongFrameException",
|
|
90
|
+
"TimeoutException",
|
|
91
|
+
# buffer
|
|
92
|
+
"ByteBuf",
|
|
93
|
+
"IndexOutOfBoundsError",
|
|
94
|
+
"ByteBufAllocator",
|
|
95
|
+
"UnpooledByteBufAllocator",
|
|
96
|
+
"PooledByteBufAllocator",
|
|
97
|
+
"DEFAULT_UNPOOLED",
|
|
98
|
+
"DEFAULT_POOLED",
|
|
99
|
+
# channel
|
|
100
|
+
"Channel",
|
|
101
|
+
"ChannelPipeline",
|
|
102
|
+
"DuplicateHandlerNameError",
|
|
103
|
+
"ChannelFuture",
|
|
104
|
+
"ChannelPromise",
|
|
105
|
+
"EventLoop",
|
|
106
|
+
"EventLoopGroup",
|
|
107
|
+
"ChannelOption",
|
|
108
|
+
"apply_socket_options",
|
|
109
|
+
"WriteBufferWaterMark",
|
|
110
|
+
"ChannelExecutor",
|
|
111
|
+
"FlowControlInboundHandler",
|
|
112
|
+
"DatagramChannel",
|
|
113
|
+
"DatagramBootstrap",
|
|
114
|
+
# handler
|
|
115
|
+
"ChannelHandler",
|
|
116
|
+
"ChannelInboundHandler",
|
|
117
|
+
"ChannelOutboundHandler",
|
|
118
|
+
"ChannelInboundHandlerAdapter",
|
|
119
|
+
"ChannelOutboundHandlerAdapter",
|
|
120
|
+
"SimpleChannelInboundHandler",
|
|
121
|
+
"ChannelHandlerContext",
|
|
122
|
+
"LengthFieldBasedFrameDecoder",
|
|
123
|
+
"LengthFieldPrepender",
|
|
124
|
+
"ByteToMessageCodec",
|
|
125
|
+
"ProtolibCodec",
|
|
126
|
+
"SslContextBuilder",
|
|
127
|
+
"IdleState",
|
|
128
|
+
"IdleStateEvent",
|
|
129
|
+
"IdleStateHandler",
|
|
130
|
+
"ReadTimeoutHandler",
|
|
131
|
+
"ReadTimeoutError",
|
|
132
|
+
"WriteTimeoutHandler",
|
|
133
|
+
"WriteTimeoutError",
|
|
134
|
+
# bootstrap
|
|
135
|
+
"ServerBootstrap",
|
|
136
|
+
"Bootstrap",
|
|
137
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ServerBootstrap / Bootstrap: high-level entry points for standing up a
|
|
3
|
+
server or client, Netty-style. They configure the ChannelInitializer
|
|
4
|
+
and start the asyncio loop underneath. They support .option(...)
|
|
5
|
+
(socket options), .ssl(...) (TLS), .group(...) (EventLoopGroup), and
|
|
6
|
+
.water_mark(...) (backpressure).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import ssl as ssl_module
|
|
13
|
+
from typing import Any, Callable, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from pynetty.channel.channel import Channel
|
|
16
|
+
from pynetty.channel.protocol_adapter import _ChannelProtocol
|
|
17
|
+
from pynetty.channel.channel_option import ChannelOption, apply_socket_options
|
|
18
|
+
from pynetty.channel.flow_control import WriteBufferWaterMark
|
|
19
|
+
from pynetty.channel.event_loop import EventLoopGroup
|
|
20
|
+
from pynetty.exceptions import ChannelException
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
ChannelInitializer = Callable[[Channel], "asyncio.Future | None"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ServerBootstrap:
|
|
27
|
+
"""Equivalent to io.netty.bootstrap.ServerBootstrap."""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self._child_initializer: Optional[ChannelInitializer] = None
|
|
31
|
+
self._server: Optional[asyncio.AbstractServer] = None
|
|
32
|
+
self._options: Dict[str, Any] = {}
|
|
33
|
+
self._ssl_context: Optional[ssl_module.SSLContext] = None
|
|
34
|
+
self._water_mark: Optional[WriteBufferWaterMark] = None
|
|
35
|
+
self._group: Optional[EventLoopGroup] = None
|
|
36
|
+
|
|
37
|
+
def child_handler(self, initializer: ChannelInitializer) -> "ServerBootstrap":
|
|
38
|
+
self._child_initializer = initializer
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
def option(self, name: str, value: Any) -> "ServerBootstrap":
|
|
42
|
+
self._options[name] = value
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
def ssl(self, context: ssl_module.SSLContext) -> "ServerBootstrap":
|
|
46
|
+
self._ssl_context = context
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
def water_mark(self, mark: WriteBufferWaterMark) -> "ServerBootstrap":
|
|
50
|
+
self._water_mark = mark
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def group(self, event_loop_group: EventLoopGroup) -> "ServerBootstrap":
|
|
54
|
+
self._group = event_loop_group
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
async def bind(self, host: str = "0.0.0.0", port: int = 0) -> "ServerBootstrap":
|
|
58
|
+
if self._child_initializer is None:
|
|
59
|
+
raise ChannelException("You must call child_handler(...) before bind()")
|
|
60
|
+
|
|
61
|
+
loop = asyncio.get_event_loop()
|
|
62
|
+
|
|
63
|
+
def factory() -> Channel:
|
|
64
|
+
channel = Channel()
|
|
65
|
+
result = self._child_initializer(channel)
|
|
66
|
+
if asyncio.iscoroutine(result):
|
|
67
|
+
loop.create_task(result)
|
|
68
|
+
return channel
|
|
69
|
+
|
|
70
|
+
backlog = self._options.get(ChannelOption.SO_BACKLOG, 100)
|
|
71
|
+
|
|
72
|
+
server = await loop.create_server(
|
|
73
|
+
lambda: _ChannelProtocol(factory, water_mark=self._water_mark),
|
|
74
|
+
host=host,
|
|
75
|
+
port=port,
|
|
76
|
+
backlog=backlog,
|
|
77
|
+
ssl=self._ssl_context,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
remaining = {k: v for k, v in self._options.items() if k != ChannelOption.SO_BACKLOG}
|
|
81
|
+
if remaining:
|
|
82
|
+
for sock in server.sockets:
|
|
83
|
+
apply_socket_options(sock, remaining)
|
|
84
|
+
|
|
85
|
+
self._server = server
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
def sockets_address(self) -> list:
|
|
89
|
+
if self._server is None:
|
|
90
|
+
return []
|
|
91
|
+
return [sock.getsockname() for sock in self._server.sockets]
|
|
92
|
+
|
|
93
|
+
async def close(self) -> None:
|
|
94
|
+
if self._server is not None:
|
|
95
|
+
self._server.close()
|
|
96
|
+
await self._server.wait_closed()
|
|
97
|
+
|
|
98
|
+
async def serve_forever(self) -> None:
|
|
99
|
+
if self._server is None:
|
|
100
|
+
raise ChannelException("Call bind() before serve_forever()")
|
|
101
|
+
async with self._server:
|
|
102
|
+
await self._server.serve_forever()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Bootstrap:
|
|
106
|
+
"""Equivalent to io.netty.bootstrap.Bootstrap (client side)."""
|
|
107
|
+
|
|
108
|
+
def __init__(self) -> None:
|
|
109
|
+
self._handler_initializer: Optional[ChannelInitializer] = None
|
|
110
|
+
self._options: Dict[str, Any] = {}
|
|
111
|
+
self._ssl_context: Optional[ssl_module.SSLContext] = None
|
|
112
|
+
self._water_mark: Optional[WriteBufferWaterMark] = None
|
|
113
|
+
self._group: Optional[EventLoopGroup] = None
|
|
114
|
+
|
|
115
|
+
def handler(self, initializer: ChannelInitializer) -> "Bootstrap":
|
|
116
|
+
self._handler_initializer = initializer
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def option(self, name: str, value: Any) -> "Bootstrap":
|
|
120
|
+
self._options[name] = value
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def ssl(self, context: ssl_module.SSLContext) -> "Bootstrap":
|
|
124
|
+
self._ssl_context = context
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def water_mark(self, mark: WriteBufferWaterMark) -> "Bootstrap":
|
|
128
|
+
self._water_mark = mark
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def group(self, event_loop_group: EventLoopGroup) -> "Bootstrap":
|
|
132
|
+
self._group = event_loop_group
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
async def connect(self, host: str, port: int) -> Channel:
|
|
136
|
+
if self._handler_initializer is None:
|
|
137
|
+
raise ChannelException("You must call handler(...) before connect()")
|
|
138
|
+
|
|
139
|
+
loop = asyncio.get_event_loop()
|
|
140
|
+
holder: dict = {}
|
|
141
|
+
|
|
142
|
+
def factory() -> Channel:
|
|
143
|
+
channel = Channel()
|
|
144
|
+
holder["channel"] = channel
|
|
145
|
+
result = self._handler_initializer(channel)
|
|
146
|
+
if asyncio.iscoroutine(result):
|
|
147
|
+
loop.create_task(result)
|
|
148
|
+
return channel
|
|
149
|
+
|
|
150
|
+
transport, protocol = await loop.create_connection(
|
|
151
|
+
lambda: _ChannelProtocol(factory, water_mark=self._water_mark),
|
|
152
|
+
host=host,
|
|
153
|
+
port=port,
|
|
154
|
+
ssl=self._ssl_context,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if self._options:
|
|
158
|
+
sock = transport.get_extra_info("socket")
|
|
159
|
+
if sock is not None:
|
|
160
|
+
apply_socket_options(sock, self._options)
|
|
161
|
+
|
|
162
|
+
return holder["channel"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pynetty.buffer.bytebuf import ByteBuf, IndexOutOfBoundsError
|
|
2
|
+
from pynetty.buffer.allocator import (
|
|
3
|
+
ByteBufAllocator,
|
|
4
|
+
UnpooledByteBufAllocator,
|
|
5
|
+
PooledByteBufAllocator,
|
|
6
|
+
DEFAULT_UNPOOLED,
|
|
7
|
+
DEFAULT_POOLED,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ByteBuf",
|
|
12
|
+
"IndexOutOfBoundsError",
|
|
13
|
+
"ByteBufAllocator",
|
|
14
|
+
"UnpooledByteBufAllocator",
|
|
15
|
+
"PooledByteBufAllocator",
|
|
16
|
+
"DEFAULT_UNPOOLED",
|
|
17
|
+
"DEFAULT_POOLED",
|
|
18
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ByteBufAllocator: equivalent to io.netty.buffer.ByteBufAllocator.
|
|
3
|
+
|
|
4
|
+
Netty uses this to avoid GC pressure on high-throughput hot paths
|
|
5
|
+
(thousands of packets/sec) by recycling buffers instead of allocating a
|
|
6
|
+
new one per message. Python's GC differs (refcounting + generational),
|
|
7
|
+
but the pattern still pays off: it avoids constantly reallocating and
|
|
8
|
+
resetting bytearrays in high-traffic game servers.
|
|
9
|
+
|
|
10
|
+
Two implementations, mirroring Netty:
|
|
11
|
+
- UnpooledByteBufAllocator: allocates a fresh ByteBuf every time
|
|
12
|
+
(simple, safe default).
|
|
13
|
+
- PooledByteBufAllocator: keeps a pool of recycled ByteBuf objects per
|
|
14
|
+
size bucket; release() returns them to the pool instead of discarding.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import threading
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
from typing import Dict, List
|
|
22
|
+
|
|
23
|
+
from pynetty.buffer.bytebuf import ByteBuf
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ByteBufAllocator:
|
|
27
|
+
"""Base interface — equivalent to Netty's ByteBufAllocator interface."""
|
|
28
|
+
|
|
29
|
+
def buffer(self, initial_capacity: int = 256) -> ByteBuf:
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
def release(self, buf: ByteBuf) -> None:
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class UnpooledByteBufAllocator(ByteBufAllocator):
|
|
37
|
+
"""Simple allocator: every buffer() call creates a fresh ByteBuf; release() is a no-op beyond clearing."""
|
|
38
|
+
|
|
39
|
+
def buffer(self, initial_capacity: int = 256) -> ByteBuf:
|
|
40
|
+
return ByteBuf()
|
|
41
|
+
|
|
42
|
+
def release(self, buf: ByteBuf) -> None:
|
|
43
|
+
buf.clear()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PooledByteBufAllocator(ByteBufAllocator):
|
|
47
|
+
"""
|
|
48
|
+
Allocator with a real pool bucketed by size (powers of 2, same
|
|
49
|
+
approach Netty uses). buffer() reuses a clean ByteBuf from the
|
|
50
|
+
matching bucket if one is available; otherwise it creates one.
|
|
51
|
+
|
|
52
|
+
release(buf) clears it and returns it to its bucket's pool, ready
|
|
53
|
+
for the next buffer(similar initial_capacity) call.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
_MIN_BUCKET = 64 # minimum bucket capacity, in bytes
|
|
57
|
+
|
|
58
|
+
def __init__(self, max_pooled_per_bucket: int = 64) -> None:
|
|
59
|
+
self._pools: Dict[int, List[ByteBuf]] = defaultdict(list)
|
|
60
|
+
self._max_pooled_per_bucket = max_pooled_per_bucket
|
|
61
|
+
self._lock = threading.Lock()
|
|
62
|
+
# Simple metrics, useful for tuning game servers
|
|
63
|
+
self.stats_allocated = 0
|
|
64
|
+
self.stats_reused = 0
|
|
65
|
+
self.stats_released = 0
|
|
66
|
+
|
|
67
|
+
def _bucket_for(self, capacity: int) -> int:
|
|
68
|
+
bucket = self._MIN_BUCKET
|
|
69
|
+
while bucket < capacity:
|
|
70
|
+
bucket *= 2
|
|
71
|
+
return bucket
|
|
72
|
+
|
|
73
|
+
def buffer(self, initial_capacity: int = 256) -> ByteBuf:
|
|
74
|
+
bucket = self._bucket_for(initial_capacity)
|
|
75
|
+
with self._lock:
|
|
76
|
+
pool = self._pools[bucket]
|
|
77
|
+
if pool:
|
|
78
|
+
buf = pool.pop()
|
|
79
|
+
self.stats_reused += 1
|
|
80
|
+
buf._pool_bucket = bucket # type: ignore[attr-defined]
|
|
81
|
+
return buf
|
|
82
|
+
self.stats_allocated += 1
|
|
83
|
+
buf = ByteBuf()
|
|
84
|
+
buf._pool_bucket = bucket # type: ignore[attr-defined]
|
|
85
|
+
return buf
|
|
86
|
+
|
|
87
|
+
def release(self, buf: ByteBuf) -> None:
|
|
88
|
+
# Use the bucket it was handed out with by buffer(); if the
|
|
89
|
+
# ByteBuf didn't come from this allocator (unmarked), fall back
|
|
90
|
+
# to the minimum bucket as a safe default.
|
|
91
|
+
bucket = getattr(buf, "_pool_bucket", None) or self._MIN_BUCKET
|
|
92
|
+
buf.clear()
|
|
93
|
+
with self._lock:
|
|
94
|
+
pool = self._pools[bucket]
|
|
95
|
+
if len(pool) < self._max_pooled_per_bucket:
|
|
96
|
+
pool.append(buf)
|
|
97
|
+
self.stats_released += 1
|
|
98
|
+
|
|
99
|
+
def pooled_count(self) -> int:
|
|
100
|
+
with self._lock:
|
|
101
|
+
return sum(len(p) for p in self._pools.values())
|
|
102
|
+
|
|
103
|
+
def __repr__(self) -> str:
|
|
104
|
+
return (
|
|
105
|
+
f"PooledByteBufAllocator(allocated={self.stats_allocated}, "
|
|
106
|
+
f"reused={self.stats_reused}, released={self.stats_released}, "
|
|
107
|
+
f"pooled_now={self.pooled_count()})"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Default instances, equivalent to Netty's ByteBufAllocator.DEFAULT
|
|
112
|
+
DEFAULT_UNPOOLED = UnpooledByteBufAllocator()
|
|
113
|
+
DEFAULT_POOLED = PooledByteBufAllocator()
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ByteBuf: Netty-style byte buffer.
|
|
3
|
+
|
|
4
|
+
Maintains independent reader/writer indices (unlike BytesIO), supports
|
|
5
|
+
dynamic growth, and exposes helpers for big-endian primitives plus
|
|
6
|
+
protobuf/Minecraft-style varints (heavily used in game protocols).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import struct
|
|
12
|
+
from typing import Union
|
|
13
|
+
|
|
14
|
+
from pynetty.exceptions import IndexOutOfBoundsError, CorruptedFrameException
|
|
15
|
+
|
|
16
|
+
__all__ = ["ByteBuf", "IndexOutOfBoundsError"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ByteBuf:
|
|
20
|
+
__slots__ = ("_data", "_reader_index", "_writer_index", "_mark", "_pool_bucket")
|
|
21
|
+
|
|
22
|
+
def __init__(self, initial: Union[bytes, bytearray, None] = None) -> None:
|
|
23
|
+
self._data = bytearray(initial) if initial is not None else bytearray()
|
|
24
|
+
self._reader_index = 0
|
|
25
|
+
self._writer_index = len(self._data)
|
|
26
|
+
self._mark = 0
|
|
27
|
+
self._pool_bucket = None
|
|
28
|
+
|
|
29
|
+
# ------------------------------------------------------------------
|
|
30
|
+
# Constructors
|
|
31
|
+
# ------------------------------------------------------------------
|
|
32
|
+
@classmethod
|
|
33
|
+
def buffer(cls, capacity: int = 256) -> "ByteBuf":
|
|
34
|
+
buf = cls()
|
|
35
|
+
buf._data = bytearray()
|
|
36
|
+
return buf
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def wrapped(cls, data: Union[bytes, bytearray]) -> "ByteBuf":
|
|
40
|
+
"""Wraps existing data; writer_index is left at the end (ready to read)."""
|
|
41
|
+
return cls(data)
|
|
42
|
+
|
|
43
|
+
# ------------------------------------------------------------------
|
|
44
|
+
# Indices
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
@property
|
|
47
|
+
def reader_index(self) -> int:
|
|
48
|
+
return self._reader_index
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def writer_index(self) -> int:
|
|
52
|
+
return self._writer_index
|
|
53
|
+
|
|
54
|
+
def readable_bytes(self) -> int:
|
|
55
|
+
return self._writer_index - self._reader_index
|
|
56
|
+
|
|
57
|
+
def is_readable(self, size: int = 1) -> bool:
|
|
58
|
+
return self.readable_bytes() >= size
|
|
59
|
+
|
|
60
|
+
def reset_reader_index(self) -> "ByteBuf":
|
|
61
|
+
self._reader_index = 0
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def mark_reader_index(self) -> "ByteBuf":
|
|
65
|
+
self._mark = self._reader_index
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def reset_to_mark(self) -> "ByteBuf":
|
|
69
|
+
self._reader_index = self._mark
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def skip_bytes(self, length: int) -> "ByteBuf":
|
|
73
|
+
self._check_readable(length)
|
|
74
|
+
self._reader_index += length
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def clear(self) -> "ByteBuf":
|
|
78
|
+
self._reader_index = 0
|
|
79
|
+
self._writer_index = 0
|
|
80
|
+
self._data = bytearray()
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
# Internal
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
def _check_readable(self, length: int) -> None:
|
|
87
|
+
if self.readable_bytes() < length:
|
|
88
|
+
raise IndexOutOfBoundsError(
|
|
89
|
+
f"readerIndex({self._reader_index}) + length({length}) exceeds "
|
|
90
|
+
f"writerIndex({self._writer_index}): {self!r}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def _ensure_writable(self, length: int) -> None:
|
|
94
|
+
needed = self._writer_index + length
|
|
95
|
+
if needed > len(self._data):
|
|
96
|
+
self._data.extend(b"\x00" * (needed - len(self._data)))
|
|
97
|
+
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
# Raw bytes
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
def write_bytes(self, data: Union[bytes, bytearray]) -> "ByteBuf":
|
|
102
|
+
self._ensure_writable(len(data))
|
|
103
|
+
self._data[self._writer_index : self._writer_index + len(data)] = data
|
|
104
|
+
self._writer_index += len(data)
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def read_bytes(self, length: int) -> bytes:
|
|
108
|
+
self._check_readable(length)
|
|
109
|
+
result = bytes(self._data[self._reader_index : self._reader_index + length])
|
|
110
|
+
self._reader_index += length
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
def read_all(self) -> bytes:
|
|
114
|
+
return self.read_bytes(self.readable_bytes())
|
|
115
|
+
|
|
116
|
+
def to_bytes(self) -> bytes:
|
|
117
|
+
"""Full copy of the written content (ignores indices)."""
|
|
118
|
+
return bytes(self._data[: self._writer_index])
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------------------------------
|
|
121
|
+
# Integers (big-endian, Netty's default)
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
def write_byte(self, value: int) -> "ByteBuf":
|
|
124
|
+
return self.write_bytes(struct.pack(">b" if value < 0 else ">B", value & 0xFF if value >= 0 else value))
|
|
125
|
+
|
|
126
|
+
def read_byte(self) -> int:
|
|
127
|
+
return struct.unpack(">b", self.read_bytes(1))[0]
|
|
128
|
+
|
|
129
|
+
def read_unsigned_byte(self) -> int:
|
|
130
|
+
return struct.unpack(">B", self.read_bytes(1))[0]
|
|
131
|
+
|
|
132
|
+
def write_short(self, value: int) -> "ByteBuf":
|
|
133
|
+
return self.write_bytes(struct.pack(">h", value))
|
|
134
|
+
|
|
135
|
+
def read_short(self) -> int:
|
|
136
|
+
return struct.unpack(">h", self.read_bytes(2))[0]
|
|
137
|
+
|
|
138
|
+
def write_unsigned_short(self, value: int) -> "ByteBuf":
|
|
139
|
+
return self.write_bytes(struct.pack(">H", value))
|
|
140
|
+
|
|
141
|
+
def read_unsigned_short(self) -> int:
|
|
142
|
+
return struct.unpack(">H", self.read_bytes(2))[0]
|
|
143
|
+
|
|
144
|
+
def write_int(self, value: int) -> "ByteBuf":
|
|
145
|
+
return self.write_bytes(struct.pack(">i", value))
|
|
146
|
+
|
|
147
|
+
def read_int(self) -> int:
|
|
148
|
+
return struct.unpack(">i", self.read_bytes(4))[0]
|
|
149
|
+
|
|
150
|
+
def write_unsigned_int(self, value: int) -> "ByteBuf":
|
|
151
|
+
return self.write_bytes(struct.pack(">I", value))
|
|
152
|
+
|
|
153
|
+
def read_unsigned_int(self) -> int:
|
|
154
|
+
return struct.unpack(">I", self.read_bytes(4))[0]
|
|
155
|
+
|
|
156
|
+
def write_long(self, value: int) -> "ByteBuf":
|
|
157
|
+
return self.write_bytes(struct.pack(">q", value))
|
|
158
|
+
|
|
159
|
+
def read_long(self) -> int:
|
|
160
|
+
return struct.unpack(">q", self.read_bytes(8))[0]
|
|
161
|
+
|
|
162
|
+
def write_float(self, value: float) -> "ByteBuf":
|
|
163
|
+
return self.write_bytes(struct.pack(">f", value))
|
|
164
|
+
|
|
165
|
+
def read_float(self) -> float:
|
|
166
|
+
return struct.unpack(">f", self.read_bytes(4))[0]
|
|
167
|
+
|
|
168
|
+
def write_double(self, value: float) -> "ByteBuf":
|
|
169
|
+
return self.write_bytes(struct.pack(">d", value))
|
|
170
|
+
|
|
171
|
+
def read_double(self) -> float:
|
|
172
|
+
return struct.unpack(">d", self.read_bytes(8))[0]
|
|
173
|
+
|
|
174
|
+
def write_boolean(self, value: bool) -> "ByteBuf":
|
|
175
|
+
return self.write_byte(1 if value else 0)
|
|
176
|
+
|
|
177
|
+
def read_boolean(self) -> bool:
|
|
178
|
+
return self.read_byte() != 0
|
|
179
|
+
|
|
180
|
+
# ------------------------------------------------------------------
|
|
181
|
+
# VarInt / VarLong (Minecraft / protobuf style) — handy for wire protocols
|
|
182
|
+
# ------------------------------------------------------------------
|
|
183
|
+
def write_varint(self, value: int) -> "ByteBuf":
|
|
184
|
+
value &= 0xFFFFFFFF
|
|
185
|
+
while True:
|
|
186
|
+
byte = value & 0x7F
|
|
187
|
+
value >>= 7
|
|
188
|
+
if value:
|
|
189
|
+
self.write_byte(byte | 0x80)
|
|
190
|
+
else:
|
|
191
|
+
self.write_byte(byte)
|
|
192
|
+
break
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def read_varint(self) -> int:
|
|
196
|
+
result = 0
|
|
197
|
+
shift = 0
|
|
198
|
+
while True:
|
|
199
|
+
byte = self.read_unsigned_byte()
|
|
200
|
+
result |= (byte & 0x7F) << shift
|
|
201
|
+
if not (byte & 0x80):
|
|
202
|
+
break
|
|
203
|
+
shift += 7
|
|
204
|
+
if shift >= 35:
|
|
205
|
+
raise CorruptedFrameException("VarInt is too big")
|
|
206
|
+
if result & 0x80000000:
|
|
207
|
+
result -= 0x100000000
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
def write_varlong(self, value: int) -> "ByteBuf":
|
|
211
|
+
value &= 0xFFFFFFFFFFFFFFFF
|
|
212
|
+
while True:
|
|
213
|
+
byte = value & 0x7F
|
|
214
|
+
value >>= 7
|
|
215
|
+
if value:
|
|
216
|
+
self.write_byte(byte | 0x80)
|
|
217
|
+
else:
|
|
218
|
+
self.write_byte(byte)
|
|
219
|
+
break
|
|
220
|
+
return self
|
|
221
|
+
|
|
222
|
+
def read_varlong(self) -> int:
|
|
223
|
+
result = 0
|
|
224
|
+
shift = 0
|
|
225
|
+
while True:
|
|
226
|
+
byte = self.read_unsigned_byte()
|
|
227
|
+
result |= (byte & 0x7F) << shift
|
|
228
|
+
if not (byte & 0x80):
|
|
229
|
+
break
|
|
230
|
+
shift += 7
|
|
231
|
+
if shift >= 70:
|
|
232
|
+
raise CorruptedFrameException("VarLong is too big")
|
|
233
|
+
if result & 0x8000000000000000:
|
|
234
|
+
result -= 0x10000000000000000
|
|
235
|
+
return result
|
|
236
|
+
|
|
237
|
+
# ------------------------------------------------------------------
|
|
238
|
+
# Strings
|
|
239
|
+
# ------------------------------------------------------------------
|
|
240
|
+
def write_string(self, value: str, encoding: str = "utf-8") -> "ByteBuf":
|
|
241
|
+
encoded = value.encode(encoding)
|
|
242
|
+
self.write_varint(len(encoded))
|
|
243
|
+
return self.write_bytes(encoded)
|
|
244
|
+
|
|
245
|
+
def read_string(self, encoding: str = "utf-8") -> str:
|
|
246
|
+
length = self.read_varint()
|
|
247
|
+
return self.read_bytes(length).decode(encoding)
|
|
248
|
+
|
|
249
|
+
def __len__(self) -> int:
|
|
250
|
+
return self.readable_bytes()
|
|
251
|
+
|
|
252
|
+
def __repr__(self) -> str:
|
|
253
|
+
return (
|
|
254
|
+
f"ByteBuf(readerIndex={self._reader_index}, "
|
|
255
|
+
f"writerIndex={self._writer_index}, capacity={len(self._data)})"
|
|
256
|
+
)
|