socketflow 0.1.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.
- socketflow/__init__.py +45 -0
- socketflow/client_side/__init__.py +3 -0
- socketflow/client_side/client.py +383 -0
- socketflow/global_side/__init__.py +13 -0
- socketflow/global_side/blueprint.py +118 -0
- socketflow/global_side/compression.py +155 -0
- socketflow/global_side/dispatcher.py +104 -0
- socketflow/global_side/event.py +69 -0
- socketflow/global_side/exceptions.py +100 -0
- socketflow/global_side/message_handler.py +38 -0
- socketflow/global_side/message_manager.py +98 -0
- socketflow/server_side/__init__.py +3 -0
- socketflow/server_side/server.py +407 -0
- socketflow-0.1.0.dist-info/METADATA +308 -0
- socketflow-0.1.0.dist-info/RECORD +17 -0
- socketflow-0.1.0.dist-info/WHEEL +5 -0
- socketflow-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import socket as socket_module
|
|
3
|
+
from ..global_side.event import (
|
|
4
|
+
EventType,
|
|
5
|
+
ClientConnectData,
|
|
6
|
+
ClientDisconnectData,
|
|
7
|
+
MessageReceivedData,
|
|
8
|
+
ErrorData,
|
|
9
|
+
ServerStartData,
|
|
10
|
+
ServerStopData,
|
|
11
|
+
)
|
|
12
|
+
from ..global_side.dispatcher import EventDispatcher
|
|
13
|
+
from ..global_side.compression import MultiCompressor
|
|
14
|
+
from ..global_side.message_manager import message_manager
|
|
15
|
+
from ..global_side.message_handler import message_handler
|
|
16
|
+
from ..global_side.exceptions import ExceptionType
|
|
17
|
+
from typing import Optional
|
|
18
|
+
import uuid
|
|
19
|
+
import time
|
|
20
|
+
import concurrent.futures
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TcpServerProtocol:
|
|
24
|
+
def __init__(self, server):
|
|
25
|
+
self.server = server
|
|
26
|
+
self.socket = None
|
|
27
|
+
self.client_addr = None
|
|
28
|
+
self._buffer = bytearray()
|
|
29
|
+
self._last_ping_time = None
|
|
30
|
+
self._missed_pings = 0
|
|
31
|
+
|
|
32
|
+
def handle_data(self, data):
|
|
33
|
+
"""Handle incoming data from client"""
|
|
34
|
+
self._buffer.extend(data)
|
|
35
|
+
self._missed_pings = 0
|
|
36
|
+
|
|
37
|
+
offset = 0
|
|
38
|
+
while len(self._buffer) - offset >= 4:
|
|
39
|
+
msg_len = int.from_bytes(self._buffer[offset : offset + 4], byteorder="big")
|
|
40
|
+
|
|
41
|
+
if len(self._buffer) - offset < 4 + msg_len:
|
|
42
|
+
break
|
|
43
|
+
|
|
44
|
+
start = offset + 4
|
|
45
|
+
end = start + msg_len
|
|
46
|
+
message_data = self._buffer[start:end]
|
|
47
|
+
|
|
48
|
+
offset = end
|
|
49
|
+
|
|
50
|
+
headers, body = message_handler.unpack_data(bytes(message_data))
|
|
51
|
+
if not headers or not isinstance(headers, dict):
|
|
52
|
+
error_msg = (
|
|
53
|
+
"Invalid message format"
|
|
54
|
+
if headers is None
|
|
55
|
+
else "Received message with invalid headers format"
|
|
56
|
+
)
|
|
57
|
+
self.server.dispatcher.emit(
|
|
58
|
+
EventType.Global.ERROR,
|
|
59
|
+
ErrorData(
|
|
60
|
+
error=ExceptionType.InvalidData(error_msg),
|
|
61
|
+
context="server.handle_data",
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
msg_type = headers.get("type")
|
|
67
|
+
if not msg_type:
|
|
68
|
+
self.server.dispatcher.emit(
|
|
69
|
+
EventType.Global.ERROR,
|
|
70
|
+
ErrorData(
|
|
71
|
+
error=ExceptionType.InvalidData(
|
|
72
|
+
"Received message without type"
|
|
73
|
+
),
|
|
74
|
+
context="server.handle_data",
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if msg_type == "__ping__":
|
|
80
|
+
pong_message = message_handler.create_pong()
|
|
81
|
+
try:
|
|
82
|
+
self.send(pong_message)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
elif msg_type == "__user__":
|
|
86
|
+
path = headers.get("path")
|
|
87
|
+
data_id = headers.get("id")
|
|
88
|
+
event_data = MessageReceivedData(
|
|
89
|
+
data=body, client_addr=self.client_addr, data_id=data_id
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if data_id and data_id in self.server.pending_responses:
|
|
93
|
+
future = self.server.pending_responses.pop(data_id)
|
|
94
|
+
if hasattr(future, "set_result") and not future.done():
|
|
95
|
+
future.set_result(body)
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
if path:
|
|
99
|
+
self.server.dispatcher.emit_path(path, event_data)
|
|
100
|
+
else:
|
|
101
|
+
self.server.dispatcher.emit(EventType.Server.MESSAGE, event_data)
|
|
102
|
+
|
|
103
|
+
if offset > 0:
|
|
104
|
+
del self._buffer[:offset]
|
|
105
|
+
|
|
106
|
+
def send(self, data):
|
|
107
|
+
"""Send data to client"""
|
|
108
|
+
if self.socket:
|
|
109
|
+
try:
|
|
110
|
+
self.socket.sendall(data)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
raise ExceptionType.MessageHandlerError(e)
|
|
113
|
+
else:
|
|
114
|
+
raise ExceptionType.NotConnected("Not connected to client")
|
|
115
|
+
|
|
116
|
+
def handle_connection_lost(self):
|
|
117
|
+
"""Handle client disconnection"""
|
|
118
|
+
self.server._clients.pop(self.client_addr, None)
|
|
119
|
+
|
|
120
|
+
for data_id, future in list(self.server.pending_responses.items()):
|
|
121
|
+
if hasattr(future, "set_exception") and not future.done():
|
|
122
|
+
future.set_exception(
|
|
123
|
+
ExceptionType.NotConnected(
|
|
124
|
+
f"Client {self.client_addr} disconnected"
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
self.server.pending_responses.clear()
|
|
128
|
+
|
|
129
|
+
self.server.dispatcher.emit(
|
|
130
|
+
EventType.Server.CLIENT_DISCONNECT,
|
|
131
|
+
ClientDisconnectData(client_addr=self.client_addr, transport=self.socket),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if self.socket:
|
|
135
|
+
self.socket.close()
|
|
136
|
+
|
|
137
|
+
self.socket = None
|
|
138
|
+
|
|
139
|
+
def keepalive_check(self):
|
|
140
|
+
"""Send periodic pings to client and check for missed pongs"""
|
|
141
|
+
while self.client_addr in self.server._clients:
|
|
142
|
+
try:
|
|
143
|
+
ping_message = message_handler.create_ping()
|
|
144
|
+
self.send(ping_message)
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
if self.client_addr not in self.server._clients:
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
time.sleep(self.server.keepalive_interval)
|
|
152
|
+
|
|
153
|
+
self._missed_pings += 1
|
|
154
|
+
|
|
155
|
+
if self._missed_pings >= self.server.keepalive_max_missed:
|
|
156
|
+
self.server.dispatcher.emit(
|
|
157
|
+
EventType.Global.ERROR,
|
|
158
|
+
ErrorData(
|
|
159
|
+
error=ExceptionType.KeepaliveTimeout("Keepalive timeout"),
|
|
160
|
+
context="server.keepalive",
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
self.handle_connection_lost()
|
|
164
|
+
break
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TcpServer:
|
|
168
|
+
def __init__(
|
|
169
|
+
self,
|
|
170
|
+
host: str = "127.0.0.1",
|
|
171
|
+
port: int = 8080,
|
|
172
|
+
compression_type: str = "zlib",
|
|
173
|
+
compression_level: int = 6,
|
|
174
|
+
compress: bool = True,
|
|
175
|
+
keepalive_interval: float = 30.0,
|
|
176
|
+
keepalive_max_missed: int = 3,
|
|
177
|
+
flow_control: bool = True,
|
|
178
|
+
recv_buffer_size: int = 65536,
|
|
179
|
+
send_buffer_size: int = 65536,
|
|
180
|
+
):
|
|
181
|
+
self.host = host
|
|
182
|
+
self.port = port
|
|
183
|
+
self.dispatcher = EventDispatcher()
|
|
184
|
+
self._server = None
|
|
185
|
+
self._clients = {}
|
|
186
|
+
self.compression_type = compression_type
|
|
187
|
+
self.compression_level = compression_level
|
|
188
|
+
self.compress = compress
|
|
189
|
+
self.keepalive_interval = keepalive_interval
|
|
190
|
+
self.keepalive_max_missed = keepalive_max_missed
|
|
191
|
+
self.flow_control_enabled = flow_control
|
|
192
|
+
self.pending_responses = {}
|
|
193
|
+
self.seperator = b"\r\nSOCKETFLOW\r\n"
|
|
194
|
+
self.recv_buffer_size = recv_buffer_size
|
|
195
|
+
self.send_buffer_size = send_buffer_size
|
|
196
|
+
|
|
197
|
+
def _handle_connection(self, client_socket, address):
|
|
198
|
+
protocol = TcpServerProtocol(self)
|
|
199
|
+
protocol.socket = client_socket
|
|
200
|
+
protocol.client_addr = address
|
|
201
|
+
self._clients[address] = protocol
|
|
202
|
+
|
|
203
|
+
client_socket.setsockopt(
|
|
204
|
+
socket_module.SOL_SOCKET, socket_module.SO_RCVBUF, self.recv_buffer_size
|
|
205
|
+
)
|
|
206
|
+
client_socket.setsockopt(
|
|
207
|
+
socket_module.SOL_SOCKET, socket_module.SO_SNDBUF, self.send_buffer_size
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
client_socket.setsockopt(
|
|
211
|
+
socket_module.SOL_SOCKET, socket_module.SO_KEEPALIVE, 1
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
threading.Thread(target=protocol.keepalive_check, daemon=True).start()
|
|
215
|
+
|
|
216
|
+
self.dispatcher.emit(
|
|
217
|
+
EventType.Server.CLIENT_CONNECT,
|
|
218
|
+
ClientConnectData(client_addr=address, transport=client_socket),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
while True:
|
|
222
|
+
try:
|
|
223
|
+
data = client_socket.recv(65536)
|
|
224
|
+
if not data:
|
|
225
|
+
break
|
|
226
|
+
protocol.handle_data(data)
|
|
227
|
+
except socket_module.timeout:
|
|
228
|
+
pass
|
|
229
|
+
except Exception:
|
|
230
|
+
break
|
|
231
|
+
protocol.handle_connection_lost()
|
|
232
|
+
|
|
233
|
+
def start(self):
|
|
234
|
+
"""Start the server"""
|
|
235
|
+
try:
|
|
236
|
+
self._server = socket_module.socket(
|
|
237
|
+
socket_module.AF_INET, socket_module.SOCK_STREAM
|
|
238
|
+
)
|
|
239
|
+
self._server.setsockopt(
|
|
240
|
+
socket_module.SOL_SOCKET, socket_module.SO_REUSEADDR, 1
|
|
241
|
+
)
|
|
242
|
+
self._server.bind((self.host, self.port))
|
|
243
|
+
self._server.listen(100)
|
|
244
|
+
|
|
245
|
+
self.dispatcher.emit(
|
|
246
|
+
EventType.Server.START, ServerStartData(host=self.host, port=self.port)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Start accepting connections in a separate thread
|
|
250
|
+
threading.Thread(target=self._accept_connections, daemon=True).start()
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
self.dispatcher.emit(
|
|
254
|
+
EventType.Global.ERROR, ErrorData(error=e, context="server.start")
|
|
255
|
+
)
|
|
256
|
+
raise
|
|
257
|
+
|
|
258
|
+
def _accept_connections(self):
|
|
259
|
+
"""Accept incoming connections"""
|
|
260
|
+
while self._server: # Check if server is still running
|
|
261
|
+
try:
|
|
262
|
+
client_socket, address = self._server.accept()
|
|
263
|
+
threading.Thread(
|
|
264
|
+
target=self._handle_connection,
|
|
265
|
+
args=(client_socket, address),
|
|
266
|
+
daemon=True,
|
|
267
|
+
).start()
|
|
268
|
+
except Exception as e:
|
|
269
|
+
if not self._server: # Server was stopped
|
|
270
|
+
break
|
|
271
|
+
self.dispatcher.emit(
|
|
272
|
+
EventType.Global.ERROR, ErrorData(error=e, context="server.accept")
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def stop(self):
|
|
276
|
+
"""Stop the server"""
|
|
277
|
+
if self._server:
|
|
278
|
+
self._server.close()
|
|
279
|
+
self._server = None
|
|
280
|
+
|
|
281
|
+
# Disconnect all existing clients
|
|
282
|
+
for client_addr, protocol in list(self._clients.items()):
|
|
283
|
+
if protocol.socket:
|
|
284
|
+
protocol.socket.close()
|
|
285
|
+
self._clients.clear()
|
|
286
|
+
|
|
287
|
+
self.dispatcher.emit(
|
|
288
|
+
EventType.Server.STOP, ServerStopData(host=self.host, port=self.port)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def send_client(
|
|
292
|
+
self,
|
|
293
|
+
client_addr: tuple,
|
|
294
|
+
data: bytes,
|
|
295
|
+
data_id: Optional[str] = None,
|
|
296
|
+
path: Optional[str] = None,
|
|
297
|
+
wait_response: bool = False,
|
|
298
|
+
wait_response_timeout: Optional[float] = 30.0,
|
|
299
|
+
):
|
|
300
|
+
if data_id is None:
|
|
301
|
+
data_id = str(uuid.uuid4())
|
|
302
|
+
|
|
303
|
+
headers = {
|
|
304
|
+
"type": "__user__",
|
|
305
|
+
"id": data_id,
|
|
306
|
+
"path": path,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if self.compress:
|
|
310
|
+
try:
|
|
311
|
+
compressed_msg = MultiCompressor.compress(
|
|
312
|
+
data, method=self.compression_type, level=self.compression_level
|
|
313
|
+
)
|
|
314
|
+
headers["compressed"] = True
|
|
315
|
+
length_bytes, encoded_message = message_manager.encode_with_length(
|
|
316
|
+
headers, compressed_msg
|
|
317
|
+
)
|
|
318
|
+
except Exception as e:
|
|
319
|
+
self.dispatcher.emit(
|
|
320
|
+
EventType.Global.ERROR,
|
|
321
|
+
ErrorData(
|
|
322
|
+
error=ExceptionType.CompressionError(
|
|
323
|
+
f"Compression failed: {e}"
|
|
324
|
+
),
|
|
325
|
+
context="server.send_client",
|
|
326
|
+
),
|
|
327
|
+
)
|
|
328
|
+
raise ExceptionType.CompressionError(f"Compression failed: {e}")
|
|
329
|
+
else:
|
|
330
|
+
length_bytes, encoded_message = message_manager.encode_with_length(
|
|
331
|
+
headers, data
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
protocol = self._clients.get(client_addr)
|
|
335
|
+
if not protocol:
|
|
336
|
+
raise ExceptionType.NotConnected(f"Client {client_addr} not connected")
|
|
337
|
+
|
|
338
|
+
protocol.send(length_bytes + encoded_message)
|
|
339
|
+
if wait_response:
|
|
340
|
+
future = concurrent.futures.Future()
|
|
341
|
+
self.pending_responses[data_id] = future
|
|
342
|
+
|
|
343
|
+
def timeout_handler():
|
|
344
|
+
if data_id in self.pending_responses:
|
|
345
|
+
self.pending_responses.pop(data_id, None)
|
|
346
|
+
if not future.done():
|
|
347
|
+
future.set_exception(
|
|
348
|
+
ExceptionType.NoResponse(
|
|
349
|
+
f"No response received within {wait_response_timeout} timeout"
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
threading.Timer(wait_response_timeout, timeout_handler).start()
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
return future.result()
|
|
357
|
+
except ExceptionType.NotConnected:
|
|
358
|
+
raise ExceptionType.NoResponse(
|
|
359
|
+
f"No response received within {wait_response_timeout} timeout - not connected"
|
|
360
|
+
)
|
|
361
|
+
except ExceptionType.NoResponse:
|
|
362
|
+
raise ExceptionType.NoResponse(
|
|
363
|
+
f"No response received within {wait_response_timeout} timeout"
|
|
364
|
+
)
|
|
365
|
+
except Exception as e:
|
|
366
|
+
raise ExceptionType.ClientError(
|
|
367
|
+
f"Error while waiting for response: {e}"
|
|
368
|
+
)
|
|
369
|
+
finally:
|
|
370
|
+
self.pending_responses.pop(data_id, None)
|
|
371
|
+
|
|
372
|
+
def disconnect_client(self, client_addr: tuple):
|
|
373
|
+
"""Disconnect a specific client"""
|
|
374
|
+
protocol = self._clients.pop(client_addr, None)
|
|
375
|
+
if protocol and protocol.socket:
|
|
376
|
+
protocol.socket.close()
|
|
377
|
+
|
|
378
|
+
def wait(self):
|
|
379
|
+
"""Wait for server to run (blocking)"""
|
|
380
|
+
try:
|
|
381
|
+
while self._server:
|
|
382
|
+
time.sleep(0.1)
|
|
383
|
+
except KeyboardInterrupt:
|
|
384
|
+
self.stop()
|
|
385
|
+
except Exception:
|
|
386
|
+
self.stop()
|
|
387
|
+
|
|
388
|
+
def start_and_wait(self):
|
|
389
|
+
"""Start server and wait (blocking)"""
|
|
390
|
+
self.start()
|
|
391
|
+
self.wait()
|
|
392
|
+
|
|
393
|
+
def get_connected_clients(self):
|
|
394
|
+
return len(self._clients)
|
|
395
|
+
|
|
396
|
+
def is_connected(self, client_addr):
|
|
397
|
+
return client_addr in self._clients
|
|
398
|
+
|
|
399
|
+
def event(self, event_type: str):
|
|
400
|
+
return self.dispatcher.event(event_type)
|
|
401
|
+
|
|
402
|
+
def path(self, path: str, middleware=None):
|
|
403
|
+
return self.dispatcher.path(path, middleware)
|
|
404
|
+
|
|
405
|
+
def register_blueprint(self, blueprint):
|
|
406
|
+
blueprint._server = self # Associate blueprint with this server
|
|
407
|
+
self.dispatcher.register_blueprint(blueprint)
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: socketflow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An asynchronous networking library for Python with advanced features
|
|
5
|
+
Home-page: https://github.com/ayammaximilian/socketflow
|
|
6
|
+
Author: SocketFlow Team
|
|
7
|
+
Author-email: contact@socketflow.dev
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Bug Reports, https://github.com/ayammaximilian/socketflow/issues
|
|
10
|
+
Project-URL: Source, https://github.com/ayammaximilian/socketflow
|
|
11
|
+
Project-URL: Documentation, https://socketflow.dev
|
|
12
|
+
Keywords: networking,tcp,socket,server,client,async,asyncio,real-time,messaging,clustering,ssl,tls,encryption,compression,middleware,events,blueprint,failover,load-balancing,redis,distributed
|
|
13
|
+
Platform: any
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Topic :: Internet
|
|
26
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
27
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
28
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
29
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
30
|
+
Classifier: Topic :: System :: Networking
|
|
31
|
+
Classifier: Topic :: Communications
|
|
32
|
+
Classifier: Topic :: Security :: Cryptography
|
|
33
|
+
Requires-Python: >=3.7
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
Dynamic: author
|
|
36
|
+
Dynamic: author-email
|
|
37
|
+
Dynamic: classifier
|
|
38
|
+
Dynamic: description
|
|
39
|
+
Dynamic: description-content-type
|
|
40
|
+
Dynamic: home-page
|
|
41
|
+
Dynamic: keywords
|
|
42
|
+
Dynamic: license
|
|
43
|
+
Dynamic: platform
|
|
44
|
+
Dynamic: project-url
|
|
45
|
+
Dynamic: requires-python
|
|
46
|
+
Dynamic: summary
|
|
47
|
+
|
|
48
|
+
# SocketFlow
|
|
49
|
+
|
|
50
|
+
A high-performance, dependency-free TCP networking library for Python with advanced features like compression, event handling, bidirectional keepalive, and more.
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- **Zero Dependencies** - Uses only Python's standard library
|
|
55
|
+
- **Bidirectional Keepalive** - Both client and server independently monitor connection health
|
|
56
|
+
- **TCP-Level Keepalive** - OS-managed keepalive for reliable connection detection
|
|
57
|
+
- **Compression** - Support for zlib, lzma, and bz2 compression
|
|
58
|
+
- **Event-Driven Architecture** - Flexible event dispatcher for handling server/client events
|
|
59
|
+
- **Blueprint System** - Organize your code with reusable blueprints
|
|
60
|
+
- **Middleware Support** - Add custom middleware to request/response processing
|
|
61
|
+
- **Path-Based Routing** - Route messages to specific handlers using paths
|
|
62
|
+
- **Efficient Buffer Handling** - O(N) buffer processing with offset pattern
|
|
63
|
+
- **Type Hints** - Full type annotations for better IDE support
|
|
64
|
+
- **Cross-Platform** - Works on Windows, Linux, and macOS
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install socketflow
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Full documentation available at:** https://socketflow.dev/
|
|
73
|
+
|
|
74
|
+
Or install from source:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git clone https://github.com/ayammaximilian/socketflow.git
|
|
78
|
+
cd socketflow
|
|
79
|
+
pip install .
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Quick Start
|
|
83
|
+
|
|
84
|
+
### Server Example
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from socketflow import TcpServer, EventType
|
|
88
|
+
|
|
89
|
+
# Create server
|
|
90
|
+
server = TcpServer(
|
|
91
|
+
host="127.0.0.1",
|
|
92
|
+
port=8080,
|
|
93
|
+
keepalive_interval=30.0,
|
|
94
|
+
keepalive_max_missed=3,
|
|
95
|
+
compress=True
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Register event handler
|
|
99
|
+
@server.event(EventType.Server.MESSAGE)
|
|
100
|
+
def handle_message(data):
|
|
101
|
+
print(f"Received: {data}")
|
|
102
|
+
return "Response"
|
|
103
|
+
|
|
104
|
+
# Start server
|
|
105
|
+
server.start()
|
|
106
|
+
server.wait() # Keep server running
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Client Example
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from socketflow import TcpClient, EventType
|
|
113
|
+
|
|
114
|
+
# Create client
|
|
115
|
+
client = TcpClient(
|
|
116
|
+
host="127.0.0.1",
|
|
117
|
+
port=8080,
|
|
118
|
+
keepalive_interval=30.0,
|
|
119
|
+
keepalive_max_missed=3,
|
|
120
|
+
compress=True
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Connect to server
|
|
124
|
+
client.connect()
|
|
125
|
+
|
|
126
|
+
# Register event handler
|
|
127
|
+
@client.event(EventType.Client.MESSAGE)
|
|
128
|
+
def handle_message(data):
|
|
129
|
+
print(f"Received: {data}")
|
|
130
|
+
|
|
131
|
+
# Send message
|
|
132
|
+
response = client.send("Hello, Server!", wait_response=True)
|
|
133
|
+
print(f"Server response: {response}")
|
|
134
|
+
|
|
135
|
+
# Disconnect
|
|
136
|
+
client.disconnect()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Configuration
|
|
140
|
+
|
|
141
|
+
### Server Options
|
|
142
|
+
|
|
143
|
+
| Parameter | Type | Default | Description |
|
|
144
|
+
|-----------|------|---------|-------------|
|
|
145
|
+
| `host` | str | "127.0.0.1" | Server host address |
|
|
146
|
+
| `port` | int | 8080 | Server port |
|
|
147
|
+
| `compression_type` | str | "zlib" | Compression algorithm (zlib, lzma, bz2) |
|
|
148
|
+
| `compression_level` | int | 6 | Compression level (1-9) |
|
|
149
|
+
| `compress` | bool | True | Enable compression |
|
|
150
|
+
| `keepalive_interval` | float | 30.0 | Keepalive interval in seconds |
|
|
151
|
+
| `keepalive_max_missed` | int | 3 | Max missed keepalives before disconnect |
|
|
152
|
+
| `recv_buffer_size` | int | 65536 | Receive buffer size |
|
|
153
|
+
| `send_buffer_size` | int | 65536 | Send buffer size |
|
|
154
|
+
|
|
155
|
+
### Client Options
|
|
156
|
+
|
|
157
|
+
| Parameter | Type | Default | Description |
|
|
158
|
+
|-----------|------|---------|-------------|
|
|
159
|
+
| `host` | str | "127.0.0.1" | Server host address |
|
|
160
|
+
| `port` | int | 8080 | Server port |
|
|
161
|
+
| `compression_type` | str | "zlib" | Compression algorithm (zlib, lzma, bz2) |
|
|
162
|
+
| `compression_level` | int | 6 | Compression level (1-9) |
|
|
163
|
+
| `compress` | bool | True | Enable compression |
|
|
164
|
+
| `keepalive_interval` | float | 30.0 | Keepalive interval in seconds |
|
|
165
|
+
| `keepalive_max_missed` | int | 3 | Max missed keepalives before disconnect |
|
|
166
|
+
| `connection_timeout` | float | 10.0 | Connection timeout in seconds |
|
|
167
|
+
| `recv_buffer_size` | int | 65536 | Receive buffer size |
|
|
168
|
+
| `send_buffer_size` | int | 65536 | Send buffer size |
|
|
169
|
+
|
|
170
|
+
## API Reference
|
|
171
|
+
|
|
172
|
+
### TcpServer
|
|
173
|
+
|
|
174
|
+
#### Methods
|
|
175
|
+
|
|
176
|
+
- `start()` - Start the server
|
|
177
|
+
- `stop()` - Stop the server and disconnect all clients
|
|
178
|
+
- `wait()` - Block until server stops
|
|
179
|
+
- `start_and_wait()` - Start server and block
|
|
180
|
+
- `send_client(client_addr, data, path=None, wait_response=False)` - Send data to specific client
|
|
181
|
+
- `disconnect_client(client_addr)` - Disconnect a specific client
|
|
182
|
+
- `get_connected_clients()` - Get number of connected clients
|
|
183
|
+
- `is_connected(client_addr)` - Check if client is connected
|
|
184
|
+
- `event(event_type)` - Decorator to register event handler
|
|
185
|
+
- `path(path, middleware=None)` - Decorator to register path handler
|
|
186
|
+
- `register_blueprint(blueprint)` - Register a blueprint
|
|
187
|
+
|
|
188
|
+
### TcpClient
|
|
189
|
+
|
|
190
|
+
#### Methods
|
|
191
|
+
|
|
192
|
+
- `connect()` - Connect to server
|
|
193
|
+
- `disconnect()` - Disconnect from server
|
|
194
|
+
- `send(data, path=None, wait_response=False)` - Send data to server
|
|
195
|
+
- `wait()` - Block until client disconnects
|
|
196
|
+
- `connect_and_wait()` - Connect and block
|
|
197
|
+
- `is_connected()` - Check if connected
|
|
198
|
+
- `event(event_type)` - Decorator to register event handler
|
|
199
|
+
- `path(path, middleware=None)` - Decorator to register path handler
|
|
200
|
+
- `register_blueprint(blueprint)` - Register a blueprint
|
|
201
|
+
|
|
202
|
+
## Events
|
|
203
|
+
|
|
204
|
+
### Server Events
|
|
205
|
+
|
|
206
|
+
- `EventType.Server.START` - Server started
|
|
207
|
+
- `EventType.Server.STOP` - Server stopped
|
|
208
|
+
- `EventType.Server.CLIENT_CONNECT` - Client connected
|
|
209
|
+
- `EventType.Server.CLIENT_DISCONNECT` - Client disconnected
|
|
210
|
+
- `EventType.Server.MESSAGE` - Message received from client
|
|
211
|
+
|
|
212
|
+
### Client Events
|
|
213
|
+
|
|
214
|
+
- `EventType.Client.CONNECT` - Connected to server
|
|
215
|
+
- `EventType.Client.DISCONNECT` - Disconnected from server
|
|
216
|
+
- `EventType.Client.MESSAGE` - Message received from server
|
|
217
|
+
|
|
218
|
+
### Global Events
|
|
219
|
+
|
|
220
|
+
- `EventType.Global.ERROR` - Error occurred
|
|
221
|
+
|
|
222
|
+
## Path-Based Routing
|
|
223
|
+
|
|
224
|
+
Send messages to specific handlers using paths:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
# Server
|
|
228
|
+
@server.path("/user/login")
|
|
229
|
+
def handle_login(data):
|
|
230
|
+
# Handle login
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
@server.path("/user/register")
|
|
234
|
+
def handle_register(data):
|
|
235
|
+
# Handle registration
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
# Client
|
|
239
|
+
client.send(data, path="/user/login")
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Blueprints
|
|
243
|
+
|
|
244
|
+
Organize your code with blueprints:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from socketflow import Blueprint
|
|
248
|
+
|
|
249
|
+
user_bp = Blueprint("user")
|
|
250
|
+
|
|
251
|
+
@user_bp.path("/login")
|
|
252
|
+
def login(data):
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
@user_bp.path("/register")
|
|
256
|
+
def register(data):
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
# Register blueprint
|
|
260
|
+
server.register_blueprint(user_bp)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Keepalive
|
|
264
|
+
|
|
265
|
+
SocketFlow implements bidirectional keepalive at two levels:
|
|
266
|
+
|
|
267
|
+
1. **Application-Level Keepalive** - Custom ping/pong messages
|
|
268
|
+
2. **TCP-Level Keepalive** - OS-managed keepalive probes
|
|
269
|
+
|
|
270
|
+
Both client and server independently monitor connection health based on their own configurations.
|
|
271
|
+
|
|
272
|
+
## Compression
|
|
273
|
+
|
|
274
|
+
Support for multiple compression algorithms:
|
|
275
|
+
|
|
276
|
+
- **zlib** - Fast compression, good balance
|
|
277
|
+
- **lzma** - High compression ratio, slower
|
|
278
|
+
- **bz2** - Good compression, moderate speed
|
|
279
|
+
|
|
280
|
+
## Error Handling
|
|
281
|
+
|
|
282
|
+
SocketFlow provides custom exception types:
|
|
283
|
+
|
|
284
|
+
- `NotConnected` - Connection not established
|
|
285
|
+
- `ConnectionTimeout` - Connection attempt timed out
|
|
286
|
+
- `KeepaliveTimeout` - Keepalive timeout
|
|
287
|
+
- `CompressionError` - Compression/decompression error
|
|
288
|
+
- `InvalidData` - Invalid message format
|
|
289
|
+
- `NoResponse` - No response received within timeout
|
|
290
|
+
- `MessageHandlerError` - Message handling error
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
MIT License - see LICENSE file for details
|
|
295
|
+
|
|
296
|
+
## Contributing
|
|
297
|
+
|
|
298
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
299
|
+
|
|
300
|
+
## Support
|
|
301
|
+
|
|
302
|
+
- GitHub Issues: https://github.com/ayammaximilian/socketflow/issues
|
|
303
|
+
- Documentation: https://socketflow.dev/
|
|
304
|
+
|
|
305
|
+
## Requirements
|
|
306
|
+
|
|
307
|
+
- Python 3.7+
|
|
308
|
+
- No external dependencies (uses only standard library)
|