【要約】Javaネットワークプログラミングの真髄 第3章 「TCPの基礎」
第3章 TCPの基礎
TCPソケットの基礎知識
TCPの概要
- TCPはクライアント/サーバタイプの端点ペアの間に,高信頼型(reliable)で双方向性のストリーミング接続を提供
- TCPの通信端点は{IPアドレス,ポート}というペアで定義
- ストリーミング
- 送信され受信されるデータが連続的なバイトの流れ(ストリーム,stream)として取り扱われ,物理的なメッセージ境界(区切り)のようなものはどこにもない
- 単純なバイトの流れ
- 二種類のソケット
- ”アクティブ”なソケット
- クライアント側のソケット
- サーバに接続しにいく
- ”パッシブ”なソケット
- リスニングソケット
- TCPサーバはソケットを作ってそれをポートにバインドし,ソケットのステート(状態)を”リスニング”にするとパッシブソケットになる
- パッシブソケットでacceptする
- acceptすると新たなアクティブソケットを作成
- サーバ・クライアント双方のアクティブソケットどうしで通信
- ”アクティブ”なソケット
TCPの特徴と費用
TCPの特徴
- データの受信は自動的に確認され,順序化され,必要なら再送される
- アプリケーションは,壊れたデータや順序の正しくないデータ,抜け落ちのあるデータ(”データホール”,dataholes)などを受け取ることはない
- ネットワークの帯域幅をフルに利用
- 帯域を飽和させたり,ほかのネットワークユーザに対して不公平なことはしない
- ”折衝型の接続(negotiated connect)”(対話をしながら接続を確立していく方式)
- ”折衝型の切断(negotiated close)”
費用
- 接続
- 接続のための折衝はパケット交換3回
- クライアントのSYNにサーバがSYN/ACKで応答
- 最後にクライアントがACKを返す
- 最初のSYNに応答(リプライ)がなかったら,時間間隔を徐々に増しながら何度かトライ
- 最初のリトライ間隔は,ふつうは3~6秒
- 失敗するたびに間隔は倍以上に増える
- リトライの制限は,75秒までやリトライ3回までなど
- 切断
- どちらもFINを送り,そのFINに対してACKで応答
- データの順序化,確認,ペース調整(pacing)
- かなりの計算処理を必要とする
- 二つの端点間を行き来するパケットの現時点の往復遅延時間(round-trip time,RTT,データを送ってからACKが返ってくるまでの時間)を判定するためには,複数回の送信の平均や予測平均値を求める処理が必要
- 輻輳回避(congestion avoidance,通信渋滞の回避)
- 再送信までの時間が指数的に増加(”指数的バックオフ”,exponential backoff)
- 三度目の再送信までの時間は基準値nの3倍ではなく2の3乗倍
- これにより送信を開始する時間が遅くなり,一般的に最初の数パケットは,不必要に遅すぎるほどのスピードで交換される
- その後のスピードは最大可能速度に達するまで指数的に増加
ソケットの初期化 - サーバサイド
コンストラクタ
注:例外省略
class ServerSocket { ServerSocket(); ServerSocket(int port); ServerSocket(int port, int backlog); Serversocket(int port, int backlog, InetAddress localAddress); }
- ポートがバインドされたソケットはすぐにそのまま使える(ServerSocket.acceptメソッドを呼べる)
ポート
- バインドしたポート番号は次のメソッドで知ることができる.
class ServerSocket { int getLocalPort(); }
- バインドする番号にゼロを指定した場合,システムが割り当てたポートが使われる
- ゼロを指定する場合は実際のポートをクライアントへ伝える仕組みが必要
- ServerSocke.acceptが返したSocketが使うローカルポート番号は,次のメソッドで知ることができる
class Socket { int getLocalPort(); }
バックログ(backlog)
- TCPの低レベルでの実装では,アプリケーションが接続を受け付けるよりも前に接続を実現できる
- リッスンしているソケットに対してTCPの実装が実現した接続は”バックログキュー”と呼ばれるキュー(queue,待ち行列)に保存される
- キューに保存された接続は接続としては完成しているがアプリケーションはacceptしていない
- キューは低レベルのTCPの実装と,リッスンするソケットを作ったサーバプロセスの間に存在
- あらかじめ完成している接続をためることで接続処理を高速化
- 接続リクエストがやってきたときにバックログキューが満杯でなければ,TCPは接続プロトコルを完了して接続をバックログキューに入れる
- バックログキューに入れられた時点では,クライアントアプリケーションは接続完了,サーバはServerSocket.acceptの結果を受け取っていない状態
- サーバが接続を受け取るとキューから削除される
- backlogパラメータは,バックログキューの最大長を指定
- 省略や負数,ゼロの場合はデフォルトの値(50)
- バックログの値を1のような非常に小さな値にすることでサーバアプリケーションの性能を意図的に落とすことができる
ローカルアドレス
- サーバソケットのローカルアドレスとは,サーバがクライアントからのリクエストをリッスンしているIPアドレス
- デフォルトでは,TCPサーバはすべてのローカルIPでリッスン
- localAddress引数にnullでない値を与えると,特定のローカルIPアドレスでリッスン
- サーバソケットがリッスンしているローカルIPアドレスは次のメソッドで知ることができる
class ServerSocket
{
InetAddress getInetAddress();
SocketAddress getLocalSocketAddress();
}
ローカルアドレスを再利用する
注:例外省略
class ServerSocket { void setReuseAddress(boolean reuse); boolean getReuseAddress(); }
受信バッファのサイズをセットする
- サーバソケットをバインドする前に,受信バッファのサイズを設定できる
- 設定は必ずバインドの前(バインドされないデフォルトのコンストラクタを使うことになる)
- ServerSocket.acceptが返すソケットは,この設定を継承
- バインド後の設定は効果なし
- 受信バッファのサイズを設定するメソッド
注:例外省略
class ServerSocket { void setReceiveBufferSize(int size); int getReceiveBufferSize(); }
バインド操作
注:例外省略
class ServerSocket { void bind(SocketAddress address); void bind(SocketAddress address, int backlog); boolean isBound(); }
ソケットの初期化 - クライアントサイド
コンストラクタ
注:例外省略
class Socket { Socket(); Socket(InetAddress host, int port); Socket(String host, int port); Socket(InetAddress host, int port, InetAddress localAddress, int localport); Socket(String host, int port, InetAddress localAddress, int localport); Socket(Proxy proxy); }
リモートホスト
- host引数は,接続先のリモートホストをInetAddressまたはStringで指定
- InetAddressはInetAddress.getByNameまたはInetAddress.getByAddressから作ることができる
- Stringでhostを指定する場合,”java.sun.com”のような文字列,または”192.168.1.24”のようなIPアドレスの文字列を指定できる
- リモートホストのアドレスは次のメソッドまたはロジックで得られる
class Socket
{
InetAddress getInetAddress();
}
ロジック
SocketAddress sa = socket.getRemoteSocketAddress(); if (sa != null) return ((InetSocketAddress)sa).getAddress(); return null;
リモートポート
class Socket { int getPort(); }
ロジック
SocketAddress sa = socket.getRemoteSocketAddress(); if (sa != null) return ((InetSocketAddress)sa).getPort(); return 0;
ローカルアドレス
- コンストラクタのlocalAddress引数は,接続が行われるローカルのIPアドレスを指定
- 指定しなかったりnullにした場合,システムがアドレスを選ぶ
- クライアントがローカルIPを指定するのは,マルチホームのホストの場合のみ
- ソケットがバインドしているローカルIPアドレスは次のメソッドまたはロジックで得られる
class Socket
{
InetAddress getLocalAddress();
}
ロジック
SocketAddress sa = socket.getLocalSocketAddress(); if (sa != null) return ((InetSocketAddress)sa).getAddress(); return null;
ローカルポート
- コンストラクタのlocalPort引数は,ソケットがバインドされるローカルポートを指定
- クライアントのローカルポートを指定することにほとんど意味はない
- ソケットがバインドされているローカルポートは次のメソッドまたはロジックで得られる
class Socket { int getLocalPort(); }
ロジック
SocketAddress sa = socket.getLocalSocketAddress(); if (sa != null) return ((InetSocketAddress)sa).getPort(); return 0;
Proxyオブジェクト
- コンストラクタに渡すProxyオブジェクトは,プロキシのタイプ(Direct,Socks,HTTP)と,そのSocketAddressを指定
受信バッファのサイズを指定する
- 受信バッファのサイズの設定と取得は次のメソッドで行う
注:例外省略
class Socket { void setReceiveBufferSize(int size); int getReceiveBufferSize(); }
- スループットを上げたい場合,接続する前に受信バッファのサイズを設定
- 接続後の受信バッファの設定は効果がない
- 接続後の送信バッファの設定には効果がある
- 送信バッファのサイズはソケットがクローズする前ならいつでも可
バインド操作
注:例外省略
class Socket { void bind(SocketAddress address); boolean isBound(); }
ローカルアドレスを再利用する
注:例外省略
class Socket { void setReuseAddress(boolean reuse); boolean getReuseAddress(); }
接続操作
注:例外省略
class Socket { void connect(SocketAddress address); void connect(SocketAddress address, int timeout); boolean isConnected(); }
- timeoutは接続タイムアウトをミリ秒で指定
- 指定しない場合やゼロの場合は無限のタイムアウトを意味し,connectの操作は接続が確立するかエラーが起きるまでブロックされる
- ホストのポートがリッスンしていなかったり,ホストが早い時期にRSTを生成した,などの状況ではtimeoutよりも早くエラー終了する場合がある
- timeoutを最後まで正常に待つのは,サーバのバックログキューが満杯のときだけ
- isConnectedメソッドは,ローカルソケットがすでに接続操作をしたかどうかを報告する
- 相手側が接続をクローズしたかどうかはわからない
- ソケット通信で,相手側がクローズしたかどうかを確実に知る方法はない
- 実際にリード/ライトしてみるしかない
クライアントの接続を受け付ける
- サーバソケットを作ってバインドしたら,次のメソッドを使ってクライアントの接続を受け付ける
注:例外省略
class ServerSocket
{
Socket accept();
}
ソケットのI/O
出力
- ソケットへの出力は,Socket.getOutputStreamから得たOutputStreamを使う(5章では別の方法が紹介)
Socket socket; // 初期化省略 OutputStream out = socket.getOutputStream(); byte[] buffer = new byte[8192]; int offset = 0; int count = buffer.length; out.write(buffer, offset, count);
- TCPソケットの上の出力はすべて,ローカルの送信バッファには同期的であり,ネットワークとリモートのアプリケーションに対しては非同期
- TCPの出力操作がやることは,送信するデータをページング(ペース調整)とタイミングのルールに合わせてローカルにバッファリングするだけ
- ローカルの送信バッファがフルなら,前の送信に対する受信確認(ACK)が返ってきて送信バッファの内容が取り除かれ,その結果としてバッファに空きができるまで,ソケットへのライトは停止(ブロック)
- 書き出すデータの量が送信バッファのサイズより大きければ,writeメソッドがリターンしたときには,最初の部分がネットワークにすでに書き出されていて,データの残りの部分だけがローカルにバッファリングされている状態になる
- 上記のコードでは,out.writeがリターンしたときには,countバイトのすべてがローカルの送信バッファに書き出されている
- 送受信が行われたという確証は得られないため,アプリケーションプロトコルレベルで確証を得る実装する
- ソケットから取得した出力ストリームはBufferedOutputStreamでラップするのがベスト
- 理想は,BufferedOutputStreamのバッファのサイズは,送信するリクエストやレスポンスの最大サイズに合わせる
- バッファは,リクエストメッセージを完全に書き出したあとで,リプライを読み取る前にフラッシュする
- Javaの各種のデータタイプを送るためには,DataOutputStreamを使う
DataOutputStream dos = new DataOutputStream(out); // 出力の例 dos.writeBoolean(…); dos.writeByte(…); dos.writeChar(…); dos.writeDouble(…); dos.writeFloat(…); dos.writeLong(…); dos.writeShort(…); dos.writeUTF(…); // Stringを書き出す
- 直列化したJavaのオブジェクトを送るには,出力ストリームをObjectOutputStreamでラップする
ObjectOutputStream oos = new OjectOutputStream(out); // 出力の例 Object object; // 初期化省略 oos.writeObject(object);
オブジェクトストリームのデッドロック
入力
- ソケットからの入力は,Socket.getInputStreamで得た入力ストリームを使って行う(他の方法は5章)
Socket socket; // 初期化省略 InputStream in = socket.getInputStream(); byte[] buffer = new byte[8192]; int offset = 0; int size = buffer.length; int count = in.read(buffer, offset, size);
- TCPソケットの上の入力操作は,少なくとも何らかのデータを受信するまでブロックされる
- 上記コードでは,countがsizeより小さいことがありえる
- 送信側がソケットをクローズしたり,出力を遮断したときだけ,readは-1を返す
- ソケットから取得した入力ストリームは,BufferedInputStreamでラップするのがベスト
Socket socket; // 初期化省略 InputStream in = socket.getInputStream(); in = new BufferedInputStream(in, socket.getReceiveBuffereSize());
- Javaの各種のデータタイプを受信するためには,DataInputStreamを使う
DataInputStream dis = new DataInputStream(in); // 入力の例 boolean bl = dis.readBoolean(); byte b = dis.readByte(); char c = dis.readChar(); double d = dis.readDouble(); float f = dis.readFloat(); long l = dis.readLong(); short s = dis.readShort(); String str = dis.readUTF();
- 直列化したJavaのオブジェクトを受信するためには,入力ストリームをObjectInputStreamでラップする
ObjectInputStream ois = new ObjectInputStream(in); // 入力の例 Object object = ois.readObject();
接続の終了
出力のシャットダウン(半閉鎖)
- 出力のシャットダウンは次のメソッドで行う
注:例外省略
class Socket { void shutdownOutput(); boolean isOutputShutdown(); }
入力のシャットダウン
- 入力のシャットダウンは次のメソッドで行う
注:例外省略
class Socket { void shutdownInput(); boolean isInputShutdown(); }
- 入力のシャットダウンはあまり使われない
- isInputShutdownメソッドはローカルソケットが入力をシャットダウンしたかどうかを調べる
接続しているソケットをクローズする
- 必要な会話が完了したらソケットをクローズ
注:例外省略
class Socket { void close(); boolean isClosed(); }
- ソケットをクローズする方法
- Socket.close()でソケットそのものをクローズ
- Socket.getOutputStreamで取得したソケットの出力ストリームをクローズ
- Socket.getInputStreamで取得したソケットの入力ストリームをクローズ
- ソケットをクローズしてその資源をすべて解放するためには,上のどれかを一度呼ぶだけで十分
- 入力ストリームやソケットそのものをクローズするよりも,出力ストリームをクローズすべき
- Socket.close()がIOExceptionを投げたら,すでにそのソケットをクローズしていたか,または,その前にバッファリングしておいたデータをTCPが送れなかったことを意味する
- isClosedメソッドは,ローカルソケットがすでにクローズしたかどうかを教える
ソケットファクトリ
- ファクトリはオブジェクトを作るオブジェクト
- ソケットファクトリは,ソケットやサーバソケット,またはこの両方を作るファクトリ
- Javaはソケットファクトリを三つのレベルで提供
java.netのソケットファクトリ
- java.netのソケット実装は,java.netのソケットファクトリを使って作られている
- java.net.socketとjava.net.ServerSocketはファサード
- ソケットの実装は,抽象クラスjava.net.SocketImplをextendsする
class SocketImpl { // … }
- 実装を供給するファクトリは,java.net.SocketImplFactoryインタフェイスをimplements
interface SocketImplFactory
{
SocketImple createSocketImpl();
}
- デフォルトのソケットファクトリが常にインストールされ,それがSocketImplオブジェクトを作り出す
- ソケットファクトリは,次のメソッドでセット
class Socket { static void setSocketFactory(SocketimplFactory factory); }
- アプリケーションが以上の仕組みを使うことはほとんどない
RMIのソケットファクトリ
SSLのソケットファクトリ
- 7章で説明
TCPの例外
- java.net.BindException
- java.net.ConnectException
- java.rmi.ConnectException
- java.lang.IllegalArgumentException
- java.nio.channel.IllegalBlockingModeException
- java.io.InterruptedIOException
- java.io.IOException
- java.net.NoRouteToHostException
- java.net.ProtocolException
- java.lang.SecurityException
- java.net.SocketBindException
- java.net.SocketTimeoutBindException
- java.net.UnknownHostBindException
ソケットのタイムアウト
- タイムアウトの設定
注:例外省略
class Socket { void setSoTimeout(int timeout); int getSoTimeout(); } class ServerSocket { void setSoTimeout(int timeout); int getSoTimeout(); }
ソケットのバッファ
メソッド
- ソケットの送信バッファと受信バッファのサイズの管理
注:例外省略
class Socket { void setReceiveBufferSize(int size); int getReceiveBufferSize(); void setSendBufferSize(int size); int getSendBufferSize(); } class ServerSocket { void setReceiveBufferSize(int size); int getReceiveBufferSize(); }
- 引数のsizeはバイト数
- 適当に丸められたり,許容範囲に合うように勝手に変えられることもある
ソケットのバッファのサイズの決め方
- バッファが大きいほどTCPの仕事は効率的になる
- イーサネットの上では4KBは小さすぎる
- ソケットのバッファサイズは,その接続の最大セグメントサイズ(MSS)の少なくとも3倍はあるべき
- MSSは通常,最大転送単位(MTU)からパケットヘッダの40バイトを引いた値
- イーサネットはMSSが1,500未満
- 8KB以上のバッファサイズなら問題ない
- 一度に大量のデータを送るアプリケーションでは,送信バッファを48KBや64KBにすることが,もっとも効果的な性能向上対策
- サーバが64KB以上の受信バッファを使うためには,リッスンするソケット(サーバソケット)の受信バッファのサイズを設定する必要がある
- TCPのバッファサイズに対してアプリケーションは,一度に少なくともそのサイズぶんをライトすることによって,TCPを助けるべき
- 少なくともそのサイズのBufferedOutputStreamやByteBufferを使うべき
- TCP接続の最大スループットはW/(RTT)
- Wは現在の受信window(RWIN)の大きさ(その可能な最大サイズが受信バッファのサイズ)
- RTTはネットワークの物理的な性能で決まってしまうため,スループットを上げるためにはwindowのサイズ,すなわち受信バッファのサイズを大きくするしかない
- 送信バッファのサイズの最適なサイズは,帯域幅(その接続のデータ転送レート)とRTT(遅延時間)で決まる
- バッファの大きさは次の式で表せるネットワークの公称能力にマッチしていなければならない
ネットワークの能力(ビット)= 帯域幅(ビット/秒)× 遅延(秒)
マルチホーミング0
クライアントの場合
- 特に気にしなくていい
TCPの総まとめ
- 簡単なエコーサーバ・クライアント