Fire Engine

消防士→ITエンジニア→研究者

「Working with TCP Sockets」を読んだ

最近Golangを書いていると、自分でTCPをListenしたり、Acceptしたりする処理を書くことがよくあるのですが、何をやっているのか全くイメージが沸いてなかったので、「Working with TCP Sockets」を読んで勉強しました。

Working With TCP Socketswww.jstorimer.com

だいたいこの手のソケットプログラミング関連の本は、C言語で解説されていることが多いと思うのですが、本書はサンプルコードが全てRubyなので、非常に読みやすいです。残念ながら英語版しかありません。
以下、私がポイントだと思ったことだけをまとめた読書ノートです。

読書ノート

前半はソケットとは?から始まり、ソケットプログラミング自体の入門的な内容で、そのあたりの理解が曖昧だった私にはとても勉強になりました。
後半は、ソケットプログラミングを使った実用的なアーキテクチャパターンの話でした。Preforkとかイベント駆動とかWebサーバの実装において非常に重要な概念が解説されていましたが、今私が一番知りたいこととは離れていたため、斜め読みしました。

Your First Socket

  • IPアドレスとポート番号のペアはそれぞれのソケットでユニークでなければならない。

Establishing Connections

  • ソケットは、以下のいずれかの役割を持つ
    1.initiator
    2.listener

いわゆるクライアントとサーバ

Server Lifecycle

  • サーバソケットの典型的なライフサイクル
    1.create
    2.bind
    3.listen
    4.accept
    5.close

  • bindは、createしたソケットとlistenするポートをbind(結びつける)
    他のソケットは同じポートをbindできない。

  • acceptメソッドでサーバが受付状態になる。
    acceptは「ブロッキング」な処理である。新しい接続を受け取るまで、現在のスレッドの処理を停止させる

  • acceptの返り値としてconnectionがreturnされるが、connectionはただのSocketクラスのインスタンスである(Rubyの場合)
    connectionのファイルディスクリプタはサーバソケットのものと異なる。各connectionは新しいSocketオブジェクトとして表され、サーバソケットはそのまま残り、新たなconnectionを受け続けることができる。

  • すべてをファイルとして扱うUnixの世界ではSocketもファイルだ。

  • それぞれのTCPコネクションは、ローカルホスト・ローカルポート・リモートホスト・リモートポートの組み合わせで一意に識別される

  • ソケットは双方向通信(読み/書き)なので、いずれか片方だけを閉じることができる

Client Lifecycle

  • クライアントの典型的なライフサイクル
    1.create
    2.bind
    3.connect
    4.close

  • クライアントでは、bindを使うことはまれである。bindを使わなかった場合、クライアントソケットはランダムなエフェメラルポートが与えられる。クライアントは外部からの接続を受け付ける必要がないので、ポート番号を明らかにする必要もない。

  • connectを呼び出すと、リモートのソケットへの接続が始まる

Exchanging Data

  • TCPコネクションは、ローカルソケットとリモートソケットをつなぐ管の連なりのようなイメージ(論理的な回線)

Sockets Can Read

  • UNIXにおいては、全てをファイルとして取り扱う。read(2)やwrite(2)はシステムコールレベルで共通しているので、ファイル、ソケット、パイプ等で共通して利用できる。

  • readの呼び出しは、全てのデータが届くまで待ちを発生させる
    この問題に対する解決策は2つある

  • クライアントがEOFを送る
    EOFは、文字というよりも、状態を表すイベントである。クライアントからEOFを送る際の最も簡単な方法は、ソケットを閉じることである。

  • サーバが部分的な読み取りを使用する
    読み取るデータの最大長を指定して、ブロックせず、すぐに利用できるデータだけが返す。

Buffering

  • writeを呼び出し、エラーなく完了した場合、それは「データの送信に成功し、クライアントが受け取った」ことを意味しない。それは、OSのカーネルによってデータが処理可能な状態になったことが保証されているだけである。

  • バッファリング
    ネットワークを越えたデータ送信は遅いので、できるだけ回数を減らしてパフォーマンスを向上させたい。
    一般的には、一度に全てを書き込むと良いパフォーマンスが得られるが、メモリに載らないよう大きなデータやファイルは分割すべきである。

  • readに読み込みの最大長を渡すと、最大長を超える部分はバッファリングされる
    カーネルは、readの読み込みに指定したサイズのメモリを確保するため、そのサイズが大きすぎると、リソースの無駄が多くなる。
    一方、サイズが小さすぎると、システムコールの呼び出し回数が増えるためパフォーマンスが劣化する。
    どのくらいの読み込みサイズが適切かは、プログラムが取り扱うデータのサイズによって異なる。

Non-blocking IO

  • ノンブロッキングIOは、コネクションの多重化によって実現される。

  • ノンブロッキングIOしたあと、読み込み書き込み可能になるまでを知るにはIO.selectを使う
    selectは登録したソケットをブロッキング状態で監視し、いずれかがデータ受信するとブロッキング状態を解除する。

Multiplexing Connections

  • コネクションの多重化とは、同時に複数のアクティブなソケットを使用することである。

  • select(2)は監視対象のコネクション数が増えると、線形的にパフォーマンスが低下する
    poll(2)はselect(2)の代替だがそれほどの差はない

  • (Linux)epoll(2) または (BSD)kqueue(2) は、select(2)およびpoll(2)のモダンな代替である

SSL Sockets

  • SSLは、公開鍵暗号によって、ソケット上でデータを安全にやりとりできる仕組みを提供する。
    SSLTCPを置き換えるので飯買う、SSLによるsecure layerはTCPの上に追加されるイメージ。

Preforking

Preforkパターンでは、接続がくるたびに子プロセスをforkするのではなく、サーバ起動時にプロセスをまとめてforkしておく
ワークフローは以下の通りである

  1. メインのサーバプロセスがlistenするソケットを作成
  2. メインのサーバプロセスが一群の子プロセスをfork
  3. それぞれの子プロセスが共有されたソケットから接続を受け取り、独立して処理する
  4. メインのサーバプロセスは子プロセスを監視する

Evented (Reactor)

イベント駆動のパターンはNginxを始めとした有名なライブラリで採用されている。
このパターンはシングルスレッド・シングルプロセスで、並列性を実現する
ワークフローは以下の通りである

  1. サーバはソケットでコネクションをモニターする
  2. 新しいコネクションを受け取ると、モニター対象ソケットのリストに追加する
  3. サーバはアクティブなコネクションをモニターしながら、ソケットをlistenする
  4. アクティブなコネクションが読み込み可能になった通知を受け取ると、サーバはコネクションからデータを読み取り、関連するコールバックを実行する
  5. アクティブなコネクションがまだ読み込み可能であるという通知を受け取ると、サーバはコネクションからデータを読み取り、再びコールバックを実行する
  6. サーバが新しいコネクションを受け付けたら、モニター対象ソケットのリストに追加する
  7. サーバは最初のコネクションが書き込み可能になったという通知を受け取ったら、レスポンスが書き込まれる

Working With TCP Sockets (English Edition)

Working With TCP Sockets (English Edition)