simple-socket-server 2.0__py3-none-any.whl → 2.1__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.
- simple_socket_server/__init__.py +184 -54
- {simple_socket_server-2.0.dist-info → simple_socket_server-2.1.dist-info}/METADATA +53 -23
- simple_socket_server-2.1.dist-info/RECORD +7 -0
- simple_socket_server-2.0.dist-info/RECORD +0 -7
- {simple_socket_server-2.0.dist-info → simple_socket_server-2.1.dist-info}/WHEEL +0 -0
- {simple_socket_server-2.0.dist-info → simple_socket_server-2.1.dist-info}/licenses/LICENSE.md +0 -0
- {simple_socket_server-2.0.dist-info → simple_socket_server-2.1.dist-info}/top_level.txt +0 -0
simple_socket_server/__init__.py
CHANGED
|
@@ -11,15 +11,17 @@ Distributed under the BSD 3-Clause license. See LICENSE.md for more info.
|
|
|
11
11
|
|
|
12
12
|
import select
|
|
13
13
|
import socket
|
|
14
|
+
import sys
|
|
14
15
|
import threading
|
|
16
|
+
import time
|
|
15
17
|
|
|
16
|
-
from typing import Dict, List, Optional, Tuple
|
|
18
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
17
19
|
|
|
18
20
|
from event_bus import EventBus
|
|
19
21
|
|
|
20
22
|
__all__ = ["SimpleSocketServer", "SimpleSocketServerException"]
|
|
21
23
|
|
|
22
|
-
__version__ = "2.
|
|
24
|
+
__version__ = "2.1"
|
|
23
25
|
|
|
24
26
|
#: Peer address as returned by accept(): (host, port).
|
|
25
27
|
Address = Tuple[str, int]
|
|
@@ -30,6 +32,11 @@ _RECV_SIZE = 1024
|
|
|
30
32
|
#: How long select() waits for socket activity, in seconds.
|
|
31
33
|
_POLL_INTERVAL = 0.1
|
|
32
34
|
|
|
35
|
+
#: Default client limit. select() refuses a descriptor set larger than
|
|
36
|
+
#: FD_SETSIZE by raising ValueError, and that limit is 512 on Windows and
|
|
37
|
+
#: 1024 elsewhere, so the default stays below both.
|
|
38
|
+
_DEFAULT_MAX_CLIENTS = 500
|
|
39
|
+
|
|
33
40
|
|
|
34
41
|
class SimpleSocketServerException(socket.error):
|
|
35
42
|
"""Raised when the listening socket cannot be created, bound or enabled."""
|
|
@@ -47,14 +54,18 @@ class SimpleSocketServer(EventBus):
|
|
|
47
54
|
``message(sock, peer, data)``
|
|
48
55
|
A chunk of bytes has been received.
|
|
49
56
|
``disconnect(sock, peer)``
|
|
50
|
-
The connection is
|
|
57
|
+
The connection is closed: the peer closed it, reset it, a send failed
|
|
58
|
+
or disconnect() was called. Queued replies are delivered before it.
|
|
51
59
|
``error(sock, peer, error)``
|
|
52
|
-
A socket error occurred; the connection is closed right after
|
|
60
|
+
A socket error occurred; the connection is closed right after, or the
|
|
61
|
+
server itself stops when the listening socket or select() fails.
|
|
53
62
|
``stop(host, port)``
|
|
54
63
|
run() has returned and every socket is closed.
|
|
55
64
|
|
|
56
65
|
Handlers are called from the thread that runs run(), one at a time, so a
|
|
57
|
-
slow handler delays every other connection.
|
|
66
|
+
slow handler delays every other connection. **Handlers must not raise**: an
|
|
67
|
+
exception propagates out of run() and stops the server for every client.
|
|
68
|
+
The sockets are still closed before run() returns.
|
|
58
69
|
"""
|
|
59
70
|
|
|
60
71
|
def __init__(self) -> None:
|
|
@@ -62,11 +73,14 @@ class SimpleSocketServer(EventBus):
|
|
|
62
73
|
self.__host = "0.0.0.0"
|
|
63
74
|
self.__port = 6666
|
|
64
75
|
self.__max_conn = 5
|
|
76
|
+
self.__max_clients = _DEFAULT_MAX_CLIENTS
|
|
65
77
|
self.__server_socket: Optional[socket.socket] = None
|
|
66
78
|
self.__inputs: List[socket.socket] = []
|
|
67
79
|
self.__outputs: List[socket.socket] = []
|
|
68
80
|
self.__pending: Dict[socket.socket, bytearray] = {}
|
|
69
81
|
self.__clients: Dict[socket.socket, Address] = {}
|
|
82
|
+
self.__closing: Set[socket.socket] = set()
|
|
83
|
+
self.__lock = threading.Lock()
|
|
70
84
|
self.__stop = threading.Event()
|
|
71
85
|
|
|
72
86
|
@property
|
|
@@ -84,19 +98,31 @@ class SimpleSocketServer(EventBus):
|
|
|
84
98
|
"""Port the listening socket is bound to."""
|
|
85
99
|
return self.__port
|
|
86
100
|
|
|
87
|
-
def run(
|
|
101
|
+
def run(
|
|
102
|
+
self,
|
|
103
|
+
host: str = "0.0.0.0",
|
|
104
|
+
port: int = 6666,
|
|
105
|
+
max_conn: int = 5,
|
|
106
|
+
max_clients: int = _DEFAULT_MAX_CLIENTS,
|
|
107
|
+
) -> None:
|
|
88
108
|
"""Serve connections until stop() is called.
|
|
89
109
|
|
|
90
110
|
The call blocks the calling thread. Once it returns, every client socket
|
|
91
111
|
and the listening socket are closed. A port of 0 makes the operating
|
|
92
112
|
system pick a free port; the actual address is reported by the start
|
|
93
113
|
event and by the host and port properties.
|
|
114
|
+
|
|
115
|
+
max_conn is the backlog of the listening socket: how many connections the
|
|
116
|
+
system may queue before accept(). max_clients is the number of clients
|
|
117
|
+
the server keeps at once; a connection above it is accepted and closed
|
|
118
|
+
immediately with an error event.
|
|
94
119
|
"""
|
|
95
120
|
if self.is_running:
|
|
96
121
|
raise RuntimeError("the server is already running")
|
|
97
122
|
self.__host = host
|
|
98
123
|
self.__port = port
|
|
99
124
|
self.__max_conn = max_conn
|
|
125
|
+
self.__max_clients = max_clients
|
|
100
126
|
self.__stop.clear()
|
|
101
127
|
self.__initialize()
|
|
102
128
|
try:
|
|
@@ -120,14 +146,32 @@ class SimpleSocketServer(EventBus):
|
|
|
120
146
|
The socket is non-blocking, so the bytes are buffered and the unwritten
|
|
121
147
|
tail is retried on the next poll; a message larger than the socket
|
|
122
148
|
buffer is delivered in full. Empty messages are ignored.
|
|
149
|
+
|
|
150
|
+
Safe to call from another thread: delivery happens during the next poll,
|
|
151
|
+
so it may take up to the poll interval. The queue has no size limit, so
|
|
152
|
+
a slow reader grows the buffer until it is disconnected.
|
|
123
153
|
"""
|
|
124
|
-
if sock not in self.__pending:
|
|
125
|
-
raise ValueError("the socket is not a client of this server")
|
|
126
154
|
if not message:
|
|
127
155
|
return
|
|
128
|
-
self.
|
|
129
|
-
|
|
130
|
-
|
|
156
|
+
with self.__lock:
|
|
157
|
+
pending = self.__pending.get(sock)
|
|
158
|
+
if pending is None:
|
|
159
|
+
raise ValueError("the socket is not a client of this server")
|
|
160
|
+
pending.extend(message)
|
|
161
|
+
if sock not in self.__outputs:
|
|
162
|
+
self.__outputs.append(sock)
|
|
163
|
+
|
|
164
|
+
def disconnect(self, sock: socket.socket) -> None:
|
|
165
|
+
"""Close a client connection gracefully.
|
|
166
|
+
|
|
167
|
+
The socket stops being read, the replies already queued for it are
|
|
168
|
+
delivered first, and only then it is closed and the disconnect event is
|
|
169
|
+
emitted. Safe to call from an event handler or from another thread.
|
|
170
|
+
|
|
171
|
+
Use it instead of sock.close(): closing a socket behind the server's
|
|
172
|
+
back leaves the select() set with a dead descriptor.
|
|
173
|
+
"""
|
|
174
|
+
self.__start_closing(sock, strict=True)
|
|
131
175
|
|
|
132
176
|
def __initialize(self) -> None:
|
|
133
177
|
server_socket = socket.socket(
|
|
@@ -156,41 +200,74 @@ class SimpleSocketServer(EventBus):
|
|
|
156
200
|
self.emit("start", self.__host, self.__port)
|
|
157
201
|
|
|
158
202
|
def __select(self) -> None:
|
|
203
|
+
self.__prune()
|
|
204
|
+
# Handlers may call disconnect() and send() from another thread, so the
|
|
205
|
+
# sequences handed to select() are snapshots taken under the lock.
|
|
206
|
+
with self.__lock:
|
|
207
|
+
sock_to_read = list(self.__inputs)
|
|
208
|
+
sock_to_write = list(self.__outputs)
|
|
209
|
+
server_socket = self.__server_socket
|
|
210
|
+
sock_to_check = [server_socket] if server_socket is not None else []
|
|
159
211
|
try:
|
|
160
212
|
sock_to_read, sock_to_write, sock_errors = select.select(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
213
|
+
sock_to_read,
|
|
214
|
+
sock_to_write,
|
|
215
|
+
sock_to_check,
|
|
164
216
|
_POLL_INTERVAL,
|
|
165
217
|
)
|
|
166
|
-
except OSError as error:
|
|
167
|
-
# select()
|
|
168
|
-
|
|
218
|
+
except (OSError, ValueError) as error:
|
|
219
|
+
# select() broke: an unusable listening socket, or a descriptor set
|
|
220
|
+
# larger than the platform allows. The poll set cannot be trusted.
|
|
221
|
+
self.emit("error", server_socket, None, error)
|
|
169
222
|
self.stop()
|
|
170
223
|
return
|
|
171
224
|
for sock in sock_errors:
|
|
172
|
-
|
|
225
|
+
# Only the listening socket is polled here: urgent (out of band)
|
|
226
|
+
# data on a client connection is not a connection error.
|
|
227
|
+
if sock is server_socket:
|
|
173
228
|
self.stop()
|
|
174
|
-
else:
|
|
175
|
-
self.__close(sock)
|
|
176
229
|
for sock in sock_to_read:
|
|
177
|
-
if sock is
|
|
230
|
+
if sock is server_socket:
|
|
178
231
|
self.__accept()
|
|
179
232
|
else:
|
|
180
233
|
self.__receive(sock)
|
|
181
234
|
for sock in sock_to_write:
|
|
182
235
|
self.__flush(sock)
|
|
183
236
|
|
|
237
|
+
def __prune(self) -> None:
|
|
238
|
+
"""Drop sockets that the application closed behind the server's back."""
|
|
239
|
+
for sock in list(self.__clients):
|
|
240
|
+
if sock.fileno() < 0:
|
|
241
|
+
self.__close(sock)
|
|
242
|
+
|
|
184
243
|
def __accept(self) -> None:
|
|
244
|
+
server_socket = self.__server_socket
|
|
245
|
+
if server_socket is None:
|
|
246
|
+
return
|
|
185
247
|
try:
|
|
186
|
-
client_socket, client_address =
|
|
248
|
+
client_socket, client_address = server_socket.accept()
|
|
249
|
+
except BlockingIOError:
|
|
250
|
+
# The connection died in the backlog before accept() reached it.
|
|
251
|
+
return
|
|
187
252
|
except OSError as error:
|
|
188
|
-
self.emit("error",
|
|
253
|
+
self.emit("error", server_socket, None, error)
|
|
254
|
+
# Out of descriptors (EMFILE) would spin the loop without a pause.
|
|
255
|
+
time.sleep(_POLL_INTERVAL)
|
|
189
256
|
return
|
|
190
257
|
client_socket.setblocking(False)
|
|
191
|
-
self.__clients
|
|
192
|
-
|
|
193
|
-
|
|
258
|
+
if len(self.__clients) >= self.__max_clients:
|
|
259
|
+
client_socket.close()
|
|
260
|
+
self.emit(
|
|
261
|
+
"error",
|
|
262
|
+
client_socket,
|
|
263
|
+
client_address,
|
|
264
|
+
ConnectionAbortedError("the client limit is reached"),
|
|
265
|
+
)
|
|
266
|
+
return
|
|
267
|
+
with self.__lock:
|
|
268
|
+
self.__clients[client_socket] = client_address
|
|
269
|
+
self.__pending[client_socket] = bytearray()
|
|
270
|
+
self.__inputs.append(client_socket)
|
|
194
271
|
self.emit("connect", client_socket, client_address)
|
|
195
272
|
|
|
196
273
|
def __receive(self, sock: socket.socket) -> None:
|
|
@@ -212,37 +289,68 @@ class SimpleSocketServer(EventBus):
|
|
|
212
289
|
return
|
|
213
290
|
if not data:
|
|
214
291
|
# An empty chunk means the peer closed its end of the connection.
|
|
215
|
-
|
|
292
|
+
# Replies queued for it are still delivered before the socket goes.
|
|
293
|
+
self.__start_closing(sock)
|
|
216
294
|
return
|
|
217
295
|
self.emit("message", sock, peer, data)
|
|
218
296
|
|
|
297
|
+
def __start_closing(self, sock: socket.socket, strict: bool = False) -> None:
|
|
298
|
+
"""Stop reading a connection and close it once its queue is drained."""
|
|
299
|
+
with self.__lock:
|
|
300
|
+
if sock not in self.__clients:
|
|
301
|
+
if strict:
|
|
302
|
+
raise ValueError("the socket is not a client of this server")
|
|
303
|
+
return
|
|
304
|
+
self.__closing.add(sock)
|
|
305
|
+
if sock in self.__inputs:
|
|
306
|
+
self.__inputs.remove(sock)
|
|
307
|
+
drained = not self.__pending.get(sock)
|
|
308
|
+
if not drained and sock not in self.__outputs:
|
|
309
|
+
self.__outputs.append(sock)
|
|
310
|
+
if drained:
|
|
311
|
+
self.__close(sock)
|
|
312
|
+
|
|
219
313
|
def __flush(self, sock: socket.socket) -> None:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
314
|
+
failure: Optional[OSError] = None
|
|
315
|
+
drained = False
|
|
316
|
+
closing = False
|
|
317
|
+
with self.__lock:
|
|
318
|
+
pending = self.__pending.get(sock)
|
|
319
|
+
if pending is None:
|
|
320
|
+
if sock in self.__outputs:
|
|
321
|
+
self.__outputs.remove(sock)
|
|
322
|
+
return
|
|
323
|
+
try:
|
|
324
|
+
with memoryview(pending) as view:
|
|
325
|
+
sent = sock.send(view)
|
|
326
|
+
except BlockingIOError:
|
|
327
|
+
# The kernel buffer is full again; the tail stays queued.
|
|
328
|
+
return
|
|
329
|
+
except OSError as error:
|
|
330
|
+
failure = error
|
|
331
|
+
else:
|
|
332
|
+
del pending[:sent]
|
|
333
|
+
drained = not pending
|
|
334
|
+
if drained and sock in self.__outputs:
|
|
335
|
+
self.__outputs.remove(sock)
|
|
336
|
+
closing = drained and sock in self.__closing
|
|
337
|
+
if failure is not None:
|
|
338
|
+
self.emit("error", sock, self.__clients.get(sock), failure)
|
|
232
339
|
self.__close(sock)
|
|
233
340
|
return
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
self.__outputs.remove(sock)
|
|
341
|
+
if closing:
|
|
342
|
+
self.__close(sock)
|
|
237
343
|
|
|
238
344
|
def __close(self, sock: socket.socket) -> None:
|
|
239
|
-
"""
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
self.__inputs
|
|
243
|
-
|
|
244
|
-
self.__outputs
|
|
245
|
-
|
|
345
|
+
"""Drop a client connection and close it. Safe to call twice."""
|
|
346
|
+
with self.__lock:
|
|
347
|
+
peer = self.__clients.pop(sock, None)
|
|
348
|
+
if sock in self.__inputs:
|
|
349
|
+
self.__inputs.remove(sock)
|
|
350
|
+
if sock in self.__outputs:
|
|
351
|
+
self.__outputs.remove(sock)
|
|
352
|
+
self.__pending.pop(sock, None)
|
|
353
|
+
self.__closing.discard(sock)
|
|
246
354
|
if peer is None:
|
|
247
355
|
sock.close()
|
|
248
356
|
return
|
|
@@ -252,11 +360,33 @@ class SimpleSocketServer(EventBus):
|
|
|
252
360
|
sock.close()
|
|
253
361
|
|
|
254
362
|
def __shutdown(self) -> None:
|
|
255
|
-
|
|
256
|
-
|
|
363
|
+
"""Close everything, no matter what the handlers do.
|
|
364
|
+
|
|
365
|
+
The listening socket goes first, so the port stops accepting; then every
|
|
366
|
+
client is closed even if a disconnect handler raises, and stop is
|
|
367
|
+
emitted in any case. The first handler failure is re-raised afterwards
|
|
368
|
+
unless run() is already unwinding because of another error.
|
|
369
|
+
"""
|
|
370
|
+
failure: Optional[BaseException] = None
|
|
257
371
|
server_socket, self.__server_socket = self.__server_socket, None
|
|
258
372
|
if server_socket is not None:
|
|
259
|
-
|
|
260
|
-
self.__inputs
|
|
373
|
+
with self.__lock:
|
|
374
|
+
if server_socket in self.__inputs:
|
|
375
|
+
self.__inputs.remove(server_socket)
|
|
261
376
|
server_socket.close()
|
|
262
|
-
|
|
377
|
+
for sock in list(self.__clients):
|
|
378
|
+
try:
|
|
379
|
+
self.__close(sock)
|
|
380
|
+
except Exception as error: # noqa: BLE001 - cleanup must finish
|
|
381
|
+
if failure is None:
|
|
382
|
+
failure = error
|
|
383
|
+
for sock in list(self.__outputs):
|
|
384
|
+
sock.close()
|
|
385
|
+
self.__outputs.clear()
|
|
386
|
+
try:
|
|
387
|
+
self.emit("stop", self.__host, self.__port)
|
|
388
|
+
except Exception as error: # noqa: BLE001 - cleanup must finish
|
|
389
|
+
if failure is None:
|
|
390
|
+
failure = error
|
|
391
|
+
if failure is not None and sys.exc_info()[0] is None:
|
|
392
|
+
raise failure
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple-socket-server
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1
|
|
4
4
|
Summary: Simple TCP socket server with select
|
|
5
5
|
Author-email: webtoucher <webtoucher@ya.ru>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -59,7 +59,7 @@ $ pip install simple-socket-server
|
|
|
59
59
|
Or you can add it as dependency in requirements.txt file of your python application:
|
|
60
60
|
|
|
61
61
|
```
|
|
62
|
-
simple-socket-server~=2.
|
|
62
|
+
simple-socket-server~=2.1
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
## Usage
|
|
@@ -127,40 +127,57 @@ print(socket_server.host, socket_server.port)
|
|
|
127
127
|
|
|
128
128
|
Handlers are called from the thread that runs `run()`, one at a time.
|
|
129
129
|
|
|
130
|
-
| Event | Arguments | When it fires
|
|
131
|
-
|
|
132
|
-
| `start` | `host, port` | the listening socket is ready
|
|
133
|
-
| `connect` | `sock, peer` | a client has been accepted
|
|
134
|
-
| `message` | `sock, peer, data` | a chunk of bytes has been received
|
|
135
|
-
| `disconnect` | `sock, peer` | the connection is
|
|
136
|
-
| `error` | `sock, peer, error` | a socket error occurred; `disconnect` follows
|
|
137
|
-
| `stop` | `host, port` | `run()` has returned and every socket is closed
|
|
130
|
+
| Event | Arguments | When it fires |
|
|
131
|
+
|--------------|-------------------------|---------------------------------------------------------------------------|
|
|
132
|
+
| `start` | `host, port` | the listening socket is ready |
|
|
133
|
+
| `connect` | `sock, peer` | a client has been accepted |
|
|
134
|
+
| `message` | `sock, peer, data` | a chunk of bytes has been received |
|
|
135
|
+
| `disconnect` | `sock, peer` | the connection is closed; the replies queued for it were delivered first |
|
|
136
|
+
| `error` | `sock, peer, error` | a socket error occurred; the connection is closed and `disconnect` follows |
|
|
137
|
+
| `stop` | `host, port` | `run()` has returned and every socket is closed |
|
|
138
138
|
|
|
139
|
-
`peer` is an `(host, port)` tuple, `data` is `bytes`, `error` is an `OSError`.
|
|
139
|
+
`peer` is an `(host, port)` tuple, `data` is `bytes`, `error` is an `OSError`. When
|
|
140
|
+
`error` reports a failure of the listening socket or of `select()` itself, `peer` is
|
|
141
|
+
`None` and the server stops.
|
|
140
142
|
|
|
141
143
|
## API
|
|
142
144
|
|
|
143
|
-
| Member
|
|
144
|
-
|
|
145
|
-
| `run(host='0.0.0.0', port=6666, max_conn=5)` | serve connections until `stop()` is called
|
|
146
|
-
| `stop()`
|
|
147
|
-
| `send(sock, message)`
|
|
148
|
-
| `
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
145
|
+
| Member | Description |
|
|
146
|
+
|------------------------------------------|----------------------------------------------------------------------|
|
|
147
|
+
| `run(host='0.0.0.0', port=6666, max_conn=5, max_clients=500)` | serve connections until `stop()` is called |
|
|
148
|
+
| `stop()` | ask `run()` to return; safe from a handler or another thread |
|
|
149
|
+
| `send(sock, message)` | queue `bytes` for a client; `ValueError` if it is not a client |
|
|
150
|
+
| `disconnect(sock)` | stop reading a client, deliver its queue, then close it |
|
|
151
|
+
| `is_running` | `True` while `run()` is serving connections |
|
|
152
|
+
| `host`, `port` | the address the listening socket is bound to |
|
|
153
|
+
| `on(event)`, `emit(...)` | inherited from `event_bus.EventBus` |
|
|
151
154
|
|
|
152
155
|
Notes:
|
|
153
156
|
|
|
154
157
|
- The server is single-threaded and handlers are called synchronously, so a slow
|
|
155
158
|
handler delays every other connection. Hand off long work to your own thread.
|
|
159
|
+
- **A handler must not raise.** The exception propagates out of `run()` and stops
|
|
160
|
+
the server for every client; the sockets are still closed before the call returns.
|
|
161
|
+
If one connection misbehaves, handle it in the handler and call `disconnect()`.
|
|
156
162
|
- `message` carries one raw `recv()` chunk of up to 1024 bytes. There is no
|
|
157
163
|
framing: the protocol you put on top has to delimit its own messages.
|
|
158
164
|
- `send()` never blocks and never truncates: because client sockets are
|
|
159
165
|
non-blocking, the bytes are buffered and the unwritten tail is retried on the
|
|
160
166
|
next poll, so a message larger than the socket buffer arrives in full.
|
|
161
|
-
-
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
- The outbound queue has no size limit: a client that stops reading grows the
|
|
168
|
+
buffer of the server until you disconnect it.
|
|
169
|
+
- `send()` and `disconnect()` are safe to call from another thread. Delivery
|
|
170
|
+
happens during the next poll, so it can take up to the poll interval (0.1 s).
|
|
171
|
+
- `max_conn` is the `listen()` backlog: how many connections the system queues
|
|
172
|
+
before `accept()`. It is not a limit of served clients. `max_clients` is that
|
|
173
|
+
limit: a connection above it is accepted, closed at once and reported through
|
|
174
|
+
`error`.
|
|
175
|
+
- Do not call `sock.close()` yourself and do not use the socket after
|
|
176
|
+
`disconnect` or `error`. Use `disconnect(sock)` instead: it stops reading the
|
|
177
|
+
client and closes the socket after the queued replies are delivered. A socket
|
|
178
|
+
closed behind the server's back is dropped on the next poll, without delivery.
|
|
179
|
+
- Urgent (out of band) data is not supported, and such data is not treated as an
|
|
180
|
+
error: the bytes stay in the stream and arrive through `message`.
|
|
164
181
|
|
|
165
182
|
## Compatibility
|
|
166
183
|
|
|
@@ -171,6 +188,10 @@ Notes:
|
|
|
171
188
|
pyright see real signatures instead of `Any`.
|
|
172
189
|
- Any server instance is independent: several servers may run in one process, each
|
|
173
190
|
in its own thread.
|
|
191
|
+
- The poll set of `select()` is limited by the platform: 512 descriptors on Windows
|
|
192
|
+
and 1024 elsewhere, the listening socket included. That is why concurrent clients
|
|
193
|
+
are capped by `max_clients`, 500 by default; a larger value makes `select()` fail,
|
|
194
|
+
and the server stops with an `error` event.
|
|
174
195
|
|
|
175
196
|
## Development
|
|
176
197
|
|
|
@@ -187,7 +208,16 @@ The test suite covers:
|
|
|
187
208
|
- the message flow: `connect`, echo round trips, ordering and large payloads,
|
|
188
209
|
- disconnects: graceful close by the peer, an abrupt reset and a send to a
|
|
189
210
|
connection that is already gone,
|
|
190
|
-
- error handling:
|
|
211
|
+
- error handling: `send()` and `disconnect()` for an unknown socket, a client limit
|
|
212
|
+
that is reached, and a port that is already taken (POSIX only: on Windows
|
|
213
|
+
`SO_REUSEADDR` allows rebinding a listening port),
|
|
214
|
+
- a half-close: the peer sends, shuts down its writing side and still receives the
|
|
215
|
+
whole reply, a 256 KiB one included,
|
|
216
|
+
- server-driven disconnects: a queued reply reaches the client before the socket is
|
|
217
|
+
closed,
|
|
218
|
+
- resilience: a handler that closes its socket does not stop the server, and
|
|
219
|
+
`stop()` closes every client and emits `stop` even if a `disconnect` handler
|
|
220
|
+
raises,
|
|
191
221
|
- independence of two servers running in one process.
|
|
192
222
|
|
|
193
223
|
Project layout:
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
simple_socket_server/__init__.py,sha256=9vi_e0QnPDT9WZb2rVmkzcs-hqzIUmQfSQ86DukSM04,14926
|
|
2
|
+
simple_socket_server/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
simple_socket_server-2.1.dist-info/licenses/LICENSE.md,sha256=viUOCJf97qThnFWXx3lGwpWwhWfuo8ug8QQAHtAFh8c,1496
|
|
4
|
+
simple_socket_server-2.1.dist-info/METADATA,sha256=cVqpCqfCBck5bLq0UXWSkDSPr438fQzBmxl08bZEiNs,10962
|
|
5
|
+
simple_socket_server-2.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
simple_socket_server-2.1.dist-info/top_level.txt,sha256=U3g9Q_CXbmmzcFW0pQ4cEZgmkbi0TORT9q3EA_C7I04,21
|
|
7
|
+
simple_socket_server-2.1.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
simple_socket_server/__init__.py,sha256=NG1iCg4yUKkq6G77v7ffvkGdrkMivnqu_vcFq9wwYQk,9173
|
|
2
|
-
simple_socket_server/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
simple_socket_server-2.0.dist-info/licenses/LICENSE.md,sha256=viUOCJf97qThnFWXx3lGwpWwhWfuo8ug8QQAHtAFh8c,1496
|
|
4
|
-
simple_socket_server-2.0.dist-info/METADATA,sha256=pehdXAJEjZO1_hgYGB3BGO49nrqExDJVQRgbwEsOUKc,8769
|
|
5
|
-
simple_socket_server-2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
-
simple_socket_server-2.0.dist-info/top_level.txt,sha256=U3g9Q_CXbmmzcFW0pQ4cEZgmkbi0TORT9q3EA_C7I04,21
|
|
7
|
-
simple_socket_server-2.0.dist-info/RECORD,,
|
|
File without changes
|
{simple_socket_server-2.0.dist-info → simple_socket_server-2.1.dist-info}/licenses/LICENSE.md
RENAMED
|
File without changes
|
|
File without changes
|