Перейти к содержимому

Как написать сервер на php

  • автор:

Вебсокеты на php. Выбираем вебсокет-сервер

Давным-давно я публиковал статью на хабре, как написать свой вебсокет-сервер с нуля. Статья переросла в библиотеку. Несколько месяцев я занимался её развитием, ещё несколько лет — поддержкой и багфиксом. Написал модуль интеграции с yii2. Какой-то энтузиаст написал интеграцию с laravel. Моя библиотека совместима с php7. Недавно я решил отказаться от её дальнейшей поддержки (причины ниже), поэтому хочу помочь её пользователям перейти на другую библиотеку.

Прежде чем начать писать свой вебсокет-сервер, я выбирал из готовых продуктов, и на тот момент их было всего два: phpdaemon и ratchet.

phpdaemon

  • зависит от установки библиотеки libevent
  • протоколы: HTTP, FastCGI, FlashPolicy, Ident, Socks4/5.

Ratchet

  • тянет за собой около десятка зависимостей
  • протоколы: websocket, http, wamp
  • поддержка windows
  • нет ssl
  • отсутствие зависимостей
  • наличие таймеров

В итоге я написал библиотеку для себя и поделился ею с сообществом на гитхабе. Сделал несколько демок (в том числе игру «танчики»). Переписал стороннюю игру (с разрешения авторов) с node.js на свою библиотеку. Делал нагрузочное тестирование. Демки работали годами без перезагрузки. Старался отвечать на тикеты в течения дня. Всё это показывало, что моя библиотека может быть использована на продакшене и многие её использовали.

Была единственная проблема. Мне хватало моей библиотеки для использования в своих проектах, а вот другим нет. Они хотели, чтобы я её развивал, а мне это было не нужно. Кому-то требовалась поддержка windows, а кому-то ssl, pg_notify, safari, pthreads и многое другое. Открытые тикеты с запросами на реализацию различного функционала висят годами.

Не так давно, я решил пересмотреть ещё раз, какие продукты могут быть полезны для пользователей моей библиотеки и был приятно удивлён, что кроме двух проектов, описанных выше появился ещё третий. Он полностью удовлетворял моим запросам и даже больше.

Workerman

  • отсутствие зависимостей
  • протоколы: websocket, http/https, tcp, сustom
  • поддержка таймеров
  • интеграция с react-компонентами
  • поддержка windows

Если загуглить «php websocket», то первая страница — это моя статья на Хабре, а вторая — «Ratchet», который кому-то может показаться сложным и он выберет из-за этого мою библиотеку или вообще откажется от идеи делать вебсокеты.

Что ж, пришло время исправить эту досадную ошибку и донести до как можно большего количества людей о существовании такой библиотеки как Workerman и привести несколько примеров по её использованию.

На главной странице проекта в гитхабе уже есть несколько примеров. Рассмотрим один из них:

websocket server

count = 4; // Emitted when new connection come $ws_worker->onConnect = function($connection) < echo "New connection\n"; >; // Emitted when data received $ws_worker->onMessage = function($connection, $data) < // Send hello $data $connection->send('hello ' . $data); >; // Emitted when connection closed $ws_worker->onClose = function($connection) < echo "Connection closed\n"; >; // Run worker Worker::runAll();

tcp server

count = 4; // Emitted when new connection come $tcp_worker->onConnect = function($connection) < echo "New Connection\n"; >; // Emitted when data received $tcp_worker->onMessage = function($connection, $data) < // send data to client $connection->send("hello $data \n"); >; // Emitted when new connection come $tcp_worker->onClose = function($connection) < echo "Connection closed\n"; >; Worker::runAll();

Чтобы запустить пример, нужно установить workerwan: composer require workerman/workerman
Пример можно запустить с помощью команды php test.php start и в консоли мы увидим:

Все команды workerman:

php test.php start
php test.php start -d -демонизировать скрипт
php test.php status
php test.php stop
php test.php restart
php test.php restart -d
php test.php reload

