Working with TCP Sockets 読書メモ 第2章 サーバのライフサイクル

目次


接続の確立

  • TCP接続は2つのエンドポイント間で確立される
  • ソケットは、以下のいずれかの役割を持つ
    1. initiator(クライアント)
    2. listener(サーバ)
    • 片方がinitiator(クライアント)、もう一方はlistener(サーバ)になる

サーバのライフサイクル

  • サーバソケットは、接続を受け付ける(listenする)
  • サーバソケットの典型的なライフサイクルは以下
    1. create
    2. bind
    3. listen
    4. accept
    5. close
require 'socket'

# はじめに、新しいTCPソケットを作成する
socket = Socket.new(:INET, :STREAM)

# listenするアドレスを保持するCの構造体をpackした文字列を作成
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')

# ソケットにアドレスを紐付ける
socket.bind(addr)

# ソケットはローカルホストの4481番に紐付けられる
# listen以降は後述

どのポートを使うべきか?

  • 0-1024(well-knownポート)は使うべきではない
  • 49152-65535(エフェメラルポート、短命なポート)も使うべきではない
    • Linuxでは32768-61000となっていることがある
  • 上記範囲外のポート、すなわち1025-49151ないし1025-32767を使うべき
    • さらに、IANAに登録されていないポートを使うことが望ましい

どのアドレスを使うべきか?

  • IPアドレスは、ハードウェアの物理的なインタフェースと紐付いている
  • ソケットをアドレスに紐付けると、ソケットは対象となるインタフェースのみをlistenする
  • 全てのインタフェースをlistenしたい場合は、0.0.0.0を使うことができる

サーバのlisten

  • ソケットを作成して、ポートに紐付けたら、接続を受け付ける(listenする)必要がある

Listenキュー

  • listenには、接続待ち(listenキュー)の上限がある
    • 最大数を超えた接続がくると、Errno::ECONNREFUSEDエラーが発生する

listenキューはどの程度の大きさにすべきか?

  • Socket::SOMAXCONNで、listenキューの最大値を取得できる
    • サーバの設定変更によって増やすこともできる
  • 通常は、サーバのデフォルトで使用可能な最大数に設定すればよい(?)
server.listen(Socket::SOMAXCONN)

サーバのaccept

  • acceptメソッドでサーバが受付状態になる
connection, _ = server.accept

acceptはブロッキング

  • acceptは「ブロッキング」な処理である
    • 新しい接続を受け取るまで、現在のスレッドの処理を停止させる
    • ペンディングされている接続の中で最後のものを取得する

ソケットが持つ情報

  • それぞれの接続は、別個のSocketオブジェクトで表現される
  • 接続にはローカルとリモートの2種類のアドレスがある
    • ローカルアドレスは接続元のエンドポイント
    • リモートアドレスは接続先のエンドポイント
  • TCP接続は、ローカルホスト・ローカルポート・リモートホスト・リモートポートの4つの情報の組み合わせで一意に識別される

acceptループ

  • acceptは1つの接続を返す
  • 接続をずっと受け付けたい場合はacceptとループを組み合わせる
require 'socket'

server = Socket.new(:INET, :STREAM)
server.bind(Socket.pack_sockaddr_in(4481, '0.0.0.0'))
server.listen(128)

loop do
  connection, _ = server.accept
  # connectionを使った処理をここに書く
  connection.close
end

サーバを閉じる

  • 接続を使用した処理が終わったら、接続を閉じる必要がある
  • closeを呼ぶことで接続を閉じることができる

終了時に接続を閉じる

  • プログラム終了時には全てのファイル記述子は閉じられる
    • ソケットもファイルなので、プログラム終了時には自動的に接続が閉じる
  • 手動で接続を閉じるべき理由は以下
    • リソースの効率的な活用
    • 開くファイル数の制限に引っかからないようにする
      • 上限はProcess.getrlimit(:NOFILE)で取得できる

部分的に接続を閉じる

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

server = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')
server.bind(addr)
server.listen(128)
connection, _ = server.accept

# 書き込みが止まる
connection.close_write

# 読み込みが止まる
connection.close_read
  • close_writeclose_readは内部ではshutdown(2)を使用している
  • closeは、現在のソケットを閉じる
  • shutdownは、現在のソケットと、そのコピー全てを閉じる
    • ただし、shutdownはソケットが使用しているリソースを開放しない
    • shutdownを使った場合でも、明示的にcloseを行う必要がある

接続をコピーする

  • Socket#dupを使って接続をコピーできる
    • 内部ではdup(2)を使ってファイル記述子をコピーしている
    • あまり一般的な方法ではない
  • Process.forkを使ってファイル記述子のコピーを作ることもできる
    • 現在の(UNIX)プロセスの正確なコピーを作る

Rubyの提供するラッパー

  • Rubyの提供するインタフェースを使うことで、定型的なパターンを簡単に書くことができる

サーバの作成

  • TCPServerクラスを使うと、サーバを簡単に作成できる
require 'socket'

server = TCPServer.new(4481)
  • 上記コードは、以下と等価である
require 'socket'

server = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')
server.bind(addr)
server.listen(5)
  • TCPServerSocketはほぼ同一のインタフェース
    • TCPServer#acceptconnectionのみ返し、remote_addressを返さないのが大きな違い
  • listenするキューのサイズはデフォルトでは5
    • サイズを変えたい場合はTCPServer#listenを使う
  • IPv4とIPv6に対応するには、以下のように書く
    • 同じポートで、片方はIPv4、もう一方はIPv6でアクセスできる
require 'socket'

servers = Socket.tcp_server_sockets(4481)

接続のハンドリング

  • 接続のハンドリングは、loopではなく、以下のように書くべき
    • ブロックの終端で自動的に接続が閉じるわけではない点に注意
require 'socket'

server = TCPServer.new(4481)

Socket.accept_loop(server) do |connection|
    # 接続を使った処理をここに書く
    connection.close
end
  • Socket.accept_loopには複数のソケットを渡せる
require 'socket'

servers = Socket.tcp_server_sockets(4481)

Socket.accept_loop(servers) do |connection|
    # 接続を使った処理をここに書く
    connection.close
end

全てをラップする

  • Rubyによるラッパーの行き着く先はSocket.tcp_server_loopである
    • このメソッドによって、今までのステップをひとまとめにできる
require 'socket'

Socket.tcp_server_loop(4481) do |connection|
    # 接続を使った処理をここに書く
    connection.close
end

この章で扱ったシステムコール

  • bind(2)
  • listen(2)
  • accept(2)
  • getsockname(2) (Socket#local_addressに対応)
  • getpeername(2) (Socket#remote_addressに対応)
  • close(2)
  • shutdown(2)

ディスカッションに参加

1件のコメント

コメントをどうぞ

コメントを残す