印刷して電車の中で読める (かもしれない) TCP ECHO サーバのソースコード (1)

アーキテクチャに関する質問として、「○○の仕組みってどうなってるの」というようなことを聞かれても、説明下手なせいか、なかなか口で説明できないことがある。そこで「UTSL (ソース読めばいいよ)」と言いたいんだけど、かといって実際に、これを読むといいんじゃないかな、という話にもしづらい。入門的なコードはあまりにも単純すぎて退屈だし、かといって何か適当な著名なプロダクトのソースも、おおよそ手軽に読めるという感じではない。

さて、前エントリで epoll(7) やら select(2) やらの内部の説明をしたわけだけど、呼び出し側がどうなっているのかという部分のイメージがつかめなければ、片手落ち、というか意味不明だろうというように思ったので、恥をしのんで、簡単なシングルスレッドのイベントループベースの (twisted っぽい) TCP ECHO サーバを書いてみた。

電車の中でも読めるというコンセプトのもと、できるだけアルゴリズム上の最適化は省いて、コードを読みやすくするよう心がけると同時に、コメントも増やし、かつほとんどを 1 ファイルにまとめるということをしてみた。

※ここをこうしたほうがいいよ、というコメントがとてもありがたいです。できれば基本的な構成要素を網羅したソースコードを目指しているので。

ソースコードの説明

この TCP サーバプログラムは

  • bitset.h
  • poller.h
  • server.c

という 3 つのソースファイルから構成されている。

bitset.h は fd_set の代替実装で、FD_SETSIZE を越えるファイルディスクリプタを扱えるようにするために必要。Linux Programming、epollの話 にはカーネルの再コンパイルが必要とあるけど、少なくとも現時点の Linux では、メモリの許す限りファイルディスクリプタを扱える。

poller.h は select(2) をラップする poller の関数群を含む。さっき libevent のソース見たらそっくりで笑った。このファイルを差し替えることで、select(2) だけでなく、poll(2) や epoll(2) に対応させることが容易にできる。それぞれの関数がどのような役割を持つのか、ということは server.c を見れば分かるように思うので、特別、コメントによる説明は加えていない。

server.c はサーバのソース本体。buffer のようなデータ構造は、ネットワークアプリケーションではいろんなところで使われるので、頭に入れておくべきな気がする。

プログラムの動作

起動すると、

time: 1243468694
time: 1243468694
time: 1243468695
time: 1243468695
time: 1243468696
time: 1243468696
     :
     :

のように、2 つの同じ数値が 0.2 秒間隔で、それぞれ 1 秒おきに表示される。これは deferral イベントという、ネットワークの通信に関係なく指定した時間に発生させることのできるイベントが発火している様子を示している。

接続を受けると

new connection: 5

のように出力され、接続を閉じるときには

closed: 5

のように出力される。

データを受信すると

read ready: 5

のように出力され、データの送信時には

write ready: 5

のように出力される。

いずれの出力も write_log() (名前は適当) という関数で行っているので、そこを目安に読むのもよさげ。

備考

  • ソース中に出てくる deferral イベントという言葉は、このプログラム固有の用語で、指定した時間に発火されるイベントのことをいう。
  • 非効率なデータ構造。たとえば buffer は部分的に vector になっている unrolled linked list とか使ったほうがよさげ。
  • エラー処理が適当。close(2) のあたりとか。いずれのシステムコールもシグナルで中断される可能性があって、その場合は EINTR が返ってくるんだけど、そういう場合も再度呼び出しを試みるということはせずに、基本的に無視している (サーバを破棄するときにまとめて閉じなおすからいいだろうというような目論見)。一応マスク可能なやつは、あえて許可しているものを除いて禁止してあるので大丈夫だと思いたいんだけど。
  • シグナルの扱いについてはもう少し考慮が必要。実験的なコードなので SIGINT でいきなり止められるようにしてあるけど、本来であれば、重要な処理を行う箇所 (クリティカルセクション) では、シグナルをブロックしておいたりするべきかと。