Пишем свой веб-сервер на Python
101Статья носит образовательный характер, мы ни к чему не призываем и не обязываем. Информация представлена исключительно в ознакомительных целях.
![](/file/12e39c0e00b40d78f1fc5.png)
Больше интересных статей на нашем канале https://t.me/NETSTALKER_RU
Подписывайтесь на канал и делитесь ссылкой на статью с друзьями!
Что такое веб-сервер?
Начнем с того, что четко ответим на вопрос, что же такое веб-сервер?
В первую очередь - это сервер. А сервер - это процесс (да, это не железка), обслуживающий клиентов. Сервер - фактически обычная программа, запущенная в операционной системе. Веб-сервер, как и большинство программ, получает данные на вход, преобразовывает их в соответствии с бизнес-требованиями и осуществляет вывод данных. Данные на вход и выход передаются по сети с использованием протокола HTTP. Входные данные - это запросы клиентов (в основном веб-браузеров и мобильных приложений). Выходные данные - это зачастую HTML-код подготовленных веб-страниц.
![](https://iximiuz.com/writing-python-web-server-part-1/client-server-opt.png)
На данном этапе логичными будут следующие вопросы: что такое HTTP и как передавать данные по сети? HTTP - это простой текстовый (т.е. данные могут быть прочитаны человеком) протокол передачи информации в сети Интернет. Протокол - это не страшное слово, а всего лишь набор соглашений между двумя и более сторонами о правилах и формате передачи данных. Его рассмотрение мы вынесем в отдельную тему, а далее попробуем понять, как можно осуществлять передачу данных по сети.
Как компьютеры взаимодействуют по сети
В Unix-подобных системах принят очень удобный подход для работы с различными устройствами ввода/вывода - рассматривать их как файлы. Реальные файлы на диске, мышки, принтеры, модемы и т.п. являются файлами. Т.е. их можно открыть, прочитать данные, записать данные и закрыть.
![](https://iximiuz.com/writing-python-web-server-part-1/dev-files-opt.png)
При открытии файла операционной системой создается т.н. файловый дескриптор. Это некоторый целочисленный идентификатор, однозначно определяющий файл в текущем процессе. Для того, чтобы прочитать или записать данные в файл, необходимо в соответствующую функцию (например, read()
или write()
) передать этот дескриптор, чтобы четко указать, с каким файлом мы собираемся взаимодействовать.
int fd = open("/path/to/my/file", ...); char buffer[1024]; read(fd, buffer, 1024); write(fd, "some data", 10); close(fd);
Очевидно, что т.к. общение компьютеров по сети - это также про ввод/вывод, то и оно должно быть организовано как работа с файлами. Для этого используется специальный тип файлов, т.н. сокеты.
Сокет - это некоторая абстракция операционной системы, представляющая собой интерфейс обмена данными между процессами. В частности и по сети. Сокет можно открыть, можно записать в него данные и прочитать данные из него.
![](https://iximiuz.com/writing-python-web-server-part-1/ac-power-socket-opt.png)
Т.к. видов межпроцессных взаимодействий с помощью сокетов множество, то и сокеты могут иметь различные конфигурации: сокет характеризуется семейством протоколов (IPv4 или IPv6 для сетевого и UNIX для локального взаимодействия), типом передачи данных (потоковая или датаграммная) и протоколом (TCP, UDP и т.п.).
Далее будет рассматриваться исключительно клиент-серверное взаимодействие по сети с использованием сокетов и стека протоколов TCP/IP.
Предположим, что наша прикладная программа хочет передать строку "Hello World" по сети, и соответствующий сокет уже открыт. Программа осуществляет запись этой строки в сокет с использованием функции write()
или send()
. Как эти данные будут переданы по сети?
Т.к. в общем случае размер передаваемых программой данных не ограничен, а за один раз сетевой адаптер (NIC) может передать фиксировнный объем информации, данные необходимо разбить на фрагменты, не превышающие этот объем. Такие фрагменты называются пакетами. Каждому пакету добавляется некоторая служебная информация, в частности содержащая адреса получателя и отправителя, и они начинают свой путь по сети.
![](https://iximiuz.com/writing-python-web-server-part-1/chunking-opt.png)
Адрес компьютера в сети - это т.н. IP-адрес. IP (Internet Protocol) - протокол, который позволил объединить множество разнородных сетей по всеми миру в одну общую сеть, которая называется Интернет. И произошло это благодаря тому, что каждому компьютеру в сети был назначен собственный адрес.
В силу особенности маршрутизации пакетов в сети, различные пакеты одной и той же логической порции данных могут следовать от отправителя к получателю разными маршрутами. Разные маршруты могут иметь различную сетевую задержку, следовательно, пакеты могут быть доставлены получателю не в том порядке, в котором они были отправлены. Более того, содержимое пакетов может быть повреждено в процессе передачи.
![](https://iximiuz.com/writing-python-web-server-part-1/reassembly-opt.png)
Вообще говоря, требование получать пакеты в том же порядке, в котором они были отправлены, не всегда является обязательным (например, при передаче потокового видео). Но, когда мы загружаем веб-страницу в браузере, мы ожидаем, что буквы на ней будут расположены ровно в том же порядке, в котором их нам отправил веб-сервер. Именно поэтому HTTP протокол работает поверх надеждного протокола передачи данных TCP, который будет рассмотрен ниже.
Чтобы организовать доставку пакетов в порядке их передачи, необходимо добавить в служебную информацию каждого пакета его номер в цепочке пакетов и на принимающей стороне делать сборку пакетов не в порядке их поступления, а в порядке, определенном этими номерами. Чтобы избежать доставки поврежденных пакетов, необходимо в каждый пакет добавить контрольную сумму и пакеты с неправильной контрольной суммой отбрасывать, ожидая, что они будут отправлены повторно.
Этим занимается специальный протокол потоковой передачи данных - TCP.
TCP - (Transmission Control Protocol — протокол управления передачей) - один из основных протоколов передачи данных в Интернете. Используется для надежной передачи данных с подтверждением доставки и сохранением порядка пакетов.
![](https://iximiuz.com/writing-python-web-server-part-1/ip-tcp-payload-opt.png)
В силу того, что передачей данных по сети по протоколу TCP на одном и том же компьютере может заниматься одновременно несколько программ, для каждого из таких сеансов передачи данных необходимо поддерживать свою последовательность пакетов. Для этого TCP вводит понятие соединения. Соединение - это просто логическое соглашение между принимающей и передающей сторонами о начальных и текущих значениях номеров пакетов и состоянии передачи. Соединение необходимо установить (обменявшись несколькими служебными пакетами), поддерживать (периодически передавать данные, чтобы не наступил таймаут), а затем закрыть (снова обменявшись несколькими служебными пакетами).
Итак, IP определяет адрес компьютера в сети. Но, в силу наличия TCP соединений, пакеты могут принадлежать различным соединениям на одной и той же машине. Для того, чтобы различать соединения, вводится понятие TCP-порт. Это всего лишь пара чисел (одно для отправителя, а другое для получателя) в служебной информации пакета, определяющая, в рамках какого соединения должен рассматриваться пакет. Т.е. адрес соединения на этой машине.
Больше интересных статей на нашем канале https://t.me/NETSTALKER_RU
Подписывайтесь на канал и делитесь ссылкой на статью с друзьями!
Простейший TCP сервер
Теперь перейдем к практике. Попробуем создать свой собственный TCP-сервер. Для этого нам понадобится модуль socket
из стандартной библиотеки Python.
Основная проблема при работе с сокетами у новичков связана с наличием обязательного магического ритуала подготовки сокетов к работе. Но имея за плечами теоретические знания, изложенные выше, кажущаяся магия превращается в осмысленные действия. Также необходимо отметить, что в случае с TCP работа с сокетами на сервере и на клиенте различается. Сервер занимается ожиданием подключений клиентов. Т.е. его IP адрес и TCP порт известны потенциальным клиентам заранее. Клиент может подключиться к серверу, т.е. выступает активной стороной. Сервер же ничего не знает об адресе клиента до момента подключения и не может выступать инициатором соединения. После того, как сервер принимает входящее соединения клиента, на стороне сервера создается еще один сокет, который является симметричным сокету клиента.
Итак, создаем серверный сокет:
# python3 import socket serv_sock = socket.socket(socket.AF_INET, # Семейство протоколов socket.SOCK_STREAM, # Тип передачи proto=0) # Протокол по умолчанию print(type(serv_sock)) # <class'socket.socket'>
А где же обещанные int fd = open("/path/to/my/socket")
? Дело в том, что системный вызов open()
не позволяет передать все необходимые для инициализации сокета параметры, поэтому для сокетов был введен специальный одноименный системный вызов socket()
. Python же является объектно-ориентированным языком, в нем вместо функций принято использовать классы и их методы. Код модуля socket
является ОО-оберткой вокрут набора системных вызовов для работе с сокетами. Его можно представить себе, как:
class socket: # Да, да, имя класса с маленькой буквы :( def __init__(self, sock_familty, sock_type, proto): self._fd = system_socket(sock_family, sock_type, proto) def write(self, data): # на самом деле вместо write используется send, но об этом ниже system_write(self._fd, data) def fileno(self): return self._fd
Т.е. доступ к целочисленному файловому дескриптору можно получить с помощью:
print(serv_sock.fileno()) # 3 или другой int
Так мы работаем с серверным сокетом, а в общем случае на серверной машине может быть несколько сетевых адаптеров, нам необходимо привязать созданный сокет к одному из них:
serv_sock.bind(('127.0.0.1', 53210)) # чтобы привязать сразу ко всем, можно использовать ''
Вызов bind()
заставляет нас указать не только IP адрес, но и порт, на котором сервер будет ожидать (слушать) подключения клиентов.
Далее необходимо явно перевести сокет в состояние ожидания подключения, сообщив об этом операционной системе:
serv_sock.listen(10) # 10 - это размер очереди входящих подключений, т.н. backlog
После этого вызова операционная система готова принимать подключения от клиентов на этом сокете, хотя наш сервер (т.е. программа) - еще нет. Что же это означает и что такое backlog?
Как мы уже выяснили, взаимодействие по сети происходит с помощью отправки пакетов, а TCP требует установления соединения, т.е. обмена между клиентом и сервером несколькими служебными пакетами, не содержащими реальных бизнес-данных. Каждое TCP соединение обладает состоянием. Упростив, их можно представить себе так:
СОЕДИНЕНИЕ УСТАНАВЛИВАЕТСЯ -> УСТАНОВЛЕНО -> СОЕДИНЕНИЕ ЗАКРЫВАЕТСЯ
Таким образом, параметр backlog определяет размер очереди для установленных соединений. Пока количество подключенных клиентов меньше, чем этот параметр, операционная система будет автоматически принимать входящие соединения на серверный сокет и помещать их в очередь. Как только количество установленных соединений в очереди достигнет значения backlog, новые соединения приниматься не будут. В зависимости от реализации (GNU Linux/BSD), OC может явно отклонять новые подключения или просто их игнорировать, давая возможность им дождаться освобождения места в очереди.
Теперь необходимо получить соединение из этой очереди:
client_sock, client_addr = serv_sock.accept()
В отличие от неблокирующего вызова listen()
, который сразу после перевода сокета в слушающее состояние, возвращает управление нашему коду, вызов accept()
является блокирующим. Это означает, что он не возвращает управление нашему коду до тех пор, пока в очереди установленных соединений не появится хотя бы одно подключение.
На этом этапе на стороне сервера мы имеем два сокета. Первый, serv_sock
, находится в состоянии LISTEN
, т.е. принимает входящие соединения. Второй, client_sock
, находится в состоянии ESTABLISHED
, т.е. готов к приему и передаче данных. Более того, client_sock
на стороне сервера и клиенсткий сокет в программе клиента являются одинаковыми и равноправными участниками сетевого взаимодействия, т.н. peer'ы. Они оба могут как принимать и отправлять данные, так и закрыть соединение с помощью вызова close()
. При этом они никак не влияют на состояние слушающего сокета.
Пример чтения и записи данных в клиентский сокет:
while True: data = client_sock.recv(1024) if not data: break client_sock.sendall(data)
И опять же справедливый вопрос - где обещанные read()
и write()
? На самом деле с сокетом можно работать и с помощью этих двух функций, но в общем случае сигнатуры read()
и write()
не позволяют передать все возможные параметры чтения/записи. Так, например, вызов send()
с нулевыми флагами равносилен вызову write()
.
Немного коснемся вопроса адресации. Каждый TCP сокет определяется двумя парами чисел: (локальный IP адрес, локальный порт)
и (удаленный IP адрес, удаленный порт)
. Рассмотрим, какие адреса на данный момент у наших сокетов:
serv_sock: laddr (ip=<server_ip>, port=53210) raddr (ip=0.0.0.0, port=*) # т.е. любой client_sock: laddr (ip=<client_ip>, port=51573) # случайный порт, назначенный системой raddr (ip=<server_ip>, port=53210) # адрес слушающего сокета на сервере
Полный код сервера выглядит так:
# python3 import socket serv_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, proto=0) serv_sock.bind(('', 53210)) serv_sock.listen(10) while True: # Бесконечно обрабатываем входящие подключения client_sock, client_addr = serv_sock.accept() print('Connected by', client_addr) while True: # Пока клиент не отключился, читаем передаваемые # им данные и отправляем их обратно data = client_sock.recv(1024) if not data: # Клиент отключился break client_sock.sendall(data) client_sock.close()
Подключиться к этому серверу можно с использованием консольной утилиты telnet
, предназначенной для текстового обмена информацией поверх протокола TCP:
telnet 127.0.0.1 53210 > Trying 192.168.0.1... > Connected to 192.168.0.1. > Escape character is '^]'. > Hello > Hello
Больше интересных статей на нашем канале https://t.me/NETSTALKER_RU
Подписывайтесь на канал и делитесь ссылкой на статью с друзьями!
Простейший TCP клиент
На клиентской стороне работа с сокетами выглядит намного проще. Здесь сокет будет только один и его задача только лишь подключиться к заранее известному IP-адресу и порту сервера, сделав вызов connect()
.
# python3 import socket client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client_sock.connect(('127.0.0.1', 53210)) client_sock.sendall(b'Hello, world') data = client_sock.recv(1024) client_sock.close() print('Received', repr(data))
На сегодня все! Спасибо всем, кто дочитал до конца!
![](/file/689bae6741d23db0fcee9.png)