Pengenalan Pemilih Java NIO

1. Gambaran keseluruhan

Dalam artikel ini, kami akan meneroka bahagian pengenalan komponen Pemilih Java NIO .

Pemilih menyediakan mekanisme untuk memantau satu atau lebih saluran NIO dan mengenali kapan satu atau lebih tersedia untuk pemindahan data.

Dengan cara ini, satu utas boleh digunakan untuk menguruskan banyak saluran , dan dengan itu banyak sambungan rangkaian.

2. Mengapa Menggunakan Pemilih?

Dengan pemilih, kita dapat menggunakan satu utas dan bukannya beberapa untuk menguruskan banyak saluran. Peralihan konteks antara utas mahal untuk sistem operasi , dan selain itu, setiap utas memerlukan memori.

Oleh itu, semakin sedikit utas yang kita gunakan, semakin baik. Walau bagaimanapun, penting untuk diingat bahawa sistem operasi moden dan CPU terus menjadi lebih baik semasa melakukan multitasking , jadi overhead multi-threading terus berkurang dari masa ke masa.

Kita akan berurusan di sini adalah bagaimana kita dapat menangani pelbagai saluran dengan satu utas menggunakan pemilih.

Perhatikan juga bahawa pemilih tidak hanya membantu anda membaca data; mereka juga dapat mendengar sambungan rangkaian masuk dan menulis data di saluran perlahan.

3. Persediaan

Untuk menggunakan pemilih, kami tidak memerlukan persediaan khas. Semua kelas yang kami perlukan adalah pakej java.nio teras dan kami hanya perlu mengimport apa yang kami perlukan.

Selepas itu, kita dapat mendaftarkan beberapa saluran dengan objek pemilih. Apabila aktiviti I / O berlaku di mana-mana saluran, pemilih memberitahu kami. Ini adalah bagaimana kita dapat membaca dari sebilangan besar sumber data dari satu utas.

Setiap saluran yang kami daftarkan dengan pemilih mestilah subkelas SelectableChannel . Ini adalah jenis saluran khas yang boleh dimasukkan ke dalam mod tidak menyekat.

4. Membuat Pemilih

Pemilih dapat dibuat dengan menggunakan kaedah terbuka statis kelas Selector , yang akan menggunakan penyedia pemilih lalai sistem untuk membuat pemilih baru:

Selector selector = Selector.open();

5. Mendaftarkan Saluran Yang Boleh Dipilih

Agar pemilih dapat memantau saluran apa pun, kita mesti mendaftarkan saluran ini dengan pemilih. Kami melakukan ini dengan menggunakan kaedah daftar saluran yang boleh dipilih.

Tetapi sebelum saluran didaftarkan dengan pemilih, saluran mesti berada dalam mod tidak menyekat:

channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

Ini bermaksud bahawa kita tidak dapat menggunakan FileChannel dengan pemilih kerana mereka tidak dapat beralih ke mod tidak menyekat seperti yang kita lakukan dengan saluran soket.

Parameter pertama adalah objek Pemilih yang kita buat sebelumnya, parameter kedua menentukan kumpulan minat , yang bermaksud peristiwa apa yang kita ingin dengarkan di saluran yang dipantau, melalui pemilih.

Terdapat empat peristiwa berbeza yang dapat kita dengarkan, masing-masing diwakili oleh pemalar di kelas SelectionKey :

  • Sambung - apabila pelanggan cuba menyambung ke pelayan. Diwakili oleh SelectionKey.OP_CONNECT
  • Terima - apabila pelayan menerima sambungan dari pelanggan. Diwakili oleh SelectionKey.OP_ACCEPT
  • Baca - apabila pelayan siap membaca dari saluran. Diwakili oleh SelectionKey.OP_READ
  • Tulis - apabila pelayan siap menulis ke saluran. Diwakili oleh SelectionKey.OP_WRITE

Objek SelectionKey yang dikembalikan mewakili pendaftaran saluran yang boleh dipilih dengan pemilih. Kami akan melihatnya lebih jauh di bahagian berikut.

6. Objek SelectionKey

Seperti yang kita lihat di bahagian sebelumnya, ketika kita mendaftarkan saluran dengan pemilih, kita mendapat objek SelectionKey . Objek ini menyimpan data yang mewakili pendaftaran saluran.

Ia mengandungi beberapa sifat penting yang mesti kita fahami dengan baik agar dapat menggunakan pemilih di saluran. Kami akan melihat hartanah ini dalam bahagian berikut.

6.1. Set Minat

Kumpulan minat menentukan kumpulan peristiwa yang kita mahu pemilih berhati-hati di saluran ini. Ia adalah nilai integer; kita boleh mendapatkan maklumat ini dengan cara berikut.

