Inleiding tot de Java NIO Selector

1. Overzicht

In dit artikel zullen we de inleidende delen van Java NIO's verkennen Selector component.

Een selector biedt een mechanisme om een ​​of meer NIO-kanalen te bewaken en te herkennen wanneer een of meer beschikbaar komen voor gegevensoverdracht.

Op deze manier een enkele thread kan worden gebruikt voor het beheren van meerdere kanalen, en dus meerdere netwerkverbindingen.

2. Waarom een ​​selector gebruiken?

Met een selector kunnen we één thread gebruiken in plaats van meerdere om meerdere kanalen te beheren. Context-switching tussen threads is duur voor het besturingssysteem, en bovendien, elke draad neemt geheugen in beslag.

Daarom, hoe minder draden we gebruiken, hoe beter. Het is echter belangrijk om dat te onthouden moderne besturingssystemen en CPU's worden steeds beter in multitasken, dus de overheadkosten van multi-threading blijven in de loop van de tijd afnemen.

We zullen het hier behandelen, hoe we meerdere kanalen kunnen behandelen met een enkele thread met behulp van een selector.

Merk ook op dat selectors u niet alleen helpen bij het lezen van gegevens; ze kunnen ook luisteren naar inkomende netwerkverbindingen en gegevens over langzame kanalen schrijven.

3. Installatie

Om de selector te gebruiken, hebben we geen speciale instellingen nodig. Alle lessen die we nodig hebben, vormen de kern java.nio pakket en we hoeven alleen maar te importeren wat we nodig hebben.

Daarna kunnen we meerdere kanalen registreren met een selectorobject. Wanneer er I / O-activiteit plaatsvindt op een van de kanalen, stelt de selector ons hiervan op de hoogte. Dit is hoe we kunnen lezen uit een groot aantal databronnen uit een enkele thread.

Elk kanaal dat we registreren met een selector moet een subklasse zijn van Selecteerbaar kanaal. Dit zijn een speciaal type kanalen die in de niet-blokkerende modus kunnen worden gezet.

4. Een selector maken

Een selector kan worden gemaakt door het aanroepen van de static Open methode van de Selector class, die de standaard selectorprovider van het systeem gebruikt om een ​​nieuwe selector te maken:

Selector selector = Selector.open ();

5. Registreren van selecteerbare kanalen

Om een ​​selector kanalen te laten monitoren, moeten we deze kanalen bij de selector registreren. Dit doen we door een beroep te doen op de registreren methode van het selecteerbare kanaal.

Maar voordat een kanaal wordt geregistreerd met een selector, moet het in de niet-blokkerende modus zijn:

channel.configureBlocking (false); SelectionKey-toets = kanaal.register (selector, SelectionKey.OP_READ);

Dit betekent dat we geen gebruik kunnen maken van FileChannels met een selector omdat ze niet in de niet-blokkerende modus kunnen worden geschakeld zoals we dat doen met stopcontactkanalen.

De eerste parameter is de Selector object dat we eerder hebben gemaakt, definieert de tweede parameter een renteverzameling, wat betekent naar welke gebeurtenissen we geïnteresseerd zijn om naar te luisteren in het bewaakte kanaal, via de selector.

Er zijn vier verschillende gebeurtenissen waar we naar kunnen luisteren, elk wordt vertegenwoordigd door een constante in de SelectionKey klasse:

  • Aansluiten wanneer een client probeert verbinding te maken met de server. Vertegenwoordigd door SelectionKey.OP_CONNECT
  • Aanvaarden wanneer de server een verbinding van een client accepteert. Vertegenwoordigd door SelectionKey.OP_ACCEPT
  • Lezen wanneer de server klaar is om van het kanaal te lezen. Vertegenwoordigd door SelectionKey.OP_READ
  • Schrijven wanneer de server klaar is om naar het kanaal te schrijven. Vertegenwoordigd door SelectionKey.OP_WRITE

