JavaソケットI/O:ブロッキング、ノンブロッキング、非同期

Unsplash

のEvi Radauscherによる写真I/Oを記述するとき、non-blockingとasynchronousという用語はしばしば互換的に使用されますが、それらの間には大きな違いがあります。 この記事では、Javaでのノンブロッキングと非同期ソケットI/O操作の理論的および実用的な違いについて説明します。

ソケットは、TCPおよびUDPプロトコルによる双方向通信を実行するエンドポイントです。 JavaソケットApiは、オペレーティングシステムの対応する機能のアダプタです。 POSIX準拠のオペレーティングシステム(Unix、Linux、Mac OS X、BSD、Solaris、AIXなど)でのソケット通信)は、バークレー音楽大学によって演奏されている。 Windowsでのソケット通信は、Windowsプログラミングモデルに準拠するための追加機能を備えたBerkeleyソケットにも基づいているWinsockによって実行されます。

この記事のPOSIX定義

は、POSIX仕様の簡略化された定義を使用しています。

ブロックされたスレッド—実行を続行する前に何らかの条件を待っているスレッド。

Blocking—返される前に要求されたアクションが実行されるのを待つソケットへの呼び出しを引き起こすソケットのプロパティ。

Non-blocking—要求されたアクションが不明な遅延なしに完了できないことが検出されたときに、ソケットへの呼び出しを遅滞なく返すソケットのプロパテ

同期I/O操作—I/O操作が完了するまで要求元スレッドをブロックするI/O操作。

非同期I/O操作—要求しているスレッドがブロックされる原因とならないI/O操作;これは、スレッドとI/O操作が同時に実行されている可能性があ

だから、POSIXの仕様によると、用語ノンブロッキングと非同期の違いは明らかです:

  • non-blocking-呼び出しを遅延なく返すソケットのプロパティ
  • asynchronous I/O—要求スレッドと同時に実行されるI/O操作(読み取りまたは書き込み)のプロパティ

I/Oモデル

以下のI/OモデルがPOSIX準拠のオペレーティングシステムで最も一般的である。:

  • ブロックI/Oモデル
  • 非ブロックI/Oモデル
  • I/O多重化モデル
  • 信号駆動I/Oモデル
  • 非同期i/Oモデル

ブロックI/Oモデル

ブロッキングi/oモデルでは、データがカーネルで受信され、カーネル空間からユーザー空間にコピーされるまで、アプリケーションはブロッキングシステムコールを

ブロックI/Oモデル

ブロックI/Oモデル

長所:

  • 実装する最も単純なI/Oモデル

短所:

  • アプリケーションがブロックされている

非ブロックI/Oモデル

非ブロックI/Oモデルでは、アプリケーションはすぐに二つの応答のいずれかを返す:

  • I/O操作をすぐに完了できる場合は、データが返されます
  • I/O操作をすぐに完了できない場合は、i/O操作がブロックされるか、デバイスが一時的に利用できないことを示すエラーコードが返されます

I/O操作を完了するには、アプリケーションは完了までbusy-wait(システムコールを繰り返します)する必要があります。

非ブロッキングI/Oモデル

非ブロッキングi/Oモデル

長所:

  • アプリケーションがブロックされていません

短所:

  • アプリケーションは完了するまでビジー待機する必要があり、多くのユーザーカーネルコンテキストスイッチを引き起こす
  • このモデルでは、カーネル内のデータ可用性とアプリケーション

I/O多重化モデル

i/O多重化モデル(ノンブロッキングI/o多重化モデルとも呼ばれる)の間にギャップが生じる可能性があるため、I/O遅延が発生する可能性がある。アプリケーションは、多くの記述子のアクティビティの監視を開始するためにブロッキングselectシステムコールを行います。 記述子ごとに、特定のI/O操作(接続、読み取りまたは書き込み、エラーの発生など)の準備状況の通知を要求することができます。). Selectシステムコールが少なくとも一つの記述子の準備ができていることを返すと、アプリケーションはノンブロッキングコールを行い、カーネル空間からユー

I/O多重化モデル

