Erlangで簡易的なTCP Proxyを作った

どうもこんにちは。久々に記事書きます。 サボりにサボってやく一年ぶりぐらい(正確には11ヶ月ぶりぐらい)でしょうか。「ちゃんと更新しないとなぁ」っと思いつつこれぐらいに月日が立ってしまったのでこれからは真面目に更新しようと思います!

ErlangでのProxy

では早速本題に入っていきたいと思います。最近趣味でElixir、Erlangを触ることが多く、よく書いてます。そこでErlangでHTTPのProxyを作りたいと思い作り始めたのでました。まずは簡易的なTCPのProxyを作ったのでまとめたいと思います。

動作環境

実装内容

rebar3でアプリケーションの作成

まずはrebar3でアプリケーションを作成します。

rebar3 new app sample_proxy

作成したら、まずはメインとなるProxyのプロセスを作成します。

Proxyプロセスの初期化

まずはProxyプロセスの初期化処理を行っていきます。

start_link(Receive, Destination) ->
  init([Receive, Destination]).

init([Receive, Destination]) ->
  {ok, ListenSocket} = gen_tcp:listen(Receive,
      [binary, {active, false}, {reuseaddr, true}, {backlog, 64}]
  ),
  spawn_link(?MODULE, accept, [ListenSocket, Destination]),
  {ok, []}.

ここで、TCPサーバで待ち受けポートでlistenします。そしてここで新しくプロセスを作成します。 (start_linkを中継している理由はgen_serverとかとインターフェイスを揃えるためです) Receiveにはフロント側となるクライアントからのリクエストを待ち受けるポートを指定します。Destinationには転送先となるサーバ情報を渡します。

TCPリクエストのaccept

では新しく生成したプロセス内を見ていきます。

accept(ListenSocket, Destination) ->
  case gen_tcp:accept(ListenSocket) of
    {ok, Socket} ->
      BackId = spawn_link(?MODULE, proxy_accept, [Destination, self()]),
      loop(Socket, BackId);
    {error, closed} ->
      ok;
    {error, Reson} ->
      io:format("fail accept ~p~n", [Reson])
  end,
  spawn(?MODULE, accept, [ListenSocket, Destination]).

gen_tcp:acceptでListenSocketでlistenしているポートに対してアクセスが来るまで待機します。ここでリクエストが来るとcaseブロック内の処理が実行されます。 {ok, Socket}となると正しいリクエストとなり、転送先のサーバへ接続をするプロセスを生成します。 その後はループ処理。実際のメイン処理となります。

転送先のサーバと接続するプロセス

転送先へ接続するためのプロセスをaccept後に生成しているのでその中身を見ていきたいと思います。

proxy_accept([{host, Host}, {port, Port}], FrontId) ->
  {ok, Con} = gen_tcp:connect(Host, Port, [binary, {packet, 0}]),
  loop(Con, FrontId).

転送先のホストとポートを指定し、gen_tcp:connectで接続を行います。 その後に、接続したコネクション情報とacceptしたプロセスIDをもとにループ処理へ入ります。

リクエストを待受、送信する

ではここからがメイン、loop関数の中を見ていきたいと思います。

loop(Socket, SendPid) ->
  inet:setopts(Socket, [{active, once}]),

  receive
    {tcp, Socket, Message} ->
      SendPid ! {recv, Message},
      loop(Socket, SendPid);

    {tcp_closed, Socket} ->
      gen_tcp:close(Socket);

    {tcp_error, Socket, Reson} ->
      io:format("Handle error ~p on ~p~n", [Reson, Socket]);

    {recv, Message} ->
      gen_tcp:send(Socket, Message),
      loop(Socket, SendPid)
  end.

第一引数に待ち受けるソケット情報、第二引数に転送先となるプロセスIDを渡します。 転送先となるプロセスIDはクライアント側の場合は転送のサーバプロセスID、転送先のサーバプロセスの場合はクライアント側のプロセスID。これで相互にメッセージのやり取りするIDがわかるということです。

receiveでメッセージが来るまで待ち受けます。 {tcp, Socket, Message}で来た場合はこちらが予測していたメッセージなのでそのまま転送先のプロセスにメッセージを送ります。

{recv, Message}で上記のメッセージを受け取ります。これはクライアントプロセスだろうが転送先サーバのプロセスだろうが同じことですね。ここで受け取ったメッセージをgen_tcp:sendでお互いの接続している先へメッセージを送信します。

リザルト

では実際に試してみたいと思います。まずはProxyの起動から

> sample_proxy:start_link(5555, [{host, {127, 0, 0, 1}}, {port, 8010}]),

5555ポートで待ち受け、転送先は127.0.0.1の8010ポートへ転送します。 次に2つのターミナルを立ち上げ、片方は転送先のサーバとしてHTTPサーバとして起動させます。 もう片方はクライアントとして使います。 転送先のサーバは「Hello \nWorld \nCunked!」を返します。 ではやってみましょう。

$ curl http://127.0.0.1:5555
Hello
World
Cunked!

はい!無事に通りました。

最後に

これぐらいならサクサクっと作れましたし、TCPの中継だけなのでそこまで手間はかからなかったです。ただエラー処理やコネクション処理などもろもろしてないのでそこら辺はおいおい実装していきます。そして何よりHTTPのProxyを作りたいのでこれからURLベースでの転送、複数台の転送サーバを持ったときの挙動などを実装していきたいと思います。