Привет, дорогой читатель!
Для глубокого понимания процесса клиент-серверного взаимодействия на прикладном уровне будем использовать PyCharm в режиме debug и примитивное однопоточное серверное приложение с использованием блокирующего сокета. Для понимания же того, что происходит на транспортном уровне, будем использовать средства операционной системы и средства Wireshark.
Листинг серверной реализации:
https://gist.github.com/alex944591/c09f4cf992b37ed79eac707b4a5d8d13
1. На нулевом этапе клиент-серверного взаимодействия необходимо наличие главного сокета – серверного. Именно он может принять соединение от клиента. Если подзабыли, что такое сокет, то почитать можно тут:
- TCP/IP (Часть 1) просто о сложном;
- TCP/IP (Часть 2) просто о сложном;
- TCP/IP (Часть 3) просто о сложном;
- TCP/IP (Часть 4) просто о сложном.
Запускаем серверное приложение и проверяем средствами ОС наличие серверного сокета:
Обратите внимание на столбец State. Этот столбец отображает текущее состояние серверного сокета. На общей схеме:
2. Первым этапом клиент-серверного взаимодействия является инициация TCP соединения клиентом. Для успешного TCP соединения необходимо трехэтапное рукопожатие (TCP three-way/triple handshake). В качестве клиента будем использовать ncat (в режиме PowerShell), а за процессом рукопожатия подглянем с помощью Wireshark:
ncat -p 65001 127.0.0.1 5001
На общей схеме это выглядит так:
Если теперь взглянуть на столбец State то увидим следующее:
Как же дело обстоит в самом серверном приложении? А давайте заглянем в режиме отладки:
Перед соединением, в нашей синхронной реализации клиент-серверного взаимодействия, приложение входит в бесконечный цикл while True и блокируется в точке ожидания подключения:
И как только выполняем команду ncat -p 65001 127.0.0.1 5001, то режим отладчика PyCharm заполняет контекст и убирает блокировку переходя к исполнению следующей строки кода:
Давайте немного разберемся с параметрами подключения:
fd – файловый дескриптор;
AddressFamily.AF_INET – указание на используемый протокол сетевого уровня - IPv4;
SocketFind.SOCK_STREAM - указание на используемый протокол транспортного уровня - TCP;
laddr (локальный серверный сокет) – 127.0.0.1: 5001
raddr (клиентский сокет) - 127.0.0.1: 65001
в результате установления соединения, в переменную client_socket попадает объект типа socket.
Этому объекту ОС выделяет буфер:
Т.е. сервер выделяет для взаимодействия с клиентом входящий буфер (recv), для приема данных от клиента и исходящий буфер (send), для передачи данных клиенту. Представьте себе это как почтовый ящик с двумя ячейками. С точки зрения сервера мы даем клиенту доступ к обоим ячейкам. Если у клиентского сокета есть что-то для сервера, то он кладет данные в ячейку recv. Сервер же в свою очередь данные для клиента помещает в ячейку send.
Вот хорошая картинка из книги Мэттью Фаулера «Asyncio и конкурентное программирование на Python»:
3. Когда на транспортном уровне установлено соединение, а прикладному уровню выделены необходимые буферы становится возможным дальше обмениваться данными. Давайте посмотрим, как это происходит с разных углов обзора:
Обзор в консоли клиента:
Обзор на сети (Wireshark):
Обзор из приложения:
Теперь отправим, что-нибудь серверу в ответ:
Далее наш сервер обработает полученный запрос и дает возможность ответить что-нибудь клиенту:
Вы заметили, что клиент и сервер работают на одном хосте? Все верно, в целях упрощения демонстрации особенностей работы прикладного и транспортного уровней мы отказались от усложнения схемы. С точки зрения клиента и сервера без разницы, где они запускаются, главное, чтобы была сетевая связность. В реальности серверные приложения работают в асинхронном исполнении. Код аналогичного простейшего сервера, но в асинхронном исполнении выглядит так:
https://gist.github.com/alex944591/26100262fb16cd60c0248d08ef4e338e
Если понравилось - ставьте лайки, делитесь с друзьями, коллегами в соцсетях. Все это придает смысл моему альтруизму :)
До скорого!