simple-socket-server 1.8__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.
Files changed (23) hide show
  1. simple_socket_server-2.1/CHANGELOG.md +168 -0
  2. {simple-socket-server-1.8 → simple_socket_server-2.1}/LICENSE.md +1 -1
  3. simple_socket_server-2.1/MANIFEST.in +1 -0
  4. simple_socket_server-2.1/PKG-INFO +248 -0
  5. simple_socket_server-2.1/README.md +211 -0
  6. simple_socket_server-2.1/pyproject.toml +61 -0
  7. {simple-socket-server-1.8 → simple_socket_server-2.1}/setup.cfg +4 -4
  8. simple_socket_server-2.1/simple_socket_server/__init__.py +392 -0
  9. simple_socket_server-2.1/simple_socket_server/py.typed +0 -0
  10. simple_socket_server-2.1/simple_socket_server.egg-info/PKG-INFO +248 -0
  11. {simple-socket-server-1.8 → simple_socket_server-2.1}/simple_socket_server.egg-info/SOURCES.txt +5 -4
  12. {simple-socket-server-1.8 → simple_socket_server-2.1}/simple_socket_server.egg-info/requires.txt +3 -0
  13. simple_socket_server-2.1/tests/test_simple_socket_server.py +517 -0
  14. simple-socket-server-1.8/MANIFEST.in +0 -3
  15. simple-socket-server-1.8/PKG-INFO +0 -78
  16. simple-socket-server-1.8/README.md +0 -58
  17. simple-socket-server-1.8/requirements.txt +0 -1
  18. simple-socket-server-1.8/setup.py +0 -27
  19. simple-socket-server-1.8/simple_socket_server/__init__.py +0 -163
  20. simple-socket-server-1.8/simple_socket_server.egg-info/PKG-INFO +0 -78
  21. simple-socket-server-1.8/simple_socket_server.egg-info/not-zip-safe +0 -1
  22. {simple-socket-server-1.8 → simple_socket_server-2.1}/simple_socket_server.egg-info/dependency_links.txt +0 -0
  23. {simple-socket-server-1.8 → simple_socket_server-2.1}/simple_socket_server.egg-info/top_level.txt +0 -0