I/O多重化モデル

長所:

  • 一つのスレッド

Consで複数の記述子に対してI/O操作を実行することができます:

  • アプリケーションはselectシステムコールでブロックされています
  • すべてのオペレーティングシステムがこのモデルを効率的にサポートしているわけではありません

信号駆動I/Oモデル

信号駆動I/Oモデルでは、アプリケーションはノンブロッキング呼び出しを行い、シグナルハンドラを登録します。 記述子がI/O操作の準備ができたら、アプリケーション用の信号が生成されます。 その後、シグナルハンドラはカーネル空間からユーザー空間にデータをコピーします。

信号駆動型I/Oモデル

信号駆動型i/Oモデル

信号駆動型i/Oモデル

長所:

  • アプリケーションがブロックされていない
  • 信号は良好なパフォーマンスを提供できます

短所:

  • すべてのオペレーティングシステムが信号

非同期I/Oモデル

をサポートしているわけではありません非同期I/Oモデル(オーバーラップI/Oモデルとも呼ばれます)では、アプリケーションはノンブロッキング呼び出しを行い、カーネル内でバックグラウンド操作を開始します。 操作が完了すると(データはカーネルで受信され、カーネル空間からユーザー空間にコピーされます)、I/O操作を完了するための完了コールバックが生成されます。

非同期I/Oモデルと信号駆動I/Oモデルの違いは、信号駆動I/Oの場合、カーネルはI/O操作を開始できるときにアプリケーションに通知しますが、非同期I/Oモデルの場合、カーネルはアプリケーションにI/O操作が完了したときに通知します。

非同期I/Oモデル

非同期i/Oモデル

長所:

  • アプリケーションがブロックされていない
  • このモデルは、最高のパフォーマンスを提供することができます

短所:

  • を実装するための最も複雑なI/Oモデルすべてのオペレーティングシステムがこのモデルを効率的にサポートしているわけではありません

Java I/O Api

Java IO APIは、ブロッキングされた一方向のデータフローを表すストリーム(InputStream,OutputStream)に基づいています。

Java NIO API

Java NIO APIは、オペレーティングシステムの低レベルI/O操作へのアダプタであるチャネル、バッファ、セレクタクラスに基づいています。

Channelクラスは、i/O操作(読み取りまたは書き込み)を実行できるエンティティ(ハードウェアデバイス、ファイル、ソケット、ソフトウェアコンポーネントなど)への接

単方向ストリームと比較して、チャネルは双方向です。

Bufferクラスは、データを読み書きするための追加のメソッドを持つ固定サイズのデータコンテナです。 チャネルに送信されるすべてのデータはバッファに書き込まれ、チャネルから受信されるすべてのデータはバッファに読み込まれます。

バイト指向のストリームと比較して、チャネルはブロック指向です。 バイト指向のI/Oは単純ですが、一部のI/Oエンティティではかなり遅くなる可能性があります。 ブロック指向のI/Oははるかに高速ですが、より複雑です。

Selectorクラスを使用すると、登録されている多くのSelectableChannelオブジェクトからのイベントを単一の呼び出しでサブスクライブできます。 イベントが到着すると、Selectorオブジェクトは対応するイベントハンドラーにそれらをディスパッ

Java NIO2API

Java NIO2APIは、非同期I/O操作(接続、読み取りまたは書き込み、エラー処理)をサポートする非同期チャネル(AsynchronousServerSocketChannel、AsynchronousSocketChannelなど)に基づいています。

非同期チャネルは、非同期I/O操作を制御するための二つのメカニズムを提供します。 最初のメカニズムはjavaを返すことです。ユーティル同時進行。保留中の操作をモデル化し、状態を照会して結果を取得するために使用できます。 第二のメカニズムは、javaを操作に渡すことです。ニオチャンネル。操作が完了または失敗した後に実行されるハンドラーメソッドを定義するCompletionHandlerオブジェクト。 両方のメカニズムに提供されているAPIは同等です。

