Hatena::Grouperlang

lnzntの Erlang 日記 このページをアンテナに追加 RSSフィード

2011年08月14日

いろはメモ - Erlang 編 - 「に」 : TCP による通信

12:56 | いろはメモ - Erlang 編 - 「に」 : TCP による通信 - lnzntの Erlang 日記 を含むブックマーク はてなブックマーク - いろはメモ - Erlang 編 - 「に」 : TCP による通信 - lnzntの Erlang 日記 いろはメモ - Erlang 編 - 「に」 : TCP による通信 - lnzntの Erlang 日記 のブックマークコメント

いろはメモ - 初心者の書いたメモです。間違いは随時直していきます。

----

TCP サーバ

シンプルすぎる TCP サーバの例。ポート 50001 を listen する。

-module(tcpserver).
-compile(export_all).

start() ->                %% データの型に list を指定、パケット化はしない
    {ok, Listen} = gen_tcp:listen(50001, [list,{packet,raw}]),
    {ok, Socket} = gen_tcp:accept(Listen),

    %% ソケットのデータはコネクションを accept したプロセス(=制御プロセス)に届く
    receive
        {tcp, Socket, Bin} ->      %% データ受信のパターン
            io:format("received : ~p~n", [Bin]);
        {tcp_closed, Socket} ->    %% コネクションクローズのパターン
            io:format("socket closed: [~w]~n", [Socket])
    end,

    gen_tcp:send(Socket, "bye\n"),

    gen_tcp:shutdown(Socket, read_write),
    gen_tcp:close(Socket),

    gen_tcp:shutdown(Listen, read_write),
    gen_tcp:close(Listen).

erl で実行してみる。

1> c(tcpserver).
{ok,tcpserver}
2> tcpserver:start().
                       %% ここでメッセージ待ちになる

telnet で "hello" を送信してみる。

$ telnet localhost 50001
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello                       # "hello" を入力して Enter
bye                         # サーバからの応答
Connection closed by foreign host.

erl 側。

   :
2> tcpserver:start().
received : "hello\r\n"     %% telnet から届いたメッセージ
ok
TCP クライアント

今度はクライアント

-module(tcpclient).
-compile(export_all).

start() ->
    {ok, Socket} = gen_tcp:connect("localhost", 50001, [list,{packet,raw}]),
    gen_tcp:send(Socket, "hello"),

    %% ソケットのデータはコネクションを connect したプロセス(=制御プロセス)に届く
    receive
        {tcp, Socket, Bin} ->    %% データ受信のパターン
                io:format("received : ~p~n", [Bin])
    end,

    gen_tcp:shutdown(Socket, read_write),
    gen_tcp:close(Socket).

さっきのサーバをもう一度起動*1

2> tcpserver:start().
                   %% ...待機状態になる。

別の erl を起動してクライアントを実行。

$ erl
Erlang R13B03 (erts-5.7.4) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.7.4  (abort with ^G)
1> c(tcpclient).
{ok,tcpclient}
2> tcpclient:start().
received : "bye\n"      %% サーバからの応答
ok

サーバの方にも、クライアントからのメッセージが届いている。

11> tcpserver:start().
received : "hello"
ok

gen_tcp:listen や gen_tcp:connect のオプションには以下のようなものが設定できる。

  • データの型。上の例では list を指定している。他の型は binary (他にもある?)
  • パケット化。上の例ではパケット化してない。{packet, 4} など指定できる
  • ソケットオプション。{reuseaddr, true} など指定できる
  • ソケットの制御モード。{active, true}、{active, false}, {active,once} のいずれか

ソケットの制御モード。デフォルトは {active, true} (らしい)。

{active, true}
アクティブ受信(ノンブロッキングモード)。サーバはブロックすることなくクライアントの要求を受けつける。(限界を超えるとマズい)
{active, false}
パッシブ受信(ブロッキングモード)。サーバが recv を呼び出すまでクライアントはブロックされる。(いくらかのバッファリングはある)
{active, once}
ハイブリッド手法(限定ブロッキング)。1つのメッセージだけアクティブになる。次のメッセージを受信できるようにするには明示的に inet:setopts を呼び出す必要がある。

---

参考リンク

Erlangリファレンスマニュアル

----

参考書籍

プログラミングErlang

プログラミングErlang

オーム社のページ(サンプルソースダウンロードなど) : Ohmsha | 商品一覧

いろはメモ - Erlang 編 - 「は」 : ポートによる外部接続

| 10:06 | いろはメモ - Erlang 編 - 「は」 : ポートによる外部接続 - lnzntの Erlang 日記 を含むブックマーク はてなブックマーク - いろはメモ - Erlang 編 - 「は」 : ポートによる外部接続 - lnzntの Erlang 日記 いろはメモ - Erlang 編 - 「は」 : ポートによる外部接続 - lnzntの Erlang 日記 のブックマークコメント