@@ -0,0 +1,168 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
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
+
61
+ ## [2.0] - 2026-11-15
62
+
63
+ ### Added
64
+
65
+ - `SimpleSocketServer.stop()`: `run()` returns on request, closes every socket and
66
+ emits `stop(host, port)`. It only raises a flag, so it is safe to call from an
67
+ event handler or from another thread.
68
+ - `is_running`, `host` and `port` properties. A port of `0` is resolved by the
69
+ system, and the `start` event plus both properties report the real address.
70
+ - `simple_socket_server.__version__`, an explicit `__all__` and a public `Address`
71
+ alias for a peer address.
72
+ - `py.typed` marker (PEP 561) and type hints on every public name, so mypy and
73
+ pyright see real signatures instead of `Any`.
74
+ - `tests/`: pytest suite covering the lifecycle, event order, echo round trips,
75
+ graceful and abrupt disconnects, large messages, error handling and port reuse.
76
+ - GitHub Actions workflows: tests on Python 3.9-3.15 and 3.13t, plus a tag-driven
77
+ release pipeline publishing to PyPI and creating a GitHub Release.
78
+ - `CHANGELOG.md` and `.gitattributes` with `* text=auto eol=lf`.
79
+
80
+ ### Fixed
81
+
82
+ - A peer closing the connection cleanly (`recv()` returning `b""`) was ignored, so
83
+ the socket stayed in the `select()` set forever: the loop spun on an idle file
84
+ descriptor and `disconnect` never fired.
85
+ - `send()` raised `KeyError` for a socket that was not a client of the server; it
86
+ raises `ValueError` now, and an empty message is a no-op.
87
+ - `send()` dropped data on a non-blocking socket: a single `send()` call can accept
88
+ only part of the buffer. The unwritten tail is buffered and retried, so a message
89
+ larger than the socket buffer arrives in full.
90
+ - Closed sockets were never removed from the client table, so a long-lived server
91
+ leaked memory with every reconnection.
92
+ - The `disconnect` event could be emitted twice for one socket, and a socket already
93
+ dropped earlier in the same poll was used again.
94
+ - The state reset on the server-socket error path left the previous listening
95
+ socket open, and `run()` reused a stale initialized flag.
96
+ - `SimpleSocketServerException` was raised with two positional arguments on a check
97
+ that could not trigger; bind and listen failures are reported now with the address.
98
+ - A `select()` failure is reported through the `error` event and stops the server
99
+ instead of escaping `run()` as an unhandled exception.
100
+
101
+ ### Changed
102
+
103
+ - Packaging moved from `setup.py` to `pyproject.toml` (PEP 621, PEP 639);
104
+ metadata, build configuration and pytest settings live in one file.
105
+ - Errors that end a connection are reported through the `error` event and followed
106
+ by `disconnect`; `message` is never emitted for a reset connection.
107
+ - `run()` no longer sleeps after `select()`: the poll interval is the `select()`
108
+ timeout alone, so the latency of a response is halved.
109
+ - The listening socket no longer tries to restart itself after an error; a broken
110
+ listening socket stops the server with an `error` event.
111
+ - `stop` is emitted when `run()` returns, after every socket is closed.
112
+ - Documentation: README rewritten with the event table, the API table, the
113
+ framing and threading caveats, compatibility and development instructions.
114
+ - Metadata: the `HTTP Servers` topic is replaced by `System :: Networking` and
115
+ `Software Development :: Libraries`, `Requires-Python` is declared, and project
116
+ URLs point to the changelog and issue tracker.
117
+
118
+ ### Removed
119
+
120
+ - Python 2 leftovers: the `coding=utf8` header and the empty `__main__` block.
121
+ - `setup.py` and `requirements.txt`: dependencies are declared in `pyproject.toml`.
122
+ - The debug `print()` call on the server-socket error path.
123
+
124
+ ### Breaking
125
+
126
+ - The `SimpleSocketServer` class is no longer a singleton: every instance is an
127
+ independent server, so two servers can run in one process.
128
+ - Python 3.9 or newer is required.
129
+ - `send()` raises `ValueError` instead of `KeyError` for an unknown socket.
130
+ - `SimpleSocketServerException` wraps the underlying `OSError` (`raise ... from`),
131
+ so `except OSError` keeps working but the arguments differ.
132
+
133
+ ## [1.8] - 2023-02-10
134
+
135
+ - Fixed getting the peer address after a disconnect.
136
+
137
+ ## [1.7] - 2023-02-06
138
+
139
+ - `EventBus.__init__` is called from the server constructor.
140
+
141
+ ## [1.6] - 2023-02-06
142
+
143
+ - Replaced the event emitter with the `event-bus` library.
144
+
145
+ ## [1.5] - 2023-02-05
146
+
147
+ - Replaced the bundled event emitter.
148
+
149
+ ## [1.4] - 2023-02-04
150
+
151
+ - `BrokenPipeError` on send is reported instead of escaping `run()`.
152
+
153
+ ## [1.3] - 2023-02-03
154
+
155
+ - `ConnectionResetError` ends the connection instead of raising from the handler.
156
+
157
+ ## [1.2] - 2023-02-02
158
+
159
+ - Removed the Flask integration.
160
+
161
+ ## [1.1] - 2023-02-01
162
+
163
+ - `start` event with the listening address.
164
+
165
+ ## [1.0] - 2023-01-24
166
+
167
+ - First published release: `select()`-based TCP server with `connect`, `message`
168
+ and `disconnect` events.
@@ -1,4 +1,4 @@
1
- Copyright (c) 2023, Alexey Kuznetsov
1
+ Copyright (c) 2023–2026, webtoucher
2
2
 
3
3
  All rights reserved.
4
4
 