非同期チャネルは、プラットフォームを独立して非同期操作を実行する標準的な方法を提供します。 ただし、java sockets APIがオペレーティングシステムのネイティブ非同期機能を利用できる量は、そのプラットフォームのサポートに依存します。

Socket echo server

上記のI/Oモデルのほとんどは、javaソケットApiを使用してechoサーバーとクライアントに実装されています。 Echoサーバーとクライアントは、次のアルゴリズムで動作します:

  1. サーバーは、登録されたTCPポート上のソケットをリッスンします7000
  2. クライアントは、動的TCPポート上のソケットからサーバーソケットに接続します
  3. クライア9175>クライアントは、ソケットからバイトを受信し、コンソールにエコーされた文字列を書き込みます
  4. クライアントが送信したのと同じバイト数を受信すると、サーバー
  5. から切断されます。

  6. サーバーが特別な文字列を受信すると、リッスンを停止します

ここでの文字列とバイト間の変換は、UTF-8エンコーディングで明示的に実行されます。

さらに、エコーサーバーの簡略化されたコードのみが提供されます。 Echoサーバーとクライアントの完全なコードへのリンクは、結論で提供されています。

ブロッキングIOエコーサーバー

次の例では、ブロッキングI/Oモデルは、Java IO APIを使用してエコーサーバーに実装されています。

サーバーソケット。acceptメソッドは、接続が受け入れられるまでブロックします。 InputStreamです。入力データが使用可能になるか、クライアントが切断されるまで、メソッドブロックを読み取ります。 出力ストリーム。すべての出力データが書き込まれるまで、write methodブロック。

public class IoEchoServer { public static void main(String args) throws IOException {
ServerSocket serverSocket = new ServerSocket(7000); while (active) {
Socket socket = serverSocket.accept(); // blocking InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream(); int read;
byte bytes = new byte;
while ((read = is.read(bytes)) != -1) { // blocking
os.write(bytes, 0, read); // blocking
} socket.close();
} serverSocket.close();
}
}

ブロッキングNIOエコーサーバー

次の例では、ブロッキングI/Oモデルは、Java NIO APIを使用してエコーサーバーに実装されています。

ServerSocketChannelオブジェクトとSocketChannelオブジェクトは、デフォルトでブロックモードで構成されています。 ServerSocketChannelです。acceptメソッドは、接続が受け入れられたときにSocketChannelオブジェクトをブロックして返します。 ServerSocketです。入力データが使用可能になるか、クライアントが切断されるまで、メソッドブロックを読み取ります。 ServerSocketです。すべての出力データが書き込まれるまで、write methodブロック。

public class NioBlockingEchoServer { public static void main(String args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("localhost", 7000)); while (active) {
SocketChannel socketChannel = serverSocketChannel.accept(); // blocking ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int read = socketChannel.read(buffer); // blocking
if (read < 0) {
break;
} buffer.flip();
socketChannel.write(buffer); // blocking
} socketChannel.close();
} serverSocketChannel.close();
}
}

非ブロッキングNIOエコーサーバー

次の例では、非ブロッキングI/Oモデルは、Java NIO APIを使用してエコーサーバーに実装されています。

ServerSocketChannelオブジェクトとSocketChannelオブジェクトは、非ブロッキングモードで明示的に構成されています。 ServerSocketChannelです。acceptメソッドはブロックせず、接続がまだ受け入れられていない場合はnullを返し、それ以外の場合はSocketChannelオブジェクトを返します。 ServerSocketです。readはブロックせず、利用可能なデータがない場合は0を返し、それ以外の場合は正のバイト数を返します。 ServerSocketです。ソケットの出力バッファに空き領域がある場合、writeメソッドはブロックしません。

public class NioNonBlockingEchoServer { public static void main(String args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(7000)); while (active) {
SocketChannel socketChannel = serverSocketChannel.accept(); // non-blocking
if (socketChannel != null) {
socketChannel.configureBlocking(false); ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int read = socketChannel.read(buffer); // non-blocking
if (read < 0) {
break;
} buffer.flip();
socketChannel.write(buffer); // can be non-blocking
} socketChannel.close();
}
} serverSocketChannel.close();
}
}

