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