takada-atです。
前回HaskellおよびRubyでエコーサーバーを発表したところ、エコーサーバーおよびネットワークプログラミングの基礎について、社内でいろいろな指摘を受けました。
今回は、指摘された点をひとつひとつ改良していきたいと思います。
リンク: Haskellでエコーサーバー
ポート番号
実は恥しながらRFCにエコーサーバーの規定があるのを知らなかったのですが、一般に「エコーサーバー」と言った場合、正式には「RFC862 - Echo Protocol」のサーバー実装を指すことが多いようです。
http://www.faqs.org/rfcs/rfc862.html
RFC862では、エコープロトコルのポート番号に 7 を割り当てています。
A server listens for TCP connections on TCP port 7.
もちろん1024以下のポート番号を利用するには、ルート権限が必要ですが、可能ならポート番号7を利用することが望ましいでしょう。
forkについて
forkしたあと、親プロセスでハンドラを閉じていないという指摘を受けましたが、これは誤解で、GHCの「forkIO」「forkOS」は、forkシステムコールとは無関係な、スレッドを新たに生成する関数です。名前が少しまぎらわしいですね。
なお余談ですが「forkIO」は疑似スレッド、「forkOS」はネイティブスレッドを生成します。
さらに余談ですが、http://ja.wikipedia.org/wiki/食事する哲学者の問題って、ひょっとして「フォーク」と「fork」をかけてるんですかね? 大発見だと思ったんですが、全然関係ない上に間違ってますか、そうですか……。
参考リンク: Control.Concurrent
シグナルハンドリングについて
いくつかのシグナルをハンドリングしておかなければ、クライアントの動作によってサーバー自体がダウンしてしまいます。
特に危険なのがSIGPIPEです。
ソケットに対し、書き込みを行なった場合、相手側がすでにclose状態だとこのシグナルが発生します。デフォルトの動作ではプロセスが強制終了してしまいます。
参考リンク:
シグナル (ソフトウェア) - Wikipedia
SIGPIPE - Wikipedia, the free encyclopedia
以上をふまえた上で、高レベルAPIを提供するNetworkライブラリではなく、より低レベルなNetwork.Socketライブラリを使うように書き換えてみます。
main部分は以下のように変わりました。
main = withSocketsDo $ do
let port = fromIntegral 7
soc <- socket AF_INET Stream 0
addr <- inet_addr "0.0.0.0"
let sockaddr = SockAddrInet port addr
bindSocket soc sockaddr
listen soc 5
-- mainスレッドではいくつかのシグナルをブロック
blockSignals $ list2set [sigPIPE]
putStrLn $ "start server, listening on: " ++ show port
acceptLoop soc `finally` sClose soc
list2set = foldr addSignal emptySignalSet
read, writeについて
以前のバージョンではソケットからの読み込み・書き込みにhGetLine, hPutStrLnを利用していたのですが、これを使うと、サーバー・クライアント間の改行コードの違いなどによって問題が発生しうるという指摘を受けました。
エコープロトコルの実装としては、行ごとの読み込みではなく、文字列をすぐ読み込んで書き込む方が望ましいでしょう。
def echo_do(soc)
while true
buf = soc.recv(1)
soc.write(buf)
end
end
テスト
以上の動作確認をtelnetを手動で立ち上げて確認するのではなく、Rubyによるテストスクリプトを作成し、こちらで動作確認を行なうことにしました。
require 'test/unit'
require 'socket'
class EchoTest < Test::Unit::TestCase
def test_echo
#エコーのテスト
soc = TCPSocket::new("localhost", 7)
["abc", "ab\na", "\n"].each do |s|
soc.write(s)
buf = soc.read(s.size)
assert_equal(s, buf)
puts buf
end
soc.close
end
def test_concurrent
#同時接続のテスト
socs = []
3.times do |i|
Thread::fork(i,socs){ |i, socs|
sleep 0.1
soc = TCPSocket::new("localhost", 7)
s = "hoge"
soc.write(s)
buf = soc.read(s.size)
assert_equal(s, buf)
puts buf
socs << soc
}
end
(ThreadGroup::Default.list - [Thread.current]).each {|th| th.join}
socs.each {|s| s.close}
end
end
ソースコード
Haskell版とRuby版、修正したものを以下に掲載します。
なお、Haskell版のコンパイルはthreadedオプションを付け以下のようにやってください。
ghc -threaded --make -o echo2 echo2.hs
Continue reading »