simple-socket-server 2.0__tar.gz → 2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,59 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.1] - 2026-11-15
9
+
10
+ ### Added
11
+
12
+ - `disconnect(sock)`: a graceful way to drop a client from a handler or from
13
+ another thread. The socket stops being read, the replies already queued for it
14
+ are delivered, and only then it is closed and `disconnect` is emitted.
15
+ - `max_clients` argument of `run()` (500 by default): a connection above the limit
16
+ is accepted, closed at once and reported through the `error` event.
17
+ - Tests for the new behaviour: a client half-close, a queued reply delivered before
18
+ the close, a socket closed inside a handler, a failing `disconnect` handler
19
+ during `stop()`, the client limit and a port that is already taken.
20
+
21
+ ### Fixed
22
+
23
+ - `select()` could kill the server with `ValueError`. A socket closed behind the
24
+ server's back left the poll set with a dead descriptor (`file descriptor cannot
25
+ be a negative integer`), and an oversized client set failed the same way
26
+ (`filedescriptor out of range`). Dead sockets are dropped before the poll,
27
+ failures of `select()` are reported through `error` and stop the server instead
28
+ of escaping `run()`, and the number of clients is capped below `FD_SETSIZE`.
29
+ - The reply to a client that half-closed the connection (`sendall()` followed by
30
+ `shutdown(SHUT_WR)`, the way `nc -N` behaves) was thrown away. A connection that
31
+ ended its input keeps being written to until its queue is drained.
32
+ - `stop()` could leave clients open and the port listening when a `disconnect`
33
+ handler raised. The listening socket is closed first, every client is closed in
34
+ its own `try`, and `stop` is emitted in any case; the first handler failure is
35
+ re-raised afterwards.
36
+ - `send()` called from another thread mutated the poll state without a lock. The
37
+ queues and the poll sets are guarded by a lock now, and handlers are never called
38
+ under it.
39
+ - Urgent (out of band) data disconnected the client without an `error` event. Only
40
+ the listening socket is passed to the exceptional set of `select()`.
41
+ - A `BlockingIOError` of `accept()` was reported as an `error` event. It is ignored
42
+ now, any other `accept()` failure pauses before the next attempt, so a descriptor
43
+ shortage does not spin the loop.
44
+
45
+ ### Changed
46
+
47
+ - A `disconnect` handler that raises no longer aborts the cleanup: `run()` closes
48
+ every socket and emits `stop` before the exception surfaces.
49
+ - `error` for a failed `select()` or a broken listening socket reports the listening
50
+ socket and `peer=None`; for a rejected connection it reports the client socket
51
+ and its address.
52
+ - Concurrent clients are limited to 500 by default. A larger `max_clients` can
53
+ exceed the descriptor limit of `select()` (512 on Windows), which stops the server
54
+ with an `error` event.
55
+ - The version test reads the package metadata instead of a hard-coded version.
56
+ - Documentation: `max_conn` is described as the `listen()` backlog and `max_clients`
57
+ as the limit of concurrent clients; the thread safety of `send()` and
58
+ `disconnect()`, the unbounded outbound queue, the `select()` descriptor limit and
59
+ the rule that a handler must not raise are spelled out.
60
+
8
61
  ## [2.0] - 2026-11-15
9
62
 
10
63
  ### Added
@@ -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:
@@ -22,7 +22,7 @@ $ pip install simple-socket-server
22
22
  Or you can add it as dependency in requirements.txt file of your python application:
23
23
 
24
24
  ```
