Haskellでechoサーバ
はいどうもー
引き続きHaskellの話題です. ちょっとHaskellでTCPソケットを使ってみたくなったので, まず簡単なものから実装してみます.
TCPソケットのチュートリアルといえばechoサーバですね!クライアントからの入力をそのまま返すサーバです.
せっかくなのできちんと複数クライアントとの同時通信を可能にしましょう.
Network.Socket
HaskellでTCPソケットを使うには, Network.Socketを使うようです. Network.Socket
ドキュメントには, 低レベルAPIがNetwork.Socketで, 高レベルAPIがNetworkと書いてあるのですが, Networkモジュールのドキュメントには, 互換性のために残してあるけどこれから使う人はNetwork.Socketを使ってくれみたいなことが書いてあります.
適当にググるとNetworkモジュールを使ったサンプルが散見されますが, ここはドキュメントにしたがって, Network.Socketを使用することにします.
ソケットの用意
TCPのサーバ側は, ソケットの作成 -> ソケットをポート番号指定でbind -> 接続受付を開始(listen) -> 接続を受け付ける(accept) というステップを踏む必要があります.
というわけでまずは指定したポート番号にbindされたソケットを用意するアクションを定義します.
import Network.Socket serveSocket :: PortNumber -> Socket serveSocket port = do soc <- socket AF_INET Stream defaultProtocol addr <- inet_addr "0.0.0.0" bind soc (SockAddrInet port addr) return soc
これで引数に渡したポート番号にbindされたソケットが作成されます.
accept
複数クライアントとの同時通信を実現するためには, Control.Concurrentのちからを借ります.
今回は forkIO を使って, 各コネクションごとにスレッドを起動していくことにします. (非同期版も作れるのかな?つくれたらつくります)
というわけで次は acceptしてforkIOするという処理を繰り返し行うアクションを定義します.
forkIO した後に実行するアクション(echoLoop)についてはとりあえずundefinedとします.
Haskellのundefined, とても便利ですね. 型で考えるっていうスタイルが実行しやすくなっているのは, ソースコード上にトップレベル関数の型指定を書きやすいHaskellの文法とundefinedのおかげって感じがします.
echoLoop :: Socket -> IO () echoLoop = undefined -- import Control.Concurrent -- import Control.Monad -- が必要 acceptLoop :: Socket -> IO () acceptLoop soc = forever $ do (conn, _addr) <- accept soc forkIO $ echoLoop conn
forever :: Monad m => m a -> m bは引数にIOアクションを受け取り, それを無限に繰り返し実行し続けます. (無限にくりかえすので返り値の型変数bは不定)
foreverの引数には, acceptしてforkIOするアクションを渡しています.
echo
最後にソケットから読み込み, そのまま書き出すechoLoop部分を作ります.
-- import Control.Exception が必要 echoLoop :: Socket -> IO () echoLoop conn = do sequence_ $ repeat $ do (str, _, _) <- recvFrom soc 64 send soc str
recvFrom :: Socket -> Int -> IO (String, Int, SockAddr)は, recvFrom conn n で, connから最大でn文字まで読み込みます.
返り値は, (読み込んだ文字列, 読み込んだ文字数, 読み込み元のアドレス??) を返します.
そして, send :: Socket -> String -> IO () でソケットに読み込んだ文字列をそのまま書き込みます.
このように, 読み込んでそのままm書き込むというアクションを repeatでつなげています. repeat :: a -> [a]は無限リストを作る関数です. repeat 0で[0, 0, 0, 0, ...というリストが作成されます.
このままでは [IO ()]型なので, これをsequence_ :: Monad m => [m a] -> m ()を使って1つのIOアクションにまとめ上げます.
Haskellが遅延評価だから出来る芸当ですね. 無限に繰り返す感あふれるコードになっている気がします. (forever使ったほうがいいと思います)
例外処理
recvFromは相手側がコネクションを切断するとEnd of fileの例外を投げます. forkIOしているので, 1つのスレッドが例外で落ちてもサーバ全体は動き続けますが, ソケットのクローズも出来ませんし, 標準エラーになんかでてきてよろしくないので修正します.
-- import Control.Exceptionが必要 echoLoop :: Socket -> IO () echoLoop conn = do sequence_ $ repeat $ do (str, _, _) <- recvFrom conn 64 send conn str `catch` (\(SomeException e) -> return ()) `finally` close conn
catchとfinallyを追加しています.
どちらもJavaとかのそれと同じように動きます.
SomeExceptionはすべての例外を補足することが出来ますが, ほんとはあんまり良くないですね. ここではEOFに達した(コネクションが切断された)という場合だけを補足したいので. (どの関数がどういう場合にどんな例外を投げるのかっていうドキュメントがわからなかったのでこのままにしておきました)
そして, 例外が発生してもしなくても, 最後にかならずソケットのクローズをするようfinallyを使います.
SomeExceptionですべての例外が捕捉出来るのって不思議じゃないですか?Haskellにはオブジェクト指向っぽい型の階層関係なんてないのに.
Haskellの多相性 - あどけない話このへんが関係しているっぽいなという感じがしますが詳しいことはよくわかりませんでした...
全体
module Main where import Network.Socket import Control.Monad import Control.Concurrent import Control.Exception main :: IO () main = do soc <- serveSocket 8080 listen soc 5 acceptLoop soc `finally` close soc serveSocket :: PortNumber -> IO Socket serveSocket port = do soc <- socket AF_INET Stream defaultProtocol addr <- inet_addr "0.0.0.0" bind soc (SockAddrInet port addr) return soc acceptLoop :: Socket -> IO () acceptLoop soc = forever $ do (conn, addr) <- accept soc forkIO $ echoLoop conn echoLoop :: Socket -> IO () echoLoop conn = do sequence_ $ repeat $ do (str, _, _) <- recvFrom conn 64 send conn str `catch` (\(SomeException e) -> return ()) `finally` close conn
main内で listenするのを忘れずに!また, acceptLoop中に例外が発生してもソケットをクローズするようにfinallyを使っています. (まぁプログラム終了するのでいらない気もします)
動作確認
telnetコマンドでテストします.
% telnet localhost 8080 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. test test aaa aaa hooooogle hooooogle
ちょっとわかりづらいですが, 入力した文字列が即座にそのまま帰ってきていることがわかります. バッファリングの関係で, 一行ずつになっていますが.
まとめ
Haskellでechoサーバ, 意外とすんなりかけましたね. 例外関係があまりよく理解できていない感じがしますが...
非同期版が気になります. 調べてみます.