В принципе, используя первый пример можно сделать чат на вебсокетах и других примеров не нужно. Но за несколько лет я понял, что в основном пользователям моей библиотеки был нужен пример того как можно отправить из своего кода на php уведомление выбранному пользователю, а не всем одновременно, как часто бывает в примерах.

  • пользователь #1 лайкает фотографию пользователя #2 и мы хотим отправить пользователю #2 об этом уведомление, если он сейчас на сайте.
  • на сайте появилось новое объявление и мы хотим отправить уведомление нашему модератору,
    чтобы он его проверил

Отправка сообщения одному пользователю:

код сервера server.php:

onWorkerStart = function() use (&$users) < // создаём локальный tcp-сервер, чтобы отправлять на него сообщения из кода нашего сайта $inner_tcp_worker = new Worker("tcp://127.0.0.1:1234"); // создаём обработчик сообщений, который будет срабатывать, // когда на локальный tcp-сокет приходит сообщение $inner_tcp_worker->onMessage = function($connection, $data) use (&$users) < $data = json_decode($data); // отправляем сообщение пользователю по userId if (isset($users[$data->user])) < $webconnection = $users[$data->user]; $webconnection->send($data->message); > >; $inner_tcp_worker->listen(); >; $ws_worker->onConnect = function($connection) use (&$users) < $connection->onWebSocketConnect = function($connection) use (&$users) < // при подключении нового пользователя сохраняем get-параметр, который же сами и передали со страницы сайта $users[$_GET['user']] = $connection; // вместо get-параметра можно также использовать параметр из cookie, например $_COOKIE['PHPSESSID'] >; >; $ws_worker->onClose = function($connection) use(&$users) < // удаляем параметр при отключении пользователя $user = array_search($connection, $users); unset($users[$user]); >; // Run worker Worker::runAll();

код клиента client.html:

      

код отправки сообщений с нашего сайта send.php:

 $user, 'message' => $message]) . "\n");

Справедливости ради я решил написать такой же пример для ratchet, но документация мне не помогла, как 3 года назад. Зато на stackoverflow предложили немного костыльный, но рабочий вариант: соединяться из своего php-скрипта по ws-соединению. Конечно это не так же просто как соединиться с tcp-сокетом с помощью stream_socket_client и отправить сообщение с помощью fwrite. Но уже что-то.

Плюс ещё остался для меня незакрытый вопрос: поддерживает ли ratchet возможность запуска нескольких воркеров и если да, то как в таком случае отправлять сообщение одному пользователю, ведь не понятно на каком он воркере. На workerman это можно сделать так.

В общем, я выбрал для себя библиотеку Workerman и рекомендую переходить на неё пользователям моей библиотеки. Все примеры лежат на гитхабе.

Update: в комментариях рекомендуют swoole. Я натыкался на эту библиотеку ранее, но у меня сложилось ложное впечатление, что что она не поддерживает php7 и после этого она выпала из моего круга зрения. А зря. Интересная библиотека.

Пишем простой сервер на Python

Ну, начнем как и везде с определений, берите тетрадь и ручку сейчас начнется нудятина. Чтобы мы cмогли написать свой сервер, нужно для начала понимать как он вообще работает, ловите определение:

Сервер – это программное обеспечение, которое ожидает запросов клиентов и обслуживает или обрабатывает их соответственно.

Если объяснять это своими словами, представьте фургон с хот-догами(сервер), проголодавшись, вы(клиент) подходите и говорите повару, что вы хотите заказать(запрос), после чего повар обрабатывает, что вы ему сказали и начинает готовить, в конечном итоге вы получаете свой хот-дог(результат) и сытый радуетесь жизни. Для наглядности посмотри схему.

Околопрактика

Для написания сервера мы будем использовать Python и модуль Socket.

Socket позволяет нам общаться с сервером с помощью сокетов. Весь код я постараюсь пояснять, дабы ты мой дорогой читатель все понял. В конце статьи будет готовый код.

Создайте два файла в одной директории:

Практика

Пишем код для серверной части, так что открывайте файл socket_server.py.

Начнем с импорта модуля и создания TCP-сокета:

import socket

Далее весь код будет с комментариями:

