1. 概要

入力と出力の処理は、Javaプログラマーにとって一般的なタスクです。 このチュートリアルでは、元のjava .io(IO)ライブラリと新しいjava .nio(NIO)ライブラリ、およびネットワークを介して通信する場合の違いについて説明します。

2. 主な機能

まず、両方のパッケージの主な機能を見てみましょう。

2.1. IO – java .io

java.ioパッケージはJava1.0で導入され、ReaderはJava1.1で導入されました。 それは提供します:

  • InputStreamおよびOutputStream –一度に1バイトのデータを提供します
  • ReaderおよびWriter –ストリームの便利なラッパー
  • ブロッキングモード–完全なメッセージを待つ

2.2. NIO – java .nio

java.nioパッケージはJava1.4で導入され、Java 1.7(NIO.2)で拡張ファイル操作およびASynchronousSocketChannelで更新されました。 ]。 それは提供します:

  • バッファ一度にデータのチャンクを読み取る
  • CharsetDecoder –生のバイトを読み取り可能な文字との間でマッピングするため
  • チャネル–外界との通信用
  • Selector SelectedChannel での多重化を有効にし、I/Oの準備ができているChannelへのアクセスを提供します
  • 非ブロッキングモード–準備ができているものをすべて読み取る

次に、サーバーにデータを送信したり、サーバーの応答を読み取ったりするときに、これらの各パッケージをどのように使用するかを見てみましょう。

3. テストサーバーを構成する

ここでは、 WireMock を使用して別のサーバーをシミュレートし、テストを個別に実行できるようにします。

実際のWebサーバーと同じように、要求をリッスンし、応答を送信するように構成します。 また、ローカルマシン上のサービスと競合しないように、動的ポートを使用します。

test スコープを使用してWireMockのMaven依存関係を追加しましょう:

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8</artifactId>
    <version>2.26.3</version>
    <scope>test</scope>
</dependency>

テストクラスで、JUnit @Rule を定義して、空きポートでWireMockを起動しましょう。 次に、事前定義されたリソースを要求したときにHTTP 200応答を返すように構成し、メッセージ本文をJSON形式のテキストとして使用します。

@Rule public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());

private String REQUESTED_RESOURCE = "/test.json";

@Before
public void setup() {
    stubFor(get(urlEqualTo(REQUESTED_RESOURCE))
      .willReturn(aResponse()
      .withStatus(200)
      .withBody("{ \"response\" : \"It worked!\" }")));
}

これでモックサーバーがセットアップされたので、いくつかのテストを実行する準備が整いました。

4. IOのブロック– java .io

Webサイトからいくつかのデータを読み取って、元のブロッキングIOモデルがどのように機能するかを見てみましょう。 java .net.Socket を使用して、オペレーティングシステムのポートの1つにアクセスします。

4.1. リクエストを送信する

この例では、リソースを取得するためのGETリクエストを作成します。 まず、WireMockサーバーがリッスンしているポートにアクセスするためのソケットを作成しましょう。

Socket socket = new Socket("localhost", wireMockRule.port())

通常のHTTPまたはHTTPS通信の場合、ポートは80または443になります。 ただし、この場合、 wireMockRule.port()を使用して、前に設定した動的ポートにアクセスします。

次に、ソケットでOutputStreamを開き、 OutputStreamWriter でラップし、PrintWriterに渡してメッセージを書き込みます。 そして、リクエストが送信されるように、バッファをフラッシュすることを確認しましょう。

OutputStream clientOutput = socket.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(clientOutput));
writer.print("GET " + TEST_JSON + " HTTP/1.0\r\n\r\n");
writer.flush();

4.2. 応答を待つ

ソケットでInputStreamを開いて応答にアクセスし、 BufferedReader でストリームを読み取り、StringBuilderに保存してみましょう。

InputStream serverInput = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(serverInput));
StringBuilder ourStore = new StringBuilder();

reader.readLine()を使用してブロックし、完全な行を待ってから、その行をストアに追加しましょう。 ストリームの終わりを示すnull、が得られるまで読み続けます。

for (String line; (line = reader.readLine()) != null;) {
   ourStore.append(line);
   ourStore.append(System.lineSeparator());
}

5. ノンブロッキングIO– java .nio

それでは、nioパッケージのノンブロッキングIOモデルが同じ例でどのように機能するかを見てみましょう。

今回は、を作成してjava .nio.channel.SocketChannelを作成し、 java.net.Socketの代わりにサーバー上のポートにアクセスして渡します。 InternetSocketAddress

5.1. リクエストを送信する

まず、SocketChannelを開きましょう。