いろはメモ - 初心者の書いたメモです。間違いは随時直していきます。

----

Erlang プロセスは「ポート」という仕組みで Erlang 外部の OSプロセスと通信ができる。

  +- ERTS (Erlang RunTime System) ---+       ****** OS のプロセス ******
  |                                  |       *                         *
  |  +-----------+         |>>>>>>>>>>-->>-->(標準入力)                *
  |  | プロセス  |<------->| ポート  |       *                         *
  |  +-----------+         |<<<<<<<<<<--<<--<(標準出力/標準エラー出力) *
  |                                  |       *                         *
  +----------------------------------+       ***************************
パケット

バイトストリームを扱いやすくするための「パケット」の概念がある。

パケットは、バイト列の前に「長さフィールド」を付加したものである。

BIF による(と思う)が、長さフィールドは 1/2/4 バイト長*2のいずれかである。

長さフィールドは NBO*3エンコーディングされ(ているようであ)る。

例: { packet, 4 } で "hello" をパケット化した場合

      [  0,  0,  0,  5, $h, $e, $l, $l, $o ]

 HEX:   00  00  00  05  68  65  6c  6c  6f
        --------------  ------------------
           長さ(=5)          "hello"

簡単な例で試す。

-module(porttest).    %% porttest.erl
-compile(export_all).

start(Command) ->
    Port = open_port({spawn, Command}, [{packet,4}]), %% open_port() でポートを作る
    io:format("port open: [~w]~n", [Port]),
    loop().

loop() ->
    receive
        {Port, {data, Data}} ->                       %% {Port, {data, Data}} がデータ受信のパターン
            io:format("received: '~p' from [~w]~n", [Data, Port]),
            loop();

        X ->
            io:format("unknown message: [~w]~n", [X]),
            throw('Unknown message received.')
    end.

外部プログラムRuby で作成してみる。(./ext.rb)

#!/usr/bin/env ruby

msg    = "hello"
packet = [msg.length, msg].pack("N A*")  # パケット化。長さフィールドは 4 バイト
                                         # ちなみに、2 バイトなら .pack("n A*")
                                         #           1 バイトなら .pack("C A*")

3.times { $>.write packet }              # パケットを 3 回出力

Ruby スクリプトに実行権を付与するのを忘れずに。

 $ chmod +x ./ext.rb

一応、Ruby スクリプトの出力の HEX ダンプを確認しておく。

$ ./ext.rb | od -c -t x1
0000000  \0  \0  \0 005   h   e   l   l   o  \0  \0  \0 005   h   e   l
         00  00  00  05  68  65  6c  6c  6f  00  00  00  05  68  65  6c
0000020   l   o  \0  \0  \0 005   h   e   l   l   o
         6c  6f  00  00  00  05  68  65  6c  6c  6f
0000033

では、erl で実行。(シェルが動かなくなったら CTLR-G + q で脱出する)

$ erl
...
1> c(porttest).
{ok,porttest}
2> porttest:start("./ext.rb").   %% ./ext.rb を指定
port open: [#Port<0.1955>]
received: '"hello"' from [#Port<0.1955>]
received: '"hello"' from [#Port<0.1955>]
received: '"hello"' from [#Port<0.1955>]
                                 %% CTRL-G 
User switch command
 --> q                           %% q [ENTER] でシェル終了
ポートへの書き出しを試す

(書き出し試すのを忘れていたので)試す。

コマンドは "/bin/cat >/tmp/file" とし、パケット化しないで生ストリームに書き出す。

-module(writetest).
-compile(export_all).

start() ->
    Port = open_port({spawn, "/bin/cat >/tmp/file"}, [stream]), %% stream で生ストリームを指定
    io:format("port open: [~w]~n", [Port]),

    Port ! {self(), {command, "hello, erlang\n"}}. %% 書き出しは Port ! {PidC, {command, Data}}
                                                   %% PidC は接続プロセスの PID

erl で実行。

1> c(writetest.erl).
{ok,writetest}
2> writetest:start().
port open: [#Port<0.1955>]
{<0.35.0>,{command,"hello, erlang\n"}}

/tmp/file に書き出されていることを確認。

$ cat /tmp/file 
hello, erlang
$ od -c -t x1 /tmp/file
0000000   h   e   l   l   o   ,       e   r   l   a   n   g  \n
         68  65  6c  6c  6f  2c  20  65  72  6c  61  6e  67  0a
0000016

/tmp/file は消しときましょう。

----

参考リンク

Erlangリファレンスマニュアル

----

参考書籍

プログラミングErlang

プログラミングErlang

オーム社のページ(サンプルソースダウンロードなど) : Ohmsha | 商品一覧

----

参考URL

*1:EADDRINUSE のエラーが出る場合は少し待ってから起動

*2:この文脈では、1 バイト == 8 ビット(== 1 オクテット)とする

*3:Network Byte Order