Java IO vs NIO

1. Ikhtisar

Menangani input dan output adalah tugas umum untuk programmer Java. Dalam tutorial ini, kita akan melihat asli java.io (IO) perpustakaan dan baru java.nio (NIO) perpustakaan dan bagaimana mereka berbeda ketika berkomunikasi di dalam jaringan.

2. Fitur Utama

Mari kita mulai dengan melihat fitur utama dari kedua paket tersebut.

2.1. IO - java.io

The java.io paket diperkenalkan di Jawa 1.0 , dengan Pembaca diperkenalkan di Jawa 1.1. Ini menyediakan:

  • InputStream dan OutputStream - yang menyediakan data satu byte dalam satu waktu
  • Pembaca dan Penulis - pembungkus kenyamanan untuk streaming
  • mode pemblokiran - untuk menunggu pesan lengkap

2.2. NIO - java.nio

The java.nio paket diperkenalkan di Jawa 1,4 dan diperbarui di Jawa 1,7 (NIO.2) dengan operasi file ditingkatkan dan ASynchronousSocketChannel . Ini menyediakan:

  • Buffer - untuk membaca potongan data dalam satu waktu
  • CharsetDecoder - untuk memetakan byte mentah ke / dari karakter yang dapat dibaca
  • Saluran - untuk berkomunikasi dengan dunia luar
  • Selector - untuk mengaktifkan multiplexing pada SelectableChannel dan memberikan akses ke setiap Channel yang siap untuk I / O
  • mode non-pemblokiran - untuk membaca apa pun yang siap

Sekarang mari kita lihat bagaimana kita menggunakan masing-masing paket ini ketika kita mengirim data ke server atau membaca tanggapannya.

3. Konfigurasi Server Pengujian Kami

Di sini kami akan menggunakan WireMock untuk mensimulasikan server lain sehingga kami dapat menjalankan pengujian kami secara mandiri.

Kami akan mengkonfigurasinya untuk mendengarkan permintaan kami dan mengirimkan tanggapan kepada kami seperti server web asli. Kami juga akan menggunakan port dinamis sehingga kami tidak mengalami konflik dengan layanan apa pun di mesin lokal kami.

Mari tambahkan dependensi Maven untuk WireMock dengan cakupan pengujian :

 com.github.tomakehurst wiremock-jre8 2.26.3 test 

Di kelas pengujian, mari tentukan JUnit @Rule untuk memulai WireMock di port gratis. Kami kemudian akan mengkonfigurasinya untuk mengembalikan kami respons HTTP 200 ketika kami meminta sumber daya yang telah ditentukan, dengan badan pesan sebagai teks dalam format 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!\" }"))); }

Sekarang setelah kami menyiapkan server tiruan, kami siap menjalankan beberapa tes.

4. Memblokir IO - java.io

Mari kita lihat bagaimana model IO pemblokiran asli bekerja dengan membaca beberapa data dari situs web. Kami akan menggunakan java.net.Socket untuk mendapatkan akses ke salah satu port sistem operasi.

4.1. Kirim Permintaan

Dalam contoh ini, kami akan membuat permintaan GET untuk mengambil sumber daya kami. Pertama, mari buat Socket untuk mengakses port yang didengarkan oleh server WireMock kita:

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

Untuk komunikasi HTTP atau HTTPS normal, port akan menjadi 80 atau 443. Namun, dalam kasus ini, kami menggunakan wireMockRule.port () untuk mengakses port dinamis yang kami siapkan sebelumnya.

Sekarang mari kita buka OutputStream pada soket , dibungkus dengan OutputStreamWriter dan berikan ke PrintWriter untuk menulis pesan kita. Dan mari kita pastikan kita membersihkan buffer sehingga permintaan kita terkirim:

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. Tunggu Responnya

Mari buka InputStream di soket untuk mengakses respons, baca aliran dengan BufferedReader , dan simpan di StringBuilder :

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

