Pengantar Java NIO Selector

1. Ikhtisar

Pada artikel ini, kita akan menjelajahi bagian pengantar komponen Selector Java NIO .

Selektor menyediakan mekanisme untuk memantau satu atau lebih saluran NIO dan mengenali ketika satu atau lebih tersedia untuk transfer data.

Dengan cara ini, satu utas dapat digunakan untuk mengelola banyak saluran , dan dengan demikian banyak koneksi jaringan.

2. Mengapa Menggunakan Selector?

Dengan selektor, kita dapat menggunakan satu utas alih-alih beberapa untuk mengelola banyak saluran. Peralihan konteks antar utas mahal untuk sistem operasi , dan sebagai tambahan, setiap utas membutuhkan memori.

Oleh karena itu, semakin sedikit utas yang kami gunakan, semakin baik. Namun, penting untuk diingat bahwa sistem operasi modern dan CPU terus menjadi lebih baik dalam multitasking , sehingga overhead multi-threading terus berkurang seiring waktu.

Yang akan kita bahas di sini adalah bagaimana kita dapat menangani banyak saluran dengan satu utas menggunakan selektor.

Perhatikan juga bahwa penyeleksi tidak hanya membantu Anda membaca data; mereka juga dapat mendengarkan koneksi jaringan yang masuk dan menulis data melalui saluran yang lambat.

3. Penyiapan

Untuk menggunakan selektor, kita tidak memerlukan pengaturan khusus. Semua kelas yang kita butuhkan adalah paket inti java.nio dan kita hanya perlu mengimpor apa yang kita butuhkan.

Setelah itu, kita dapat mendaftarkan beberapa saluran dengan objek selektor. Saat aktivitas I / O terjadi di salah satu saluran, pemilih memberi tahu kami. Beginilah cara kita membaca dari sejumlah besar sumber data dari satu utas.

Setiap saluran yang kami daftarkan dengan selektor harus merupakan sub-kelas dari Saluran Pilihan . Ini adalah jenis saluran khusus yang dapat ditempatkan dalam mode non-pemblokiran.

4. Membuat Selector

Selektor dapat dibuat dengan menjalankan metode buka statis kelas Selector , yang akan menggunakan penyedia pemilih default sistem untuk membuat selektor baru:

Selector selector = Selector.open();

5. Mendaftarkan Saluran yang Dapat Dipilih

Agar pemilih dapat memantau saluran apa pun, kita harus mendaftarkan saluran ini dengan selektor. Kami melakukan ini dengan menjalankan metode register dari saluran yang dapat dipilih.

Tetapi sebelum saluran didaftarkan dengan pemilih, itu harus dalam mode non-pemblokiran:

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

Ini berarti bahwa kita tidak dapat menggunakan FileChannel dengan selektor karena mereka tidak dapat dialihkan ke mode non-pemblokiran seperti yang kita lakukan dengan saluran soket.

Parameter pertama adalah objek Selector yang kita buat sebelumnya, parameter kedua mendefinisikan set minat , artinya peristiwa apa yang ingin kita dengarkan di saluran yang dipantau, melalui selector.

Ada empat peristiwa berbeda yang dapat kita dengarkan, masing-masing diwakili oleh konstanta di kelas SelectionKey :

  • Hubungkan - ketika klien mencoba untuk terhubung ke server. Diwakili oleh SelectionKey.OP_CONNECT
  • Terima - ketika server menerima koneksi dari klien. Diwakili oleh SelectionKey.OP_ACCEPT
  • Baca - saat server siap membaca dari saluran. Diwakili oleh SelectionKey.OP_READ
  • Tulis - jika server siap untuk menulis ke saluran. Diwakili oleh SelectionKey.OP_WRITE

Objek yang dikembalikan SelectionKey mewakili pendaftaran saluran yang dapat dipilih dengan selektor. Kami akan melihatnya lebih jauh di bagian berikut.

6. Objek SelectionKey

Seperti yang kita lihat di bagian sebelumnya, ketika kita mendaftarkan saluran dengan selektor, kita mendapatkan objek SelectionKey . Objek ini menyimpan data yang mewakili pendaftaran saluran.

Ini berisi beberapa properti penting yang harus kita pahami dengan baik untuk dapat menggunakan selektor pada saluran. Kami akan melihat properti ini di subbagian berikut.

6.1. Set Bunga

Himpunan minat menentukan rangkaian acara yang kami ingin diperhatikan oleh pemilih di saluran ini. Ini adalah nilai integer; kita bisa mendapatkan informasi ini dengan cara berikut.

Pertama, kita harus set bunga dikembalikan oleh SelectionKey 's interestOps metode. Kemudian kami memiliki acara konstan di SelectionKey yang kami lihat sebelumnya.

Saat kita DAN kedua nilai ini, kita mendapatkan nilai boolean yang memberi tahu kita apakah acara sedang diawasi 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 Siap

Set siap menentukan kumpulan acara yang salurannya siap. Ini juga merupakan nilai integer; kita bisa mendapatkan informasi ini dengan cara berikut.

Kami punya set siap dikembalikan oleh SelectionKey 's readyOps metode. Saat kita DAN nilai ini dengan konstanta peristiwa seperti yang kita lakukan dalam kasus set yang diminati, kita mendapatkan boolean yang mewakili apakah saluran siap untuk nilai tertentu atau tidak.

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

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

6.3. Saluran

Mengakses saluran yang sedang ditonton dari objek SelectionKey sangat sederhana. Kami hanya memanggil metode saluran :

Channel channel = key.channel();

6.4. Selector

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

Selector selector = key.selector();

6.5. Melampirkan Objek

Kita bisa melampirkan objek ke SelectionKey. Terkadang kami mungkin ingin memberi saluran ID khusus atau melampirkan segala jenis objek Java yang mungkin ingin kami lacak.

Melampirkan objek adalah cara praktis untuk melakukannya. Berikut adalah cara Anda melampirkan dan mendapatkan objek dari SelectionKey :

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

Alternatifnya, kita dapat memilih untuk melampirkan objek selama registrasi saluran. Kami menambahkannya sebagai parameter ketiga ke metode register saluran , seperti:

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

7. Pemilihan Tombol Saluran

Sejauh ini, kita telah melihat cara membuat selektor, mendaftarkan saluran ke sana, dan memeriksa properti objek SelectionKey yang mewakili pendaftaran saluran ke selektor.

Ini hanya setengah dari proses, sekarang kita harus melakukan proses berkelanjutan untuk memilih set siap yang telah kita lihat sebelumnya. Kami melakukan seleksi menggunakan selector ini pilih metode, 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 klien untuk mendapatkan pesan yang dikirim oleh server. Kami mengembalikan ini sebagai gema dari pesan kami.

8.3. Menguji

Di dalam kelas yang disebut EchoTest.java , kita akan membuat kasus uji yang memulai server, mengirim pesan ke server dan hanya lewat ketika pesan yang sama diterima kembali dari server. Sebagai langkah terakhir, kasus uji menghentikan server sebelum selesai.

Kami sekarang dapat menjalankan pengujian:

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. Kesimpulan

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

Kode sumber lengkap dan semua cuplikan kode untuk artikel ini tersedia di proyek GitHub saya.