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.
- simple_socket_server-2.1/CHANGELOG.md +168 -0
- {simple-socket-server-1.8 → simple_socket_server-2.1}/LICENSE.md +1 -1
- simple_socket_server-2.1/MANIFEST.in +1 -0
- simple_socket_server-2.1/PKG-INFO +248 -0
- simple_socket_server-2.1/README.md +211 -0
- simple_socket_server-2.1/pyproject.toml +61 -0
- {simple-socket-server-1.8 → simple_socket_server-2.1}/setup.cfg +4 -4
- simple_socket_server-2.1/simple_socket_server/__init__.py +392 -0
- simple_socket_server-2.1/simple_socket_server/py.typed +0 -0
- simple_socket_server-2.1/simple_socket_server.egg-info/PKG-INFO +248 -0
- {simple-socket-server-1.8 → simple_socket_server-2.1}/simple_socket_server.egg-info/SOURCES.txt +5 -4
- {simple-socket-server-1.8 → simple_socket_server-2.1}/simple_socket_server.egg-info/requires.txt +3 -0
- simple_socket_server-2.1/tests/test_simple_socket_server.py +517 -0
- simple-socket-server-1.8/MANIFEST.in +0 -3
- simple-socket-server-1.8/PKG-INFO +0 -78
- simple-socket-server-1.8/README.md +0 -58
- simple-socket-server-1.8/requirements.txt +0 -1
- simple-socket-server-1.8/setup.py +0 -27
- simple-socket-server-1.8/simple_socket_server/__init__.py +0 -163
- simple-socket-server-1.8/simple_socket_server.egg-info/PKG-INFO +0 -78
- simple-socket-server-1.8/simple_socket_server.egg-info/not-zip-safe +0 -1
- {simple-socket-server-1.8 → simple_socket_server-2.1}/simple_socket_server.egg-info/dependency_links.txt +0 -0
- {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.
|
|
@@ -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
|
+

|
|
41
|
+
[](https://pypi.python.org/pypi/simple-socket-server)
|
|
42
|
+
[](https://pypi.python.org/pypi/simple-socket-server)
|
|
43
|
+
[](https://pypi.python.org/pypi/simple-socket-server)
|
|
44
|
+
[](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
|
+

|
|
4
|
+
[](https://pypi.python.org/pypi/simple-socket-server)
|
|
5
|
+
[](https://pypi.python.org/pypi/simple-socket-server)
|
|
6
|
+
[](https://pypi.python.org/pypi/simple-socket-server)
|
|
7
|
+
[](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.
|