Het geretourneerde object SelectionKey vertegenwoordigt de registratie van het selecteerbare kanaal met de selector. We zullen het in de volgende sectie verder bekijken.

6. Het SelectionKey Voorwerp

Zoals we in de vorige sectie hebben gezien, krijgen we een SelectionKey voorwerp. Dit object bevat gegevens die de registratie van het kanaal vertegenwoordigen.

Het bevat enkele belangrijke eigenschappen die we goed moeten begrijpen om de selector op het kanaal te kunnen gebruiken. We zullen deze eigenschappen in de volgende onderafdelingen bekijken.

6.1. De rente-set

Een interesseset definieert de reeks gebeurtenissen waarop we willen dat de selector op dit kanaal let. Het is een geheel getal; we kunnen deze informatie op de volgende manier verkrijgen.

Ten eerste hebben we de rente die is vastgesteld door de SelectionKey‘S interestOps methode. Dan hebben we de gebeurtenis constant in SelectionKey we keken eerder.

Als we EN deze twee waarden hebben, krijgen we een booleaanse waarde die ons vertelt of er naar de gebeurtenis wordt gekeken of niet:

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. De Ready Set

De kant-en-klare set definieert de reeks gebeurtenissen waarvoor het kanaal klaar is. Het is ook een geheel getal; we kunnen deze informatie op de volgende manier verkrijgen.

We hebben de kant-en-klare set teruggestuurd SelectionKey‘S readyOps methode. Wanneer we AND deze waarde met de gebeurtenisconstanten zoals we deden in het geval van interesse, krijgen we een boolean die aangeeft of het kanaal klaar is voor een bepaalde waarde of niet.

Een andere alternatieve en kortere manier om dit te doen, is door te gebruiken SelectieKey 's gemaksmethoden voor hetzelfde doel:

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

6.3. Het kanaal

Toegang krijgen tot het kanaal dat wordt bekeken vanaf het SelectionKey object is heel eenvoudig. We noemen gewoon de kanaal methode:

Kanaalkanaal = key.channel ();

6.4. De selector

Net als bij het verkrijgen van een kanaal, is het heel gemakkelijk om het Selector object uit de SelectionKey voorwerp:

Keuzeschakelaar = key.selector ();

6.5. Objecten bijvoegen

We kunnen een object aan een SelectionKey. Soms willen we een kanaal een aangepaste ID geven of een willekeurig Java-object bijvoegen dat we willen bijhouden.

Objecten bevestigen is een handige manier om het te doen. Hier ziet u hoe u objecten van een SelectionKey:

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

Als alternatief kunnen we ervoor kiezen om een ​​object te bevestigen tijdens de kanaalregistratie. We voegen het als derde parameter toe aan kanalen registreren methode, zoals zo:

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

7. Selectie van kanaaltoetsen

Tot nu toe hebben we gekeken hoe we een selector kunnen maken, kanalen kunnen registreren en de eigenschappen van het SelectionKey object dat de registratie van een kanaal bij een selector vertegenwoordigt.

Dit is slechts de helft van het proces, nu moeten we een continu proces uitvoeren om de kant-en-klare set te selecteren die we eerder hebben bekeken. We doen selectie met behulp van selector's selecteer methode, zoals zo:

int kanalen = selector.select ();

Deze methode blokkeert totdat ten minste één kanaal gereed is voor een bewerking. Het geretourneerde integer getal vertegenwoordigt het aantal sleutels waarvan de kanalen gereed zijn voor een bewerking.

Vervolgens halen we meestal de set geselecteerde sleutels op voor verwerking:

Stel selectedKeys = selector.selectedKeys ();

De set die we hebben verkregen is van SelectionKey objecten vertegenwoordigt elke sleutel een geregistreerd kanaal dat klaar is voor een operatie.

Hierna herhalen we deze set meestal en voor elke toets verkrijgen we het kanaal en voeren we alle bewerkingen uit die in ons belang verschijnen.

