
Немного о сетевом движке на Erlang
nightblaze
опубликовал в
Программирование
В данном уроке речь пойдет не о полноценном сетевом движке, а лишь о простой попытке «пощупать» язык и его возможности. Этого будет вполне достаточно, чтобы понять, использовать ли Erlang в своем проекте или поискать что-нибудь другое. Поэтому буду опускать многие моменты, связанные с языком программирования. Для тех же, кто заинтересован в его изучении советую книги Programming Erlang: Software for a Concurrent World и Erlang and OTP in Action. Так же если вы не способны потратить 5 или 10 минут на изучение исходного кода, то эта статья не для вас.
Не буду рассказывать о преимуществах Erlang. Лишь скажу, что это простой и мощный язык программирования, хоть и с достаточно необчным синтаксисом (на первый взгляд :) ).
Подготовка
Установка Erlang
Когда я в первый раз столкнулся с разработкой под Erlang, то у меня были небольшие затруднения с установкой, вызванные не знанием *unix на котором она проивозводилась. Вот небольшой туториал как поставить Erlang на Debian 6 Squezee.Rebar
Rebar — это инструмент для управления зависимостями, компиляции и тестирования Erlang приложений и релизов. Более подробно почитать о нем можно здесь. Так же по этой ссылке можно узнать как установить этот полезный инструмент.В бой!
Создадим папку для проекта и каркас сервера.mkdir myserver && cd myserver
rebar create-app appid=myserver
Команда create-app создаст три «основных» файла.
Теперь в корневой директории проекта создадим файл rebar.config. Внутри него будет всего одна строчка, которая означает, что наш проект использует фреймворк Ranch, который будет отвечать за работу с сетью.
{deps, [
{ranch, ".*", {git, "git://github.com/extend/ranch.git", {branch, "master"}}}
]}.
Сейчас структура приложения должна выглядеть так:

