MajiMeM

- 備忘録 -

【要約】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乗倍
    • これにより送信を開始する時間が遅くなり,一般的に最初の数パケットは,不必要に遅すぎるほどのスピードで交換される
    • その後のスピードは最大可能速度に達するまで指数的に増加
TCPとリクエスト・リプライのトランザクション

ソケットの初期化 - サーバサイド

コンストラクタ

注:例外省略

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();
}
バインド操作
  • ServerSocketのデフォルトコンストラクタを使って作ったServerSocketは”バインド”してからacceptする
  • バインドを行うメソッド

注:例外省略

class ServerSocket
{
    void bind(SocketAddress address);
    void bind(SocketAddress address, int backlog);
    boolean isBound();
}
  • ServerSocketをクローズすると再利用はできないため,バインドもできない
  • ServerSocket.bindメソッドは,Berkeley Sockets APIのbind()とlisten()の両方の機能を持つ

ソケットの初期化 - クライアントサイド

コンストラクタ

注:例外省略

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;
リモートポート
  • コンストラクタのport引数は,接続先のリモートポート(サーバがリッスンしているポート)を指定
  • リモートポートは次のメソッドまたはロジックで得られる

メソッド

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();
}
  • 出力のシャットダウンの効果
    • ローカル側では,ソケットへのライトはIOException
    • リモート側では,ソケットからのリードは-1またはnullまたはIOException
    • TCPの正常な接続終了手順(FINとそれへのACK)がキューに入れられ,送信途中だったデータがすべて送信され確認(ACK)されたあとに送られる
  • 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メソッドは,ローカルソケットがすでにクローズしたかどうかを教える
TCPサーバをシャットダウンする
  • TCPサーバをシャットダウンするためには,リッスンしているソケットをクローズすることが必要

注:例外省略

Class ServerSocket
{
    void close();
    boolean isClosed();
}
  • クローズしたあと,そのサーバソケットのacceptを呼び出すと,SocketExceptionが投げられる
  • すでにacceptしたソケットは,自分をacceptしたServerSocketがクローズしても影響を受けない
  • isClosedメソッドは,ローカルソケットがクローズしたかどうかを教える

ソケットファクトリ

  • ファクトリはオブジェクトを作るオブジェクト
  • ソケットファクトリは,ソケットやサーバソケット,またはこの両方を作るファクトリ
  • Javaはソケットファクトリを三つのレベルで提供
    • java.netのソケットファクトリ
    • RMIのソケットファクトリ
    • SSLのソケットファクトリ
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オブジェクトを作り出す
    • タイプはprotectedクラスのjava.net.PlainSocketImpl
    • このクラスのネイティブメソッドが,C言語で書かれたローカルのソケットAPIにインタフェイスする
  • ソケットファクトリは,次のメソッドでセット
class Socket
{
    static void setSocketFactory(SocketimplFactory factory);
}
  • アプリケーションが以上の仕組みを使うことはほとんどない
RMIのソケットファクトリ
  • RMIのソケットファクトリをJavaRMI APIが使って,RMIのためのソケットとサーバソケットを供給
    • JRMPを使うため
  • この機能によってRMI/JRMPがSSLのような他の中間的プロトコルの上に重なる
  • 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サーバは通常,すべてのローカルIPアドレスでリッスンする
  • サーバがたった一つのサブネットに対してサービスを提供する場合は,適切なローカルIPアドレスにバインド
クライアントの場合
  • 特に気にしなくていい

TCPの総まとめ

Related Posts Plugin for WordPress, Blogger...