Эрланг на практике. TCP и UDP сокеты. — Эрланг на практике
Пора применить эрланг по его прямому назначению -- для реализации сетевого сервиса. Чаще всего такие сервисы делают на базе веб-сервера, поверх протокола HTTP. Но мы возьмем уровень ниже -- TCP и UDP сокеты.
Я полагаю, вы уже знаете, как устроена сеть, что такое Internet Protocol, User Datagram Protocol и Transmission Control Protocol. Эта тема большинству программистов известна. Но если вы почему-то ее упустили, то придется сперва наверстать упущенное, и потом вернуться к этому уроку.
UDP сокет
Вспомним в общих чертах, что такое UDP:
- протокол передачи коротких сообщений (Datagram);
- быстрая доставка;
- без постоянного соединения между клиентом и сервером, без состояния;
- доставка сообщения и очередность доставки не гарантируется.
Для работы с UDP используется модуль gen_udp.
Давайте запустим две ноды и наладим общение между ними.
На 1-й ноде откроем UDP на порту 2000:
Вызываем gen_udp:open/2, передаем номер порта и список опций. Список всех возможных опций довольно большой, но нас интересуют две из них:
binary -- сокет открыт в бинарном режиме. Как вариант, сокет можно открыть в текстовом режиме, указав опцию list. Разница в том, как мы интерпретируем данные, полученные из сокета -- как поток байт, или как текст.
-- сокет открыт в активном режиме, значит данные, приходящие в сокет, будут посылаться в виде сообщений в почтовый ящик потока, владельца сокета. Подробнее об этом ниже.
На 2-й ноде откроем UDP на порту 2001:
И пошлем сообщение с 1-й ноды на 2-ю:
Вызываем gen_udp:send/4, передаем сокет, адрес и порт получателя, и само сообщение.
Адрес может быть доменным именем в виде строки или атома, или адресом IPv4 в виде кортежа из 4-х чисел, или адресом IPv6 в виде кортежа из 8 чисел.
На 2-й ноде убедимся, что сообщение пришло:
Сообщение приходит в виде кортежа .
Пошлем сообщение с 2-й ноды на 1-ю:
На 1-й ноде убедимся, что сообщение пришло:
Как видим, тут все просто.
Активный и пассивный режим сокета
И gen_udp, и gen_tcp, оба имеют одну важную настройку: режим работы с входящими данными. Это может быть либо активный режим , либо пассивный режим .
В активном режиме поток получает входящие пакеты в виде сообщений в своем почтовом ящике. И их можно получить и обработать вызовом receive, как любые другие сообщения.
Для udp сокета это сообщения вида:
мы их уже видели:
Для tcp сокета аналогичные сообщения:
Активный режим прост в использовании, но опасен тем, что клиент может переполнить очередь сообщений потока, исчерпать память и обрушить ноду. Поэтому рекомендуется пассивный режим.
В пассивном режиме данные нужно забрать самому вызовами gen_udp:recv/3 и gen_tcp:recv/3:
Здесь мы указываем, сколько байт данных хотим прочитать из сокета. Если там есть эти данные, то мы получаем их сразу. Если нет, то вызов блокируется, пока не придет достаточное количество данных. Можно указать Timeout, чтобы не блокировать поток надолго.
Однако, gen_udp:recv игнорирует аргумент Length, и возвращает все данные, которые есть в сокете. Или блокируется и ждет каких-нибудь данных, если в сокете ничего нет. Непонятно, зачем вообще аргумент Length присутствует в АПИ.
Для gen_tcp:recv аргумент Length работает как надо. Если только не указана опция , о которой речь пойдет ниже.
Еще есть вариант . В этом случае сокет запускается в активном режиме, получает первый пакет данных как сообщение, и сразу переключается в пассивный режим.
И с 17-й версии эрланг добавился вариант , где указывается количество пакетов, которые приходят в активном режиме, после которого сокет переключается в пассивный режим.
TCP сокет
Вспомним в общих чертах, что такое TCP:
- надежный протокол передачи данных, гарантирует доставку сообщения и очередность доставки;
- постоянное соединение клиента и сервера, имеет состояние;
- дополнительные накладные расходы на установку и закрытие соединения и на передачу данных.
Надо заметить, что долго держать постоянные соединения с многими тысячами клиентов накладно. Все соединения должны работать независимо друг от друга, а это значит -- в разных потоках. Для многих языков программирования (но не для эрланг) это серьезная проблема.
Именно поэтому так популярен протокол HTTP, который хоть и работает поверх TCP сокета, но подразумевает короткое время взаимодействия. Это позволяет относительно небольшим числом потоков (десятки-сотни) обслуживать значительно большее число клиентов (тысячи, десятки тысяч).
В некоторых случаях остается необходимость иметь долгоживущие постоянные соединения между клиентом и сервером. Например, для чатов или для многопользовательских игр. И здесь эрланг имеет мало конкурентов.
Для работы с TCP используется модуль gen_tcp.
Работать с TCP сокетом сложнее, чем с UDP. У нас появляются роли клиента и сервера, требующие разной реализации. Рассмотрим вариант реализации сервера.
Есть два вида сокета: Listen Socket и Accept Socket. Listen Socket один, он принимает все запросы на соединение. Accept Socket нужно много, по одному для каждого соединения. Поток, в котором создается сокет, становится владельцем сокета. Если поток-владелец завершается, то сокет автоматически закрывается. Поэтому для каждого сокета мы создаем отдельный поток.
Listen Socket должен работать всегда, а для этого его поток-владелец не должен завершаться. Поэтому в server/1 мы добавили вызов timer:sleep(infinity). Это заблокирует поток и не даст ему завершиться. Такая реализация, конечно, учебная. По хорошему нужно предусмотреть возможность корректно остановить сервер, а здесь этого нет.
Accept Socket и поток для него можно было бы создавать динамически, по мере появления клиентов. В начале можно создать один такой поток, вызвать в нем gen_tcp:accept/1 и ждать клиента. Этот вызов является блокирующим. Он завершается, когда появляется клиент. Дальше можно обслуживать текущего клиента в этом потоке, и создать новый поток, ожидающий нового клиента.
Но здесь у нас другая реализация. Мы заранее создаем пул из нескольких потоков, и все они ждут клиентов. После завершения работы с одним клиентом сокет не закрывается, а ждет нового. Таким образом, вместо того, чтобы постоянно открывать новые сокеты и закрывать старые, мы используем пул долгоживущих сокетов.
Это эффективнее при большом количестве клиентов. Во-первых, из-за того, что мы быстрее принимаем соединения. Во-вторых, из-за того, что мы более аккуратно распоряжаемся сокетами как системным ресурсом.
Потоки принадлежат эрланговской ноде, и мы можем создавать их сколько угодно. Но сокеты принадлежат операционной системе. Их количество лимитировано, хотя и довольно большое. (Речь идет о лимите на количество файловых дескрипторов, которое операционная система позволяет открыть пользовательскому процессу, обычно это 2 10 - 2 16).
Размер пула у нас игрушечный -- 5 пар поток-сокет. Реально нужен пул из нескольких сотен таких пар. Хорошо бы еще иметь возможность увеличивать и уменьшать этот пул в рантайме, чтобы подстраиваться под текущую нагрузку.
Текущая сессия с клиентом обрабатывается в функции handle_connection/2. Видно, что сокет работает в активном режиме, и поток получает сообщения вида , где Msg -- это бинарные данные, пришедшие от клиента. Эти данные мы отравляет обратно клиенту, то есть, реализуем банальный эхо-сервис :)
Когда клиент закрывает соединение, поток получает сообщение _, возвращается обратно в accept/2 и ждет следующего клиента.
Вот как выглядит работа такого сервера с двумя telnet-клиентами:
Сервер в пассивном режимеЭто все хорошо, но хороший сервер должен работать в пассивном режиме. То есть, он должен получать данные от клиента не в виде сообщений в почтовый ящик, а вызовом gen_tcp:recv/2,3.
Нюанс в том, что тут нужно указать, сколько данных мы хотим прочитать. А откуда сервер может знать, сколько данных ему прислал клиент? Ну, видимо, клиент сам должен сказать, сколько данных он собирается прислать. Для этого клиент сперва посылает небольшой служебный пакет, в котором указывает размер своих данных, и затем посылает сами данные.
Например, если клиент хочет послать данные <<"Hello">>, размер которых 5 байт, то он посылает сперва <<5>>, затем <<"Hello">>. Соответственно, сервер сперва читает этот служебный пакет, и по нему определяет, сколько данных нужно прочитать дальше.
Теперь нужно решить, сколько байт должен занимать этот служебный пакет. Если это будет 1 байт, то в него нельзя упаковать число больше 255. В 2 байта можно упаковать число 65535, в 4 байта 4294967295. 1 байт, очевидно, мало. Вполне вероятно, что клиенту будет нужно послать данных больше, чем 255 байт. Заголовок в 2 байта вполне подходит. Заголовок в 4 байта иногда бывает нужен.
Итак, клиент посылает служебный пакет размером в 2 байта, где указано, сколько данных последуют за ним, а затем сами эти данные:
Полный код клиента:
Сервер сперва читает 2 байта, определяет размер данных и затем читает все данные:
В коде сервера функции start/0 и start/1 не изменились, остальное немного поменялось:
Пример сессии со стороны клиента:
И со стороны сервера:
Все это хорошо, но на самом деле нет необходимости вручную разбираться с заголовочным пакетом. Это уже реализовано в gen_tcp. Нужно указать размер служебного пакета в настройках при открытии сокета на стороне клиента:
и на стороне сервера:
и необходимость самому формировать и разбирать эти заголовки пропадает.
На стороне клиента упрощается отправка:
и на стороне сервера упрощается получение:
Теперь при вызове gen_tcp:recv/2 мы указываем Length = 0. gen_tcp сам знает, сколько байт нужно прочитать из сокета.
Работа с текстовыми протоколамиКроме варианта со служебным заголовком, есть и другой подход. Можно читать из сокета по одному байту, пока не встретится специальный байт, символизирующий конец пакета. Это может быть нулевой байт, или символ перевода строки.
Такой вариант характерен для текстовых протоколов (SMTP, POP3, FTP).
Писать свою реализацию чтения из сокета нет необходимости, все уже реализовано в gen_tcp. Нужно только указать в настройках сокета вместо опцию .
В остальном код сервера остается без изменений. Но теперь мы можем опять вернуться к telnet-клиенту.
TCP-сервер, текстовый протокол и telnet-клиент нам понадобятся в курсовой работе.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты.
Нашли опечатку или неточность?Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.
Что-то не получается или материал кажется сложным?Загляните в раздел «Обсуждение»:
- задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
- расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
- изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Урок «Как эффективно учиться на Хекслете»
- Вебинар «Как самостоятельно учиться»
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.