Mari gunakan reader.readLine () untuk memblokir, menunggu baris lengkap, lalu tambahkan baris tersebut ke toko kita. Kami akan terus membaca sampai kami mendapatkan null, yang menunjukkan akhir dari streaming:

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

5. Non-Blocking IO - java.nio

Sekarang, mari kita lihat bagaimana model IO non-pemblokiran paket nio bekerja dengan contoh yang sama.

Kali ini, kami akan membuat java.nio.channel . SocketChannel untuk mengakses port di server kami, bukan java.net.Socket , dan berikan InetSocketAddress .

5.1. Kirim Permintaan

Pertama, mari buka SocketChannel kami :

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

Dan sekarang, mari kita dapatkan UTF-8 Charset standar untuk menyandikan dan menulis pesan kita:

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

5.2. Baca Responnya

Setelah kami mengirim permintaan, kami dapat membaca respons dalam mode non-pemblokiran, menggunakan buffer mentah.

Karena kita akan memproses teks, kita memerlukan ByteBuffer untuk byte mentah dan CharBuffer untuk karakter yang dikonversi (dibantu oleh CharsetDecoder ):

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

CharBuffer kami akan memiliki sisa ruang jika data dikirim dalam kumpulan karakter multi-byte.

Note that if we need especially fast performance, we can create a MappedByteBuffer in native memory using ByteBuffer.allocateDirect(). However, in our case, using allocate() from the standard heap is fast enough.

When dealing with buffers, we need to know how big the buffer is (the capacity), where we are in the buffer (the current position), and how far we can go (the limit).

So, let's read from our SocketChannel, passing it our ByteBuffer to store our data. Our read from the SocketChannel will finish with our ByteBuffer‘s current position set to the next byte to write to (just after the last byte written), but with its limit unchanged:

socketChannel.read(byteBuffer)

Our SocketChannel.read() returns the number of bytes read that could be written into our buffer. This will be -1 if the socket was disconnected.

When our buffer doesn't have any space left because we haven't processed all its data yet, then SocketChannel.read() will return zero bytes read but our buffer.position() will still be greater than zero.

To make sure that we start reading from the right place in the buffer, we'll use Buffer.flip() to set our ByteBuffer‘s current position to zero and its limit to the last byte that was written by the SocketChannel. We'll then save the buffer contents using our storeBufferContents method, which we'll look at later. Lastly, we'll use buffer.compact() to compact the buffer and set the current position ready for our next read from the SocketChannel.

Since our data may arrive in parts, let's wrap our buffer-reading code in a loop with termination conditions to check if our socket is still connected or if we've been disconnected but still have data left in our buffer:

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

And let's not forget to close() our socket (unless we opened it in a try-with-resources block):

socketChannel.close();

5.3. Storing Data From Our Buffer

The response from the server will contain headers, which may make the amount of data exceed the size of our buffer. So, we'll use a StringBuilder to build our complete message as it arrives.

To store our message, we first decode the raw bytes into characters in our CharBuffer. Then we'll flip the pointers so that we can read our character data, and append it to our expandable StringBuilder. Lastly, we'll clear the CharBuffer ready for the next write/read cycle.

Jadi sekarang, mari implementasikan metode storeBufferContents () lengkap kami dengan meneruskan buffer, CharsetDecoder , dan StringBuilder kami :

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

6. Kesimpulan

Pada artikel ini, kita telah melihat bagaimana model java.io asli memblokir , menunggu permintaan dan menggunakan Stream untuk memanipulasi data yang diterimanya.

Sebaliknya, para java.nio perpustakaan memungkinkan untuk non-memblokir komunikasi menggunakan Buffer dan Saluran dan dapat memberikan akses memori langsung untuk performa yang lebih cepat. Namun, dengan kecepatan ini muncul kerumitan tambahan dalam menangani buffer.

Seperti biasa, kode untuk artikel ini tersedia di GitHub.