{ "cells": [ { "cell_type": "markdown", "id": "5ad5c77d", "metadata": {}, "source": [ "# Таймауты и обработка сетевых ошибок #" ] }, { "cell_type": "markdown", "id": "ab4a8b58", "metadata": {}, "source": [ "Итак, на этой лекции мы поговорим про обработку сетевых ошибок, а также обсудим таймауты для сокетов.\n", "\n", "Как правило, все обучающие примеры для работы с сетью выглядят очень просто. На самом деле, как дело доходит до настоящих программ, которые работают в нашей жизни, они все выглядят гораздо сложнее. Связано это прежде всего с обработкой ошибок. Как правило, сеть может работать стабильно не всегда. В ней могут появляться задержки, могут не доходить пакеты, а также могут быть разрывы соединений. Поэтому при написании ваших сетевых программ необходимо быть к этому готовыми. Необходимо работать с сокетами правильно. Прежде всего нужно задавать таймауты при сетевых операциях, а также очень грамотно обрабатывать сетевые ошибки. Давайте рассмотрим обучающий пример, в котором создается тот же самый знакомый нам код сервера." ] }, { "cell_type": "code", "execution_count": null, "id": "46db3230", "metadata": {}, "outputs": [], "source": [ "# создание сокета, таймауты и обработка ошибок\n", "# сервер\n", "import socket\n", "\n", "with socket.socket() as sock:\n", " sock.bind((\"\", 10001))\n", " sock.listen()\n", " \n", " while True:\n", " conn, addr = sock.accept()\n", " conn.settimeout(5) # timeout := None|0|gt 0\n", " with conn:\n", " while True:\n", " try:\n", " data = conn.recv(1024)\n", " \n", " except socket.timeout:\n", " print(\"close connection by timeout\")\n", " break\n", " \n", " if not data:\n", " break\n", " \n", " print(data.decode(\"utf8\"))" ] }, { "cell_type": "markdown", "id": "6fdcd045", "metadata": {}, "source": [ "Мы создаем сокет при помощи контекстного менеджера. Вызываем методы `bind` и `listen`. Затем в бесконечном цикле вызываем метод `accept`. И слушаем наш сокет, получаем новое соединение. После того, как мы получили это соединение, мы вызываем метод `settimeout` для нашего объекта. И передаем туда значение \"5\". По умолчанию все вызовы для этого объекта соединения, например вызов `recv`, или вызов `send`, они будут заблокированы до тех пор, пока данные на другой стороне кто-то не сможет прочитать или записать. По умолчанию таймаута нет. Мы можем передать значение `None` и это как раз будет значением по умолчанию. Если мы передадим `timeout = 0`, это будет означать немного другое. Это переведет наш сокет в неблокирующий режим. Про неблокирующий режим мы будем говорить чуть позднее. Также можно задать свой таймаут. \n", "\n", "Итак, если мы задали таймаут и вызвали метод `recv` у нашего сокета, и в этот сокет не поступило данных в течении пяти секунд, то будет сгенерировано исключение `socket.timeout`. Его можно перехватить и обработать. Как обрабатывать, вам нужно решать самим. Можно, например, закрывать соединение. Можно, например, если мы пишем в это соединение, повторно отправлять туда данные, или делать `retry`. Все зависит от требований к вашей программе. Но если вы работаете с таймаутами, вам нужно быть готовым к этому как на стороне сервера, так и на стороне клиента. \n", "\n", "Итак, в данном случае мы просто закрываем соединение. Метод `close` будет вызван автоматически, когда будет выход из нашего контекстного менеджера.\n", "\n", "Рассмотрим код на клиенте." ] }, { "cell_type": "code", "execution_count": null, "id": "e06bb19a", "metadata": {}, "outputs": [], "source": [ "# создание сокета, таймауты и обработка ошибок\n", "# клиент\n", "import socket\n", "\n", "with socket.create_connection((\"127.0.0.1\", 10001), 5) as sock:\n", " # set socket read timeout\n", " sock.settimeout(2)\n", " \n", " try:\n", " sock.sendall(\"ping\".encode(\"utf8\"))\n", " \n", " except socket.timeout:\n", " print(\"send data timeout\")\n", " \n", " except socket.error as ex:\n", " print(\"send data error:\", ex)" ] }, { "cell_type": "markdown", "id": "80927eea", "metadata": {}, "source": [ "На клиенте существует так называемый `connect timeout` и `socket read timeout`. Их еще называют именно так. `connect timeout` мы задаем в методе `create_connection` и этот таймаут будет распространяться только на установку соединения с нашим сервером. То есть если в течении 5 секунд наш сервер не смог подключить соединение, и наше соединение не было установлено, то возникнет исключение `socket.timeout` и нам на стороне клиента нужно будет его обработать. То есть сделать переподключение, подождать некоторое время и попробовать создать это соединение снова. В данном случае эти таймауты могут немножко различаться. Так как, например, на установку соединения может требоваться немного больше времени, поэтому этот таймаут может быть задан большим. После того, как соединение установлено, мы можем задать таймаут на все операции с нашим сокетом. Например, если мы не смогли записать данные, или прочитать данные с сокета, то сделать соответствующую обработку. Также может возникнуть любое другое исключение и его точно так же нужно будет обработать. Базовый класс для обработки исключений модуля `socket` - это `socket.error`. Давайте попробуем запустить наш пример и посмотрим, как работают таймауты в консоли. Для этого нам потребуется код нашего сервера. Итак, в нем мы создаем сокет и `accept`'им новое соединение. Также обрабатываем чтение из новых соединений с таймаутом. Запускаем наш пример при помощи команды python3." ] }, { "cell_type": "code", "execution_count": null, "id": "29bf11f2", "metadata": {}, "outputs": [], "source": [ "! python ex1.py" ] }, { "cell_type": "markdown", "id": "d46e4532", "metadata": {}, "source": [ "И теперь перейдем к клиентской части. Запускаем консоль. Интерпретатор python3. Давайте попробуем подключиться к нашему серверу. Для этого импортируем модуль `socket`, создаем объект сокета при помощи метода `create_connection`. Указываем адресную пару. Порт 10001, который мы указали на стороне сервера. Все, наше соединение готово." ] }, { "cell_type": "code", "execution_count": 1, "id": "5a1a72aa", "metadata": {}, "outputs": [], "source": [ "import socket\n", "\n", "sock = socket.create_connection((\"127.0.0.1\", 10001))" ] }, { "cell_type": "markdown", "id": "47e6bd08", "metadata": {}, "source": [ "Давайте взглянем на сервер. Сервер уже получил `socket.timeout` и закрыл наше соединение. Давайте попробуем записать данные. Помним, что это должны быть байты." ] }, { "cell_type": "code", "execution_count": 2, "id": "c0369597", "metadata": {}, "outputs": [], "source": [ "sock.sendall(\"Привет\".encode(\"utf-8\"))" ] }, { "cell_type": "markdown", "id": "4d57a6de", "metadata": {}, "source": [ "На первый взгляд вроде мы записали. Но если попробуем еще раз, то получим исключение `BrokenPipeError`." ] }, { "cell_type": "code", "execution_count": 3, "id": "41223ae7", "metadata": { "scrolled": false }, "outputs": [ { "ename": "BrokenPipeError", "evalue": "[Errno 32] Broken pipe", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mBrokenPipeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0msock\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msendall\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"Привет\"\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mencode\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"utf-8\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mBrokenPipeError\u001b[0m: [Errno 32] Broken pipe" ] } ], "source": [ "sock.sendall(\"Привет\".encode(\"utf-8\"))" ] }, { "cell_type": "markdown", "id": "6685acd6", "metadata": {}, "source": [ "Это исключение является подклассом исключений `socket.error`. Давайте проверим это. Для этого можно воспользоваться функцией `issubclass`." ] }, { "cell_type": "code", "execution_count": 4, "id": "1d8cdd3b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "issubclass(BrokenPipeError, socket.error)" ] }, { "cell_type": "markdown", "id": "c4b0b5a2", "metadata": {}, "source": [ "Да, это действительно так. Итак, для того, чтобы отправить данные, нам необходимо повторно переподключиться к нашему серверу и успеть в течении пяти секунд это сделать." ] }, { "cell_type": "code", "execution_count": 5, "id": "ad4898d0", "metadata": {}, "outputs": [], "source": [ "sock = socket.create_connection((\"127.0.0.1\", 10001))\n", "sock.sendall(\"Привет\".encode(\"utf-8\"))" ] }, { "cell_type": "markdown", "id": "3d9d884d", "metadata": {}, "source": [ "Итак, на этой лекции мы обсудили обработку сетевых ошибок, поговорили о том, как правильно их обрабатывать. Как работать с таймаутами. Все настоящие сетевые программы должны это делать. Обязательно нужно быть готовым, что сеть может работать нестабильно, и обрабатывать правильно таймауты и сетевые ошибки.\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 }