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.

271 lines
21 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": "68b87897",
"metadata": {},
"source": [
"# Сокеты, клиент-сервер #"
]
},
{
"cell_type": "markdown",
"id": "56b06ed6",
"metadata": {},
"source": [
"В предыдущих лекциях мы рассмотрели то, как устроены процессы и потоки в Python. В следующих лекциях мы будем изучать то, как устроены сокеты и как работают сетевые программы, и те знания, которые мы приобрели в предыдущих лекциях, понадобятся нам для организации взаимодействия по сети.\n",
"\n",
"Итак, давайте разберем, что такое сокеты, как они устроены и попробуем написать свою первую программу клиент-сервер, которая будет обмениваться данными между собой.\n",
"\n",
"Сокеты — это, прежде всего, кросс-платформенный механизм для обмена данными между отдельными процессами. Эти процессы могут работать на разных серверах, они могут быть написаны на разных языках, и, прежде всего, программа на Python, которая использует механизм сокетов, она осуществляет системные вызовы и взаимодействие с ядром операционной системы.\n",
"\n",
"Как правило, для организации сетевого взаимодействия нужен сервер, который изначально создает некое соединение и начинает «слушать» все запросы, которые поступают в него и программа-клиент, которая присоединяется к серверу и отправляет ему нужные данные. Давайте рассмотрим пример серверной программы."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fbc55f45",
"metadata": {},
"outputs": [],
"source": [
"# создание сокета, сервер\n",
"import socket\n",
"\n",
"# https://docs.python.org/3/library/socket.html\n",
"sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
"sock.bind((\"127.0.0.1\", 10001)) # max port 65535\n",
"sock.listen(socket.SOMAXCONN)\n",
"\n",
"conn, addr = sock.accept()\n",
"\n",
"while True:\n",
" data = conn.recv(1024)\n",
" \n",
" if not data:\n",
" break\n",
" \n",
" # process data\n",
" print(data.decode(\"utf8\"))\n",
" \n",
"conn.close()\n",
"sock.close()"
]
},
{
"cell_type": "markdown",
"id": "9a9c078e",
"metadata": {},
"source": [
"Для того чтобы создать сокет, мы должны импортировать модуль `socket`. Далее мы должны создать объект типа `socket` из модуля `socket`. В него необходимо передать некоторые параметры. В данном случае это некоторое семейство — мы используем `address family`, мы используем конкретную константу `AF_INET` (`IPv4`), а также тип сокета. В данном примере мы используем потоковый сокет `SOCK_STREAM` (`TCP`).\n",
"\n",
"Полную информацию по типам сокетов, по типам `address family` можно посмотреть в документации на Python, либо в документации про то, как устроена сеть в операционной системе Linux."
]
},
{
"cell_type": "markdown",
"id": "b16a58fa",
"metadata": {},
"source": [
"Итак, мы создали объект `socket` — это потоковый сокет. Далее мы должны вызвать метод `bind`. В метод `bind` мы должны передать некую адресную пару — это `host`, в данном случае мы передаем `127.0.0.1`, и порт `127.0.0.1` будет означать, что наш сервер будет слушать все входящие соединения только локально на одной машине. Если мы укажем пустую строчку, либо адрес `0.0.0.0`, то наш сервер будет слушать входящие соединения со всех интерфейсов. Порт — это некая целочисленная константа, существуют некоторые зарезервированные порты, например, 80-й порт, обычно на нем работает HTTP-сервер, 43-й порт, 443-й порт. Как правило, порты с номерами до 2,000 являются системными, и мы должны использовать адреса больше значений 2,000, но максимальное значение для порта — это 65,535 (2 байта). Итак, системный вызов `bind` зарегистрировал нашу адресную пару в операционной системе. Двигаемся дальше.\n",
"\n",
"Далее, для того чтобы начать принимать соединения, мы должны вызвать метод `listen`. У метода `listen` есть необязательный параметр — это так называемый `backlog`, или размер очереди входящих соединений, которые еще не обработаны, для которых не был вызван метод `accept`. Если наш сервер будет не успевать принимать входящие соединения, то все эти соединения будут копиться в этой очереди, и если она превысит это максимальное значение, то операционная система выдаст ошибку `ConnectionRefused` для клиентской программы. Двигаемся дальше.\n",
"\n",
"Мы создали сокет, зарегистрировали адресную пару, вызвали метод `listen`. Далее мы должны вызвать метод `accept`, для того чтобы начать принимать входящее клиентское соединение. Системный вызов `accept` по умолчанию заблокируется, до тех пор, пока не появится клиентское соединение. Итак, если клиент вызовет метод `connect`, то наш метод `accept` вернет нам объект, который будет являться полнодуплексным каналом. У этого объекта будут доступны методы записи в этот канал и методы чтения. В нашем примере мы в бесконечном цикле будем вызывать чтение из нашего полнодуплексного канала. Если мы ничего не прочитали, это будет означать, что клиент закрыл соединение и нам необходимо тоже прекратить работу. В качестве обработки наших данных, которые мы прочитали с канала, мы просто выводим эти данные в консоль.\n",
"\n",
"После того как мы закончили работу с нашим клиентом, мы вызываем метод `close` для нашего объекта, который представляет собой полнодуплексный канал, и также закрываем сокет, который слушает новые соединения со стороны клиента. Давайте рассмотрим код на стороне клиента."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b93b286f",
"metadata": {},
"outputs": [],
"source": [
"# создание сокета, клиент\n",
"import socket\n",
"\n",
"sock = socket.socket()\n",
"sock.connect((\"127.0.0.1\", 10001))\n",
"sock.sendall(\"ping\".encode(\"utf8\"))\n",
"sock.close()\n",
"\n",
"# более короткая запись\n",
"sock = socket.create_connection((\"127.0.0.1\", 10001))\n",
"sock.sendall(\"ping\".encode(\"utf8\"))\n",
"sock.close()"
]
},
{
"cell_type": "markdown",
"id": "d3810db6",
"metadata": {},
"source": [
"Для того чтобы установить соединение с сервером, мы должны создать объект типа `socket.socket`. По умолчанию создается потоковый сокет с семейством `address family AF_INET`. После этого мы должны вызвать метод `connect`. `Connect` заблокируется до тех пор, пока сервер со своей стороны не вызовет метод `accept`. После того как системный вызов `connect` отработал, наш сокет готов к работе, и для него можно вызывать методы `send`, `sendall` или `recv`, для того чтобы получать данные с сервера. То есть, по сути, мы получили такой же полнодуплексный канал, с которым можно работать, отправлять и получать данные. После того как мы завершили работу с нашим клиентским сокетом, необходимо вызвать метод `close`.\n",
"\n",
"В Python существует более короткая запись для создания клиентского сокета — это вызов метода модуля `socket create_connection`. В `create_connection` мы передаем адресную пару, необязательный `timeout`. Про `timeout` мы еще с вами будем говорить в следующих лекциях. Этот вызов возвращает нам проключенное соединение, готовое для того, чтобы делать отправку или прием данных. \n",
"\n",
"Давайте попробуем запустить наш код и посмотрим, как он работает на самом деле. Нам потребуется код нашего сервера. Давайте запустим его при помощи команды python3."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "727da51b",
"metadata": {},
"outputs": [],
"source": [
"! python server.py"
]
},
{
"cell_type": "markdown",
"id": "953309cd",
"metadata": {},
"source": [
"Итак, наш сервер создал сокет и готов принимать новые соединения. Давайте попробуем присоединиться к нашему серверу. Импортируем модуль `socket`, создаем новое соединение при помощи `socket.create_connection`. Передаем адресную пару в виде хоста и порт — 10001. Итак, наш сокет готов со стороны клиента, для того чтобы отправлять и принимать данные. Давайте попробуем отправить данные. Хочу обратить ваше внимание, что при работе с данными по сети мы вынуждены отправлять именно байты, а не строки. Поэтому мы передаем байты. Пусть это будет строка `ping`."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "9321a4d3",
"metadata": {},
"outputs": [],
"source": [
"import socket\n",
"\n",
"sock = socket.create_connection((\"127.0.0.1\", 10001))\n",
"sock.sendall(\"ping\".encode(\"utf8\"))"
]
},
{
"cell_type": "markdown",
"id": "374ba711",
"metadata": {},
"source": [
"Давайте посмотрим на сервер — сервер получил эти данные и вывел нашу переданную строчку `ping`. Давайте попробуем закрыть соединение со стороны клиента. Для этого нам нужно вызвать метод `close`."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "1923afea",
"metadata": {},
"outputs": [],
"source": [
"sock.close()"
]
},
{
"cell_type": "markdown",
"id": "ac06e974",
"metadata": {},
"source": [
"Итак, мы закрыли соединение со стороны клиента. Наш сервер прекратил цикл `while` и закрыл соединение, и наша программа со стороны сервера завершила свою работу. Как я уже говорил, `socket` — это кроссплатформенный механизм и необязательно программа-клиент и сервер должны быть написаны на одном и том же языке.\n",
"\n",
"Давайте посмотрим, как наш пример будет работать, если мы воспользуемся вместо клиента программой `telnet`. Точно так же укажем адрес и порт, куда мы собираемся отправлять наши данные."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "ddf2e8cb",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Trying 127.0.0.1...\n",
"Connected to 127.0.0.1.\n",
"Escape character is '^]'.\n",
"^C\n",
"Connection closed by foreign host.\n"
]
}
],
"source": [
"! telnet 127.0.0.1 10001"
]
},
{
"cell_type": "markdown",
"id": "62f8277b",
"metadata": {},
"source": [
"Итак, `telnet` выполнил подключение, написал нам, что он приконнектился. Давайте попробуем отправить данные. Опять на стороне сервера мы видим, что данные получены Давайте попробуем закрыть соединение. Итак, мы видим, что наша серверная программа завершила свою работу. Ещё раз хочу обратить ваше внимание, что сокет — это кроссплатформенный механизм, который можно использовать на различных языках, в том числе и на Python. Хочется ещё обратить и остановить ваше внимание на некоторых ключевых моментах в нашей серверной и клиентской программе. В частности, это вызовы методов `close` для наших объектов, для соединения, на котором мы акцептим новые соединения клиентов, а также для объекта `connection`, который является полнодуплексным каналом. Наш код носит обучающий характер, и в нём нет обработки ошибок. Как правило, сетевые программы настоящие, они выглядят более сложно, в них, естественно обрабатываются ошибки, и одним из требований к сетевым программам является то, что необходимо правильно и грамотно завершать работу с созданными `connect`'ами и со всеми открытыми сокетами, тем самым контролируя ресурсы процесса, в котором всё это работает.\n",
"\n",
"В Python существует более удобный механизм для работы с сокетами в виде контекстных менеджеров. Давайте рассмотрим пример, в котором мы выполняем те же самые задачи, но используем контекстный менеджер."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "78a642ef",
"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",
" with conn:\n",
" while True:\n",
" data = conn.recv(1024)\n",
"\n",
" if not data:\n",
" break\n",
"\n",
" print(data.decode(\"utf8\"))\n",
"\n",
"# клиент\n",
"import socket\n",
"\n",
"with socket.create_connection((\"127.0.0.1\", 10001)) as sock:\n",
" sock.sendall(\"ping\".encode(\"utf8\"))"
]
},
{
"cell_type": "markdown",
"id": "913dadb2",
"metadata": {},
"source": [
"Итак, мы используем на стороне сервера конструкцию `with socket.socket`, создаём наш объект типа `socket`, вызываем методы `bind`, `listen`. Затем в бесконечном цикле вызываем `accept` и получаем новые соединения от клиентов. Для полученного объекта мы опять используем контекстный менеджер и мы не заботимся о вызовах метода `close`. После того как контекстный менеджер завершит свою работу, он автоматически вызовет метод `close` для нужных нам объектов. Это очень удобно, и это позволяет допускать вам меньшее количество ошибок при работе с сокетами. На стороне клиента мы используем снова контекстный менеджер для вызова `socket.create.connection` и опять не заботимся о вызове метода `close`. Предпочтительнее работать с контекстными менеджерами при написании клиент-серверных программ на языке Python.\n",
"\n",
"Итак, на этой лекции мы обсудили, что такое сокеты, как они устроены, мы попробовали написать свою первую сетевую программу типа клиент-сервер, попробовали поотправлять данные с клиента на сервер, и мы обсудили роль контекстных менеджеров в языке Python для написания сетевых программ. \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
}