Tijdens de levensduur van een kanaal kan het meerdere keren worden geselecteerd, aangezien de sleutel ervan in de set klaar voor verschillende evenementen verschijnt. Daarom moeten we een continue lus hebben om kanaalgebeurtenissen vast te leggen en te verwerken wanneer ze zich voordoen.

8. Volledig voorbeeld

Om de kennis die we in de vorige secties hebben opgedaan te versterken, gaan we een compleet client-server-voorbeeld bouwen.

Om onze code gemakkelijk te testen, bouwen we een echoserver en een echo-client. Bij dit soort instellingen maakt de client verbinding met de server en begint hij er berichten naartoe te sturen. De server echoot berichten terug die door elke client zijn verzonden.

Wanneer de server een specifiek bericht tegenkomt, zoals einde, interpreteert het het als het einde van de communicatie en verbreekt het de verbinding met de cliënt.

8.1. De server

Hier is onze code voor EchoServer.java:

openbare klasse EchoServer {privé statische laatste String POISON_PILL = "POISON_PILL"; public static void main (String [] args) gooit IOException {Selector selector = Selector.open (); ServerSocketChannel serverSocket = ServerSocketChannel.open (); serverSocket.bind (nieuw InetSocketAddress ("localhost", 5454)); serverSocket.configureBlocking (false); serverSocket.register (selector, SelectionKey.OP_ACCEPT); ByteBuffer-buffer = ByteBuffer.allocate (256); while (true) {selector.select (); Stel selectedKeys = selector.selectedKeys (); Iterator iter = selectedKeys.iterator (); while (iter.hasNext ()) {SelectionKey-toets = iter.next (); if (key.isAcceptable ()) {register (selector, serverSocket); } if (key.isReadable ()) {answerWithEcho (buffer, sleutel); } iter.remove (); }}} privé statische ongeldige answerWithEcho (ByteBuffer-buffer, SelectionKey-sleutel) gooit IOException {SocketChannel-client = (SocketChannel) key.channel (); client.read (buffer); if (nieuwe String (buffer.array ()). trim (). equals (POISON_PILL)) {client.close (); System.out.println ("Accepteert geen klantberichten meer"); } else {buffer.flip (); client.write (buffer); buffer.clear (); }} privé statisch ongeldig register (Selector selector, ServerSocketChannel serverSocket) gooit IOException {SocketChannel client = serverSocket.accept (); client.configureBlocking (false); client.register (selector, SelectionKey.OP_READ); } public static Process start () gooit 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 = nieuwe ProcessBuilder (javaBin, "-cp", classpath, className); terugkeer builder.start (); }}

Dit is wat er gebeurt; we creëren een Selector object door de static Open methode. Vervolgens maken we een kanaal ook door het statisch aan te roepen Open methode, in het bijzonder een ServerSocketChannel voorbeeld.

Dit is zo omdat ServerSocketChannel is selecteerbaar en goed voor een stream-georiënteerde luisteraansluiting.

We binden het vervolgens aan een poort naar keuze. Onthoud dat we eerder zeiden dat voordat we een selecteerbaar kanaal registreren bij een selector, we het eerst in de niet-blokkerende modus moeten zetten. Dus vervolgens doen we dit en registreren we het kanaal naar de selector.

We hebben de SelectionKey instantie van dit kanaal in dit stadium, dus we zullen het niet onthouden.

Java NIO gebruikt een ander buffergeoriënteerd model dan een stroomgeoriënteerd model. Socketcommunicatie vindt dus meestal plaats door te schrijven naar en te lezen uit een buffer.

We creëren daarom een ​​nieuw ByteBuffer waar de server naar zal schrijven en van zal lezen. We initialiseren het tot 256 bytes, het is gewoon een willekeurige waarde, afhankelijk van hoeveel gegevens we heen en weer willen verzenden.