Пришла пора писать код.
Откройте в любом текстовом редакторе файл myserver_app.erl. В метод start/2 надо добавить строчки для запуска Ranch.
AcceptorCount = 100,
Port = 8080,
{ok, _} = ranch:start_listener(myserver_pool, AcceptorCount, ranch_tcp, [{port, Port}], myserver_sup, [])
Так же для удобства запуска, добавим метод start/0.
start() ->
application:start(ranch),
application:start(myserver).
Файл myserver_app.erl полностью
-module(myserver_app).
-behaviour(application).
-export([start/0, start/2, stop/1]).
start() ->
application:start(ranch),
application:start(myserver).
start(_StartType, _StartArgs) ->
AcceptorCount = 100,
Port = 8080,
{ok, _} = ranch:start_listener(myserver_pool, AcceptorCount,
ranch_tcp, [{port, Port}], myserver_sup, []),
myserver_sup:start_link().
stop(_State) ->
ok.
AcceptorCount определяет количество процессов, готовых принять соединение. Для каждого проекта это число должно быть своим и зависит от количества пользователей.
Теперь откройте файл myserver_sup.erl. Измените метод init/1, чтобы он выглядел так
init([]) ->
AChild = {myserver_connection_server, {myserver_connection_server, start_link, []},
temporary, 2000, worker, [myserver_connection_server]},
Children = [AChild],
RestartStrategy = {simple_one_for_one, 10, 10},
{ok, {RestartStrategy, Children}}.
И добавьте метод start_link/4.
start_link(Ref, Socket, Transport, Opts) ->
supervisor:start_child(?MODULE, [Ref, Socket, Transport, Opts]).
Файл myserver_sup.erl полностью
-module(myserver_sup).
-behaviour(supervisor).
-export([start_link/0, start_link/4]).
-export([init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
start_link(Ref, Socket, Transport, Opts) ->
supervisor:start_child(?MODULE, [Ref, Socket, Transport, Opts]).
init([]) ->
AChild = {myserver_connection_server, {myserver_connection_server, start_link, []},
temporary, 2000, worker, [myserver_connection_server]},
Children = [AChild],
RestartStrategy = {simple_one_for_one, 10, 10},
{ok, {RestartStrategy, Children}}.
Создайте новый файл myserver_connection_server.erl и поместите его в папку src. Он будет отвечать за работу с каждым конкретным соединением. Фактически каждый процесс myserver_connection_server — это пользователь в онлайне. Он достаточно объемный (по сравнению с другими), поэтому приведу сразу весь исходный код.
myserver_connection_server.erl
-module(myserver_connection_server).
-behaviour(gen_server).
-behaviour(ranch_protocol).
-export([stop/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([start_link/4]).
-define(SERVER, ?MODULE).
-record(state, {type = undefined, ref, socket, transport, opts}).
stop() ->
gen_server:cast(self(), stop).
start_link(Ref, Socket, Transport, Opts) ->
gen_server:start_link(?MODULE, [Ref, Socket, Transport, Opts], []).
init(Args) ->
[Ref, Socket, Transport, Opts] = Args,
{ok, #state{type = accept_client, ref = Ref, socket = Socket, transport = Transport, opts = Opts}, 0}.
handle_call(Request, _From, State) ->
io:format("Unknown handle_call request in module '~p'. Request = ~w~n", [?MODULE, Request]),
Reply = ok,
{reply, Reply, State}.
handle_cast(Request, State) ->
case Request of
stop ->
{stop, normal, State};
_ ->
io:format("Unknown handle_cast request in module '~p'. Request = ~w~n", [?MODULE, Request]),
{noreply, State}
end.
handle_info(Info, State) ->
Socket = State#state.socket,
Transport = State#state.transport,
{OK, Closed, Error} = Transport:messages(),
{RemoteIP, RemotePort} = get_ip_and_port(Transport, Socket),
case Info of
timeout ->
Type = State#state.type,
case Type of
accept_client ->
Ref = State#state.ref,
ok = ranch:accept_ack(Ref),
ok = Transport:setopts(Socket, [{active, once}]),
io:format(">>> Client did connect ~w:~w ('~w'). ~n", [RemoteIP, RemotePort, self()]),
send_data_to_client(State, [<<60, 60, 60, " Hello! Your name is ">>, erlang:pid_to_list(self()), <<"\r\n">>]),
NewState = State#state{type = undefined},
{noreply, NewState};
_ ->
io:format("Unknown handle_info #state.type in module '~p'. #state.type = ~w~n", [?MODULE, Type]),
{noreply, State}
end;
{OK, Socket, Data} ->
ok = Transport:setopts(Socket, [{active, once}]),
io:format(">>> Data received from ~w:~w ('~w'). Data = ~p~n", [RemoteIP, RemotePort, self(), Data]),
parse_data(Data),
{noreply, State};
{Closed, Socket} ->
io:format(">>> Client '~w' did disconnected.~n", [self()]),
{stop, normal, State};
{Error, Socket, Reason} ->
io:format(">>> Error happend. Client ~w:~w ('~w'). Reason = ~p~n", [RemoteIP, RemotePort, self(), Reason]),
ok = Transport:setopts(Socket, [{active, once}]),
{noreply, State};
_ ->
io:format("Unknown handle_info info. Info = ~w~n", [Info]),
{noreply, State}
end.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
send_data_to_client(State, Packet) ->
Socket = State#state.socket,
Transport = State#state.transport,
case Transport:send(Socket, Packet) of
ok ->
ok;
Error ->
{RemoteIP, RemotePort} = get_ip_and_port(Transport, Socket),
case Error of
{error, closed} ->
io:format(">>> Client '~w' did disconnected.~n", [self()]),
stop();
_ ->
io:format("Unknown error send data to ~w:~w ('~w'). Error = ~w. Data = ~p~n", [RemoteIP, RemotePort, self(), Error, Packet])
end,
ok
end.
get_ip_and_port(Transport, Socket) ->
PeerName = Transport:peername(Socket),
{RemoteIP, RemotePort} = case PeerName of
{ok, {Address, Port}} ->
{Address, Port};
_ ->
{unknown_ip, unknown_port}
end,
{RemoteIP, RemotePort}.
parse_data(Data) ->
F = fun() ->
<<Type:8, CommandCode:16, Parameters/binary>> = Data,
{Type, CommandCode, Parameters}
end,
try F() of
{Type, CommandCode, Parameters} ->
io:format(">>> Get request of type '~p'. Command code '~p'. Parameters '~p'~n", [Type, CommandCode, Parameters]),
ok
catch
_:_ ->
ok
end,
ok.
Перед финальным шагом немного расскажу что тут происходит.
Когда к серверу подключается пользователь, то вызывается метод start_link/4 модуля myserver_sup. Этот супервизор создает gen_server для каждого пользователя. Т.е. мы можем асинхронно обрабатывать все запросы и отвечать на них.
Метод init/1 в модуле myserver_connection_server синхронный. Чтобы не блокировать процессы, метод возвращает кортеж {ok, State, Timeout}, где Timeout — это число милисекунд, через которое вызовется метод handle_info/2. При этом параметр Info будет равен атому timeout. Именно в нем и производим дальнейшую инициализацию. Принимаем клиента (ok = ranch:accept_ack(Ref)) и отправляем ему сообщение (send_data_to_client(...)).
Самое интересное происходит в методе parse_data/1. С помощью pattern matching с легкостью распарсиваются пришедшие от клиента данные. Должен заметить, что тут мы используем свой протокол обмена данными (очень простой). Т.е. каждое сообщение имеет формат
Type: 1 байт, CommandCode: 2 байта, Parameters: остальное
Type может служить каким-нибудь флагом. CommandCode выступает в качестве названия метода API.
О всей мощи bit syntax можно почитать тут или тут.
Теперь осталось создать только make файл, чтобы было удобно запускать приложение. Он должен располагаться в корне приложения.
Makefile
REBAR = `which rebar`
REBAR_GET_DEPS = $(REBAR) get-deps
REBAR_UPDATE_DEPS = $(REBAR) update-deps
REBAR_COMPILE = $(REBAR) compile
REBAR_CLEAN = $(REBAR) clean
all: deps compile
deps:
@( $(REBAR_GET_DEPS) )
update:
@( $(REBAR_CLEAN) $(REBAR_UPDATE_DEPS) $(REBAR_COMPILE) )
compile: clean
@( $(REBAR_COMPILE))
clean:
@( $(REBAR_CLEAN) )
run:
@(erl -pa ebin deps/*/ebin \
-s application start myserver)
.PHONY: all deps update compile clean run
Теперь выполняем
make && make run
и подключаемся к серверу
telnet localhost 8080
2 комментария
Для новичков она непонятна. Возникают вопросы, наподобие таких: почему выбран именно erlang, в чём его преимущества перед тем же apache/php или nodejs? Зачем использовать Ranch, какие ещё есть фреймворки и почему выбор пал именно на него?
Для тех, кто в теме, она тоже принесёт мало пользы — они это уже и так знают.
Если рассматривать статью как некий туториал по написанию сервера, то тогда нужно бы расписать вкратце процесс взаимодействия клиента с сервером, что нужно подавать на входе и что ожидается на выходе. Ну и в идеале, было бы хорошо описать практическое применение данного кода… :)
Статью лайкнул, жду ещё =)
Nodejs немного щупал. Не в восторге от его callback ада. Плюс нет автоматической поддержки нескольких ядер из коробки (возможно уже есть. смотрел давно).
Erlang выбрал, потому что давно хотел выучить функциональный язык. Плюс полная распределенность, отказоустойчивость и… легкость :) Вообще, как уже писал, статья не для тех кто не может при желании нагуглить такую информацию.
Erlang имеет одну особенность. От новичка до достаточно продвинутого программиста на этом языке проходит очень мало времени. Так что эта статья для новичков, любящих покопаться. Не люблю статьи, в которых все можно копипастить от а до я.
Вот тут полностью с тобой согласен. Все дело в том, что когда писал статью, у меня начал сыпаться винт (вышла из строя механика, винт начал неприятно трещать). Бэкапы все есть, поэтому особо не беспокоился. Не статья все равно вышла скомканой.