01 февраля 2011

20 бит от Джесси Фармера. Сетевое программирование в Эрланге.

2 мая 2008 года Так как я учусь Эрлангу, я подумал, что мой первый нетривиальный кусочек кода будет касаться отличительных особенностей языка: сетевого программирования.

Сетевое программирование (или программирование сокетов) является занозой в жопе в большинстве языков. Я впервые узнал, как это сделать в Си прочитав Руководство Сетевого Программирования от Beej. Прочтите и Вы, если осмелитесь.

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

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

Я не хотел писать какие-либо пользовательские приложения в ближайшее время, но я подумал: «Если я собираюсь учить Эрланг, сначала я должен узнать его сильные стороны».

С этой целью я решил попробовать повторить набор классических демонов Юникс: echo и chargen.

Echo

Echo — это сервис, которая точь-в-точь возвращает назад все присланные по TCP данные. Код на Эрланге:

-module(echo).
-author(‘Jesse E.I. Farmer ’).

-export([listen/1]).

-define(TCP_OPTIONS, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]).

% Call echo:listen(Port) to start the service.
% Вызываем echo:listen(Port) для запуска сервиса.
listen(Port) ->
    {ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
    accept(LSocket).

% Wait for incoming connections and spawn the echo loop when we get one.
% Ожидаем входящее соединение и запускаем процесс для обработки клиента.
accept(LSocket) ->
    {ok, Socket} = gen_tcp:accept(LSocket),
    spawn(fun() -> loop(Socket) end),
    accept(LSocket).

% Echo back whatever data we receive on Socket.
% Возвращаем все присланные данные обратно в сокет.
loop(Socket) ->
    case gen_tcp:recv(Socket, 0) of
        {ok, Data} ->
            gen_tcp:send(Socket, Data),
            loop(Socket);
        {error, closed} ->
            ok
    end.


Мы можем запустить сервис с помощью вызова echo:listen(<порт>) из оболочки Эрланга, например, echo:listen(8888) запустит echo сервер на порту 8888 на Вашем компьютере. Вы можете подключиться с помощью telnet к порту 8888 — telnet 127.0.0.1 8888 — и наблюдать результат.

Вот описание программы по функциям.
listen(Port)
Создает сокет, ожидающий входящих соединений на порту Port и отдающий контроль функции accept. accept(LSocket)
Ожидает входящие подключения на сокете LSocket. Как только она получает входящее соединение, она создает новый процесс, который запускает цикличную функцию, а потом ждет следующего соединения.
loop(Socket) Ожидает данные для сокета Socket. Как только получает данные, немедленно отсылает их обратно через сокет. Если происходит ошибка, завершает свою работу.

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

Порождение процессов
Процессы в Эрланге являются основным типом данных. Они следуют «актор» модели параллельных вычислений и делают сетевые процессы простыми.

Мы создаем новые процессы функцией spawn, которая принимает Fun, или объект-функцию в качестве параметра. Вы можете считать параметр обычной функцией. Контроль созданного процесса передается этому объекту-функции.

Объекты-функции.

Эрланг, будучи функциональным языком программирования, поддерживает тип «функция». Функции могут создавать новые функции, возвращать их, изменять.

Синтаксис для создания нового объекта-функции выглядит примерно так:
MyFunction = fun(…) ->
    % Your Erlang code here
    % Здесь Ваш Эрланг код
    end.


Chargen

Chargen это сервис, который возвращает поток символов при подключении к нему. Вы можете прочесть их все, но это не интересно.
Код:

-module(chargen).
-author(‘Jesse E.I. Farmer ’).

-export([listen/1]).

-define(START_CHAR, 33).
-define(END_CHAR, 127).
-define(LINE_LENGTH, 72).

-define(TCP_OPTIONS, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]).

% Call chargen:listen(Port) to start the service.
% Вызов chargen:listen(Port) для запуска сервера.
listen(Port) ->
    {ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
    accept(LSocket).

% Wait for incoming connections and spawn the chargen loop when we get one.
% Ожидаем входящее соединение и запускаем процесс для обработки клиента.
accept(LSocket) ->
    {ok, Socket} = gen_tcp:accept(LSocket),
    spawn(fun() -> loop(Socket) end),
    accept(LSocket).

loop(Socket) ->
    loop(Socket, ?START_CHAR).

loop(Socket, ?END_CHAR) ->
    loop(Socket, ?START_CHAR);
loop(Socket, StartChar) ->
    Line = make_line(StartChar),
    case gen_tcp:send(Socket, Line) of
        {error, _Reason} ->
            exit(normal);
        ok ->
            loop(Socket, StartChar+1)
    end.

make_line(StartChar) ->
    make_line(StartChar, 0).

% Generate a new chargen line — [13, 10] is CRLF.
% Генерирует новую строку — [13, 10] - CRLF
make_line(_, ?LINE_LENGTH) ->
    [13, 10];
make_line(?END_CHAR, Pos) ->
    make_line(?START_CHAR, Pos);
make_line(StartChar, Pos) ->
    [StartChar | make_line(StartChar + 1, Pos + 1)].


Как и в случае с echo сервером мы можем запустить сервис chargen в оболочке Эрланга с помощью chargen:listen(8888) на 8888 порту (или на другом порту на Ваш выбор).

accept и listen такие же, как и в echo сервисе, он есть некоторые различия в другом:
loop(Socket, StartChar)
Вызов make_line(StartChar), чтобы получить CHARGEN строку начинающуюся с StartChar, запись данной строки в сокет и переход к следующей строке.
make_line(StartChar, Pos)
Рекурсивно генерирует CHARGEN строку, отслеживает текущую позицию с помощью Pos.
Есть еще несколько концептуальных различий.

«Макросы»

В Эрланге как и в Си можно определить постоянные с помощью директивы -define. Они раскрываются во время компиляции. Вы можете ссылаться на них поставив перед именем знак вопроса «?». Таким образом они отличаются от переменных.

Вызов функции по шаблону


Как и с функцией присваивания, вызов функции происходит по шаблону. При вызове функция ищет первое совпадение с шаблоном. Например, если мы вызовем loop(Socket) она найдет подходящее определение, а именно, определение с одним параметром.

Мы можем изменить аргументы, которые влияют на поведение функции loop. ?END_CHAR равен 127, поэтому если мы возовем loop(Socket, 127) мы попадем в функцию, сигнатура которой полностью совпадает и вторым параметром у которой указан символ 127.

make_line работает по такому же принципу. Если мы находимся на последней позиции в строке мы возвращаем символы возврата каретки и перевода строки и останавливаем рекурсию.

Заключение

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

Статьи по Теме

Erlang: A Generalized TCP Server
Erlang: An Introduction to Records
Erlang: A Generic Server Tutorial
Learning Erlang

2 комментария:

  1. Поправте шаблон блогера что бы код не вылазил за тело 'post' (ну или воспользуйтесь моим tips'ом Включение исходных текстов программ в блоги blogger. )

    ОтветитьУдалить
  2. Спасибо, поправил.

    P.S. Не уверен правда, что я дружу с html/css.

    ОтветитьУдалить