Ten slotte voeren we het selectieproces uit. We selecteren de gereed gemaakte kanalen, halen hun selectietoetsen op, herhalen de toetsen en voeren de bewerkingen uit waarvoor elk kanaal gereed is.

We doen dit in een oneindige lus, aangezien servers meestal moeten blijven draaien, of er nu een activiteit is of niet.

De enige operatie a ServerSocketChannel kan hanteren is een AANVAARDEN operatie. Als we de verbinding van een klant accepteren, krijgen we een SocketChannel object waarop we kunnen lezen en schrijven. We zetten het in de niet-blokkerende modus en registreren het voor een READ-bewerking op de selector.

Tijdens een van de volgende selecties wordt dit nieuwe kanaal read-ready. We halen het op en lezen de inhoud in de buffer. Zoals het een echoserver is, moeten we deze inhoud terugschrijven naar de client.

Als we willen schrijven naar een buffer waaruit we hebben gelezen, moeten we de omdraaien() methode.

We hebben de buffer uiteindelijk in de schrijfmodus gezet door de omdraaien methode en schrijf er gewoon naar.

De begin() methode is zo gedefinieerd dat de echoserver kan worden gestart als een afzonderlijk proces tijdens het testen van de unit.

8.2. De cliënt

Hier is onze code voor EchoClient.java:

openbare klasse EchoClient {privé statische SocketChannel-client; privé statische ByteBuffer-buffer; privé statische EchoClient-instantie; openbare statische EchoClient start () {if (instantie == null) instantie = nieuwe EchoClient (); terugkeer instantie; } public static void stop () gooit IOException {client.close (); buffer = null; } private EchoClient () {probeer {client = SocketChannel.open (nieuw InetSocketAddress ("localhost", 5454)); buffer = ByteBuffer.allocate (256); } catch (IOException e) {e.printStackTrace (); }} openbare String sendMessage (String msg) {buffer = ByteBuffer.wrap (msg.getBytes ()); String response = null; probeer {client.write (buffer); buffer.clear (); client.read (buffer); response = nieuwe String (buffer.array ()). trim (); System.out.println ("response =" + antwoord); buffer.clear (); } catch (IOException e) {e.printStackTrace (); } antwoord terug; }}

De client is eenvoudiger dan de server.

We gebruiken een singleton-patroon om het binnen het begin statische methode. We noemen de private constructor vanuit deze methode.

In de private constructor openen we een verbinding op dezelfde poort waarop het serverkanaal was gebonden en nog steeds op dezelfde host.

We creëren dan een buffer waarnaar we kunnen schrijven en waaruit we kunnen lezen.

Eindelijk hebben we een bericht versturen methode die leest omwikkelt elke string die we ernaar doorgeven in een bytebuffer die via het kanaal naar de server wordt verzonden.

We lezen vervolgens van het clientkanaal om het bericht door de server te laten verzenden. We retourneren dit als de echo van onze boodschap.

8.3. Testen

Binnen riep een klas EchoTest.javagaan we een testcase maken die de server start, berichten naar de server stuurt en pas doorgeeft als dezelfde berichten van de server worden teruggekregen. Als laatste stap stopt de testcase de server voordat deze is voltooid.

We kunnen nu de test uitvoeren:

openbare klasse EchoTest {Processerver; EchoClient-client; @Before public void setup () gooit IOException, InterruptedException {server = EchoServer.start (); client = EchoClient.start (); } @Test openbare ongeldig gegevenServerClient_whenServerEchosMessage_thenCorrect () {String resp1 = client.sendMessage ("hallo"); String resp2 = client.sendMessage ("wereld"); assertEquals ("hallo", resp1); assertEquals ("wereld", resp2); } @After public void teardown () gooit IOException {server.destroy (); EchoClient.stop (); }}

9. Conclusie

In dit artikel hebben we het basisgebruik van de Java NIO Selector-component behandeld.

De volledige broncode en alle codefragmenten voor dit artikel zijn beschikbaar in mijn GitHub-project.