TCP/IP proxy на Erlang'e

По мотивам недавнего поста про изучение новых языков, я таки добил версию на Erlang’е. Если тут есть спецы по языку, буду признателен за критику.

Программа по функциям идентична версии на Питоне за исключением отсутствия дублирования лога в файл и продвинутого разбора флагов командной строки.

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

Про Эрланг. После многократных и пока полностью неуспешных заходов на Хаскелл и после все еще неудачных попыток на Lisp или Scheme написать что-то более менее реальное и жизненное, Эрланг был реальным прорывом для меня.

Удивительно, невозможность изменять переменные (представьте, что программируя на С++ надо все переменные делать const) является фантастическим способом борьбы с опечатками при cut-and-paste. Также когда делаешь циклы через хвостовую рекурсию, сразу осознаешь, как эффективно работать со списками, чтобы их не копировать, а всегда “таскать” за хвост или голову.

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

Например, истересен способ реализации многопотокового TCP/IP сервера. Обычно при его программировании есть распростраенный прием: один главный поток, принимающий соединения, и когда соединение принято, создается новый поток-исполнитель, который обрабатывает соединение и после этого умирает.

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

Для меня это было немного необычно.

-module(tcp_proxy).

-define(WIDTH, 16).

main([ListenPort, RemoteHost, RemotePort]) ->
    ListenPortN = list_to_integer(ListenPort),
    start(ListenPortN, RemoteHost, list_to_integer(RemotePort));

main(_) -> usage().

usage() ->
    io:format("~ntcp_proxy.erl local_port remote_port remote_host~n~n", []),
    io:format("Example:~n~n", []),
    io:format("tcp_proxy.erl 50000 google.com 80~n~n", []).

start(ListenPort, CalleeHost, CalleePort) ->
    io:format("Start listening on port ~p and forwarding data to ~s:~p~n",
              [ListenPort, CalleeHost, CalleePort]),
    {ok, ListenSocket} = gen_tcp:listen(ListenPort, [binary, {packet, 0},
                                                    {reuseaddr, true},
                                                    {active, true}]),
    io:format("Listener socket is started ~s~n", [socket_info(ListenSocket)]),
    spawn(fun() -> acceptor(ListenSocket, CalleeHost, CalleePort, 0) end),
    register(logger, spawn(fun() -> logger() end)),
    wait().

% Infinine loop to make sure that the main thread doesn't exit.
wait() -> receive _ -> true end, wait().

format_socket_info(Info) ->
    {ok, {\{A, B, C, D}, Port}} = Info,
    lists:flatten(io_lib:format("~p.~p.~p.~p:~p", [A, B, C, D, Port])).

peer_info(Socket) -> format_socket_info(inet:peername(Socket)).

socket_info(Socket) -> format_socket_info(inet:sockname(Socket)).

acceptor(ListenSocket, RemoteHost, RemotePort, ConnN) ->
    case gen_tcp:accept(ListenSocket) of
      {ok, LocalSocket} ->
          spawn(fun() -> acceptor(ListenSocket, RemoteHost, RemotePort, ConnN + 1) end),
          LocalInfo = peer_info(LocalSocket),
          logger ! {message, "~4.16.0B: Incoming connection from ~s~n", [ConnN, LocalInfo]},
          case gen_tcp:connect(RemoteHost, RemotePort, [binary, {packet, 0}]) of
            {ok, RemoteSocket} ->
              RemoteInfo = peer_info(RemoteSocket),
              logger ! {message, "~4.16.0B: Connected to ~s~n", [ConnN, RemoteInfo]},
              exchange_data(LocalSocket, RemoteSocket, LocalInfo, RemoteInfo, ConnN, 0),
              logger ! {message, "~4.16.0B: Finished~n", [ConnN]};
            {error, Reason} ->
              logger ! {message, "~4.16.0B: Unable to connect to ~s:~s (error: ~p)~n",
                       [ConnN, RemoteHost, RemotePort, Reason]}
          end;
      {error, Reason} ->
          logger ! {message, "Socket accept error '~w'~n", [Reason]}
    end.

exchange_data(LocalSocket, RemoteSocket, LocalInfo, RemoteInfo, ConnN, PacketN) ->
    receive
        {tcp, RemoteSocket, Bin} ->
            logger ! {received, ConnN, Bin, RemoteInfo, PacketN},
            gen_tcp:send(LocalSocket, Bin),
            logger ! {sent, ConnN, LocalInfo, PacketN},
            exchange_data(LocalSocket, RemoteSocket, LocalInfo, RemoteInfo, ConnN, PacketN + 1);
        {tcp, LocalSocket, Bin} ->
            logger ! {received, ConnN, Bin, LocalInfo, PacketN},
            gen_tcp:send(RemoteSocket, Bin),
            logger ! {sent, ConnN, RemoteInfo, PacketN},
            exchange_data(LocalSocket, RemoteSocket, LocalInfo, RemoteInfo, ConnN, PacketN + 1);
        {tcp_closed, RemoteSocket} ->
            logger ! {message, "~4.16.0B: Disconnected from ~s~n", [ConnN, RemoteInfo]};
        {tcp_closed, LocalSocket} ->
            logger ! {message, "~4.16.0B: Disconnected from ~s~n", [ConnN, LocalInfo]}
    end.