@@ -0,0 +1 @@
1
+ include CHANGELOG.md
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.4
2
+ Name: simple-socket-server
3
+ Version: 2.1
4
+ Summary: Simple TCP socket server with select
5
+ Author-email: webtoucher <webtoucher@ya.ru>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/webtoucher/simple-socket-server
8
+ Project-URL: Repository, https://github.com/webtoucher/simple-socket-server
9
+ Project-URL: Issues, https://github.com/webtoucher/simple-socket-server/issues
10
+ Project-URL: Changelog, https://github.com/webtoucher/simple-socket-server/blob/master/CHANGELOG.md
11
+ Keywords: socket,tcp,server,select,networking,event-driven
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Programming Language :: Python :: 3.15
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Classifier: Topic :: System :: Networking
25
+ Classifier: Operating System :: Microsoft :: Windows
26
+ Classifier: Operating System :: POSIX
27
+ Classifier: Operating System :: Unix
28
+ Classifier: Operating System :: MacOS
29
+ Classifier: Typing :: Typed
30
+ Requires-Python: >=3.9
31
+ Description-Content-Type: text/markdown
32
+ License-File: LICENSE.md
33
+ Requires-Dist: event-bus~=1.0.2
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=7; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # Simple TCP socket server with select
39
+
40
+ ![License](https://img.shields.io/badge/License-BSD%203--Clause-green)
41
+ [![Downloads](https://img.shields.io/pypi/dm/simple-socket-server.svg?color=orange)](https://pypi.python.org/pypi/simple-socket-server)
42
+ [![Latest Version](https://img.shields.io/pypi/v/simple-socket-server.svg)](https://pypi.python.org/pypi/simple-socket-server)
43
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/simple-socket-server.svg)](https://pypi.python.org/pypi/simple-socket-server)
44
+ [![tests](https://github.com/webtoucher/simple-socket-server/actions/workflows/tests.yml/badge.svg)](https://github.com/webtoucher/simple-socket-server/actions/workflows/tests.yml)
45
+
46
+ A TCP server on a single `select()` loop with no networking framework underneath:
47
+ you subscribe handlers to events and the server calls them. Type hints are shipped
48
+ with the package, the only runtime dependency is the event bus that dispatches
49
+ those events.
50
+
51
+ ## Installation
52
+
53
+ Install it with pip:
54
+
55
+ ```shell
56
+ $ pip install simple-socket-server
57
+ ```
58
+
59
+ Or you can add it as dependency in requirements.txt file of your python application:
60
+
61
+ ```
62
+ simple-socket-server~=2.1
63
+ ```
64
+
65
+ ## Usage
66
+
67
+ Easy way to understand how it works is testing socket server via telnet terminal:
68
+
69
+ ```python
70
+ from socket import socket
71
+ from simple_socket_server import SimpleSocketServer
72
+
73
+ socket_server = SimpleSocketServer()
74
+
75
+
76
+ @socket_server.on('start')
77
+ def on_start(host, port):
78
+ print('Listening on %s:%s' % (host, port))
79
+
80
+
81
+ @socket_server.on('connect')
82
+ def on_connect(sock: socket, peer):
83
+ print('New connection from %s:%s' % peer)
84
+ socket_server.send(sock, b'What is your name?\r\n')
85
+
86
+
87
+ @socket_server.on('disconnect')
88
+ def on_disconnect(_sock, peer):
89
+ print('Connection from %s:%s is closed' % peer)
90
+
91
+
92
+ @socket_server.on('message')
93
+ def on_message(sock: socket, peer, message: bytes):
94
+ print('Incoming data from %s:%s' % peer, message)
95
+ socket_server.send(sock, b'Hi, ' + message)
96
+
97
+
98
+ socket_server.run(host='0.0.0.0', port=5000)
99
+ ```
100
+
101
+ Then you can connect to server:
102
+
103
+ ```shell
104
+ telnet 127.0.0.1 5000
105
+ ```
106
+
107
+ `run()` blocks, so the server stops when someone asks it to. That may be a signal
108
+ handler, a timer or an event handler:
109
+
110
+ ```python
111
+ @another_server.on('message')
112
+ def on_message(sock, peer, message):
113
+ if message.strip() == b'quit':
114
+ another_server.stop()
115
+ ```
116
+
117
+ A port of `0` lets the operating system pick a free port; the real address comes
118
+ back in the `start` event and in the `host` and `port` properties:
119
+
120
+ ```python
121
+ socket_server.run(host='127.0.0.1', port=0)
122
+ # ... inside the start handler:
123
+ print(socket_server.host, socket_server.port)
124
+ ```
125
+
126
+ ## Events
127
+
128
+ Handlers are called from the thread that runs `run()`, one at a time.
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 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
+
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.
142
+
143
+ ## API
144
+
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` |
154
+
155
+ Notes:
156
+
157
+ - The server is single-threaded and handlers are called synchronously, so a slow
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()`.
162
+ - `message` carries one raw `recv()` chunk of up to 1024 bytes. There is no
163
+ framing: the protocol you put on top has to delimit its own messages.
164
+ - `send()` never blocks and never truncates: because client sockets are
165
+ non-blocking, the bytes are buffered and the unwritten tail is retried on the
166
+ next poll, so a message larger than the socket buffer arrives in full.
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`.
181
+
182
+ ## Compatibility
183
+
184
+ - Python **3.9 and newer**, including free-threaded builds: CI runs every version
185
+ from 3.9 to 3.15 plus the free-threaded `3.13t`.
186
+ - One runtime dependency: [`event-bus`](https://pypi.org/project/event-bus/).
187
+ - Type hints are shipped with the package (`py.typed`, PEP 561), so mypy and
188
+ pyright see real signatures instead of `Any`.
189
+ - Any server instance is independent: several servers may run in one process, each
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.
195
+
196
+ ## Development
197
+
198
+ ```shell
199
+ $ python -m venv .venv
200
+ $ .venv\Scripts\activate # Windows; on POSIX: source .venv/bin/activate
201
+ $ pip install -e ".[dev]" # installs the package and pytest
202
+ $ python -m pytest
203
+ ```
204
+
205
+ The test suite covers:
206
+
207
+ - the lifecycle: `start`, `stop`, `is_running`, socket cleanup and port reuse,
208
+ - the message flow: `connect`, echo round trips, ordering and large payloads,
209
+ - disconnects: graceful close by the peer, an abrupt reset and a send to a
210
+ connection that is already gone,
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,
221
+ - independence of two servers running in one process.
222
+
223
+ Project layout:
224
+
225
+ ```
226
+ simple_socket_server/__init__.py the whole library
227
+ tests/ test suite
228
+ pyproject.toml metadata, build config and pytest settings
229
+ .github/workflows/ CI: tests on Python 3.9-3.15 and 3.13t, release pipeline
230
+ ```
231
+
232
+ To build and verify a release:
233
+
234
+ ```shell
235
+ $ pip install build twine
236
+ $ python -m build
237
+ $ python -m twine check dist/*
238
+ ```
239
+
240
+ ## Changelog
241
+
242
+ See [CHANGELOG.md](CHANGELOG.md). GitHub Releases are generated from the matching
243
+ entry of that file, so the history is available both in git and on the releases
244
+ page.
245
+
246
+ ## License
247
+
248
+ Distributed under the BSD 3-Clause license. See [LICENSE.md](LICENSE.md) for details.
@@ -0,0 +1,211 @@
1
+ # Simple TCP socket server with select
2
+
3
+ ![License](https://img.shields.io/badge/License-BSD%203--Clause-green)
4
+ [![Downloads](https://img.shields.io/pypi/dm/simple-socket-server.svg?color=orange)](https://pypi.python.org/pypi/simple-socket-server)
5
+ [![Latest Version](https://img.shields.io/pypi/v/simple-socket-server.svg)](https://pypi.python.org/pypi/simple-socket-server)
6
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/simple-socket-server.svg)](https://pypi.python.org/pypi/simple-socket-server)
7
+ [![tests](https://github.com/webtoucher/simple-socket-server/actions/workflows/tests.yml/badge.svg)](https://github.com/webtoucher/simple-socket-server/actions/workflows/tests.yml)
8
+
9
+ A TCP server on a single `select()` loop with no networking framework underneath:
10
+ you subscribe handlers to events and the server calls them. Type hints are shipped
11
+ with the package, the only runtime dependency is the event bus that dispatches
12
+ those events.
13
+
14
+ ## Installation
15
+
16
+ Install it with pip:
17
+
18
+ ```shell
19
+ $ pip install simple-socket-server
20
+ ```
21
+
22
+ Or you can add it as dependency in requirements.txt file of your python application:
23
+
24
+ ```
25
+ simple-socket-server~=2.1
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ Easy way to understand how it works is testing socket server via telnet terminal:
31
+
32
+ ```python
33
+ from socket import socket
34
+ from simple_socket_server import SimpleSocketServer
35
+
36
+ socket_server = SimpleSocketServer()
37
+
38
+
39
+ @socket_server.on('start')
40
+ def on_start(host, port):
41
+ print('Listening on %s:%s' % (host, port))
42
+
43
+
44
+ @socket_server.on('connect')
45
+ def on_connect(sock: socket, peer):
46
+ print('New connection from %s:%s' % peer)
47
+ socket_server.send(sock, b'What is your name?\r\n')
48
+
49
+
50
+ @socket_server.on('disconnect')
51
+ def on_disconnect(_sock, peer):
52
+ print('Connection from %s:%s is closed' % peer)
53
+
54
+
55
+ @socket_server.on('message')
56
+ def on_message(sock: socket, peer, message: bytes):
57
+ print('Incoming data from %s:%s' % peer, message)
58
+ socket_server.send(sock, b'Hi, ' + message)
59
+
60
+
61
+ socket_server.run(host='0.0.0.0', port=5000)
62
+ ```
63
+
64
+ Then you can connect to server:
65
+
66
+ ```shell
67
+ telnet 127.0.0.1 5000
68
+ ```
69
+
70
+ `run()` blocks, so the server stops when someone asks it to. That may be a signal
71
+ handler, a timer or an event handler:
72
+
73
+ ```python
74
+ @another_server.on('message')
75
+ def on_message(sock, peer, message):
76
+ if message.strip() == b'quit':
77
+ another_server.stop()
78
+ ```
79
+
80
+ A port of `0` lets the operating system pick a free port; the real address comes
81
+ back in the `start` event and in the `host` and `port` properties:
82
+
83
+ ```python
84
+ socket_server.run(host='127.0.0.1', port=0)
85
+ # ... inside the start handler:
86
+ print(socket_server.host, socket_server.port)
87
+ ```
88
+
89
+ ## Events
90
+
91
+ Handlers are called from the thread that runs `run()`, one at a time.
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 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
+
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.
105
+
106
+ ## API
107
+
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` |
117
+
118
+ Notes:
119
+
120
+ - The server is single-threaded and handlers are called synchronously, so a slow
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()`.
125
+ - `message` carries one raw `recv()` chunk of up to 1024 bytes. There is no
126
+ framing: the protocol you put on top has to delimit its own messages.
127
+ - `send()` never blocks and never truncates: because client sockets are
128
+ non-blocking, the bytes are buffered and the unwritten tail is retried on the
129
+ next poll, so a message larger than the socket buffer arrives in full.
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`.
144
+
145
+ ## Compatibility
146
+
147
+ - Python **3.9 and newer**, including free-threaded builds: CI runs every version
148
+ from 3.9 to 3.15 plus the free-threaded `3.13t`.
149
+ - One runtime dependency: [`event-bus`](https://pypi.org/project/event-bus/).
150
+ - Type hints are shipped with the package (`py.typed`, PEP 561), so mypy and
151
+ pyright see real signatures instead of `Any`.
152
+ - Any server instance is independent: several servers may run in one process, each
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.
158
+
159
+ ## Development
160
+
161
+ ```shell
162
+ $ python -m venv .venv
163
+ $ .venv\Scripts\activate # Windows; on POSIX: source .venv/bin/activate
164
+ $ pip install -e ".[dev]" # installs the package and pytest
165
+ $ python -m pytest
166
+ ```
167
+
168
+ The test suite covers:
169
+
170
+ - the lifecycle: `start`, `stop`, `is_running`, socket cleanup and port reuse,
171
+ - the message flow: `connect`, echo round trips, ordering and large payloads,
172
+ - disconnects: graceful close by the peer, an abrupt reset and a send to a
173
+ connection that is already gone,
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,
184
+ - independence of two servers running in one process.
185
+
186
+ Project layout:
187
+
188
+ ```
189
+ simple_socket_server/__init__.py the whole library
190
+ tests/ test suite
191
+ pyproject.toml metadata, build config and pytest settings
192
+ .github/workflows/ CI: tests on Python 3.9-3.15 and 3.13t, release pipeline
193
+ ```
194
+
195
+ To build and verify a release:
196
+
197
+ ```shell
198
+ $ pip install build twine
199
+ $ python -m build
200
+ $ python -m twine check dist/*
201
+ ```
202
+
203
+ ## Changelog
204
+
205
+ See [CHANGELOG.md](CHANGELOG.md). GitHub Releases are generated from the matching
206
+ entry of that file, so the history is available both in git and on the releases
207
+ page.
208
+
209
+ ## License
210
+
211
+ Distributed under the BSD 3-Clause license. See [LICENSE.md](LICENSE.md) for details.