You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

235 lines
11 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{
"cells": [
{
"cell_type": "markdown",
"id": "cb9b2246",
"metadata": {},
"source": [
"# Сервер для приема метрик #"
]
},
{
"cell_type": "markdown",
"id": "db5acdef",
"metadata": {},
"source": [
"В предыдущем блоке вы разработали клиентское сетевое приложение — клиента для сервера метрик, который умеет отправлять и получать всевозможные метрики. Пришло время финального задания — нужно реализовать серверную часть самостоятельно.\n",
"\n",
"Как обычно вам необходимо разработать программу в одном файле-модуле. Сервер должен соответствовать протоколу, который был описан в задании к предыдущему блоку. Он должен уметь принимать от клиентов команды `put` и `get`, разбирать их, и формировать ответ согласно протоколу. По запросу `put` требуется сохранять метрики в структурах данных в памяти процесса. По запросу `get` сервер обязан отдавать данные в правильной последовательности.\n",
"\n",
"На верхнем уровне вашего модуля должна быть объявлена функция `run_server(host, port)` — она принимает адрес и порт, на которых должен быть запущен сервер.\n",
"\n",
"Для проверки правильности решения мы воспользуемся своей реализацией клиента и будем отправлять на ваш сервер `put` и `get` запросы, ожидая в ответ правильные данные от сервера (согласно объявленному протоколу). Все запросы будут выполняться с таймаутом — сервер должен отвечать за приемлемое время.\n",
"\n",
"Сервер должен быть готов к неправильным командам со стороны клиента и отдавать клиенту ошибку в формате, оговоренном в протоколе. В таком случае работа сервера не должна завершаться аварийно.\n",
"\n",
"В последнем блоке мы с вами разбирали пример tcp-сервера на `asyncio`:\n",
"\n",
"```python\n",
"import asyncio\n",
"\n",
"\n",
"class ClientServerProtocol(asyncio.Protocol):\n",
" def connection_made(self, transport):\n",
" self.transport = transport\n",
"\n",
" def data_received(self, data):\n",
" resp = process_data(data.decode())\n",
" self.transport.write(resp.encode())\n",
"\n",
"\n",
"loop = asyncio.get_event_loop()\n",
"coro = loop.create_server(\n",
" ClientServerProtocol,\n",
" \"127.0.0.1\", 8181\n",
")\n",
"\n",
"server = loop.run_until_complete(coro)\n",
"\n",
"try:\n",
" loop.run_forever()\n",
" \n",
"except KeyboardInterrupt:\n",
" pass\n",
"\n",
"server.close()\n",
"\n",
"loop.run_until_complete(server.wait_closed())\n",
"loop.close()\n",
"```\n",
"\n",
"Данный код создает tcp-соединение для адрса `127.0.0.1:8181` и слушает все входящие запросы. При подключении клиента будет создан новый экземпляр класса `ClientServerProtocol`, а при поступлении новых данных вызовется метод этого объекта - `data_received`. Внутри `asyncio.Protocol` спрятана вся магия обработки запросов через корутины, остается реализовать протокол взаимодействия между клиентом и сервером.\n",
"\n",
"Этот код может использоваться как основа для реализации сервера. Это не обязательное требование. Для реализации задачи вы можете использовать любые вызовы из стандартной библиотеки Python 3. Сервер должен обрабатывать запросы от нескольких клиентов одновременно.\n",
"\n",
"В процессе разработки сервера для тестирования работоспособности вы можете использовать клиент, написанный в предыдущем блоке.\n",
"\n",
"Давайте еще раз посмотрим на текстовый протокол в действии при использовании утилиты `telnet`:\n",
"\n",
"```\n",
"$: telnet 127.0.0.1 8888\n",
"Trying 127.0.0.1...\n",
"Connected to localhost.\n",
"Escape character is '^]'.\n",
"> get test_key\n",
"< ok\n",
"< \n",
"> got test_key\n",
"< error\n",
"< wrong command\n",
"< \n",
"> put test_key 12.0 1503319740\n",
"< ok\n",
"< \n",
"> put test_key 13.0 1503319739\n",
"< ok\n",
"< \n",
"> get test_key \n",
"< ok\n",
"< test_key 13.0 1503319739\n",
"< test_key 12.0 1503319740\n",
"< \n",
"> put another_key 10 1503319739\n",
"< ok\n",
"< \n",
"> get *\n",
"< ok\n",
"< test_key 13.0 1503319739\n",
"< test_key 12.0 1503319740\n",
"< another_key 10.0 1503319739\n",
"< \n",
"```\n",
"\n",
"Также вы можете воспользоваться вспомогательным скриптом, который использует реализацию клиента в пятом блоке, для локального тестирования написанного вами сервера:\n",
"\n",
"```python\n",
"\"\"\"\n",
"Это вспомогательный скрипт для тестирования сервера из задания в 6 блоке.\n",
"\n",
"Для запуска скрипта на локальном компьютере разместите рядом файл client.py,\n",
"где содержится код клиента, который реализован в задание блока 5.\n",
"\n",
"Сначала запускаете ваш сервер на адресе 127.0.0.1 и порту 8888, а затем \n",
"запускаете этот скрипт.\n",
"\n",
"\"\"\"\n",
"\n",
"import sys\n",
"\n",
"from client import Client, ClientProtocolError, ClientSocketError\n",
"\n",
"\n",
"def run(host, port):\n",
"\n",
" client1 = Client(host, port, timeout=5)\n",
" client2 = Client(host, port, timeout=5)\n",
"\n",
" try:\n",
" client1.send(\"malformed command test\\n\")\n",
" client2.send(\"malformed command test\\n\")\n",
"\n",
" except ClientSocketError as err:\n",
" print(f\"Ошибка общения с сервером: {err.__class__}: {err}\")\n",
" sys.exit(1)\n",
"\n",
" except ClientProtocolError:\n",
" pass\n",
"\n",
" else:\n",
" print(\n",
" \"Неверная команда, отправленная серверу, должна возвращать ошибку протокола\"\n",
" )\n",
" sys.exit(1)\n",
"\n",
" try:\n",
" client1.put(\"k1\", 0.25, timestamp=1)\n",
" client2.put(\"k1\", 2.156, timestamp=2)\n",
" client1.put(\"k1\", 0.35, timestamp=3)\n",
" client2.put(\"k2\", 30, timestamp=4)\n",
" client1.put(\"k2\", 40, timestamp=5)\n",
" client1.put(\"k2\", 40, timestamp=5)\n",
"\n",
" except Exception as err:\n",
" print(f\"Ошибка вызова client.put(...) {err.__class__}: {err}\")\n",
" sys.exit(1)\n",
"\n",
" expected_metrics = {\n",
" \"k1\": [(1, 0.25), (2, 2.156), (3, 0.35)],\n",
" \"k2\": [(4, 30.0), (5, 40.0)],\n",
" }\n",
"\n",
" try:\n",
" metrics = client1.get(\"*\")\n",
" if metrics != expected_metrics:\n",
" print(\n",
" f\"client.get('*') вернул неверный результат. Ожидается: {expected_metrics}. Получено: {metrics}\"\n",
" )\n",
" sys.exit(1)\n",
"\n",
" except Exception as err:\n",
" print(f\"Ошибка вызова client.get('*') {err.__class__}: {err}\")\n",
" sys.exit(1)\n",
"\n",
" expected_metrics = {\"k2\": [(4, 30.0), (5, 40.0)]}\n",
"\n",
" try:\n",
" metrics = client2.get(\"k2\")\n",
" if metrics != expected_metrics:\n",
" print(\n",
" f\"client.get('k2') вернул неверный результат. Ожидается: {expected_metrics}. Получено: {metrics}\"\n",
" )\n",
" sys.exit(1)\n",
"\n",
" except Exception as err:\n",
" print(f\"Ошибка вызова client.get('k2') {err.__class__}: {err}\")\n",
" sys.exit(1)\n",
"\n",
" try:\n",
" result = client1.get(\"k3\")\n",
" if result != {}:\n",
" print(\n",
" f\"Ошибка вызова метода get с ключом, который еще не был добавлен. Ожидается: пустой словарь. Получено: {result}\"\n",
" )\n",
" sys.exit(1)\n",
"\n",
" except Exception as err:\n",
" print(\n",
" f\"Ошибка вызова метода get с ключом, который еще не был добавлен: {err.__class__} {err}\"\n",
" )\n",
" sys.exit(1)\n",
"\n",
" print(\"Похоже, что все верно!\")\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" run(\"127.0.0.1\", 8888)\n",
"\n",
"```\n",
"\n",
"Успехов в разработке!"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.8"
}
},
"nbformat": 4,
"nbformat_minor": 5
}