InetSocketAddress address = new InetSocketAddress("localhost", wireMockRule.port());
SocketChannel socketChannel = SocketChannel.open(address);

次に、標準のUTF-8 Charset を取得して、メッセージをエンコードして書き込みます。

Charset charset = StandardCharsets.UTF_8;
socket.write(charset.encode(CharBuffer.wrap("GET " + REQUESTED_RESOURCE + " HTTP/1.0\r\n\r\n")));

5.2. 回答を読む

リクエストを送信した後、rawバッファを使用して非ブロッキングモードでレスポンスを読み取ることができます。

テキストを処理するので、生のバイトには ByteBuffer が必要であり、変換された文字には CharBuffer が必要です( CharsetDecoder によって支援されます)。

ByteBuffer byteBuffer = ByteBuffer.allocate(8192);
CharsetDecoder charsetDecoder = charset.newDecoder();
CharBuffer charBuffer = CharBuffer.allocate(8192);

データがマルチバイト文字セットで送信される場合、CharBufferにはスペースが残ります。

特に高速なパフォーマンスが必要な場合は、 ByteBuffer.allocateDirect()を使用してネイティブメモリにMappedByteBufferを作成できることに注意してください。 ただし、この場合、標準ヒープから alllocate()を使用すると十分に高速です。

バッファを処理するときは、バッファの大きさ(容量)、バッファ内の場所(現在の位置)、、およびどのくらいの距離かを知る必要があります。行くことができます(制限)。

それでは、SocketChannel からを読み取り、ByteBufferを渡してデータを保存しましょう。 SocketChannelからのreadは、 ByteBuffer 現在の位置をに書き込む次のバイトに設定して終了します(最後に書き込まれたバイト)、ただし、制限は変更されていません

socketChannel.read(byteBuffer)

SocketChannel.read()は、バッファに書き込むことができるで読み取られたバイト数を返します。 ソケットが切断された場合、これは-1になります。

すべてのデータをまだ処理していないためにバッファにスペースが残っていない場合、 SocketChannel.read()は読み取られたゼロバイトを返しますが、 buffer.position()はまだゼロより大きくなります。

バッファ内の正しい場所から読み取りを開始するために、 Buffer.flip()を使用して、ByteBufferの現在の位置をゼロに設定し、その制限をSocketChannel[X216Xによって書き込まれた最後のバイトに設定します。 ] 次に、 storeBufferContents メソッドを使用してバッファーの内容を保存します。これについては、後で説明します。 最後に、 buffer.compact() バッファを圧縮し、現在の位置を次の読み取りに備えて設定します。 SocketChannel。

データが部分的に到着する可能性があるため、終了条件を含むループでバッファ読み取りコードをラップして、ソケットがまだ接続されているかどうか、または切断されているがデータがバッファに残っているかどうかを確認しましょう。

while (socketChannel.read(byteBuffer) != -1 || byteBuffer.position() > 0) {
    byteBuffer.flip();
    storeBufferContents(byteBuffer, charBuffer, charsetDecoder, ourStore);
    byteBuffer.compact();
}

そして、ソケットを close()することを忘れないでください(try-with-resourcesブロックで開いた場合を除く)。

socketChannel.close();

5.3. バッファからのデータの保存

サーバーからの応答にはヘッダーが含まれるため、データ量がバッファーのサイズを超える可能性があります。 したがって、 StringBuilder を使用して、到着したメッセージ全体を作成します。

メッセージを保存するために、最初にで生のバイトをCharBufferの文字にデコードします。 次に、ポインタを反転して文字データを読み取り、拡張可能な StringBuilderに追加します。最後に、CharBufferをクリアして次の書き込みの準備をします/読み取りサイクル。

それでは、バッファ、 CharsetDecoder 、および StringBuilderを渡す完全なstoreBufferContents()メソッドを実装しましょう。

void storeBufferContents(ByteBuffer byteBuffer, CharBuffer charBuffer, 
  CharsetDecoder charsetDecoder, StringBuilder ourStore) {
    charsetDecoder.decode(byteBuffer, charBuffer, true);
    charBuffer.flip();
    ourStore.append(charBuffer);
    charBuffer.clear();
}

6. 結論

この記事では、元のjava .ioモデルがをブロックし、リクエストを待機し、Streamを使用して受信したデータを操作する方法を説明しました。

対照的に、 java.nioライブラリは、バッファおよびチャネルを使用したノンブロッキング通信を可能にし、パフォーマンスを高速化するためのダイレクトメモリアクセスを提供できます。 ただし、この速度では、バッファの処理がさらに複雑になります。

いつものように、この記事のコードはGitHubから入手できます。