Pertama, kita mempunyai set faedah yang dikembalikan oleh SelectionKey 's interestOps kaedah. Kemudian kita mempunyai pemalar acara di SelectionKey yang kita lihat sebelumnya.

Apabila kita DAN dua nilai ini, kita mendapat nilai boolean yang memberitahu kita sama ada acara itu ditonton atau tidak:

int interestSet = selectionKey.interestOps(); boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT; boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT; boolean isInterestedInRead = interestSet & SelectionKey.OP_READ; boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

6.2. Set Sedia

Set siap mentakrifkan rangkaian acara yang salurannya bersedia. Ia juga merupakan nilai integer; kita boleh mendapatkan maklumat ini dengan cara berikut.

Kami mempunyai set siap dikembalikan oleh SelectionKey 's readyOps kaedah. Apabila kita DAN nilai ini dengan peristiwa berterusan seperti yang kita lakukan dalam kes minat, kita mendapat boolean yang mewakili sama ada saluran itu siap untuk nilai tertentu atau tidak.

Alternatif lain dan cara yang lebih pendek untuk melakukan ini adalah dengan menggunakan kaedah kemudahan SelectionKey untuk tujuan yang sama:

selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWriteable();

6.3. Saluran

Mengakses saluran yang ditonton dari objek SelectionKey sangat mudah. Kami hanya memanggil kaedah saluran :

Channel channel = key.channel();

6.4. Pemilih

Sama seperti mendapatkan saluran, sangat mudah untuk mendapatkan objek Pemilih dari objek SelectionKey :

Selector selector = key.selector();

6.5. Melampirkan Objek

Kita boleh melampirkan objek ke SelectionKey. Kadang-kadang kita mungkin ingin memberikan saluran ID khas atau melampirkan apa-apa objek Java yang mungkin ingin kita lacak.

Melekatkan objek adalah cara yang berguna untuk melakukannya. Inilah cara anda melampirkan dan mendapatkan objek dari SelectionKey :

key.attach(Object); Object object = key.attachment();

Sebagai alternatif, kita boleh memilih untuk melampirkan objek semasa pendaftaran saluran. Kami menambahkannya sebagai parameter ketiga untuk kaedah pendaftaran saluran , seperti:

SelectionKey key = channel.register( selector, SelectionKey.OP_ACCEPT, object);

7. Pemilihan Kekunci Saluran

Sejauh ini, kami telah melihat bagaimana membuat pemilih, mendaftarkan saluran ke sana dan memeriksa sifat objek SelectionKey yang mewakili pendaftaran saluran ke pemilih.

Ini hanya separuh dari proses, sekarang kita harus melakukan proses berterusan memilih set siap yang kita lihat sebelumnya. Kami melakukan pemilihan menggunakan kaedah pilih pemilih , seperti:

int channels = selector.select();

This method blocks until at least one channel is ready for an operation. The integer returned represents the number of keys whose channels are ready for an operation.

Next, we usually retrieve the set of selected keys for processing:

Set selectedKeys = selector.selectedKeys();

The set we have obtained is of SelectionKey objects, each key represents a registered channel which is ready for an operation.

After this, we usually iterate over this set and for each key, we obtain the channel and perform any of the operations that appear in our interest set on it.

During the lifetime of a channel, it may be selected several times as its key appears in the ready set for different events. This is why we must have a continuous loop to capture and process channel events as and when they occur.

8. Complete Example

To cement the knowledge we have gained in the previous sections, we're going to build a complete client-server example.

For ease of testing out our code, we'll build an echo server and an echo client. In this kind of setup, the client connects to the server and starts sending messages to it. The server echoes back messages sent by each client.

When the server encounters a specific message, such as end, it interprets it as the end of the communication and closes the connection with the client.

8.1. The Server

Here is our code for EchoServer.java:

public class EchoServer { private static final String POISON_PILL = "POISON_PILL"; public static void main(String[] args) throws IOException { Selector selector = Selector.open(); ServerSocketChannel serverSocket = ServerSocketChannel.open(); serverSocket.bind(new InetSocketAddress("localhost", 5454)); serverSocket.configureBlocking(false); serverSocket.register(selector, SelectionKey.OP_ACCEPT); ByteBuffer buffer = ByteBuffer.allocate(256); while (true) { selector.select(); Set selectedKeys = selector.selectedKeys(); Iterator iter = selectedKeys.iterator(); while (iter.hasNext()) { SelectionKey key = iter.next(); if (key.isAcceptable()) { register(selector, serverSocket); } if (key.isReadable()) { answerWithEcho(buffer, key); } iter.remove(); } } } private static void answerWithEcho(ByteBuffer buffer, SelectionKey key) throws IOException { SocketChannel client = (SocketChannel) key.channel(); client.read(buffer); if (new String(buffer.array()).trim().equals(POISON_PILL)) { client.close(); System.out.println("Not accepting client messages anymore"); } else { buffer.flip(); client.write(buffer); buffer.clear(); } } private static void register(Selector selector, ServerSocketChannel serverSocket) throws IOException { SocketChannel client = serverSocket.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); } public static Process start() throws IOException, InterruptedException { String javaHome = System.getProperty("java.home"); String javaBin = javaHome + File.separator + "bin" + File.separator + "java"; String classpath = System.getProperty("java.class.path"); String className = EchoServer.class.getCanonicalName(); ProcessBuilder builder = new ProcessBuilder(javaBin, "-cp", classpath, className); return builder.start(); } }

