По мотивам недавнего поста про изучение новых языков, я таки добил версию на 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
Вывод: Эрланг - прекрасный вариант для начала функциональной карьеры.
Посты по теме: