-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.py
More file actions
290 lines (235 loc) · 13.9 KB
/
server.py
File metadata and controls
290 lines (235 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
"""
TCP-сервер для обработки PING-запросов от клиентов.
Сервер реализует протокол:
1. Принимает сообщения в формате "[номер] PING\\n" от клиентов
2. С 10% вероятностью игнорирует запрос
3. В остальных случаях отвечает "[номер_ответа/номер_запроса] PONG (ID_клиента)\\n"
4. Каждые 5 секунд отправляет всем keepalive: "[номер] keepalive\\n"
5. Ведет лог в формате CSV: дата;время_получения;запрос;время_отправки;ответ
┌─────────────────────────────────────────────────────┐
│ EVENT LOOP │
│ (одна программа, один процесс, один главный цикл) │
├─────────────────────────────────────────────────────┤
│ ЗАДАЧА 1: server.serve_forever() │
│ ▼ │
│ • Жду нового клиента... │
│ • Клиент подключился! │
│ • Запускаю handle_client() для него НОВАЯ ЗАДАЧА│
│ • Возвращаюсь ждать следующего клиента │
│ │
│ ЗАДАЧА 2: keepalive() │
│ ▼ │
│ • Жду 5 секунд... (await asyncio.sleep(5)) │
│ • Прошло 5 секунд! │
│ • Отправляю keepalive всем клиентам │
│ • Возвращаюсь ждать еще 5 секунд │
│ │
│ ЗАДАЧА 3: handle_client() для Клиента 1 │
│ ▼ │
│ • Жду сообщение от клиента... │
│ • Получил "[0] PING" │
│ • Обрабатываю... │
│ • Отправляю ответ │
│ • Возвращаюсь ждать следующее сообщение │
│ │
│ ЗАДАЧА 4: handle_client() для Клиента 2 │
│ ▼ │
│ • Тоже жду сообщение... │
│ • И т.д. │
└─────────────────────────────────────────────────────┘
"""
import asyncio
import random
import datetime
from typing import Dict, Optional
class Server:
"""TCP-сервер для обработки PING/PONG сообщений."""
def __init__(self) -> None:
"""
Инициализирует TCP-сервер.
Атрибуты:
response_counter: int - сквозная нумерация всех ответов сервера
clients: Dict[asyncio.StreamWriter, int] - словарь подключений: writer -> client_id
next_client_id: int - следующий доступный ID для нового клиента
"""
self.response_counter: int = 0 # Сквозная нумерация всех ответов
self.clients: Dict[asyncio.StreamWriter, int] = (
{}
) # writer -> client_id
self.next_client_id: int = 1 # ID следующего клиента
async def handle_client(
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
) -> None:
"""
Обрабатывает подключение одного клиента.
Эта корутина запускается для каждого нового клиента и:
1. Регистрирует клиента с уникальным ID
2. Читает сообщения от клиента построчно
3. Обрабатывает PING запросы
4. Отправляет PONG ответы
5. Корректно закрывает соединение при отключении
Args:
reader: asyncio.StreamReader - поток для чтения данных от клиента
writer: asyncio.StreamWriter - поток для отправки данных клиенту
Процесс работы:
КЛИЕНТ -> СЕРВЕР: "[0] PING\\n"
СЕРВЕР -> КЛИЕНТ: "[0/0] PONG (1)\\n" (после задержки 100-1000мс)
"""
client_id: int = (
self.next_client_id
) # хитрая система увеличения id клиента
# зафиксировали в словаре
self.clients[writer] = client_id
self.next_client_id += 1 # и вот он стал на единицу больше
print(f"Клиент {client_id} подключился")
try:
while True:
# Чтение сообщения от клиента (ждет до символа \\n -
# это и есть в аски таблице байт 0x0a перевода на новую строку LF)
data: bytes = await reader.readline()
if not data: # Клиент отключился
break
# Декодируем Убираем пробелы и \\n
message: str = data.decode().strip()
# Время получения
receive_time: datetime.datetime = datetime.datetime.now()
# 10% шанс игнорировать запрос
if random.random() < 0.1:
self.log_ignored(message, receive_time)
continue # сброс и новая итерация цикла
# Имитация обработки: задержка 100-1000 мс
await asyncio.sleep(random.uniform(0.1, 1.0))
# Извлекаем номер запроса, т.е. цифру 0 из: "[0] PING" -> 0
req_num: int = int(
message.split('[')[1].split(']')[0]
) # жоское место, последовательно разрезаем по ключевым символам
response: str = (
f"[{self.response_counter}/{req_num}] PONG ({client_id})\n"
)
send_time: datetime.datetime = datetime.datetime.now()
# Отправка ответа клиенту
writer.write(response.encode(encoding="utf-8"))
await writer.drain()
# Логирование успешной обработки
self.log_message(
message, receive_time, response.strip(), send_time
)
self.response_counter += 1
except Exception:
# Любая ошибка = разрыв соединения
pass
finally:
# Очистка ресурсов при отключении клиента
del self.clients[writer]
writer.close()
def log_ignored(
self, message: str, receive_time: datetime.datetime
) -> None:
"""
Логирует игнорированные сообщения в формате CSV.
Формат записи:
ГГГГ-ММ-ДД;ЧЧ:ММ:СС.ммм;запрос;(проигнорировано)
Пример:
2024-01-15;14:30:25.123;[0] PING;(проигнорировано)
Args:
message: str - текст запроса от клиента (например, "[0] PING")
receive_time: datetime.datetime - время получения запроса
"""
date_str: str = datetime.datetime.now().strftime('%Y-%m-%d')
time_str: str = receive_time.strftime('%H:%M:%S.%f')[:-3]
with open('server.log', 'a', encoding='UTF-8') as f:
f.write(f"{date_str};{time_str};{message};(проигнорировано)\n")
def log_message(
self,
message: str,
receive_time: datetime.datetime,
response: str,
send_time: datetime.datetime,
) -> None:
"""
Логирует успешно обработанные сообщения в формате CSV.
Формат записи:
ГГГГ-ММ-ДД;ЧЧ:ММ:СС.ммм_получения;запрос;ЧЧ:ММ:СС.ммм_отправки;ответ
Пример:
2024-01-15;14:30:25.123;[0] PING;14:30:25.567;[0/0] PONG (1)
Args:
message: str - текст запроса от клиента
receive_time: datetime.datetime - время получения запроса
response: str - текст ответа сервера
send_time: datetime.datetime - время отправки ответа
"""
date_str: str = datetime.datetime.now().strftime('%Y-%m-%d')
recv_str: str = receive_time.strftime('%H:%M:%S.%f')[:-3]
send_str: str = send_time.strftime('%H:%M:%S.%f')[:-3]
with open('server.log', 'a', encoding='UTF-8') as f:
f.write(f"{date_str};{recv_str};{message};{send_str};{response}\n")
async def keepalive(self) -> None:
"""
Периодическая отправка keepalive сообщений всем подключенным клиентам.
Работает в бесконечном цикле:
1. Ждет 5 секунд
2. Формирует keepalive сообщение со сквозным номером
3. Отправляет всем подключенным клиентам
4. Увеличивает счетчик ответов
Формат keepalive:
[номер] keepalive\\n
Пример:
[5] keepalive\\n
"""
while True:
await asyncio.sleep(5)
# Формируем keepalive сообщение
keepalive_msg: str = f"[{self.response_counter}] keepalive\n"
# Отправляем всем подключенным клиентам (список из ключей словаря)
for writer in list(self.clients.keys()):
try:
writer.write(keepalive_msg.encode(encoding="utf-8"))
await writer.drain()
except:
# Клиент отключился, продолжаем с остальными, т.е. поглотили исключение
pass
self.response_counter += 1
async def start(self) -> None:
"""
Запускает TCP-сервер и начинает принимать подключения.
Процесс запуска:
1. Создает TCP-сервер на 127.0.0.1:8888
2. Запускает фоновую задачу keepalive
3. Начинает принимать подключения клиентов
4. Для каждого клиента запускает handle_client() в отдельной корутине
5. Работает до принудительной остановки (Ctrl+C)
Использует asyncio.start_server() для создания асинхронного TCP-сервера.
"""
# Создание TCP-сервера
# (первый аргумент - функция обратного вызова, переменная без вызова сразу)
server: asyncio.Server = await asyncio.start_server(
self.handle_client, '127.0.0.1', 8888
)
# Запуск фоновой задачи keepalive
asyncio.create_task(self.keepalive())
# Запуск основного цикла сервера
async with server:
print("Сервер запущен на порту 8888")
await server.serve_forever()
if __name__ == "__main__":
"""
Точка входа для запуска сервера напрямую.
При запуске скрипта напрямую:
1. Очищается лог-файл server.log
2. Создается экземпляр Server
3. Запускается асинхронный цикл с server.start()
4. Обрабатывается Ctrl+C для корректного завершения
"""
# Очищаем лог файл при каждом запуске
open('server.log', 'w').close()
try:
server: Server = Server()
asyncio.run(server.start())
except KeyboardInterrupt:
print("\nСервер остановлен")
# Если у нас одна коробка 11,5 руб, а коробок 1000, то мы бы получили 11500 руб.
# а так мы получим после дождя 10960 руб.
# Разница 540 руб.
------------------------
10768 руб получили после дождя, а хотели бы получить 11500
купили за 10000 руб. итого прибыл 768 руб.