25
- simple-socket-server~=2.0
25
+ simple-socket-server~=2.1
26
26
  ```
27
27
 
28
28
  ## Usage
@@ -90,40 +90,57 @@ print(socket_server.host, socket_server.port)
90
90
 
91
91
  Handlers are called from the thread that runs `run()`, one at a time.
92
92
 
93
- | Event | Arguments | When it fires |
94
- |--------------|-------------------------|------------------------------------------------------------------|
95
- | `start` | `host, port` | the listening socket is ready |
96
- | `connect` | `sock, peer` | a client has been accepted |
97
- | `message` | `sock, peer, data` | a chunk of bytes has been received |
98
- | `disconnect` | `sock, peer` | the connection is gone; the socket is closed right after |
99
- | `error` | `sock, peer, error` | a socket error occurred; `disconnect` follows |
100
- | `stop` | `host, port` | `run()` has returned and every socket is closed |
93
+ | Event | Arguments | When it fires |
94
+ |--------------|-------------------------|---------------------------------------------------------------------------|
95
+ | `start` | `host, port` | the listening socket is ready |
96
+ | `connect` | `sock, peer` | a client has been accepted |
97
+ | `message` | `sock, peer, data` | a chunk of bytes has been received |
98
+ | `disconnect` | `sock, peer` | the connection is closed; the replies queued for it were delivered first |
99
+ | `error` | `sock, peer, error` | a socket error occurred; the connection is closed and `disconnect` follows |
100
+ | `stop` | `host, port` | `run()` has returned and every socket is closed |
101
101
 
102
- `peer` is an `(host, port)` tuple, `data` is `bytes`, `error` is an `OSError`.
102
+ `peer` is an `(host, port)` tuple, `data` is `bytes`, `error` is an `OSError`. When
103
+ `error` reports a failure of the listening socket or of `select()` itself, `peer` is
104
+ `None` and the server stops.
103
105
 
104
106
  ## API
105
107
 
106
- | Member | Description |
107
- |---------------------------------------|----------------------------------------------------------------------|
108
- | `run(host='0.0.0.0', port=6666, max_conn=5)` | serve connections until `stop()` is called |
109
- | `stop()` | ask `run()` to return; safe from a handler or another thread |
110
- | `send(sock, message)` | queue `bytes` for a client; `ValueError` if it is not a client |
111
- | `is_running` | `True` while `run()` is serving connections |
112
- | `host`, `port` | the address the listening socket is bound to |
113
- | `on(event)`, `emit(...)` | inherited from `event_bus.EventBus` |
108
+ | Member | Description |
109
+ |------------------------------------------|----------------------------------------------------------------------|
110
+ | `run(host='0.0.0.0', port=6666, max_conn=5, max_clients=500)` | serve connections until `stop()` is called |
111
+ | `stop()` | ask `run()` to return; safe from a handler or another thread |
112
+ | `send(sock, message)` | queue `bytes` for a client; `ValueError` if it is not a client |
113
+ | `disconnect(sock)` | stop reading a client, deliver its queue, then close it |
114
+ | `is_running` | `True` while `run()` is serving connections |
115
+ | `host`, `port` | the address the listening socket is bound to |
116
+ | `on(event)`, `emit(...)` | inherited from `event_bus.EventBus` |
114
117
 
115
118
  Notes:
116
119
 
117
120
  - The server is single-threaded and handlers are called synchronously, so a slow
118
121
  handler delays every other connection. Hand off long work to your own thread.
122
+ - **A handler must not raise.** The exception propagates out of `run()` and stops
123
+ the server for every client; the sockets are still closed before the call returns.
124
+ If one connection misbehaves, handle it in the handler and call `disconnect()`.
119
125
  - `message` carries one raw `recv()` chunk of up to 1024 bytes. There is no
120
126
  framing: the protocol you put on top has to delimit its own messages.
121
127
  - `send()` never blocks and never truncates: because client sockets are
122
128
  non-blocking, the bytes are buffered and the unwritten tail is retried on the
123
129
  next poll, so a message larger than the socket buffer arrives in full.
124
- - Do not use the socket after `disconnect` or `error`: it is closed.
125
- - An exception raised by a handler propagates out of `run()`. The socket state is
126
- still cleaned up before the call returns.
130
+ - The outbound queue has no size limit: a client that stops reading grows the
131
+ buffer of the server until you disconnect it.
132
+ - `send()` and `disconnect()` are safe to call from another thread. Delivery
133
+ happens during the next poll, so it can take up to the poll interval (0.1 s).
134
+ - `max_conn` is the `listen()` backlog: how many connections the system queues
135
+ before `accept()`. It is not a limit of served clients. `max_clients` is that
136
+ limit: a connection above it is accepted, closed at once and reported through
137
+ `error`.
138
+ - Do not call `sock.close()` yourself and do not use the socket after
139
+ `disconnect` or `error`. Use `disconnect(sock)` instead: it stops reading the
140
+ client and closes the socket after the queued replies are delivered. A socket
141
+ closed behind the server's back is dropped on the next poll, without delivery.
142
+ - Urgent (out of band) data is not supported, and such data is not treated as an
143
+ error: the bytes stay in the stream and arrive through `message`.
127
144
 
128
145
  ## Compatibility
129
146
 
@@ -134,6 +151,10 @@ Notes:
134
151
  pyright see real signatures instead of `Any`.
135
152
  - Any server instance is independent: several servers may run in one process, each
136
153
  in its own thread.
154
+ - The poll set of `select()` is limited by the platform: 512 descriptors on Windows
155
+ and 1024 elsewhere, the listening socket included. That is why concurrent clients
156
+ are capped by `max_clients`, 500 by default; a larger value makes `select()` fail,
157
+ and the server stops with an `error` event.
137
158
 
138
159
  ## Development
139
160
 
@@ -150,7 +171,16 @@ The test suite covers:
150
171
  - the message flow: `connect`, echo round trips, ordering and large payloads,
151
172
  - disconnects: graceful close by the peer, an abrupt reset and a send to a
152
173
  connection that is already gone,
153
- - error handling: an unknown socket for `send()` and a port that is already taken,
174
+ - error handling: `send()` and `disconnect()` for an unknown socket, a client limit
175
+ that is reached, and a port that is already taken (POSIX only: on Windows
176
+ `SO_REUSEADDR` allows rebinding a listening port),
177
+ - a half-close: the peer sends, shuts down its writing side and still receives the
178
+ whole reply, a 256 KiB one included,
179
+ - server-driven disconnects: a queued reply reaches the client before the socket is
180
+ closed,
181
+ - resilience: a handler that closes its socket does not stop the server, and
182
+ `stop()` closes every client and emits `stop` even if a `disconnect` handler
183
+ raises,
154
184
  - independence of two servers running in one process.
155
185
 
156
186
  Project layout:
@@ -0,0 +1,392 @@
1
+ """
2
+ Simple TCP socket server built on select().
3
+
4
+ The server is single-threaded: run() polls the sockets and calls the handlers
5
+ registered for its events. No third-party networking code is involved, the only
6
+ runtime dependency is the event bus used to dispatch those events.
7
+
8
+ Copyright (c) 2023–2026 webtoucher
9
+ Distributed under the BSD 3-Clause license. See LICENSE.md for more info.
10
+ """
11
+
12
+ import select
13
+ import socket
14
+ import sys
15
+ import threading
16
+ import time
17
+
18
+ from typing import Dict, List, Optional, Set, Tuple
19
+
20
+ from event_bus import EventBus
21
+
22
+ __all__ = ["SimpleSocketServer", "SimpleSocketServerException"]
23
+
24
+ __version__ = "2.1"
25
+
26
+ #: Peer address as returned by accept(): (host, port).
27
+ Address = Tuple[str, int]
28
+
29
+ #: Size of a single recv() chunk; a message event carries one such chunk.
30
+ _RECV_SIZE = 1024
31
+
32
+ #: How long select() waits for socket activity, in seconds.
33
+ _POLL_INTERVAL = 0.1
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
+
40
+
41
+ class SimpleSocketServerException(socket.error):
42
+ """Raised when the listening socket cannot be created, bound or enabled."""
43
+
44
+
45
+ class SimpleSocketServer(EventBus):
46
+ """Single-threaded TCP server dispatching events through an event bus.
47
+
48
+ Events, in the order they may arrive:
49
+
50
+ ``start(host, port)``
51
+ The listening socket is ready.
52
+ ``connect(sock, peer)``
53
+ A client has been accepted.
54
+ ``message(sock, peer, data)``
55
+ A chunk of bytes has been received.
56
+ ``disconnect(sock, peer)``
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.
59
+ ``error(sock, peer, error)``
60
+ A socket error occurred; the connection is closed right after, or the
61
+ server itself stops when the listening socket or select() fails.
62
+ ``stop(host, port)``
63
+ run() has returned and every socket is closed.
64
+
65
+ Handlers are called from the thread that runs run(), one at a time, so a
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.
69
+ """
70
+
71
+ def __init__(self) -> None:
72
+ super().__init__()
73
+ self.__host = "0.0.0.0"
74
+ self.__port = 6666
75
+ self.__max_conn = 5
76
+ self.__max_clients = _DEFAULT_MAX_CLIENTS
77
+ self.__server_socket: Optional[socket.socket] = None
78
+ self.__inputs: List[socket.socket] = []
79
+ self.__outputs: List[socket.socket] = []
80
+ self.__pending: Dict[socket.socket, bytearray] = {}
81
+ self.__clients: Dict[socket.socket, Address] = {}
82
+ self.__closing: Set[socket.socket] = set()
83
+ self.__lock = threading.Lock()
84
+ self.__stop = threading.Event()
85
+
86
+ @property
87
+ def is_running(self) -> bool:
88
+ """True while run() is serving connections."""
89
+ return self.__server_socket is not None and not self.__stop.is_set()
90
+
91
+ @property
92
+ def host(self) -> str:
93
+ """Host the listening socket is bound to."""
94
+ return self.__host
95
+
96
+ @property
97
+ def port(self) -> int:
98
+ """Port the listening socket is bound to."""
99
+ return self.__port
100
+
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:
108
+ """Serve connections until stop() is called.
109
+
110
+ The call blocks the calling thread. Once it returns, every client socket
111
+ and the listening socket are closed. A port of 0 makes the operating
112
+ system pick a free port; the actual address is reported by the start
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.
119
+ """
120
+ if self.is_running:
121
+ raise RuntimeError("the server is already running")
122
+ self.__host = host
123
+ self.__port = port
124
+ self.__max_conn = max_conn
125
+ self.__max_clients = max_clients
126
+ self.__stop.clear()
127
+ self.__initialize()
128
+ try:
129
+ while not self.__stop.is_set():
130
+ self.__select()
131
+ finally:
132
+ self.__shutdown()
133
+
134
+ def stop(self) -> None:
135
+ """Ask run() to return.
136
+
137
+ It only raises a flag, so it is safe to call from an event handler or
138
+ from another thread. run() finishes the current poll and then closes
139
+ every socket.
140
+ """
141
+ self.__stop.set()
142
+
143
+ def send(self, sock: socket.socket, message: bytes) -> None:
144
+ """Queue message for delivery to a client of this server.
145
+
146
+ The socket is non-blocking, so the bytes are buffered and the unwritten
147
+ tail is retried on the next poll; a message larger than the socket
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.
153
+ """
154
+ if not message:
155
+ return
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)
175
+
176
+ def __initialize(self) -> None:
177
+ server_socket = socket.socket(
178
+ socket.AF_INET,
179
+ socket.SOCK_STREAM,
180
+ socket.IPPROTO_TCP,
181
+ )
182
+ try:
183
+ server_socket.setblocking(False)
184
+ server_socket.setsockopt(
185
+ socket.SOL_SOCKET,
186
+ socket.SO_REUSEADDR,
187
+ 1,
188
+ )
189
+ server_socket.bind((self.__host, self.__port))
190
+ server_socket.listen(self.__max_conn)
191
+ except OSError as error:
192
+ server_socket.close()
193
+ raise SimpleSocketServerException(
194
+ "cannot listen on %s:%s" % (self.__host, self.__port)
195
+ ) from error
196
+ self.__server_socket = server_socket
197
+ self.__inputs.append(server_socket)
198
+ # A port of 0 is resolved by the system, so report the real address.
199
+ self.__host, self.__port = server_socket.getsockname()[:2]
200
+ self.emit("start", self.__host, self.__port)
201
+
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 []
211
+ try:
212
+ sock_to_read, sock_to_write, sock_errors = select.select(
213
+ sock_to_read,
214
+ sock_to_write,
215
+ sock_to_check,
216
+ _POLL_INTERVAL,
217
+ )
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)
222
+ self.stop()
223
+ return
224
+ for sock in sock_errors:
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:
228
+ self.stop()
229
+ for sock in sock_to_read:
230
+ if sock is server_socket:
231
+ self.__accept()
232
+ else:
233
+ self.__receive(sock)
234
+ for sock in sock_to_write:
235
+ self.__flush(sock)
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
+
243
+ def __accept(self) -> None:
244
+ server_socket = self.__server_socket
245
+ if server_socket is None:
246
+ return
247
+ try:
248
+ client_socket, client_address = server_socket.accept()
249
+ except BlockingIOError:
250
+ # The connection died in the backlog before accept() reached it.
251
+ return
252
+ except OSError as 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)
256
+ return
257
+ client_socket.setblocking(False)
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)
271
+ self.emit("connect", client_socket, client_address)
272
+
273
+ def __receive(self, sock: socket.socket) -> None:
274
+ peer = self.__clients.get(sock)
275
+ if peer is None:
276
+ # The socket was already dropped during this poll.
277
+ return
278
+ try:
279
+ data = sock.recv(_RECV_SIZE)
280
+ except BlockingIOError:
281
+ # select() woke us up for nothing; try again on the next poll.
282
+ return
283
+ except (ConnectionResetError, ConnectionAbortedError, BrokenPipeError):
284
+ self.__close(sock)
285
+ return
286
+ except OSError as error:
287
+ self.emit("error", sock, peer, error)
288
+ self.__close(sock)
289
+ return
290
+ if not data:
291
+ # An empty chunk means the peer closed its end of the connection.
292
+ # Replies queued for it are still delivered before the socket goes.
293
+ self.__start_closing(sock)
294
+ return
295
+ self.emit("message", sock, peer, data)
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
+
313
+ def __flush(self, sock: socket.socket) -> None:
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)
339
+ self.__close(sock)
340
+ return
341
+ if closing:
342
+ self.__close(sock)
343
+
344
+ def __close(self, sock: socket.socket) -> 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)
354
+ if peer is None:
355
+ sock.close()
356
+ return
357
+ try:
358
+ self.emit("disconnect", sock, peer)
359
+ finally:
360
+ sock.close()
361
+
362
+ def __shutdown(self) -> None:
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
371
+ server_socket, self.__server_socket = self.__server_socket, None
372
+ if server_socket is not None:
373
+ with self.__lock:
374
+ if server_socket in self.__inputs:
375
+ self.__inputs.remove(server_socket)
376
+ server_socket.close()
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:
@@ -1,6 +1,8 @@
1
1
  """Tests for the simple_socket_server package."""
2
2
 
3
+ import importlib.metadata
3
4
  import os
5
+ import re
4
6
  import socket
5
7
  import struct
6
8
  import threading
@@ -39,6 +41,7 @@ class RunningServer:
39
41
  self.run_kwargs = {"host": LOCAL_HOST, "port": 0, "max_conn": 5}
40
42
  self.run_kwargs.update(run_kwargs)
41
43
  self.events = []
44
+ self.error = None
42
45
  self._lock = threading.Lock()
43
46
  self._started = threading.Event()
44
47
  self._finished = threading.Event()
@@ -56,6 +59,8 @@ class RunningServer:
56
59
  def _run(self):
57
60
  try:
58
61
  self.server.run(**self.run_kwargs)
62
+ except BaseException as error: # noqa: BLE001 - kept for the assertions
63
+ self.error = error
59
64
  finally:
60
65
  self._finished.set()
61
66
 
@@ -341,5 +346,172 @@ def test_public_api_is_explicit():
341
346
  for name in simple_socket_server.__all__:
342
347
  assert not name.startswith("_"), "%s is not a public name" % name
343
348
  assert hasattr(simple_socket_server, name), "%s is in __all__ but missing" % name
344
- assert simple_socket_server.__version__ == "2.0"
349
+ assert re.match(r"^\d+\.\d+(\.\d+)?$", simple_socket_server.__version__)
345
350
  assert issubclass(SimpleSocketServerException, OSError)
351
+
352
+
353
+ def test_declared_version_matches_the_package_metadata():
354
+ try:
355
+ installed = importlib.metadata.version("simple-socket-server")
356
+ except importlib.metadata.PackageNotFoundError:
357
+ pytest.skip("the package is not installed, metadata is unavailable")
358
+ assert installed == simple_socket_server.__version__
359
+
360
+
361
+ @pytest.mark.skipif(
362
+ os.name == "nt",
363
+ reason="SO_REUSEADDR lets Windows bind a port that is already listening",
364
+ )
365
+ def test_taken_port_raises_server_exception():
366
+ blocker = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
367
+ blocker.bind((LOCAL_HOST, 0))
368
+ blocker.listen(1)
369
+ try:
370
+ server = SimpleSocketServer()
371
+ with pytest.raises(SimpleSocketServerException):
372
+ server.run(host=LOCAL_HOST, port=blocker.getsockname()[1])
373
+ assert not server.is_running
374
+ finally:
375
+ blocker.close()
376
+
377
+
378
+ def test_client_limit_is_reported_through_the_error_event():
379
+ server = RunningServer(max_clients=1).start()
380
+ try:
381
+ first = server.connect()
382
+ second = server.connect()
383
+ try:
384
+ (_sock, _peer, error), = server.wait_for("error")
385
+ assert isinstance(error, ConnectionAbortedError)
386
+ assert server.thread.is_alive(), "one excessive client does not stop the loop"
387
+ second.settimeout(10)
388
+ assert second.recv(1) == b"", "the excessive client is closed at once"
389
+ assert len(server.events_of("connect")) == 1
390
+ finally:
391
+ first.close()
392
+ second.close()
393
+ finally:
394
+ server.stop()
395
+
396
+
397
+ def half_close_client(server, request, timeout=10.0):
398
+ """Send a request, close the write half and read the reply until EOF."""
399
+ client = server.connect()
400
+ try:
401
+ client.sendall(request)
402
+ client.shutdown(socket.SHUT_WR)
403
+ client.settimeout(timeout)
404
+ reply = b""
405
+ while True:
406
+ chunk = client.recv(65536)
407
+ if not chunk:
408
+ return reply
409
+ reply += chunk
410
+ finally:
411
+ client.close()
412
+
413
+
414
+ def test_reply_survives_a_half_close_of_the_client():
415
+ def on_message(sock, _peer, data):
416
+ state["server"].send(sock, b"echo:" + data)
417
+
418
+ state = {}
419
+ server = RunningServer({"message": on_message})
420
+ state["server"] = server.server
421
+ server.start()
422
+ try:
423
+ # The reply used to be dropped whenever the EOF arrived before the flush.
424
+ for attempt in range(10):
425
+ reply = half_close_client(server, b"ping")
426
+ assert reply == b"echo:ping", "attempt %d lost the reply" % attempt
427
+ finally:
428
+ server.stop()
429
+
430
+
431
+ def test_large_reply_survives_a_half_close_of_the_client():
432
+ payload = bytes(range(256)) * 1024 # 256 KiB, far above the socket buffer
433
+
434
+ def on_message(sock, _peer, _data):
435
+ state["server"].send(sock, payload)
436
+
437
+ state = {}
438
+ server = RunningServer({"message": on_message})
439
+ state["server"] = server.server
440
+ server.start()
441
+ try:
442
+ assert half_close_client(server, b"go", timeout=30) == payload
443
+ finally:
444
+ server.stop()
445
+
446
+
447
+ def test_disconnect_delivers_the_queue_before_it_closes():
448
+ def on_message(sock, _peer, _data):
449
+ state["server"].send(sock, b"bye")
450
+ state["server"].disconnect(sock)
451
+
452
+ state = {}
453
+ server = RunningServer({"message": on_message})
454
+ state["server"] = server.server
455
+ server.start()
456
+ try:
457
+ assert half_close_client(server, b"go") == b"bye"
458
+ server.wait_for("disconnect")
459
+ finally:
460
+ server.stop()
461
+
462
+
463
+ def test_disconnect_rejects_a_foreign_socket(running_server):
464
+ stranger = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
465
+ try:
466
+ with pytest.raises(ValueError):
467
+ running_server.server.disconnect(stranger)
468
+ finally:
469
+ stranger.close()
470
+
471
+
472
+ def test_closing_the_socket_inside_a_handler_keeps_the_server_alive():
473
+ def on_message(sock, _peer, _data):
474
+ sock.close()
475
+
476
+ server = RunningServer({"message": on_message}).start()
477
+ try:
478
+ victim = server.connect()
479
+ server.wait_for("connect")
480
+ victim.sendall(b"x")
481
+ server.wait_for("disconnect")
482
+
483
+ assert server.thread.is_alive(), "run() survived a socket closed by a handler"
484
+ assert server.error is None, server.error
485
+
486
+ second = server.connect()
487
+ try:
488
+ server.wait_for("connect", count=2)
489
+ assert server.thread.is_alive()
490
+ finally:
491
+ victim.close()
492
+ second.close()
493
+ finally:
494
+ server.stop()
495
+
496
+
497
+ def test_stop_closes_everything_even_if_a_disconnect_handler_raises():
498
+ def bad_disconnect(_sock, _peer):
499
+ raise RuntimeError("boom on disconnect")
500
+
501
+ server = RunningServer({"disconnect": bad_disconnect}).start()
502
+ first = server.connect()
503
+ second = server.connect()
504
+ server.wait_for("connect", count=2)
505
+
506
+ server.stop()
507
+
508
+ assert server.events_of("stop"), "the stop event arrives despite the failure"
509
+ assert isinstance(server.error, RuntimeError), "the handler failure is not swallowed"
510
+ first.settimeout(10)
511
+ second.settimeout(10)
512
+ assert first.recv(1) == b"", "every client is closed"
513
+ assert second.recv(1) == b"", "every client is closed, not just the first one"
514
+ first.close()
515
+ second.close()
516
+ with pytest.raises(OSError):
517
+ socket.create_connection(server.address, timeout=2)
@@ -1,262 +0,0 @@
1
- """
2
- Simple TCP socket server built on select().
3
-
4
- The server is single-threaded: run() polls the sockets and calls the handlers
5
- registered for its events. No third-party networking code is involved, the only
6
- runtime dependency is the event bus used to dispatch those events.
7
-
8
- Copyright (c) 2023–2026 webtoucher
9
- Distributed under the BSD 3-Clause license. See LICENSE.md for more info.
10
- """
11
-
12
- import select
13
- import socket
14
- import threading
15
-
16
- from typing import Dict, List, Optional, Tuple
17
-
18
- from event_bus import EventBus
19
-
20
- __all__ = ["SimpleSocketServer", "SimpleSocketServerException"]
21
-
22
- __version__ = "2.0"
23
-
24
- #: Peer address as returned by accept(): (host, port).
25
- Address = Tuple[str, int]
26
-
27
- #: Size of a single recv() chunk; a message event carries one such chunk.
28
- _RECV_SIZE = 1024
29
-
30
- #: How long select() waits for socket activity, in seconds.
31
- _POLL_INTERVAL = 0.1
32
-
33
-
34
- class SimpleSocketServerException(socket.error):
35
- """Raised when the listening socket cannot be created, bound or enabled."""
36
-
37
-
38
- class SimpleSocketServer(EventBus):
39
- """Single-threaded TCP server dispatching events through an event bus.
40
-
41
- Events, in the order they may arrive:
42
-
43
- ``start(host, port)``
44
- The listening socket is ready.
45
- ``connect(sock, peer)``
46
- A client has been accepted.
47
- ``message(sock, peer, data)``
48
- A chunk of bytes has been received.
49
- ``disconnect(sock, peer)``
50
- The connection is gone: the peer closed it, reset it or a send failed.
51
- ``error(sock, peer, error)``
52
- A socket error occurred; the connection is closed right after.
53
- ``stop(host, port)``
54
- run() has returned and every socket is closed.
55
-
56
- Handlers are called from the thread that runs run(), one at a time, so a
57
- slow handler delays every other connection.
58
- """
59
-
60
- def __init__(self) -> None:
61
- super().__init__()
62
- self.__host = "0.0.0.0"
63
- self.__port = 6666
64
- self.__max_conn = 5
65
- self.__server_socket: Optional[socket.socket] = None
66
- self.__inputs: List[socket.socket] = []
67
- self.__outputs: List[socket.socket] = []
68
- self.__pending: Dict[socket.socket, bytearray] = {}
69
- self.__clients: Dict[socket.socket, Address] = {}
70
- self.__stop = threading.Event()
71
-
72
- @property
73
- def is_running(self) -> bool:
74
- """True while run() is serving connections."""
75
- return self.__server_socket is not None and not self.__stop.is_set()
76
-
77
- @property
78
- def host(self) -> str:
79
- """Host the listening socket is bound to."""
80
- return self.__host
81
-
82
- @property
83
- def port(self) -> int:
84
- """Port the listening socket is bound to."""
85
- return self.__port
86
-
87
- def run(self, host: str = "0.0.0.0", port: int = 6666, max_conn: int = 5) -> None:
88
- """Serve connections until stop() is called.
89
-
90
- The call blocks the calling thread. Once it returns, every client socket
91
- and the listening socket are closed. A port of 0 makes the operating
92
- system pick a free port; the actual address is reported by the start
93
- event and by the host and port properties.
94
- """
95
- if self.is_running:
96
- raise RuntimeError("the server is already running")
97
- self.__host = host
98
- self.__port = port
99
- self.__max_conn = max_conn
100
- self.__stop.clear()
101
- self.__initialize()
102
- try:
103
- while not self.__stop.is_set():
104
- self.__select()
105
- finally:
106
- self.__shutdown()
107
-
108
- def stop(self) -> None:
109
- """Ask run() to return.
110
-
111
- It only raises a flag, so it is safe to call from an event handler or
112
- from another thread. run() finishes the current poll and then closes
113
- every socket.
114
- """
115
- self.__stop.set()
116
-
117
- def send(self, sock: socket.socket, message: bytes) -> None:
118
- """Queue message for delivery to a client of this server.
119
-
120
- The socket is non-blocking, so the bytes are buffered and the unwritten
121
- tail is retried on the next poll; a message larger than the socket
122
- buffer is delivered in full. Empty messages are ignored.
123
- """
124
- if sock not in self.__pending:
125
- raise ValueError("the socket is not a client of this server")
126
- if not message:
127
- return
128
- self.__pending[sock].extend(message)
129
- if sock not in self.__outputs:
130
- self.__outputs.append(sock)
131
-
132
- def __initialize(self) -> None:
133
- server_socket = socket.socket(
134
- socket.AF_INET,
135
- socket.SOCK_STREAM,
136
- socket.IPPROTO_TCP,
137
- )
138
- try:
139
- server_socket.setblocking(False)
140
- server_socket.setsockopt(
141
- socket.SOL_SOCKET,
142
- socket.SO_REUSEADDR,
143
- 1,
144
- )
145
- server_socket.bind((self.__host, self.__port))
146
- server_socket.listen(self.__max_conn)
147
- except OSError as error:
148
- server_socket.close()
149
- raise SimpleSocketServerException(
150
- "cannot listen on %s:%s" % (self.__host, self.__port)
151
- ) from error
152
- self.__server_socket = server_socket
153
- self.__inputs.append(server_socket)
154
- # A port of 0 is resolved by the system, so report the real address.
155
- self.__host, self.__port = server_socket.getsockname()[:2]
156
- self.emit("start", self.__host, self.__port)
157
-
158
- def __select(self) -> None:
159
- try:
160
- sock_to_read, sock_to_write, sock_errors = select.select(
161
- self.__inputs,
162
- self.__outputs,
163
- self.__inputs,
164
- _POLL_INTERVAL,
165
- )
166
- except OSError as error:
167
- # select() itself failed, so the listening socket is unusable.
168
- self.emit("error", None, None, error)
169
- self.stop()
170
- return
171
- for sock in sock_errors:
172
- if sock is self.__server_socket:
173
- self.stop()
174
- else:
175
- self.__close(sock)
176
- for sock in sock_to_read:
177
- if sock is self.__server_socket:
178
- self.__accept()
179
- else:
180
- self.__receive(sock)
181
- for sock in sock_to_write:
182
- self.__flush(sock)
183
-
184
- def __accept(self) -> None:
185
- try:
186
- client_socket, client_address = self.__server_socket.accept()
187
- except OSError as error:
188
- self.emit("error", self.__server_socket, None, error)
189
- return
190
- client_socket.setblocking(False)
191
- self.__clients[client_socket] = client_address
192
- self.__pending[client_socket] = bytearray()
193
- self.__inputs.append(client_socket)
194
- self.emit("connect", client_socket, client_address)
195
-
196
- def __receive(self, sock: socket.socket) -> None:
197
- peer = self.__clients.get(sock)
198
- if peer is None:
199
- # The socket was already dropped during this poll.
200
- return
201
- try:
202
- data = sock.recv(_RECV_SIZE)
203
- except BlockingIOError:
204
- # select() woke us up for nothing; try again on the next poll.
205
- return
206
- except (ConnectionResetError, ConnectionAbortedError, BrokenPipeError):
207
- self.__close(sock)
208
- return
209
- except OSError as error:
210
- self.emit("error", sock, peer, error)
211
- self.__close(sock)
212
- return
213
- if not data:
214
- # An empty chunk means the peer closed its end of the connection.
215
- self.__close(sock)
216
- return
217
- self.emit("message", sock, peer, data)
218
-
219
- 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)
232
- self.__close(sock)
233
- return
234
- del pending[:sent]
235
- if not pending and sock in self.__outputs:
236
- self.__outputs.remove(sock)
237
-
238
- 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)
246
- if peer is None:
247
- sock.close()
248
- return
249
- try:
250
- self.emit("disconnect", sock, peer)
251
- finally:
252
- sock.close()
253
-
254
- def __shutdown(self) -> None:
255
- for sock in list(self.__clients):
256
- self.__close(sock)
257
- server_socket, self.__server_socket = self.__server_socket, None
258
- if server_socket is not None:
259
- if server_socket in self.__inputs:
260
- self.__inputs.remove(server_socket)
261
- server_socket.close()
262
- self.emit("stop", self.__host, self.__port)