simple-socket-server 2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simple_socket_server/__init__.py +262 -0
- simple_socket_server/py.typed +0 -0
- simple_socket_server-2.0.dist-info/METADATA +218 -0
- simple_socket_server-2.0.dist-info/RECORD +7 -0
- simple_socket_server-2.0.dist-info/WHEEL +5 -0
- simple_socket_server-2.0.dist-info/licenses/LICENSE.md +13 -0
- simple_socket_server-2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple TCP socket server built on select().
|
|
3
|
+
|
|
4
|
+
The server is single-threaded: run() polls the sockets and calls the handlers
|
|
5
|
+
registered for its events. No third-party networking code is involved, the only
|
|
6
|
+
runtime dependency is the event bus used to dispatch those events.
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2023–2026 webtoucher
|
|
9
|
+
Distributed under the BSD 3-Clause license. See LICENSE.md for more info.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import select
|
|
13
|
+
import socket
|
|
14
|
+
import threading
|
|
15
|
+
|
|
16
|
+
from typing import Dict, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
from event_bus import EventBus
|
|
19
|
+
|
|
20
|
+
__all__ = ["SimpleSocketServer", "SimpleSocketServerException"]
|
|
21
|
+
|
|
22
|
+
__version__ = "2.0"
|
|
23
|
+
|
|
24
|
+
#: Peer address as returned by accept(): (host, port).
|
|
25
|
+
Address = Tuple[str, int]
|
|
26
|
+
|
|
27
|
+
#: Size of a single recv() chunk; a message event carries one such chunk.
|
|
28
|
+
_RECV_SIZE = 1024
|
|
29
|
+
|
|
30
|
+
#: How long select() waits for socket activity, in seconds.
|
|
31
|
+
_POLL_INTERVAL = 0.1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SimpleSocketServerException(socket.error):
|
|
35
|
+
"""Raised when the listening socket cannot be created, bound or enabled."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SimpleSocketServer(EventBus):
|
|
39
|
+
"""Single-threaded TCP server dispatching events through an event bus.
|
|
40
|
+
|
|
41
|
+
Events, in the order they may arrive:
|
|
42
|
+
|
|
43
|
+
``start(host, port)``
|
|
44
|
+
The listening socket is ready.
|
|
45
|
+
``connect(sock, peer)``
|
|
46
|
+
A client has been accepted.
|
|
47
|
+
``message(sock, peer, data)``
|
|
48
|
+
A chunk of bytes has been received.
|
|
49
|
+
``disconnect(sock, peer)``
|
|
50
|
+
The connection is gone: the peer closed it, reset it or a send failed.
|
|
51
|
+
``error(sock, peer, error)``
|
|
52
|
+
A socket error occurred; the connection is closed right after.
|
|
53
|
+
``stop(host, port)``
|
|
54
|
+
run() has returned and every socket is closed.
|
|
55
|
+
|
|
56
|
+
Handlers are called from the thread that runs run(), one at a time, so a
|
|
57
|
+
slow handler delays every other connection.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self) -> None:
|
|
61
|
+
super().__init__()
|
|
62
|
+
self.__host = "0.0.0.0"
|
|
63
|
+
self.__port = 6666
|
|
64
|
+
self.__max_conn = 5
|
|
65
|
+
self.__server_socket: Optional[socket.socket] = None
|
|
66
|
+
self.__inputs: List[socket.socket] = []
|
|
67
|
+
self.__outputs: List[socket.socket] = []
|
|
68
|
+
self.__pending: Dict[socket.socket, bytearray] = {}
|
|
69
|
+
self.__clients: Dict[socket.socket, Address] = {}
|
|
70
|
+
self.__stop = threading.Event()
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def is_running(self) -> bool:
|
|
74
|
+
"""True while run() is serving connections."""
|
|
75
|
+
return self.__server_socket is not None and not self.__stop.is_set()
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def host(self) -> str:
|
|
79
|
+
"""Host the listening socket is bound to."""
|
|
80
|
+
return self.__host
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def port(self) -> int:
|
|
84
|
+
"""Port the listening socket is bound to."""
|
|
85
|
+
return self.__port
|
|
86
|
+
|
|
87
|
+
def run(self, host: str = "0.0.0.0", port: int = 6666, max_conn: int = 5) -> None:
|
|
88
|
+
"""Serve connections until stop() is called.
|
|
89
|
+
|
|
90
|
+
The call blocks the calling thread. Once it returns, every client socket
|
|
91
|
+
and the listening socket are closed. A port of 0 makes the operating
|
|
92
|
+
system pick a free port; the actual address is reported by the start
|
|
93
|
+
event and by the host and port properties.
|
|
94
|
+
"""
|
|
95
|
+
if self.is_running:
|
|
96
|
+
raise RuntimeError("the server is already running")
|
|
97
|
+
self.__host = host
|
|
98
|
+
self.__port = port
|
|
99
|
+
self.__max_conn = max_conn
|
|
100
|
+
self.__stop.clear()
|
|
101
|
+
self.__initialize()
|
|
102
|
+
try:
|
|
103
|
+
while not self.__stop.is_set():
|
|
104
|
+
self.__select()
|
|
105
|
+
finally:
|
|
106
|
+
self.__shutdown()
|
|
107
|
+
|
|
108
|
+
def stop(self) -> None:
|
|
109
|
+
"""Ask run() to return.
|
|
110
|
+
|
|
111
|
+
It only raises a flag, so it is safe to call from an event handler or
|
|
112
|
+
from another thread. run() finishes the current poll and then closes
|
|
113
|
+
every socket.
|
|
114
|
+
"""
|
|
115
|
+
self.__stop.set()
|
|
116
|
+
|
|
117
|
+
def send(self, sock: socket.socket, message: bytes) -> None:
|
|
118
|
+
"""Queue message for delivery to a client of this server.
|
|
119
|
+
|
|
120
|
+
The socket is non-blocking, so the bytes are buffered and the unwritten
|
|
121
|
+
tail is retried on the next poll; a message larger than the socket
|
|
122
|
+
buffer is delivered in full. Empty messages are ignored.
|
|
123
|
+
"""
|
|
124
|
+
if sock not in self.__pending:
|
|
125
|
+
raise ValueError("the socket is not a client of this server")
|
|
126
|
+
if not message:
|
|
127
|
+
return
|
|
128
|
+
self.__pending[sock].extend(message)
|
|
129
|
+
if sock not in self.__outputs:
|
|
130
|
+
self.__outputs.append(sock)
|
|
131
|
+
|
|
132
|
+
def __initialize(self) -> None:
|
|
133
|
+
server_socket = socket.socket(
|
|
134
|
+
socket.AF_INET,
|
|
135
|
+
socket.SOCK_STREAM,
|
|
136
|
+
socket.IPPROTO_TCP,
|
|
137
|
+
)
|
|
138
|
+
try:
|
|
139
|
+
server_socket.setblocking(False)
|
|
140
|
+
server_socket.setsockopt(
|
|
141
|
+
socket.SOL_SOCKET,
|
|
142
|
+
socket.SO_REUSEADDR,
|
|
143
|
+
1,
|
|
144
|
+
)
|
|
145
|
+
server_socket.bind((self.__host, self.__port))
|
|
146
|
+
server_socket.listen(self.__max_conn)
|
|
147
|
+
except OSError as error:
|
|
148
|
+
server_socket.close()
|
|
149
|
+
raise SimpleSocketServerException(
|
|
150
|
+
"cannot listen on %s:%s" % (self.__host, self.__port)
|
|
151
|
+
) from error
|
|
152
|
+
self.__server_socket = server_socket
|
|
153
|
+
self.__inputs.append(server_socket)
|
|
154
|
+
# A port of 0 is resolved by the system, so report the real address.
|
|
155
|
+
self.__host, self.__port = server_socket.getsockname()[:2]
|
|
156
|
+
self.emit("start", self.__host, self.__port)
|
|
157
|
+
|
|
158
|
+
def __select(self) -> None:
|
|
159
|
+
try:
|
|
160
|
+
sock_to_read, sock_to_write, sock_errors = select.select(
|
|
161
|
+
self.__inputs,
|
|
162
|
+
self.__outputs,
|
|
163
|
+
self.__inputs,
|
|
164
|
+
_POLL_INTERVAL,
|
|
165
|
+
)
|
|
166
|
+
except OSError as error:
|
|
167
|
+
# select() itself failed, so the listening socket is unusable.
|
|
168
|
+
self.emit("error", None, None, error)
|
|
169
|
+
self.stop()
|
|
170
|
+
return
|
|
171
|
+
for sock in sock_errors:
|
|
172
|
+
if sock is self.__server_socket:
|
|
173
|
+
self.stop()
|
|
174
|
+
else:
|
|
175
|
+
self.__close(sock)
|
|
176
|
+
for sock in sock_to_read:
|
|
177
|
+
if sock is self.__server_socket:
|
|
178
|
+
self.__accept()
|
|
179
|
+
else:
|
|
180
|
+
self.__receive(sock)
|
|
181
|
+
for sock in sock_to_write:
|
|
182
|
+
self.__flush(sock)
|
|
183
|
+
|
|
184
|
+
def __accept(self) -> None:
|
|
185
|
+
try:
|
|
186
|
+
client_socket, client_address = self.__server_socket.accept()
|
|
187
|
+
except OSError as error:
|
|
188
|
+
self.emit("error", self.__server_socket, None, error)
|
|
189
|
+
return
|
|
190
|
+
client_socket.setblocking(False)
|
|
191
|
+
self.__clients[client_socket] = client_address
|
|
192
|
+
self.__pending[client_socket] = bytearray()
|
|
193
|
+
self.__inputs.append(client_socket)
|
|
194
|
+
self.emit("connect", client_socket, client_address)
|
|
195
|
+
|
|
196
|
+
def __receive(self, sock: socket.socket) -> None:
|
|
197
|
+
peer = self.__clients.get(sock)
|
|
198
|
+
if peer is None:
|
|
199
|
+
# The socket was already dropped during this poll.
|
|
200
|
+
return
|
|
201
|
+
try:
|
|
202
|
+
data = sock.recv(_RECV_SIZE)
|
|
203
|
+
except BlockingIOError:
|
|
204
|
+
# select() woke us up for nothing; try again on the next poll.
|
|
205
|
+
return
|
|
206
|
+
except (ConnectionResetError, ConnectionAbortedError, BrokenPipeError):
|
|
207
|
+
self.__close(sock)
|
|
208
|
+
return
|
|
209
|
+
except OSError as error:
|
|
210
|
+
self.emit("error", sock, peer, error)
|
|
211
|
+
self.__close(sock)
|
|
212
|
+
return
|
|
213
|
+
if not data:
|
|
214
|
+
# An empty chunk means the peer closed its end of the connection.
|
|
215
|
+
self.__close(sock)
|
|
216
|
+
return
|
|
217
|
+
self.emit("message", sock, peer, data)
|
|
218
|
+
|
|
219
|
+
def __flush(self, sock: socket.socket) -> None:
|
|
220
|
+
pending = self.__pending.get(sock)
|
|
221
|
+
if pending is None:
|
|
222
|
+
if sock in self.__outputs:
|
|
223
|
+
self.__outputs.remove(sock)
|
|
224
|
+
return
|
|
225
|
+
try:
|
|
226
|
+
sent = sock.send(pending)
|
|
227
|
+
except BlockingIOError:
|
|
228
|
+
# The kernel buffer is full again; the tail stays queued.
|
|
229
|
+
return
|
|
230
|
+
except OSError as error:
|
|
231
|
+
self.emit("error", sock, self.__clients.get(sock), error)
|
|
232
|
+
self.__close(sock)
|
|
233
|
+
return
|
|
234
|
+
del pending[:sent]
|
|
235
|
+
if not pending and sock in self.__outputs:
|
|
236
|
+
self.__outputs.remove(sock)
|
|
237
|
+
|
|
238
|
+
def __close(self, sock: socket.socket) -> None:
|
|
239
|
+
"""Close a client connection and forget it. Safe to call twice."""
|
|
240
|
+
peer = self.__clients.pop(sock, None)
|
|
241
|
+
if sock in self.__inputs:
|
|
242
|
+
self.__inputs.remove(sock)
|
|
243
|
+
if sock in self.__outputs:
|
|
244
|
+
self.__outputs.remove(sock)
|
|
245
|
+
self.__pending.pop(sock, None)
|
|
246
|
+
if peer is None:
|
|
247
|
+
sock.close()
|
|
248
|
+
return
|
|
249
|
+
try:
|
|
250
|
+
self.emit("disconnect", sock, peer)
|
|
251
|
+
finally:
|
|
252
|
+
sock.close()
|
|
253
|
+
|
|
254
|
+
def __shutdown(self) -> None:
|
|
255
|
+
for sock in list(self.__clients):
|
|
256
|
+
self.__close(sock)
|
|
257
|
+
server_socket, self.__server_socket = self.__server_socket, None
|
|
258
|
+
if server_socket is not None:
|
|
259
|
+
if server_socket in self.__inputs:
|
|
260
|
+
self.__inputs.remove(server_socket)
|
|
261
|
+
server_socket.close()
|
|
262
|
+
self.emit("stop", self.__host, self.__port)
|
|
File without changes
|
|
@@ -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,7 @@
|
|
|
1
|
+
simple_socket_server/__init__.py,sha256=NG1iCg4yUKkq6G77v7ffvkGdrkMivnqu_vcFq9wwYQk,9173
|
|
2
|
+
simple_socket_server/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
simple_socket_server-2.0.dist-info/licenses/LICENSE.md,sha256=viUOCJf97qThnFWXx3lGwpWwhWfuo8ug8QQAHtAFh8c,1496
|
|
4
|
+
simple_socket_server-2.0.dist-info/METADATA,sha256=pehdXAJEjZO1_hgYGB3BGO49nrqExDJVQRgbwEsOUKc,8769
|
|
5
|
+
simple_socket_server-2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
simple_socket_server-2.0.dist-info/top_level.txt,sha256=U3g9Q_CXbmmzcFW0pQ4cEZgmkbi0TORT9q3EA_C7I04,21
|
|
7
|
+
simple_socket_server-2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright (c) 2023–2026, webtoucher
|
|
2
|
+
|
|
3
|
+
All rights reserved.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
8
|
+
|
|
9
|
+
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
10
|
+
|
|
11
|
+
* Neither the names of Alexey Kuznetsov or webtoucher nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
12
|
+
|
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
simple_socket_server
|