多重化NIO echo server

次の例では、多重化I/Oモデルがecho server Java NIO APIに実装されています。

初期化中に、非ブロッキングモードで構成されている複数のServerSocketChannelオブジェクトが、SelectionKeyを使用して同じSelectorオブジェクトに登録されます。接続受け入れのイベントが興味深いことを指定するためのOP_ACCEPT引数。

メインループでは、セレクタ。selectメソッドは、登録されたイベントの少なくとも1つが発生するまでブロックします。 その後、セレクタ。selectedKeysメソッドは、イベントが発生したSelectionKeyオブジェクトのセットを返します。 SelectionKeyオブジェクトを反復処理すると、どのI/Oイベント(connect、accept、read、write)が発生したのか、どのソケットオブジェクト(ServerSocketChannel、SocketChannel)がそのイベントに関連付けられてい

チャンネルが何らかの操作の準備ができているという選択キーの表示はヒントであり、保証ではありません。

public class NioMultiplexingEchoServer { public static void main(String args) throws IOException {
final int ports = 8;
ServerSocketChannel serverSocketChannels = new ServerSocketChannel; Selector selector = Selector.open(); for (int p = 0; p < ports; p++) {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannels = serverSocketChannel;
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress("localhost", 7000 + p)); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} while (active) {
selector.select(); // blocking Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator();
while (keysIterator.hasNext()) {
SelectionKey key = keysIterator.next(); if (key.isAcceptable()) {
accept(selector, key);
}
if (key.isReadable()) {
keysIterator.remove();
read(selector, key);
}
if (key.isWritable()) {
keysIterator.remove();
write(key);
}
}
} for (ServerSocketChannel serverSocketChannel : serverSocketChannels) {
serverSocketChannel.close();
}
}
}

SelectionKeyオブジェクトが接続受け入れイベントが発生したことを示すと、ServerSocketChannelが作成されます。接続を受け入れるためのacceptコール(非ブロッキングにすることができます)。 その後、新しいSocketChannelオブジェクトが非ブロッキングモードで構成され、SelectionKeyを使用して同じSelectorオブジェクトに登録されます。現在、読書のイベントが興味深いことを指定するためのOP_READ引数。

private static void accept(Selector selector, SelectionKey key) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept(); // can be non-blocking
if (socketChannel != null) {
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
}

SelectionKeyオブジェクトが読み取りイベントが発生したことを示すと、SocketChannelが作成されます。socketchannelオブジェクトから新しいByteByfferオブジェクトにデータを読み取るためのread呼び出し(非ブロッキングにすることができます)。 その後、Socketchannelオブジェクトは、SelectionKeyと同じSelectorオブジェクトに登録されます。Writeのイベントが興味深いものになるように指定するためのOP_WRITE引数。 さらに、このByteBufferオブジェクトは、添付ファイルとして登録時に使用されます。

private static void read(Selector selector, SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer); // can be non-blocking buffer.flip();
socketChannel.register(selector, SelectionKey.OP_WRITE, buffer);
}

SelectionKeysオブジェクトが書き込みイベントが発生したことを示すと、SocketChannelが作成されます。SelectionKeyから抽出されたByteByfferオブジェクトからSocketChannelオブジェクトにデータを書き込むためのwrite呼び出し(非ブロッキングにすることができます)。添付方法。 その後、SocketChannel。cloase呼び出しは、接続を閉じます。

private static void write(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = (ByteBuffer) key.attachment(); socketChannel.write(buffer); // can be non-blocking
socketChannel.close();
}

すべての読み取りまたは書き込みの後、SelectionKeyオブジェクトは、その再利用を防ぐためにSelectionKeyオブジェクトのセットから削除されます。 しかし、接続の受け入れのためのSelectionKeyオブジェクトは、次の同様の操作を行う能力を持つために削除されません。

非同期NIO2エコーサーバー

次の例では、非同期I/Oモデルは、Java NIO2APIを使用してエコーサーバーに実装されています。 ここでは、a Y S Y N C L O Uusserversocketchannel、AsynchronousSocketChannelクラスが完了ハンドラーメカニズムで使用されます。