logger() ->
    receive
        {received, Pid, Msg, From, PacketN} ->
            io:format("~4.16.0B: Received (#~p) ~p byte(s) from ~s~n",
                      [Pid, PacketN, byte_size(Msg), From]),
            dump_bin(Pid, Msg),
            logger();
        {sent, Pid, ToSocket, PacketN} ->
            io:format("~4.16.0B: Sent (#~p) to ~s~n", [Pid, PacketN, ToSocket]),
            logger();
        {message, Format, Args} ->
            io:format(Format, Args),
            logger()
    end.

dump_list(Prefix, L, Offset) ->
    {H, T} = lists:split(lists:min([?WIDTH, length(L)]), L),
    io:format("~4.16.0B: ", [Prefix]),
    io:format("~4.16.0B: ", [Offset]),
    io:format("~-*s| ", [?WIDTH * 3, dump_numbers(H)]),
    io:format("~-*s", [?WIDTH, dump_chars(H)]),
    io:format("~n", []),
    if length(T) > 0 -> dump_list(Prefix, T, Offset + 16); true -> [] end.

dump_numbers(L) when (is_list(L)) ->
    lists:flatten([io_lib:format("~2.16.0B ", [X]) || X <- L]).

dump_chars(L) ->
    lists:map(fun(X) ->
                if X >= 32 andalso X < 128 -> X;
                   true -> $.
                end
              end, L).

dump_bin(Prefix, Bin) ->
    dump_list(Prefix, binary_to_list(Bin), 0).

В работе может выводить примерно следующее:

alexander:erlang/>./tcp_proxy.sh 50000 pop.yandex.ru 110
Start listening on port 50000 and forwarding data to pop.yandex.ru:110
Listener socket is started 0.0.0.0:50000
0000: Incoming connection from 127.0.0.1:51402
0000: Connected to 213.180.204.37:110
0000: Received (#0) 38 byte(s) from 213.180.204.37:110
0000: 0000: 2B 4F 4B 20 50 4F 50 20 59 61 21 20 76 31 2E 30 | +OK POP Ya! v1.0
0000: 0010: 2E 30 6E 61 40 32 35 20 67 55 62 44 54 51 64 5A | .0na@25 gUbDTQdZ
0000: 0020: 6D 6D 49 31 0D 0A                               | mmI1..
0000: Sent (#0) to 127.0.0.1:51402
0000: Received (#1) 11 byte(s) from 127.0.0.1:51402
0000: 0000: 55 53 45 52 20 74 65 73 74 0D 0A                | USER test..
0000: Sent (#1) to 213.180.204.37:110
0000: Received (#2) 23 byte(s) from 213.180.204.37:110
0000: 0000: 2B 4F 4B 20 70 61 73 73 77 6F 72 64 2C 20 70 6C | +OK password, pl
0000: 0010: 65 61 73 65 2E 0D 0A                            | ease...
0000: Sent (#2) to 127.0.0.1:51402
0000: Received (#3) 11 byte(s) from 127.0.0.1:51402
0000: 0000: 50 41 53 53 20 70 61 73 73 0D 0A                | PASS pass..
0000: Sent (#3) to 213.180.204.37:110
0000: Received (#4) 72 byte(s) from 213.180.204.37:110
0000: 0000: 2D 45 52 52 20 5B 41 55 54 48 5D 20 6C 6F 67 69 | -ERR [AUTH] logi
0000: 0010: 6E 20 66 61 69 6C 75 72 65 20 6F 72 20 50 4F 50 | n failure or POP
0000: 0020: 33 20 64 69 73 61 62 6C 65 64 2C 20 74 72 79 20 | 3 disabled, try
0000: 0030: 6C 61 74 65 72 2E 20 73 63 3D 67 55 62 44 54 51 | later. sc=gUbDTQ
0000: 0040: 64 5A 6D 6D 49 31 0D 0A                         | dZmmI1..
0000: Sent (#4) to 127.0.0.1:51402
0000: Disconnected from 213.180.204.37:110
0000: Finished
0001: Incoming connection from 127.0.0.1:51405
0001: Connected to 213.180.204.37:110
0001: Received (#0) 38 byte(s) from 213.180.204.37:110
0001: 0000: 2B 4F 4B 20 50 4F 50 20 59 61 21 20 76 31 2E 30 | +OK POP Ya! v1.0
0001: 0010: 2E 30 6E 61 40 33 30 20 70 55 62 41 72 52 33 74 | .0na@30 pUbArR3t
0001: 0020: 6A 65 41 31 0D 0A                               | jeA1..
0001: Sent (#0) to 127.0.0.1:51405
0001: Received (#1) 6 byte(s) from 127.0.0.1:51405
0001: 0000: 51 55 49 54 0D 0A                               | QUIT..
0001: Sent (#1) to 213.180.204.37:110
0001: Received (#2) 20 byte(s) from 213.180.204.37:110
0001: 0000: 2B 4F 4B 20 73 68 75 74 74 69 6E 67 20 64 6F 77 | +OK shutting dow
0001: 0010: 6E 2E 0D 0A                                     | n...
0001: Sent (#2) to 127.0.0.1:51405
0001: Disconnected from 213.180.204.37:110
0001: Finished

Вывод: Эрланг - прекрасный вариант для начала функциональной карьеры.

Посты по теме:


Оригинальный пост | Disclaimer

Комментарии