Java IO vs NIO

1. Gambaran keseluruhan

Mengendalikan input dan output adalah tugas biasa bagi pengaturcara Java. Dalam tutorial ini, kita akan melihat perpustakaan java.io (IO) yang asal dan perpustakaan java.nio (NIO) yang lebih baru dan bagaimana perbezaannya ketika berkomunikasi di rangkaian.

2. Ciri Utama

Mari mulakan dengan melihat ciri utama kedua-dua pakej.

2.1. IO - java.io

The java.io pakej diperkenalkan pada Java 1.0 , dengan Reader diperkenalkan di Jawa 1.1. Ia menyediakan:

  • InputStream dan OutputStream - yang menyediakan data satu bait pada satu masa
  • Pembaca dan Penulis - pembungkus kemudahan untuk aliran
  • mod menyekat - untuk menunggu mesej lengkap

2.2. NIO - java.nio

The java.nio pakej diperkenalkan di Jawa 1.4 dan dikemaskini di Jawa 1.7 (NIO.2) dengan operasi fail dipertingkatkan dan ASynchronousSocketChannel . Ia menyediakan:

  • Penyangga - untuk membaca sekumpulan data pada satu masa
  • CharsetDecoder - untuk memetakan bait mentah ke / dari watak yang boleh dibaca
  • Saluran - untuk berkomunikasi dengan dunia luar
  • Selector - untuk membolehkan multiplexing pada SelectableChannel dan menyediakan akses ke mana-mana Saluran yang siap untuk I / O
  • mod tidak menyekat - untuk membaca apa sahaja yang sudah siap

Sekarang mari kita lihat bagaimana kita menggunakan setiap pakej ini ketika kita menghantar data ke pelayan atau membaca responsnya.

3. Konfigurasikan Pelayan Uji Kami

Di sini kita akan menggunakan WireMock untuk mensimulasikan pelayan lain supaya kita dapat menjalankan ujian kita secara bebas.

Kami akan mengkonfigurasinya untuk mendengarkan permintaan kami dan menghantar respons seperti yang dilayan oleh pelayan web sebenar. Kami juga akan menggunakan port dinamik sehingga kami tidak bertentangan dengan perkhidmatan di mesin tempatan kami.

Mari tambahkan kebergantungan Maven untuk WireMock dengan skop ujian :

 com.github.tomakehurst wiremock-jre8 2.26.3 test 

Dalam kelas ujian, mari kita tentukan JUnit @ Peraturan untuk memulakan WireMock di port percuma. Kami kemudian akan mengkonfigurasinya untuk mengembalikan respons HTTP 200 kepada kami apabila kami meminta sumber yang telah ditentukan, dengan isi mesej sebagai beberapa 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 kita telah menyediakan pelayan tiruan kita, kita sudah bersedia untuk menjalankan beberapa ujian.

4. Menyekat IO - java.io

Mari lihat bagaimana model IO sekatan asal berfungsi dengan membaca beberapa data dari laman web. Kami akan menggunakan java.net.Socket untuk mendapatkan akses ke salah satu port sistem operasi.

4.1. Hantar Permintaan

Dalam contoh ini, kami akan membuat permintaan GET untuk mendapatkan sumber kami. Pertama, mari buat Socket untuk mengakses port yang didengarkan oleh pelayan WireMock kami:

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

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

Sekarang mari kita buka OutputStream pada soket , dibungkus dengan OutputStreamWriter dan menyebarkannya ke PrintWriter untuk menulis mesej kami. Dan mari kita pastikan kita mengosongkan penyangga sehingga permintaan kita dihantar:

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 Respons

Mari buka InputStream pada 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 menyekat, menunggu barisan lengkap, kemudian tambahkan talian ke kedai kami. Kami akan terus membaca sehingga kami mendapat nol, yang menunjukkan akhir aliran:

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

5. Tidak Menyekat IO - java.nio

Sekarang, mari kita lihat bagaimana model IO tanpa sekatan pakej nio berfungsi dengan contoh yang sama.

Kali ini, kami akan membuat saluran java.nio.channel . SocketChannel untuk mengakses port di pelayan kami dan bukannya java.net.Socket , dan berikannya sebagai InetSocketAddress .

5.1. Hantar Permintaan

Pertama, mari buka SocketChannel kami :

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

Dan sekarang, mari kita mendapatkan standard UTF-8 Set aksara untuk mengekod dan menulis mesej kami:

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

5.2. Baca Maklum Balas

Setelah kami menghantar permintaan, kami dapat membaca respons dalam mod tidak menyekat, menggunakan penyangga mentah.

Oleh kerana kami akan memproses teks, kami memerlukan ByteBuffer untuk bait mentah dan CharBuffer untuk watak yang ditukar (dibantu oleh CharsetDecoder ):

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

CharBuffer kami akan mempunyai ruang yang tersisa jika data dihantar dalam kumpulan watak berbilang bait.

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 kita laksanakan kaedah storeBufferContents () yang lengkap dalam penyangga kami, CharsetDecoder , dan StringBuilder :

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

6. Kesimpulannya

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

Sebaliknya, yang java.nio perpustakaan membolehkan bukan menyekat komunikasi menggunakan Buffer s dan Channel s dan boleh memberikan capaian ingatan terus untuk prestasi yang lebih cepat. Namun, dengan kelajuan ini muncul kerumitan tambahan dalam menangani penyangga.

Seperti biasa, kod untuk artikel ini terdapat di GitHub.