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.
@@ -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.0"
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 gone: the peer closed it, reset it or a send failed.
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(self, host: str = "0.0.0.0", port: int = 6666, max_conn: int = 5) -> None:
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.__pending[sock].extend(message)
129
- if sock not in self.__outputs:
130
- self.__outputs.append(sock)
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
- self.__inputs,
162
- self.__outputs,
163
- self.__inputs,
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() itself failed, so the listening socket is unusable.
168
- self.emit("error", None, None, error)
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
- if sock is self.__server_socket:
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 self.__server_socket:
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 = self.__server_socket.accept()
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", self.__server_socket, None, 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[client_socket] = client_address
192
- self.__pending[client_socket] = bytearray()
193
- self.__inputs.append(client_socket)
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
- self.__close(sock)
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
- pending = self.__pending.get(sock)
221
- if pending is None:
222
- if sock in self.__outputs:
223
- self.__outputs.remove(sock)
224
- return
225
- try:
226
- sent = sock.send(pending)
227
- except BlockingIOError:
228
- # The kernel buffer is full again; the tail stays queued.
229
- return
230
- except OSError as error:
231
- self.emit("error", sock, self.__clients.get(sock), error)
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
- del pending[:sent]
235
- if not pending and sock in self.__outputs:
236
- self.__outputs.remove(sock)
341
+ if closing:
342
+ self.__close(sock)
237
343
 
238
344
  def __close(self, sock: socket.socket) -> None:
239
- """Close a client connection and forget it. Safe to call twice."""
240
- peer = self.__clients.pop(sock, None)
241
- if sock in self.__inputs:
242
- self.__inputs.remove(sock)
243
- if sock in self.__outputs:
244
- self.__outputs.remove(sock)
245
- self.__pending.pop(sock, None)
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
- for sock in list(self.__clients):
256
- self.__close(sock)
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
- if server_socket in self.__inputs:
260
- self.__inputs.remove(server_socket)
373
+ with self.__lock:
374
+ if server_socket in self.__inputs:
375
+ self.__inputs.remove(server_socket)
261
376
  server_socket.close()
262
- self.emit("stop", self.__host, self.__port)
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.0
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.0
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 gone; the socket is closed right after |
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 | Description |
144
- |---------------------------------------|----------------------------------------------------------------------|
145
- | `run(host='0.0.0.0', port=6666, max_conn=5)` | serve connections until `stop()` is called |
146
- | `stop()` | ask `run()` to return; safe from a handler or another thread |
147
- | `send(sock, message)` | queue `bytes` for a client; `ValueError` if it is not a client |
148
- | `is_running` | `True` while `run()` is serving connections |
149
- | `host`, `port` | the address the listening socket is bound to |
150
- | `on(event)`, `emit(...)` | inherited from `event_bus.EventBus` |
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
- - Do not use the socket after `disconnect` or `error`: it is closed.
162
- - An exception raised by a handler propagates out of `run()`. The socket state is
163
- still cleaned up before the call returns.
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: an unknown socket for `send()` and a port that is already taken,
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,,