simple-tcp-server 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GGN_2015
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: simple-tcp-server
3
+ Version: 0.1.0
4
+ Summary: Simple TCP server/client framework.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: GGN_2015
8
+ Author-email: neko@jlulug.org
9
+ Requires-Python: >=3.10
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Description-Content-Type: text/markdown
18
+
19
+ # simple_tcp_server
20
+ Simple TCP server framework.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install simple_tcp_server
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ from simple_tcp_server import SimpleTcpServer, SimpleTcpClient
32
+ import threading
33
+ import time
34
+
35
+ HOST = "127.0.0.1"
36
+ PORT = 9999
37
+
38
+ def is_prime(msg: bytes) -> bytes:
39
+ n = int(msg.decode())
40
+ if n < 2:
41
+ return b"$"
42
+ for i in range(2, int(n**0.5)+1):
43
+ if n % i == 0:
44
+ return b"$"
45
+ return b"$$"
46
+
47
+ server = SimpleTcpServer(HOST, PORT, is_prime, quit_token=b"quit")
48
+ t = threading.Thread(target=server.mainloop, daemon=True)
49
+ t.start()
50
+
51
+ time.sleep(0.2)
52
+
53
+ client = SimpleTcpClient(HOST, PORT)
54
+
55
+ numbers = [b"2", b"10", b"17", b"97", b"1234567"]
56
+ for n in numbers:
57
+ res = client.request(n)
58
+ print(f"{n}: {res}")
59
+
60
+ # close server by quit_token
61
+ client.request(b"quit")
62
+ for n in numbers:
63
+ res = client.request(n)
64
+ print(f"{n}: {res}")
65
+
66
+ client.close()
67
+ ```
68
+
@@ -0,0 +1,49 @@
1
+ # simple_tcp_server
2
+ Simple TCP server framework.
3
+
4
+ ## Installation
5
+
6
+ ```bash
7
+ pip install simple_tcp_server
8
+ ```
9
+
10
+ ## Usage
11
+
12
+ ```python
13
+ from simple_tcp_server import SimpleTcpServer, SimpleTcpClient
14
+ import threading
15
+ import time
16
+
17
+ HOST = "127.0.0.1"
18
+ PORT = 9999
19
+
20
+ def is_prime(msg: bytes) -> bytes:
21
+ n = int(msg.decode())
22
+ if n < 2:
23
+ return b"$"
24
+ for i in range(2, int(n**0.5)+1):
25
+ if n % i == 0:
26
+ return b"$"
27
+ return b"$$"
28
+
29
+ server = SimpleTcpServer(HOST, PORT, is_prime, quit_token=b"quit")
30
+ t = threading.Thread(target=server.mainloop, daemon=True)
31
+ t.start()
32
+
33
+ time.sleep(0.2)
34
+
35
+ client = SimpleTcpClient(HOST, PORT)
36
+
37
+ numbers = [b"2", b"10", b"17", b"97", b"1234567"]
38
+ for n in numbers:
39
+ res = client.request(n)
40
+ print(f"{n}: {res}")
41
+
42
+ # close server by quit_token
43
+ client.request(b"quit")
44
+ for n in numbers:
45
+ res = client.request(n)
46
+ print(f"{n}: {res}")
47
+
48
+ client.close()
49
+ ```
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "simple-tcp-server"
3
+ version = "0.1.0"
4
+ description = "Simple TCP server/client framework."
5
+ authors = [
6
+ {name = "GGN_2015",email = "neko@jlulug.org"}
7
+ ]
8
+ license = {text = "MIT"}
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ ]
13
+
14
+
15
+ [build-system]
16
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
17
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,27 @@
1
+ import re
2
+
3
+ # 单字节标志符号
4
+ half_eoq = b"$"
5
+
6
+ # 替换转义字符
7
+ def replace_octal_bytes(data: bytes) -> bytes:
8
+ return re.sub(
9
+ re.escape(half_eoq) + rb"(\d{3})",
10
+ lambda m: bytes([int(m[1], 8)]),
11
+ data
12
+ )
13
+
14
+ # 处理转义字符
15
+ def anti_escape(msg:bytes) -> bytes:
16
+ return replace_octal_bytes(msg)
17
+
18
+ # 将字符串中的 _eoq() 进行特殊处理
19
+ # 本质上就是补上一个三位八进制数
20
+ def escape(msg:bytes) -> bytes:
21
+ number = f"{half_eoq[0]:03o}".encode()
22
+ return msg.replace(half_eoq, half_eoq + number)
23
+
24
+
25
+ # 获取结束标志(长度总为 2)
26
+ def eoq() -> bytes:
27
+ return half_eoq + half_eoq
@@ -0,0 +1,7 @@
1
+ from .sts import SimpleTcpServer
2
+ from .stc import SimpleTcpClient
3
+
4
+ __all__ = [
5
+ "SimpleTcpServer",
6
+ "SimpleTcpClient"
7
+ ]
@@ -0,0 +1,64 @@
1
+ import socket
2
+ from typing import Optional
3
+ from . import Utils
4
+
5
+ class SimpleTcpClient:
6
+ def __init__(self, host: str, port: int):
7
+ # 协议配置(必须与服务端一致)
8
+ self.max_buffer = 4096
9
+
10
+ # 连接状态
11
+ self.connected = False
12
+
13
+ # 初始化 socket
14
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
15
+ try:
16
+ self.sock.connect((host, port))
17
+ self.connected = True # 连接成功
18
+ except Exception:
19
+ self.connected = False
20
+
21
+ self.buffer = b""
22
+
23
+ # 析构时自动关闭
24
+ def __del__(self):
25
+ self.close()
26
+
27
+ # 安全关闭
28
+ def close(self):
29
+ self.connected = False
30
+ try:
31
+ self.sock.close()
32
+ except Exception:
33
+ pass
34
+
35
+ # 发送并接收(长连接,不抛异常)
36
+ # 如果连接断开,返回 None
37
+ def request(self, msg: bytes) -> Optional[bytes]:
38
+ # 已经断了
39
+ if not self.connected:
40
+ return None
41
+
42
+ # 1. 发送数据(try 保护)
43
+ try:
44
+ send_msg = Utils.escape(msg) + Utils.eoq()
45
+ self.sock.sendall(send_msg)
46
+ except Exception:
47
+ self.close()
48
+ return None
49
+
50
+ # 2. 循环接收数据(try 保护)
51
+ while (Utils.eoq() not in self.buffer) and self.connected:
52
+ try:
53
+ data = self.sock.recv(self.max_buffer)
54
+ if not data: # 服务器关闭连接
55
+ self.close()
56
+ return None
57
+ self.buffer += data
58
+ except Exception:
59
+ self.close()
60
+ return None
61
+
62
+ # 3. 解析消息
63
+ resp, self.buffer = self.buffer.split(Utils.eoq(), 1)
64
+ return Utils.anti_escape(resp)
@@ -0,0 +1,195 @@
1
+ from typing import Callable, Tuple
2
+ import socket
3
+ import time
4
+ from . import Utils
5
+
6
+ _BytesToBytes = Callable[[bytes], bytes]
7
+ _ClientAddress = Tuple[str, int]
8
+
9
+ class SimpleTcpServer:
10
+ def __init__(self,
11
+ host:str,
12
+ port:int,
13
+ worker_function:_BytesToBytes,
14
+ quit_token:bytes,
15
+ max_listen:int=5,
16
+ client_timeout:float=10.0) -> None:
17
+
18
+ self.host = host
19
+ self.port = port
20
+ self.worker_function = worker_function
21
+ self.quit_token = quit_token
22
+ self.max_listen = max_listen
23
+ self.client_timeout = client_timeout
24
+ self.max_buffer = 1024
25
+
26
+ # 系统运行状态
27
+ self.running = True
28
+
29
+ # 初始化 server_socket
30
+ self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
31
+ self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
32
+ self.server_socket.bind((self.host, self.port))
33
+ self.server_socket.listen(self.max_listen)
34
+ self.server_socket.setblocking(False) # 非阻塞模式
35
+
36
+ # 连接池
37
+ self.conn_pool:dict[_ClientAddress, socket.socket] = dict()
38
+ self.conn_buff:dict[_ClientAddress, bytes] = dict() # 缓冲区池子
39
+ self.last_seen:dict[_ClientAddress, float] = dict() # 记录最晚一次收到消息的时刻
40
+
41
+ # 断开一个现有连接
42
+ # 此连接必须存在
43
+ def _conn_close(self, addr:_ClientAddress):
44
+ self.conn_pool[addr].close()
45
+ del self.conn_pool[addr]
46
+ del self.conn_buff[addr]
47
+ del self.last_seen[addr]
48
+
49
+ # 处理一个用户入项连接
50
+ def _handle(self, conn:socket.socket, addr:_ClientAddress):
51
+
52
+ # 出现了客户端地址冲突
53
+ # 把旧的客户端强制切断
54
+ if self.conn_pool.get(addr) is not None:
55
+ self._conn_close(addr)
56
+
57
+ # 此时一定没有地址重复的问题, 可以将其加入到地址序列中
58
+ conn.setblocking(False)
59
+ self.conn_pool[addr] = conn # 套接字
60
+ self.conn_buff[addr] = b"" # 缓冲区
61
+ self.last_seen[addr] = time.time() # 初次连接视为消息
62
+
63
+ # 试图发现新的连接
64
+ def _try_accept(self):
65
+ try:
66
+ conn, addr = self.server_socket.accept()
67
+ self._handle(conn, addr)
68
+ except BlockingIOError: # 没有发现新的连接
69
+ pass
70
+
71
+ # 试图对一个连接获取数据
72
+ # 返回当前连接是不是真的死了
73
+ def _acquire_once(self, addr:_ClientAddress) -> bool:
74
+ conn = self.conn_pool[addr]
75
+ died = False # 假定还没死
76
+ try:
77
+ msg = conn.recv(self.max_buffer)
78
+ if msg == b"": # 读到一个空消息说明对方死掉了
79
+ died = True
80
+
81
+ # 追加非空消息
82
+ else:
83
+ self.conn_buff[addr] += msg
84
+ self.last_seen[addr] = time.time()
85
+
86
+ # 阻塞错误说明对方没写东西,但是还没死, 其他错误说明对方死了
87
+ except Exception as err:
88
+ died = not isinstance(err, BlockingIOError)
89
+ return died
90
+
91
+ # 统计 bytes 串出现次数
92
+ def _count_bytes(self, data: bytes, sub: bytes) -> int:
93
+ if not sub:
94
+ return 0
95
+ i = 0 # 初始化位置
96
+ count = 0
97
+ len_sub = len(sub)
98
+ while (i := data.find(sub, i)) != -1: # 不允许重叠
99
+ count += 1
100
+ i += len_sub
101
+ return count
102
+
103
+ # 计算单次请求结果并发送
104
+ # 如果对方死了返回 True
105
+ def _calc_and_resp(self, addr:_ClientAddress, msg_now:bytes) -> bool:
106
+ msg_get = Utils.anti_escape(msg_now)
107
+
108
+ # 为了避免外包程序报错,需要使用 try 保护
109
+ try:
110
+ ret_ans = self.worker_function(msg_get)
111
+ except Exception as err:
112
+ ret_ans = str(err).encode("utf-8")
113
+
114
+ send_ans = Utils.escape(ret_ans) # 避免数据中 eoq() 无法正常解读
115
+ died = False # 检查对方是否死了
116
+ try:
117
+ self.conn_pool[addr].sendall(send_ans + Utils.eoq())
118
+ self.last_seen[addr] = time.time() # 我们发消息也算对方的行为
119
+ except Exception:
120
+ died = True
121
+ return died # 正常 sendall 了说明对方没死
122
+
123
+ # 试图给一个客户端写回信
124
+ # 返回当前连接是不是真的死了
125
+ def _response_once(self, addr:_ClientAddress) -> bool:
126
+ buff = self.conn_buff[addr]
127
+
128
+ # 当前没有收到任何信息,无从判断这家伙死没死
129
+ if len(buff) == 0:
130
+ return False
131
+
132
+ # 不是协商消息,检查信息完整性
133
+ # 所有的消息以 $$ 作为结尾,消息内部保证没有连续的 $$
134
+ if self._count_bytes(buff, Utils.eoq()) == 0:
135
+ return False
136
+
137
+ # 处理当前消息
138
+ msg_now, self.conn_buff[addr] = buff.split(Utils.eoq(), maxsplit=1)
139
+ assert len(msg_now) != 0
140
+
141
+ # 检测到了退出标志
142
+ # 需要准备将服务器关停(但是不用马上关停也行)
143
+ if msg_now == self.quit_token:
144
+ self.running = False
145
+
146
+ # 计算单次请求结果并发送回去
147
+ # 如果对方死了返回 True
148
+ return self._calc_and_resp(addr, msg_now)
149
+
150
+ # 如果想让对方死,那就返回 True
151
+ # 否则就返回 False
152
+ def _chk_timeout(self, addr:_ClientAddress) -> bool:
153
+ if time.time() - self.last_seen[addr] >= self.client_timeout:
154
+ return True # 调用者会自动负责清理资源
155
+ return False
156
+
157
+ # 对所有客户端进行一次 worker 操作
158
+ # 如果客户端死了 worker 操作必须返回 True
159
+ def _chk_all_temp(self, worker:Callable[[_ClientAddress], bool]):
160
+ for addr in self._all_addr(): # 遍历所有连接
161
+ if worker(addr): # 试图对所有地址对象进行统一操作
162
+ self._conn_close(addr)
163
+
164
+ # 试图从每个池子里获取数据
165
+ # 同时清理池子里的死鱼
166
+ def _acquire_all(self):
167
+ self._chk_all_temp(self._acquire_once)
168
+
169
+ # 试图给所有可以回信的人
170
+ # 写回信,同时清理池子
171
+ def _response_all(self):
172
+ self._chk_all_temp(self._response_once)
173
+
174
+ # 获取当前所有客户端地址
175
+ def _all_addr(self) -> list[_ClientAddress]:
176
+ return list(self.conn_pool.keys())
177
+
178
+ # 回收资源
179
+ # 把所有人踢下线(回收缓冲区资源)
180
+ def _kick_all(self):
181
+ for addr in self._all_addr():
182
+ self._conn_close(addr)
183
+
184
+ # 把所有超时对象踢下线
185
+ def _kick_timeout(self):
186
+ self._chk_all_temp(self._chk_timeout)
187
+
188
+ # 启动服务器主循环(单线程)
189
+ def mainloop(self):
190
+ while self.running:
191
+ self._try_accept() # 试图发现新的链接
192
+ self._acquire_all() # 试图从现有池子中获取数据(我们假定池子里都是活的)
193
+ self._response_all() # 试图给所有人写回信
194
+ self._kick_timeout() # 把所有长时间不吱声的人给踢下线
195
+ self._kick_all()