simple-socket-server 1.8__tar.gz → 2.0__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.0/CHANGELOG.md +115 -0
- {simple-socket-server-1.8 → simple_socket_server-2.0}/LICENSE.md +1 -1
- simple_socket_server-2.0/MANIFEST.in +1 -0
- simple_socket_server-2.0/PKG-INFO +218 -0
- simple_socket_server-2.0/README.md +181 -0
- simple_socket_server-2.0/pyproject.toml +61 -0
- {simple-socket-server-1.8 → simple_socket_server-2.0}/setup.cfg +4 -4
- simple_socket_server-2.0/simple_socket_server/__init__.py +262 -0
- simple_socket_server-2.0/simple_socket_server/py.typed +0 -0
- simple_socket_server-2.0/simple_socket_server.egg-info/PKG-INFO +218 -0
- {simple-socket-server-1.8 → simple_socket_server-2.0}/simple_socket_server.egg-info/SOURCES.txt +5 -4
- {simple-socket-server-1.8 → simple_socket_server-2.0}/simple_socket_server.egg-info/requires.txt +3 -0
- simple_socket_server-2.0/tests/test_simple_socket_server.py +345 -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.0}/simple_socket_server.egg-info/dependency_links.txt +0 -0
- {simple-socket-server-1.8 → simple_socket_server-2.0}/simple_socket_server.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
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.0] - 2026-11-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `SimpleSocketServer.stop()`: `run()` returns on request, closes every socket and
|
|
13
|
+
emits `stop(host, port)`. It only raises a flag, so it is safe to call from an
|
|
14
|
+
event handler or from another thread.
|
|
15
|
+
- `is_running`, `host` and `port` properties. A port of `0` is resolved by the
|
|
16
|
+
system, and the `start` event plus both properties report the real address.
|
|
17
|
+
- `simple_socket_server.__version__`, an explicit `__all__` and a public `Address`
|
|
18
|
+
alias for a peer address.
|
|
19
|
+
- `py.typed` marker (PEP 561) and type hints on every public name, so mypy and
|
|
20
|
+
pyright see real signatures instead of `Any`.
|
|
21
|
+
- `tests/`: pytest suite covering the lifecycle, event order, echo round trips,
|
|
22
|
+
graceful and abrupt disconnects, large messages, error handling and port reuse.
|
|
23
|
+
- GitHub Actions workflows: tests on Python 3.9-3.15 and 3.13t, plus a tag-driven
|
|
24
|
+
release pipeline publishing to PyPI and creating a GitHub Release.
|
|
25
|
+
- `CHANGELOG.md` and `.gitattributes` with `* text=auto eol=lf`.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- A peer closing the connection cleanly (`recv()` returning `b""`) was ignored, so
|
|
30
|
+
the socket stayed in the `select()` set forever: the loop spun on an idle file
|
|
31
|
+
descriptor and `disconnect` never fired.
|
|
32
|
+
- `send()` raised `KeyError` for a socket that was not a client of the server; it
|
|
33
|
+
raises `ValueError` now, and an empty message is a no-op.
|
|
34
|
+
- `send()` dropped data on a non-blocking socket: a single `send()` call can accept
|
|
35
|
+
only part of the buffer. The unwritten tail is buffered and retried, so a message
|
|
36
|
+
larger than the socket buffer arrives in full.
|
|
37
|
+
- Closed sockets were never removed from the client table, so a long-lived server
|
|
38
|
+
leaked memory with every reconnection.
|
|
39
|
+
- The `disconnect` event could be emitted twice for one socket, and a socket already
|
|
40
|
+
dropped earlier in the same poll was used again.
|
|
41
|
+
- The state reset on the server-socket error path left the previous listening
|
|
42
|
+
socket open, and `run()` reused a stale initialized flag.
|
|
43
|
+
- `SimpleSocketServerException` was raised with two positional arguments on a check
|
|
44
|
+
that could not trigger; bind and listen failures are reported now with the address.
|
|
45
|
+
- A `select()` failure is reported through the `error` event and stops the server
|
|
46
|
+
instead of escaping `run()` as an unhandled exception.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- Packaging moved from `setup.py` to `pyproject.toml` (PEP 621, PEP 639);
|
|
51
|
+
metadata, build configuration and pytest settings live in one file.
|
|
52
|
+
- Errors that end a connection are reported through the `error` event and followed
|
|
53
|
+
by `disconnect`; `message` is never emitted for a reset connection.
|
|
54
|
+
- `run()` no longer sleeps after `select()`: the poll interval is the `select()`
|
|
55
|
+
timeout alone, so the latency of a response is halved.
|
|
56
|
+
- The listening socket no longer tries to restart itself after an error; a broken
|
|
57
|
+
listening socket stops the server with an `error` event.
|
|
58
|
+
- `stop` is emitted when `run()` returns, after every socket is closed.
|
|
59
|
+
- Documentation: README rewritten with the event table, the API table, the
|
|
60
|
+
framing and threading caveats, compatibility and development instructions.
|
|
61
|
+
- Metadata: the `HTTP Servers` topic is replaced by `System :: Networking` and
|
|
62
|
+
`Software Development :: Libraries`, `Requires-Python` is declared, and project
|
|
63
|
+
URLs point to the changelog and issue tracker.
|
|
64
|
+
|
|
65
|
+
### Removed
|
|
66
|
+
|
|
67
|
+
- Python 2 leftovers: the `coding=utf8` header and the empty `__main__` block.
|
|
68
|
+
- `setup.py` and `requirements.txt`: dependencies are declared in `pyproject.toml`.
|
|
69
|
+
- The debug `print()` call on the server-socket error path.
|
|
70
|
+
|
|
71
|
+
### Breaking
|
|
72
|
+
|
|
73
|
+
- The `SimpleSocketServer` class is no longer a singleton: every instance is an
|
|
74
|
+
independent server, so two servers can run in one process.
|
|
75
|
+
- Python 3.9 or newer is required.
|
|
76
|
+
- `send()` raises `ValueError` instead of `KeyError` for an unknown socket.
|
|
77
|
+
- `SimpleSocketServerException` wraps the underlying `OSError` (`raise ... from`),
|
|
78
|
+
so `except OSError` keeps working but the arguments differ.
|
|
79
|
+
|
|
80
|
+
## [1.8] - 2023-02-10
|
|
81
|
+
|
|
82
|
+
- Fixed getting the peer address after a disconnect.
|
|
83
|
+
|
|
84
|
+
## [1.7] - 2023-02-06
|
|
85
|
+
|
|
86
|
+
- `EventBus.__init__` is called from the server constructor.
|
|
87
|
+
|
|
88
|
+
## [1.6] - 2023-02-06
|
|
89
|
+
|
|
90
|
+
- Replaced the event emitter with the `event-bus` library.
|
|
91
|
+
|
|
92
|
+
## [1.5] - 2023-02-05
|
|
93
|
+
|
|
94
|
+
- Replaced the bundled event emitter.
|
|
95
|
+
|
|
96
|
+
## [1.4] - 2023-02-04
|
|
97
|
+
|
|
98
|
+
- `BrokenPipeError` on send is reported instead of escaping `run()`.
|
|
99
|
+
|
|
100
|
+
## [1.3] - 2023-02-03
|
|
101
|
+
|
|
102
|
+
- `ConnectionResetError` ends the connection instead of raising from the handler.
|
|
103
|
+
|
|
104
|
+
## [1.2] - 2023-02-02
|
|
105
|
+
|
|
106
|
+
- Removed the Flask integration.
|
|
107
|
+
|
|
108
|
+
## [1.1] - 2023-02-01
|
|
109
|
+
|
|
110
|
+
- `start` event with the listening address.
|
|
111
|
+
|
|
112
|
+
## [1.0] - 2023-01-24
|
|
113
|
+
|
|
114
|
+
- First published release: `select()`-based TCP server with `connect`, `message`
|
|
115
|
+
and `disconnect` events.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include CHANGELOG.md
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple-socket-server
|
|
3
|
+
Version: 2.0
|
|
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.0
|
|
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 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 |
|
|
138
|
+
|
|
139
|
+
`peer` is an `(host, port)` tuple, `data` is `bytes`, `error` is an `OSError`.
|
|
140
|
+
|
|
141
|
+
## API
|
|
142
|
+
|
|
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` |
|
|
151
|
+
|
|
152
|
+
Notes:
|
|
153
|
+
|
|
154
|
+
- The server is single-threaded and handlers are called synchronously, so a slow
|
|
155
|
+
handler delays every other connection. Hand off long work to your own thread.
|
|
156
|
+
- `message` carries one raw `recv()` chunk of up to 1024 bytes. There is no
|
|
157
|
+
framing: the protocol you put on top has to delimit its own messages.
|
|
158
|
+
- `send()` never blocks and never truncates: because client sockets are
|
|
159
|
+
non-blocking, the bytes are buffered and the unwritten tail is retried on the
|
|
160
|
+
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.
|
|
164
|
+
|
|
165
|
+
## Compatibility
|
|
166
|
+
|
|
167
|
+
- Python **3.9 and newer**, including free-threaded builds: CI runs every version
|
|
168
|
+
from 3.9 to 3.15 plus the free-threaded `3.13t`.
|
|
169
|
+
- One runtime dependency: [`event-bus`](https://pypi.org/project/event-bus/).
|
|
170
|
+
- Type hints are shipped with the package (`py.typed`, PEP 561), so mypy and
|
|
171
|
+
pyright see real signatures instead of `Any`.
|
|
172
|
+
- Any server instance is independent: several servers may run in one process, each
|
|
173
|
+
in its own thread.
|
|
174
|
+
|
|
175
|
+
## Development
|
|
176
|
+
|
|
177
|
+
```shell
|
|
178
|
+
$ python -m venv .venv
|
|
179
|
+
$ .venv\Scripts\activate # Windows; on POSIX: source .venv/bin/activate
|
|
180
|
+
$ pip install -e ".[dev]" # installs the package and pytest
|
|
181
|
+
$ python -m pytest
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The test suite covers:
|
|
185
|
+
|
|
186
|
+
- the lifecycle: `start`, `stop`, `is_running`, socket cleanup and port reuse,
|
|
187
|
+
- the message flow: `connect`, echo round trips, ordering and large payloads,
|
|
188
|
+
- disconnects: graceful close by the peer, an abrupt reset and a send to a
|
|
189
|
+
connection that is already gone,
|
|
190
|
+
- error handling: an unknown socket for `send()` and a port that is already taken,
|
|
191
|
+
- independence of two servers running in one process.
|
|
192
|
+
|
|
193
|
+
Project layout:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
simple_socket_server/__init__.py the whole library
|
|
197
|
+
tests/ test suite
|
|
198
|
+
pyproject.toml metadata, build config and pytest settings
|
|
199
|
+
.github/workflows/ CI: tests on Python 3.9-3.15 and 3.13t, release pipeline
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
To build and verify a release:
|
|
203
|
+
|
|
204
|
+
```shell
|
|
205
|
+
$ pip install build twine
|
|
206
|
+
$ python -m build
|
|
207
|
+
$ python -m twine check dist/*
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Changelog
|
|
211
|
+
|
|
212
|
+
See [CHANGELOG.md](CHANGELOG.md). GitHub Releases are generated from the matching
|
|
213
|
+
entry of that file, so the history is available both in git and on the releases
|
|
214
|
+
page.
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
Distributed under the BSD 3-Clause license. See [LICENSE.md](LICENSE.md) for details.
|
|
@@ -0,0 +1,181 @@
|
|
|
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.0
|
|
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 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 |
|
|
101
|
+
|
|
102
|
+
`peer` is an `(host, port)` tuple, `data` is `bytes`, `error` is an `OSError`.
|
|
103
|
+
|
|
104
|
+
## API
|
|
105
|
+
|
|
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` |
|
|
114
|
+
|
|
115
|
+
Notes:
|
|
116
|
+
|
|
117
|
+
- The server is single-threaded and handlers are called synchronously, so a slow
|
|
118
|
+
handler delays every other connection. Hand off long work to your own thread.
|
|
119
|
+
- `message` carries one raw `recv()` chunk of up to 1024 bytes. There is no
|
|
120
|
+
framing: the protocol you put on top has to delimit its own messages.
|
|
121
|
+
- `send()` never blocks and never truncates: because client sockets are
|
|
122
|
+
non-blocking, the bytes are buffered and the unwritten tail is retried on the
|
|
123
|
+
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.
|
|
127
|
+
|
|
128
|
+
## Compatibility
|
|
129
|
+
|
|
130
|
+
- Python **3.9 and newer**, including free-threaded builds: CI runs every version
|
|
131
|
+
from 3.9 to 3.15 plus the free-threaded `3.13t`.
|
|
132
|
+
- One runtime dependency: [`event-bus`](https://pypi.org/project/event-bus/).
|
|
133
|
+
- Type hints are shipped with the package (`py.typed`, PEP 561), so mypy and
|
|
134
|
+
pyright see real signatures instead of `Any`.
|
|
135
|
+
- Any server instance is independent: several servers may run in one process, each
|
|
136
|
+
in its own thread.
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
```shell
|
|
141
|
+
$ python -m venv .venv
|
|
142
|
+
$ .venv\Scripts\activate # Windows; on POSIX: source .venv/bin/activate
|
|
143
|
+
$ pip install -e ".[dev]" # installs the package and pytest
|
|
144
|
+
$ python -m pytest
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The test suite covers:
|
|
148
|
+
|
|
149
|
+
- the lifecycle: `start`, `stop`, `is_running`, socket cleanup and port reuse,
|
|
150
|
+
- the message flow: `connect`, echo round trips, ordering and large payloads,
|
|
151
|
+
- disconnects: graceful close by the peer, an abrupt reset and a send to a
|
|
152
|
+
connection that is already gone,
|
|
153
|
+
- error handling: an unknown socket for `send()` and a port that is already taken,
|
|
154
|
+
- independence of two servers running in one process.
|
|
155
|
+
|
|
156
|
+
Project layout:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
simple_socket_server/__init__.py the whole library
|
|
160
|
+
tests/ test suite
|
|
161
|
+
pyproject.toml metadata, build config and pytest settings
|
|
162
|
+
.github/workflows/ CI: tests on Python 3.9-3.15 and 3.13t, release pipeline
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
To build and verify a release:
|
|
166
|
+
|
|
167
|
+
```shell
|
|
168
|
+
$ pip install build twine
|
|
169
|
+
$ python -m build
|
|
170
|
+
$ python -m twine check dist/*
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Changelog
|
|
174
|
+
|
|
175
|
+
See [CHANGELOG.md](CHANGELOG.md). GitHub Releases are generated from the matching
|
|
176
|
+
entry of that file, so the history is available both in git and on the releases
|
|
177
|
+
page.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
Distributed under the BSD 3-Clause license. See [LICENSE.md](LICENSE.md) for details.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "simple-socket-server"
|
|
7
|
+
description = "Simple TCP socket server with select"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
license = "BSD-3-Clause"
|
|
11
|
+
license-files = ["LICENSE.md"]
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "webtoucher", email = "webtoucher@ya.ru" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["socket", "tcp", "server", "select", "networking", "event-driven"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Programming Language :: Python :: 3.15",
|
|
28
|
+
"Topic :: Software Development :: Libraries",
|
|
29
|
+
"Topic :: System :: Networking",
|
|
30
|
+
"Operating System :: Microsoft :: Windows",
|
|
31
|
+
"Operating System :: POSIX",
|
|
32
|
+
"Operating System :: Unix",
|
|
33
|
+
"Operating System :: MacOS",
|
|
34
|
+
"Typing :: Typed",
|
|
35
|
+
]
|
|
36
|
+
dynamic = ["version"]
|
|
37
|
+
dependencies = [
|
|
38
|
+
"event-bus~=1.0.2",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
dev = ["pytest>=7"]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/webtoucher/simple-socket-server"
|
|
46
|
+
Repository = "https://github.com/webtoucher/simple-socket-server"
|
|
47
|
+
Issues = "https://github.com/webtoucher/simple-socket-server/issues"
|
|
48
|
+
Changelog = "https://github.com/webtoucher/simple-socket-server/blob/master/CHANGELOG.md"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools]
|
|
51
|
+
packages = ["simple_socket_server"]
|
|
52
|
+
|
|
53
|
+
[tool.setuptools.dynamic]
|
|
54
|
+
version = { attr = "simple_socket_server.__version__" }
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.package-data]
|
|
57
|
+
simple_socket_server = ["py.typed"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
addopts = "-ra"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[egg_info]
|
|
2
|
-
tag_build =
|
|
3
|
-
tag_date = 0
|
|
4
|
-
|
|
1
|
+
[egg_info]
|
|
2
|
+
tag_build =
|
|
3
|
+
tag_date = 0
|
|
4
|
+
|