29 января 2011

Двадцать бит от Джесси Фармера. Ерланг: Базовый TCP сервер

Данный документ является переводом статьи Джесси Фармера: Базовый TCP сервер.

В последних моих статьях об Ерланге мы рассмотрели основы сетевого программирования с использованием модуля gen_tcp и Ерланг/OTP (Открытая телекоммуникационная платформа) gen_server. Давайте объединим эти два понятия. Прим. переводчика: ссылки на эти статьи не работали.
В умах большинства людей "сервер" означает сетевой сервер, но Ерланг использует этот термин наиболее абстрактно. gen_server действительно сервер, который использует механизм передачи сообщений Ерланга в качестве протокола. Мы можем привить TCP сервер к поведению gen_server, но для этого нужно немного потрудиться.

Структура Сетевого Сервера

Большинство серверов сети имеют одинаковую архитектуру. Сначала они создают сокет, который прослушивает некоторый порт на некотором интерфейсе. Потом они принимают соединения, и делают это в цикле, пока их кто-нибудь не завершит.

Чтобы увидеть это в действии. Напомню простой эхо-сервер из моей статьи о программировании для сети.


-module(echo).
-author(‘Jesse E.I. Farmer <jesse@20bits.com>’).
-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.


Как Вы заметили, функция listen создает сокет и немедленно вызывает функцию accept. Последняя ждет входящего подключения, порождает новый процесс с «рабочей» функцией (loop), которая и делает всю работу по приему/передаче, и далее вновь ожидает входящего подключения.

В этом коде родительский процесс является владельцем как слушающего сокета, так и принимающего подключения цикла. Вы узнаете, что не работает не так хорошо, когда мы попытаемся интегрировать accept/listen цикл с gen_server.

Абстрагирование Сетевого Сервера

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

-module(my_server).
start(Port) ->
    connection_handler:start(my_server, Port, business_logic).

business_logic(Socket) ->
% Read data from the network socket and do our thang!
% Чтение данных из сетевого сокета и их обработка

Давайте пойдем дальше и сделаем именно это.

Реализация Обычного Сетевого Сервера

Проблема при реализации сетевого сервера с помощью поведения gen_server заключается в том, что функция gen_tcp:accept является блокирующей. Например, если мы вызовем ее в инициализационной процедуре, то весь механизм поведения gen_server будет заблокирован пока хоть один клиент не подключиться.

Есть два способа обойти эту проблему. Один предполагает низкоуровневый механизм соединения, который поддерживает неблокирующий (или асинхронный) вызов accept. Существует целое семейство функций, в первую очередь gen_tcp:controlling_process, которые помогают Вам управлять процессом: «Кто и какие сообщения получает при подключении клиента».

Более простое, и на мой взгляд, изящное решение — это использовать один процесс, которому принадлежит сокет. Этот процесс делает две вещи: порождает новых «слушателей» и ожидает сообщения «соединение установлено». Когда он получает сообщение, он знает, что нужно создать нового «слушателя».
Прим. переводчика: данный механизм применен в postgresql сервере, для процессного (не-потокового) распараллеливания подключений. Процессным распараллеливанием, но уже пула клиентских сокетов, занимаются для google chrome.

«Слушатель» волен применять блокирующую gen_tcp:accept, поскольку он работает в своем собственном процессе. Когда он принимает соединение, он отсылает асинхронное сообщений родительскому процессе и тотчас вызывает функцию бизнес-логики.

Вот код. Где уместно я его прокомментировал, так что надеюсь, что анализирование не составит труда.


-module(socket_server).
-author(‘Jesse E.I. Farmer <jesse@20bits.com>’).
-behavior(gen_server).

-export([init/1, code_change/3, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
-export([accept_loop/1]).
-export([start/3]).

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

-record(server_state, {
        port,
        loop,
        ip=any,
        lsocket=null}).

start(Name, Port, Loop) ->
    State = #server_state{port = Port, loop = Loop},
    gen_server:start_link({local, Name}, ?MODULE, State, []).

init(State = #server_state{port=Port}) ->
    case gen_tcp:listen(Port, ?TCP_OPTIONS) of
        {ok, LSocket} ->
            NewState = State#server_state{lsocket = LSocket},
            {ok, accept(NewState)};
        {error, Reason} ->
            {stop, Reason}
    end.

handle_cast({accepted, _Pid}, State=#server_state{}) ->
    {noreply, accept(State)}.

accept_loop({Server, LSocket, {M, F}}) ->
    {ok, Socket} = gen_tcp:accept(LSocket),
    % Let the server spawn a new process and replace this loop
    % with the echo loop, to avoid blocking
    % Просим главный процесс создать нового "слушателя" и вызываем цикл обработки данных,
    % чтобы избежать блокировки  
    gen_server:cast(Server, {accepted, self()}),
    M:F(Socket).
    
% To be more robust we should be using spawn_link and trapping exits
% Для большей безопасности мы должны использовать spawn_link и обрабатывать ошибки
accept(State = #server_state{lsocket=LSocket, loop = Loop}) ->
    proc_lib:spawn(?MODULE, accept_loop, [{self(), LSocket, Loop}]),
    State.

% These are just here to suppress warnings.
% Это здесь только для того чтобы скрыть предупреждения
handle_call(_Msg, _Caller, State) -> {noreply, State}.
handle_info(_Msg, Library) -> {noreply, Library}.
terminate(_Reason, _Library) -> ok.
code_change(_OldVersion, Library, _Extra) -> {ok, Library}.


Мы используем gen_server:cast для передачи асинхронных сообщений в главный процесс. Когда главный процесс получает сообщение «соединение установлено» он создает нового «слушателя».

На данный момент этот сервер не очень надежный, потому что если текущий «слушатель» перестанет работать по какой-либо причине, сервер перестанет принимать входящие подключения. Чтобы сделать его более OTP'нутым мы должны обрабатывать ошибки «слушателя», создавая нового в случае поломки предыдущего.

«Базовый» Эхо-Сервер
Эхо-Сервер является простейшим для написания сервером, так что давайте сделаем это с помощью нашего нового абстрактного сокет-сервера.

-module(echo_server).
-author(‘Jesse E.I. Farmer <jesse@20bits.com>’).

-export([start/0, loop/1]).

% echo_server specific code
% Специфичный код эхо-сервера
start() ->
    socket_server:start(?MODULE, 7000, {?MODULE, loop}).
loop(Socket) ->
    case gen_tcp:recv(Socket, 0) of
        {ok, Data} ->
            gen_tcp:send(Socket, Data),
            loop(Socket);
        {error, closed} ->
            ok
    end.


Как Вы уже заметили, «сервер» не содержит ничего лишнего, кроме бизнес-логики. Работа с соединениями была обобщена и реализована в socket_server'е. Цикл в нашем базовом сервере фактически также совпадает с циклом в нашей оригинальном Эхо-Сервере.

Надеюсь, вы смогли понять все то, что я сделал. Я, наконец, чувствую, что я начинаю понимать Ерланг.

Кроме того, не стесняйтесь оставлять комментарии, особенно если у вас есть какие-то мысли о том, как я могу улучшить свой код. Здоровья Вам.

Статьи по теме
Erlang: A Generic Server Tutorial
Learning Erlang
Erlang: An Introduction to Records
An EventMachine Tutorial
Network Programming in Erlang
Создание неблокирующего TCP сервера с использованием принципов OTP 

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

  1. Очень хорошая статья, но что значит M:F(Socket)?

    ОтветитьУдалить
    Ответы
    1. А, все разобрался. Это для бизнес логики, неплохо.
      Спасибо за перевод)

      Удалить