This is what is happening; we create a Selector object by calling the static open method. We then create a channel also by calling its static open method, specifically a ServerSocketChannel instance.

This is because ServerSocketChannel is selectable and good for a stream-oriented listening socket.

We then bind it to a port of our choice. Remember we said earlier that before registering a selectable channel to a selector, we must first set it to non-blocking mode. So next we do this and then register the channel to the selector.

We don't need the SelectionKey instance of this channel at this stage, so we will not remember it.

Java NIO uses a buffer-oriented model other than a stream-oriented model. So socket communication usually takes place by writing to and reading from a buffer.

We, therefore, create a new ByteBuffer which the server will be writing to and reading from. We initialize it to 256 bytes, it's just an arbitrary value, depending on how much data we plan to transfer to and fro.

Finally, we perform the selection process. We select the ready channels, retrieve their selection keys, iterate over the keys and perform the operations for which each channel is ready.

We do this in an infinite loop since servers usually need to keep running whether there is an activity or not.

The only operation a ServerSocketChannel can handle is an ACCEPT operation. When we accept the connection from a client, we obtain a SocketChannel object on which we can do read and writes. We set it to non-blocking mode and register it for a READ operation to the selector.

During one of the subsequent selections, this new channel will become read-ready. We retrieve it and read it contents into the buffer. True to it's as an echo server, we must write this content back to the client.

When we desire to write to a buffer from which we have been reading, we must call the flip() method.

We finally set the buffer to write mode by calling the flip method and simply write to it.

The start() method is defined so that the echo server can be started as a separate process during unit testing.

8.2. The Client

Here is our code for EchoClient.java:

public class EchoClient { private static SocketChannel client; private static ByteBuffer buffer; private static EchoClient instance; public static EchoClient start() { if (instance == null) instance = new EchoClient(); return instance; } public static void stop() throws IOException { client.close(); buffer = null; } private EchoClient() { try { client = SocketChannel.open(new InetSocketAddress("localhost", 5454)); buffer = ByteBuffer.allocate(256); } catch (IOException e) { e.printStackTrace(); } } public String sendMessage(String msg) { buffer = ByteBuffer.wrap(msg.getBytes()); String response = null; try { client.write(buffer); buffer.clear(); client.read(buffer); response = new String(buffer.array()).trim(); System.out.println("response=" + response); buffer.clear(); } catch (IOException e) { e.printStackTrace(); } return response; } }

The client is simpler than the server.

We use a singleton pattern to instantiate it inside the start static method. We call the private constructor from this method.

In the private constructor, we open a connection on the same port on which the server channel was bound and still on the same host.

We then create a buffer to which we can write and from which we can read.

Finally, we have a sendMessage method which reads wraps any string we pass to it into a byte buffer which is transmitted over the channel to the server.

Kami kemudian membaca dari saluran pelanggan untuk mendapatkan mesej yang dihantar oleh pelayan. Kami mengembalikan ini sebagai gema mesej kami.

8.3. Ujian

Di dalam kelas yang disebut EchoTest.java , kita akan membuat kes ujian yang memulakan pelayan, menghantar mesej ke pelayan dan hanya berlalu apabila mesej yang sama diterima kembali dari pelayan. Sebagai langkah terakhir, kes ujian menghentikan pelayan sebelum selesai.

Kami kini boleh menjalankan ujian:

public class EchoTest { Process server; EchoClient client; @Before public void setup() throws IOException, InterruptedException { server = EchoServer.start(); client = EchoClient.start(); } @Test public void givenServerClient_whenServerEchosMessage_thenCorrect() { String resp1 = client.sendMessage("hello"); String resp2 = client.sendMessage("world"); assertEquals("hello", resp1); assertEquals("world", resp2); } @After public void teardown() throws IOException { server.destroy(); EchoClient.stop(); } }

9. Kesimpulannya

Dalam artikel ini, kami telah membahas penggunaan dasar komponen Java NIO Selector.

Kod sumber yang lengkap dan semua coretan kod untuk artikel ini terdapat di projek GitHub saya.