目次
接続の確立
- TCP接続は2つのエンドポイント間で確立される
- ソケットは、以下のいずれかの役割を持つ
- initiator(クライアント)
- listener(サーバ)
- 片方がinitiator(クライアント)、もう一方はlistener(サーバ)になる
サーバのライフサイクル
- サーバソケットは、接続を受け付ける(listenする)
- サーバソケットの典型的なライフサイクルは以下
- create
- bind
- listen
- accept
- 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
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_write
とclose_read
は内部ではshutdown(2)
を使用している
close
は、現在のソケットを閉じる
shutdown
は、現在のソケットと、そのコピー全てを閉じる
- ただし、
shutdown
はソケットが使用しているリソースを開放しない
shutdown
を使った場合でも、明示的にclose
を行う必要がある
接続をコピーする
Socket#dup
を使って接続をコピーできる
- 内部では
dup(2)
を使ってファイル記述子をコピーしている
- あまり一般的な方法ではない
Process.fork
を使ってファイル記述子のコピーを作ることもできる
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)
TCPServer
とSocket
はほぼ同一のインタフェース
TCPServer#accept
がconnection
のみ返し、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)
コメントをどうぞ