s.bind(('localhost', 3030)) # Привязываем серверный сокет к localhost и 3030 порту. s.listen(1) # Начинаем прослушивать входящие соединения conn, addr = s.accept() # Метод который принимает входящее соединение.

Добавим вечный цикл, который будет считывать данные с клиентской части, и отправлять их обратно.

while True: # Создаем вечный цикл. data = conn.recv(1024) # Получаем данные из сокета. if not data: break conn.sendall(data) # Отправляем данные в сокет. print(data.decode('utf-8')) # Выводим информацию на печать. conn.close()

Переходим к клиентской части, весь код теперь пишем в файле socket_client.py.

Начало у клиентской части такое-же как и у серверной.

import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

Далее подключимся к нашему серверу и отправим сообщение «Hello. Habr!».

s.connect(('localhost', 3030)) # Подключаемся к нашему серверу. s.sendall('Hello, Habr!'.encode('utf-8')) # Отправляем фразу. data = s.recv(1024) #Получаем данные из сокета. s.close()

Слева сервер, справа клиент

Заключение

Вот мы с вами и написали свой первый сервер, рад был стараться для вас, ниже будет готовый код.

socket_server.py:

import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('localhost', 3030)) # Привязываем серверный сокет к localhost и 3030 порту. s.listen(1) # Начинаем прослушивать входящие соединения. conn, addr = s.accept() # Метод который принимает входящее соединение. while True: data = conn.recv(1024) # Получаем данные из сокета. if not data: break conn.sendall(data) # Отправляем данные в сокет. print(data.decode('utf-8')) conn.close()

socket_client.py:

import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('localhost', 3030)) # Подключаемся к нашему серверу. s.sendall('Hello, Habr!'.encode('utf-8')) # Отправляем фразу. data = s.recv(1024) #Получаем данные из сокета. s.close()

Сокеты: Сервер на PHP

Сокеты: Сервер на PHP

В предыдущей статье я рассказывал про сокеты на PHP. И сказал, что необходимо написать сервер, принимающий запросы и отдающий ответы. И клиента, посылающего запросы к серверу. В этой статье мы разберём код для классического сервера, принимающего число, возводящий его в квадрат и возвращающий результат клиенту.

Сразу привожу код сервера на PHP с подробными комментариями:

header(‘Content-Type: text/plain;’); //Мы будем выводить простой текст
set_time_limit(0); //Скрипт должен работать постоянно
ob_implicit_flush(); //Все echo должны сразу же отправляться клиенту
$address = ‘localhost’; //Адрес работы сервера
$port = 1985; //Порт работы сервера (лучше какой-нибудь редкоиспользуемый)
if (($sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) < 0) //AF_INET - семейство протоколов
//SOCK_STREAM — тип сокета
//SOL_TCP — протокол
echo «Ошибка создания сокета»;
>
else echo «Сокет создан\n»;
>
//Связываем дескриптор сокета с указанным адресом и портом
if (($ret = socket_bind($sock, $address, $port)) < 0) echo "Ошибка связи сокета с адресом и портом";
>
else echo «Сокет успешно связан с адресом и портом\n»;
>
//Начинаем прослушивание сокета (максимум 5 одновременных соединений)
if (($ret = socket_listen($sock, 5)) < 0) echo "Ошибка при попытке прослушивания сокета";
>
else echo «Ждём подключение клиента\n»;
>
do //Принимаем соединение с сокетом
if (($msgsock = socket_accept($sock)) < 0) echo "Ошибка при старте соединений с сокетом";
> else echo «Сокет готов к приёму сообщений\n»;
>
$msg = «Hello!»; //Сообщение клиенту
echo «Сообщение от сервера: $msg»;
socket_write($msgsock, $msg, strlen($msg)); //Запись в сокет
//Бесконечный цикл ожидания клиентов
do echo ‘Сообщение от клиента: ‘;
if (false === ($buf = socket_read($msgsock, 1024))) echo «Ошибка при чтении сообщения от клиента»; >
else echo $buf.»\n»; //Сообщение от клиента
>
//Если клиент передал exit, то отключаем соединение
if ($buf == ‘exit’) socket_close($msgsock);
break 2;
>
if (!is_numeric($buf)) echo «Сообщение от сервера: передано НЕ число\n»;
else $buf = $buf * $buf;
echo «Сообщение от сервера: ($buf)\n»;
>
socket_write($msgsock, $buf, strlen($buf));
> while (true);
> while (true);
//Останавливаем работу с сокетом
if (isset($sock)) socket_close($sock);
echo «Сокет успешно закрыт»;
>
?>

