Lua Copasを使ってみる

Copasとは、lua-socketを使う際に、非同期通信を行うためのライブラリです。
すごく便利な割に、使い方があまり出てこなかったので、ここで書こうかと思います。
http://keplerproject.github.io/copas/index.html

lua-socketを使用して、単純にTCPリクエストを受け付けるサーバをluaで書いてみると、以下のような感じになります。

local socket = require("socket")
local server = assert(socket.bind("*",80))
local ip, port = server:getsockname()

while true do
    local client = server:accept()
    local line, err = client:receive()
    print(line)
    if not err then client:send("OK\n") end
    client:close()
end

しかし、これだと一つのTCPのセッションが完了するまで、プログラムが待ち状態になってしまい、他のユーザからの通信を受け付けません。 この状況、luaのコルーチンとtimeout機能を使えば回避できるのですが、これをいちいち書いているのはめんど臭いです。
そこで登場するのがCopas
まずは使い方を見てみます。

luaはマルチスレッド環境に対応していませんので、コルーチンを作っても擬似的な並列処理になります。その点、Golangのゴルーチンは勝手にOSのスレッドに多重化してくれるので優秀ですね。

Copasのインストール

luarocks(luaのライブラリマネージャ)でcopasをインストール出来ます。

luarocks install copas

ubuntuだったらapt-getでも入ります。

apt-get install lua-copas

lua-socketも同じ方法で導入することが出来ます。

Copasの使い方

Copasでは通信を処理するHandler関数を定義します。
内部的には、このHandlerをコルーチンで呼び出し、Handlerの処理がビジーになったら別のHandlerに切り替える、といったことを行なっているようです。

以下プログラムのように適当にHandlerを定義します。
中身はなんでも良いです。 ちなみに引数のcの部分はどこかにバインドされたソケットが入ります。   次にcopas.addserver()で上記のHandlerををセットします。 これはなんらかの通信に対するディスパッチャの役割を担ってくれる関数になります。 copas.addserver(server, handler[, timeout])の引数は3つあり、1つ目がソケット、2つ目がHandler(またはHandlerを呼び出す関数)、3つ目がtimeoutの時間です。

copas.wrap(socket)はCopasでsocketを扱う際に便利な関数です。通常、copasで呼び出したhandler内でsend、receiveを行う際には、copas.send(socket) copas.receive(socket)みたいな感じで、いちいちcopasを呼び出さないといけませんが、copas.wrap(socket)としてしまえば、lua-socketと同じくsocket:send()という感じでデータの送受信が可能になります。

最後にcopas.loop()で全てのserverが呼び出されます。

local socket = require("socket")
local copas = require("copas")

function Handler(c,host,port)
    local peer = host .. ":" .. port
    print("connection form",peer)
    c:send("Hello\r\n")
    print("data from",peer,(c:receive"*l"))
end

local server = assert(socket.bind("*",80))
copas.addserver(server ,function(c) return Handler(copas.wrap(c),c:getpeername()) end)
copas.loop()

これで、一度に複数のリクエストが来ても対応できるようになりました。
Copasを使うことで、非同期通信のプログラムがものの数行で書けます。
すごく便利です。

少し応用

今度は、先ほどのプログラムの応用編ということで、通信するポートによって呼び出すハンドラを変えてみます。
以下のプログラムでは、80番ポートへの通信には英語で、90番ポートへの通信にはスペイン語で挨拶を返すプログラムです。
例では、ポート毎に応答を変えていますが、同じ書き方でsrc-ip毎に返答を変えることもできます。
このように、複数の条件でcopas.addserverを呼び出すことでポート番号や通信元アドレスに応じたHandler呼び出しができるようになります。

local socket = require("socket")
local copas = require("copas")

function Router(address, port, handler)
    local server = assert(socket.bind(address,port))
    return copas.addserver(server , function(c) return handler(copas.wrap(c),c:getpeername()) end )
end

function HandlerEnglish(c,host,port)
    local peer = host .. ":" .. port
    print("connection form",peer)
    c:send("hello!\r\n")
    print("data from",peer,(c:receive"*l"))
end

function HandlerSpanish(c,host,port)
    local peer = host .. ":" .. port
    print("connection form",peer)
    c:send("hola!\r\n")
    print("data from",peer,(c:receive"*l"))
end

Router("*",80,HandlerEnglish)
Router("*",90,HandlerSpanish)

copas.loop()