AsynchronousServerSocketChannelです。acceptメソッドは、非同期接続の受け入れ操作を開始します。

public class Nio2CompletionHandlerEchoServer { public static void main(String args) throws IOException {
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(7000)); AcceptCompletionHandler acceptCompletionHandler = new AcceptCompletionHandler(serverSocketChannel);
serverSocketChannel.accept(null, acceptCompletionHandler); System.in.read();
}
}

接続が受け入れられる(または操作が失敗する)と、Acceptcompletionhandlerクラスが呼び出され、AsynchronousSocketChannelによって呼び出されます。read(ByteBuffer destination,a attachment,CompletionHandler<Integer,? スーパー A>ハンドラ)メソッドは、AsynchronousSocketChannelオブジェクトから新しいByteBufferオブジェクトへの非同期読み取り操作を開始します。

class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, Void> { private final AsynchronousServerSocketChannel serverSocketChannel; AcceptCompletionHandler(AsynchronousServerSocketChannel serverSocketChannel) {
this.serverSocketChannel = serverSocketChannel;
} @Override
public void completed(AsynchronousSocketChannel socketChannel, Void attachment) {
serverSocketChannel.accept(null, this); // non-blocking ByteBuffer buffer = ByteBuffer.allocate(1024);
ReadCompletionHandler readCompletionHandler = new ReadCompletionHandler(socketChannel, buffer);
socketChannel.read(buffer, null, readCompletionHandler); // non-blocking
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

読み取り操作が完了(または失敗)すると、Readcompletionhandlerクラスが呼び出され、AsynchronousSocketChannelによって呼び出されます。write(ByteBuffer source,a attachment,CompletionHandler<Integer,? スーパー A>ハンドラ)メソッドは、BytebufferオブジェクトからAsynchronousSocketChannelオブジェクトへの非同期書き込み操作を開始します。

class ReadCompletionHandler implements CompletionHandler<Integer, Void> { private final AsynchronousSocketChannel socketChannel;
private final ByteBuffer buffer; ReadCompletionHandler(AsynchronousSocketChannel socketChannel, ByteBuffer buffer) {
this.socketChannel = socketChannel;
this.buffer = buffer;
} @Override
public void completed(Integer bytesRead, Void attachment) {
WriteCompletionHandler writeCompletionHandler = new WriteCompletionHandler(socketChannel);
buffer.flip();
socketChannel.write(buffer, null, writeCompletionHandler); // non-blocking
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

書き込み操作が完了(または失敗)すると、Writecompletionhandlerクラスが呼び出され、AsynchronousSocketChannelによって呼び出されます。closeメソッドは、接続を閉じます。

class WriteCompletionHandler implements CompletionHandler<Integer, Void> { private final AsynchronousSocketChannel socketChannel; WriteCompletionHandler(AsynchronousSocketChannel socketChannel) {
this.socketChannel = socketChannel;
} @Override
public void completed(Integer bytesWritten, Void attachment) {
try {
socketChannel.close();
} catch (IOException e) {
// exception handling
}
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

この例では、必要なすべてのオブジェクト(AsynchronousSocketChannel、ByteBuffer)が適切な完了ハンドラーのコンストラクター引数として渡されるため、非同期I/O操作は添付ファイルなし

結論

ソケット通信のI/Oモデルの選択は、トラフィックのパラメータによって異なります。 I/O要求が長く、頻度が低い場合は、一般的に非同期I/Oが適しています。 ただし、I/O要求が短く高速な場合、カーネル呼び出しの処理のオーバーヘッドにより、同期I/Oがはるかに優れている可能性があります。

JavaはさまざまなオペレーティングシステムでソケットI/Oを実行する標準的な方法を提供していますが、実際のパフォーマンスは実装によって大き Dan Kegelのよく知られた記事The C10K problemでこれらの違いの研究を開始することは可能です。

完全なコード例はGitHubリポジトリにあります。

コメントを残す

メールアドレスが公開されることはありません。