Данный код я постарался тщательно прокомментировать, поэтому, надеюсь, Вам в нём всё понятно. Главное понять следующее: сервер — это непрерывно выполняющийся скрипт, который просто ждёт подключения клиентов. Затем принимающий запрос и на основании этого запроса, возвращающий клиенту ответ. Это самое главное, что Вам необходимо понять.

А в следующей статье мы с Вами напишем клиента на PHP, который и будет подключаться к данному серверу и отправлять различные запросы.

Создано 16.01.2012 14:51:02

  • Михаил Русаков
  • Копирование материалов разрешается только с указанием автора (Михаил Русаков) и индексируемой прямой ссылкой на сайт (http://myrusakov.ru)!

    Добавляйтесь ко мне в друзья ВКонтакте: http://vk.com/myrusakov.
    Если Вы хотите дать оценку мне и моей работе, то напишите её в моей группе: http://vk.com/rusakovmy.

    Если Вы не хотите пропустить новые материалы на сайте,
    то Вы можете подписаться на обновления: Подписаться на обновления

    Если у Вас остались какие-либо вопросы, либо у Вас есть желание высказаться по поводу этой статьи, то Вы можете оставить свой комментарий внизу страницы.

    Порекомендуйте эту статью друзьям:

    Если Вам понравился сайт, то разместите ссылку на него (у себя на сайте, на форуме, в контакте):

    1. Кнопка:
      Она выглядит вот так:
    2. Текстовая ссылка:
      Она выглядит вот так: Как создать свой сайт
    3. BB-код ссылки для форумов (например, можете поставить её в подписи):

    Комментарии ( 14 ):

    ankalitkin 17.01.2012 16:33:51

    У меня ошибка, пишет что функции socket_create нет.

    Admin 17.01.2012 19:22:53

    Надо включить эту библиотеку. Для этого в php.ini расскоментируйте строчку: extension=php_sockets.dll, затем перезапустите сервер.

    Chunin 15.03.2012 15:41:55

    Надо включить эту библиотеку. Для этого в php.ini расскоментируйте строчку: extension=php_sockets.dll, затем перезапустите сервер. — Вот я всё по инструкции сделал,а оно мне:
    Fatal error: Call to undefined function socket_create() in Z:\home\localhost\www\server.php on line 7

    Admin 15.03.2012 15:44:00

    Дополнительные модули к Денверу установите (скачайте их с http://denwer.ru).

    Chunin 15.03.2012 20:39:18

    А что нужно именно скачать,можна ссылку ))

    Admin 15.03.2012 21:19:56

    Дополнительные модули нужно скачать: http://www.denwer.ru/packages/php5.html

    sofus 23.08.2012 17:30:02

    Ваш совет помог мне включить поддержку сокетов на денвере,спасибо!

    tr-td table 11.06.2014 15:52:30

    После установки дополнительных модулей ошибка сохранилась.

    des 27.10.2012 17:12:11

    Все отлично, но вот есть вопрос: здесь если мы получили коннект клиента, то пока он не перестанет слать что либо серверу(здесь пока не пошлет exit), другие клиенты будут как бы в очереди и не будут обрабатываться. Есть варианты одновременной обработки нескольких клиентов?

    vladlaas 18.05.2013 23:15:44

    Скажите хоть. Надо ли запускать самостоятельно «server.php» или его просто надо разместить и он сам будет непрерывно работать! Опишите пожалуйста, Может из за денвера моего не х..на не работает!?

    Admin 19.05.2013 10:53:03

    Надо запускать и сам он непрерывно работать не будет.

    vladlaas 19.05.2013 21:27:13

    «Главное понять следующее: сервер — это непрерывно выполняющийся скрипт» Зачем тогда вообще эти сокеты нужны? Где они используются?

    _Gven_ 13.08.2013 11:45:06

    А как он запускается?

    Maxxx4791 24.01.2015 15:38:30

    Скажите пожалуйста, а как его запускать. этот сокет сервер на Денвере?

    Для добавления комментариев надо войти в систему.
    Если Вы ещё не зарегистрированы на сайте, то сначала зарегистрируйтесь.

    Copyright © 2010-2024 Русаков Михаил Юрьевич. Все права защищены.

    Пишем свой веб-сервер на Python: сокеты

    Подпишись на обновления блогa, чтобы не пропустить следующий пост!

    Оглавление
    • Что определяет хорошего разработчика ПО?
    • Что же такое веб-сервер?
    • Как общаться с клиентами по сети
    • Простейший TCP сервер
    • Простейший TCP клиент
    • Заключение
    • Cсылки по теме
    Лирическое отступление: что определяет хорошего разработчика?

    Доктор Манхэттен что-то собирает силой мысли

    Разработка ПО — это инженерная дисциплина. Если вы хотите стать действительно профессиональным разработчиком, то необходимо в себе развивать качества инженера, а именно: системный подход к решению задач и аналитический склад ума. Для вас должно перестать существовать слово магия. Вы должны точно знать как и почему работают системы, с которыми вы взаимодействуете (между прочим, полезное качество, которое находит применение и за пределами IT).

    К сожалениею (или к счастью, ибо благоприятно складывается на уровне доходов тех, кто осознал), существует огромное множество людей, которые пишут код без должного понимания важности этих принципов. Да, такие горе-программисты могут создавать работающие до поры до времени системы, собирая их из найденных в Интернете кусочков кода, даже не удосужившись прочитать, как они реализованы. Но как только возникает первая нестандартная проблема, решение которой не удается найти на StackOverflow, вышеупомянутые персонажи превращаются в беспомощных жертв кажущейся простоты современной разработки ПО.

    Для того, чтобы не оказаться одним из таких бедолаг, необходимо постоянно инвестировать свое время в получение фундаментальных знаний из области Computer Science. В частности, для прикладных разработчиков в большинстве случаев таким фундаментом является операционная система, в которой выполняются созданные ими программы.

    Веб-фреймворки и контейнеры приложений рождаются и умирают, а инструменты, которыми они пользуются, и принципы, на которых они основаны, остаются неизменными уже десятки лет. Это означает, что вложение времени в изучение базовых понятий и принципов намного выгоднее в долгосрочной перспективе. Сегодня мы рассмотрим одну из основных для веб-разработчика концепций — сокеты. А в качестве прикладного аспекта, мы разберемся, что же такое на самом деле веб-сервер и начнем писать свой.

    Что такое веб-сервер?

    Начнем с того, что четко ответим на вопрос, что же такое веб-сервер?

    В первую очередь — это сервер. А сервер — это процесс (да, это не железка), обслуживающий клиентов. Сервер — фактически обычная программа, запущенная в операционной системе. Веб-сервер, как и большинство программ, получает данные на вход, преобразовывает их в соответствии с бизнес-требованиями и осуществляет вывод данных. Данные на вход и выход передаются по сети с использованием протокола HTTP. Входные данные — это запросы клиентов (в основном веб-браузеров и мобильных приложений). Выходные данные — это зачастую HTML-код подготовленных веб-страниц.

    Клиент общается с сервером по сети

    На данном этапе логичными будут следующие вопросы: что такое HTTP и как передавать данные по сети? HTTP — это простой текстовый (т.е. данные могут быть прочитаны человеком) протокол передачи информации в сети Интернет. Протокол — это не страшное слово, а всего лишь набор соглашений между двумя и более сторонами о правилах и формате передачи данных. Его рассмотрение мы вынесем в отдельную тему, а далее попробуем понять, как можно осуществлять передачу данных по сети.

    Как компьютеры взаимодействуют по сети

    В Unix-подобных системах принят очень удобный подход для работы с различными устройствами ввода/вывода — рассматривать их как файлы. Реальные файлы на диске, мышки, принтеры, модемы и т.п. являются файлами. Т.е. их можно открыть, прочитать данные, записать данные и закрыть.

    ls /dev показывает список устройств Linux

    При открытии файла операционной системой создается т.н. файловый дескриптор. Это некоторый целочисленный идентификатор, однозначно определяющий файл в текущем процессе. Для того, чтобы прочитать или записать данные в файл, необходимо в соответсвующую функцию (например, read() или write() ) передать этот дескриптор, чтобы четко указать, с каким файлом мы собираемся взаимодействовать.

    int fd = open("/path/to/my/file", . ); char buffer[1024]; read(fd, buffer, 1024); write(fd, "some data", 10); close(fd); 

    Очевидно, что т.к. общение компьютеров по сети — это также про ввод/вывод, то и оно должно быть организовано как работа с файлами. Для этого используется специальный тип файлов, т.н. сокеты.

    Сокет — это некоторая абстракция операционной системы, представляющая собой интерфейс обмена данными между процессами. В частности и по сети. Сокет можно открыть, можно записать в него данные и прочитать данные из него.

    Berkeley Sockets напоминают собой всем известную электрическую розетку

    Т.к. видов межпроцессных взаимодействий с помощью сокетов множество, то и сокеты могут иметь различные конфигурации: сокет характеризуется семейством протоколов (IPv4 или IPv6 для сетевого и UNIX для локального взаимодействия), типом передачи данных (потоковая или датаграммная) и протоколом (TCP, UDP и т.п.).

    Далее будет рассматриваться исключительно клиент-серверное взаимодействие по сети с использованием сокетов и стека протоколов TCP/IP.

    Предположим, что наша прикладная программа хочет передать строку «Hello World» по сети, и соответствующий сокет уже открыт. Программа осуществляет запись этой строки в сокет с использованием функции write() или send() . Как эти данные будут переданы по сети?

    Т.к. в общем случае размер передаваемых программой данных не ограничен, а за один раз сетевой адаптер (NIC) может передать фиксировнный объем информации, данные необходимо разбить на фрагменты, не превышающие этот объем. Такие фрагменты называются пакетами. Каждому пакету добавляется некоторая служебная информация, в частности содержащая адреса получателя и отправителя, и они начинают свой путь по сети.

    Компьютер отправляет данные по сети разделив на фрагменты

    Адрес компьютера в сети — это т.н. IP-адрес. IP (Internet Protocol) — протокол, который позволил объединить множество разнородных сетей по всеми миру в одну общую сеть, которая называется Интернет. И произошло это благодаря тому, что каждому компьютеру в сети был назначен собственный адрес.

    В силу особенности маршрутизации пакетов в сети, различные пакеты одной и той же логической порции данных могут следовать от отправителя к получателю разными маршрутами. Разные маршруты могут иметь различную сетевую задержку, следовательно, пакеты могут быть доставлены получателю не в том порядке, в котором они были отправлены. Более того, содержимое пакетов может быть повреждено в процессе передачи.

    TCP reassembly - восстанавливаем порядок пакетов на принимающей стороне

    Вообще говоря, требование получать пакеты в том же порядке, в котором они были отправлены, не всегда является обязательным (например, при передаче потокового видео). Но, когда мы загружаем веб-страницу в браузере, мы ожидаем, что буквы на ней будут расположены ровно в том же порядке, в котором их нам отправил веб-сервер. Именно поэтому HTTP протокол работает поверх надеждного протокола передачи данных TCP, который будет рассмотрен ниже.

    Чтобы организовать доставку пакетов в порядке их передачи, необходимо добавить в служебную информацию каждого пакета его номер в цепочке пакетов и на принимающей стороне делать сборку пакетов не в порядке их поступления, а в порядке, определенном этими номерами. Чтобы избежать доставки поврежденных пакетов, необходимо в каждый пакет добавить контрольную сумму и пакеты с неправильной контрольной суммой отбрасывать, ожидая, что они будут отправлены повторно.

    Этим занимается специальный протокол потоковой передачи данных — TCP.

    TCP — (Transmission Control Protocol — протокол управления передачей) — один из основных протоколов передачи данных в Интернете. Используется для надежной передачи данных с подтверждением доставки и сохранением порядка пакетов.

    TCP segment внутри IP пакета

    В силу того, что передачей данных по сети по протоколу TCP на одном и том же компьютере может заниматься одновременно несколько программ, для каждого из таких сеансов передачи данных необходимо поддерживать свою последовательность пакетов. Для этого TCP вводит понятие соединения. Соединение — это просто логическое соглашение между принимающей и передающей сторонами о начальных и текущих значениях номеров пакетов и состоянии передачи. Соединение необходимо установить (обменявшись несколькими служебными пакетами), поддерживать (периодически передавать данные, чтобы не наступил таймаут), а затем закрыть (снова обменявшись несколькими служебными пакетами).

    Итак, IP определяет адрес компьютера в сети. Но, в силу наличия TCP соединений, пакеты могут принадлежать различным соединениям на одной и той же машине. Для того, чтобы различать соединения, вводится понятие TCP-порт. Это всего лишь пара чисел (одно для отправителя, а другое для получателя) в служебной информации пакета, определяющая, в рамках какого соединения должен рассматриваться пакет. Т.е. адрес соединения на этой машине.

    Простейший TCP сервер

    Теперь перейдем к практике. Попробуем создать свой собственный TCP-сервер. Для этого нам понадобится модуль socket из стандартной библиотеки Python.

    Основная проблема при работе с сокетами у новичков связана с наличием обязательного магического ритуала подготовки сокетов к работе. Но имея за плечами теоретические знания, изложенные выше, кажущаяся магия превращается в осмысленные действия. Также необходимо отметить, что в случае с TCP работа с сокетами на сервере и на клиенте различается. Сервер занимается ожиданием подключений клиентов. Т.е. его IP адрес и TCP порт известны потенциальным клиентам заранее. Клиент может подключиться к серверу, т.е. выступает активной стороной. Сервер же ничего не знает об адресе клиента до момента подключения и не может выступать инициатором соединения. После того, как сервер принимает входящее соединения клиента, на стороне сервера создается еще один сокет, который является симметричным сокету клиента.

    Итак, создаем серверный сокет:

    # python3 import socket serv_sock = socket.socket(socket.AF_INET, # задамем семейство протоколов 'Интернет' (INET) socket.SOCK_STREAM, # задаем тип передачи данных 'потоковый' (TCP) proto=0) # выбираем протокол 'по умолчанию' для TCP, т.е. IP print(type(serv_sock)) #

    А где же обещанные 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 адрес, но и порт, на котором сервер будет ожидать (слушать) подключения клиентов.

    Далее необходимо явно перевести сокет в состояние ожидания подключения, сообщив об этом операционной системе:

    backlog = 10 # Размер очереди входящих подключений, т.н. backlog serv_sock.listen(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=, port=53210) raddr (ip=0.0.0.0, port=*) # т.е. любой client_sock: laddr (ip=, port=51573) # случайный порт, назначенный системой raddr (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 

    Простейший 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)) 

    Заключение

    Запоминать что-то без понимания, как это работает — злое зло не самый разумный подход для разработчика. Работа с сокетами тому отличный пример. На первый взгляд может показаться, что уложить в голове последовательность приготовления клиентских и серверных сокетов к работе практически не возможно. Это происходит из-за того, что не сразу понятен смысл производимых манипуляций. Однако, понимая, как осуществляется сетевое взаимодействие, API сокетов сразу становится прозрачным и легко оседает в подкорке. А с точки зрения полезности полученных знаний, я считаю. что понимание принципов сетевого взаимодействия жизненно важно для разработки и отладки действительно сложных веб-проектов.

    Другие статьи из серии:

    • Пишем свой веб-сервер на Python: процессы, потоки и асинхронный I/O
    • Пишем свой веб-сервер на Python: протокол HTTP
    • Пишем свой веб-сервер на Python: стандарт WSGI
    • Пишем свой веб-сервер на Python: фреймворк Flask

    Ссылки по теме

    Справочная информация:

    Литература

    • Beej’s Guide to Network Programming — отличные основы
    • UNIX Network Programming — продвинутый уровень

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *