Introduksjon til Java NIO Selector

1. Oversikt

I denne artikkelen vil vi utforske de innledende delene av Java NIO-er Velger komponent.

En velger gir en mekanisme for å overvåke en eller flere NIO-kanaler og gjenkjenne når en eller flere blir tilgjengelige for dataoverføring.

Denne måten, en enkelt tråd kan brukes til å administrere flere kanaler, og dermed flere nettverkstilkoblinger.

2. Hvorfor bruke en velger?

Med en velger kan vi bruke en tråd i stedet for flere for å administrere flere kanaler. Det er kostbart å bytte mellom trådene for operativsystemet, og i tillegg hver tråd tar opp minne.

Derfor, jo færre tråder vi bruker, jo bedre. Det er imidlertid viktig å huske det moderne operativsystemer og CPU-er blir stadig bedre på multitasking, slik at kostnadene ved multitråding fortsetter å avta over tid.

Vi skal håndtere her er hvordan vi kan håndtere flere kanaler med en enkelt tråd ved hjelp av en velger.

Legg også merke til at velgerne ikke bare hjelper deg å lese data; de kan også lytte etter innkommende nettverkstilkoblinger og skrive data på tvers av langsomme kanaler.

3. Oppsett

For å bruke velgeren trenger vi ikke noe spesielt oppsett. Alle klassene vi trenger er kjernen java.nio pakken, og vi må bare importere det vi trenger.

Etter det kan vi registrere flere kanaler med et velgerobjekt. Når I / O-aktivitet skjer på noen av kanalene, gir velgeren oss beskjed. Slik kan vi lese fra et stort antall datakilder fra en enkelt tråd.

Enhver kanal vi registrerer hos en velger må være en underklasse av Valgbar kanal. Dette er en spesiell type kanaler som kan settes i ikke-blokkerende modus.

4. Opprette en velger

En velger kan opprettes ved å påkalle det statiske åpen metoden for Velger klasse, som vil bruke systemets standardvelgerleverandør til å opprette en ny velger:

Selector selector = Selector.open ();

5. Registrere valgbare kanaler

For at en velger kan overvåke kanaler, må vi registrere disse kanalene hos velgeren. Vi gjør dette ved å påkalle registrere metoden til den valgbare kanalen.

Men før en kanal blir registrert med en velger, må den være i ikke-blokkerende modus:

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

Dette betyr at vi ikke kan bruke FileChannels med en velger, siden de ikke kan byttes til ikke-blokkeringsmodus slik vi gjør med stikkontakter.

Den første parameteren er Velger objektet vi opprettet tidligere, definerer den andre parameteren et interessesett, som betyr hvilke hendelser vi er interessert i å lytte etter i den overvåkede kanalen, via velgeren.

Det er fire forskjellige hendelser vi kan lytte etter, hver er representert med en konstant i SelectionKey klasse:

  • Koble når en klient prøver å koble til serveren. Representert av SelectionKey.OP_CONNECT
  • Aksepterer når serveren godtar en tilkobling fra en klient. Representert av SelectionKey.OP_ACCEPT
  • Lese når serveren er klar til å lese fra kanalen. Representert av SelectionKey.OP_READ
  • Skrive når serveren er klar til å skrive til kanalen. Representert av SelectionKey.OP_WRITE

Det returnerte objektet SelectionKey representerer den valgbare kanalens registrering hos velgeren. Vi ser nærmere på det i neste avsnitt.

6. Den SelectionKey Gjenstand

Som vi så i forrige avsnitt, når vi registrerer en kanal med en velger, får vi en SelectionKey gjenstand. Dette objektet inneholder data som representerer registreringen av kanalen.

Den inneholder noen viktige egenskaper som vi må forstå for å kunne bruke velgeren på kanalen. Vi ser på disse egenskapene i de følgende underavsnittene.

6.1. Rentesettet

Et interessesett definerer settet med hendelser som vi ønsker at velgeren skal se opp for på denne kanalen. Det er et heltall; vi kan få denne informasjonen på følgende måte.

For det første har vi renten satt av SelectionKey‘S interestOps metode. Så har vi hendelsen konstant i SelectionKey vi så på tidligere.

Når vi OG disse to verdiene får vi en boolsk verdi som forteller oss om hendelsen blir sett på eller ikke:

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

6.2. Det klare settet

Klarsett definerer settet med hendelser som kanalen er klar for. Det er også et heltall; vi kan få denne informasjonen på følgende måte.

Vi har klart sett tilbake SelectionKey‘S readyOps metode. Når vi OG denne verdien med hendelseskonstantene som vi gjorde i tilfelle av interesse, får vi en boolsk som representerer om kanalen er klar for en bestemt verdi eller ikke.

Et annet alternativ og kortere måte å gjøre dette på er å bruke SelectionKey 's bekvemmelighetsmetoder for samme formål:

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

6.3. Kanalen

Åpne kanalen som blir sett fra SelectionKey objektet er veldig enkelt. Vi bare kaller kanal metode:

Channel channel = key.channel ();

6.4. Selektoren

Akkurat som å få en kanal, er det veldig enkelt å få tak i Velger objekt fra SelectionKey gjenstand:

Velgervelger = key.selector ();

6.5. Feste gjenstander

Vi kan feste et objekt til en SelectionKey. Noen ganger kan det være lurt å gi en kanal en egendefinert ID eller legge ved noen form for Java-objekt vi kanskje vil ha oversikt over.

Å feste gjenstander er en praktisk måte å gjøre det på. Slik fester du og henter gjenstander fra en SelectionKey:

key.attach (Objekt); Objektobjekt = key.attachment ();

Alternativt kan vi velge å legge ved et objekt under kanalregistrering. Vi legger den til som en tredje parameter til kanalene registrere metode, slik:

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

7. Valg av kanaltast

Så langt har vi sett på hvordan vi kan lage en velger, registrere kanaler for den og inspisere egenskapene til SelectionKey objekt som representerer en kanals registrering til en velger.

Dette er bare halvparten av prosessen, nå må vi utføre en kontinuerlig prosess for å velge det ferdige settet som vi så på tidligere. Vi gjør valg ved å velge å velge metode, slik:

int kanaler = selector.select ();

Denne metoden blokkeres til minst en kanal er klar for operasjon. Heltallet som returneres representerer antall nøkler hvis kanaler er klare for en operasjon.

Deretter henter vi vanligvis settet med valgte nøkler for behandling:

Still utvalgte nøkler = selector.selectedKeys ();

Settet vi har fått er av SelectionKey objekter, representerer hver nøkkel en registrert kanal som er klar for en operasjon.

Etter dette gjentar vi vanligvis dette settet, og for hver nøkkel får vi kanalen og utfører noen av operasjonene som vises i vår interesse som er satt på den.

I løpet av en kanals levetid kan den velges flere ganger da nøkkelen vises i det klare settet for forskjellige hendelser. Dette er grunnen til at vi må ha en kontinuerlig sløyfe for å fange opp og behandle kanalhendelser når og når de inntreffer.

8. Komplett eksempel

For å sementere kunnskapen vi har fått i de forrige avsnittene, skal vi bygge et komplett eksempel på klientserver.

For å gjøre det enkelt å teste ut koden vår, bygger vi en ekkoserver og en ekkoklient. I denne typen oppsett kobles klienten til serveren og begynner å sende meldinger til den. Serveren ekko tilbake meldinger sendt av hver klient.

Når serveren møter en bestemt melding, for eksempel slutt, tolker den det som slutten på kommunikasjonen og lukker forbindelsen med klienten.

8.1. Serveren

Her er koden vår for EchoServer.java:

offentlig klasse EchoServer {privat statisk finale String POISON_PILL = "POISON_PILL"; public static void main (String [] args) kaster IOException {Selector selector = Selector.open (); ServerSocketChannel serverSocket = ServerSocketChannel.open (); serverSocket.bind (ny InetSocketAddress ("localhost", 5454)); serverSocket.configureBlocking (false); serverSocket.register (selector, SelectionKey.OP_ACCEPT); ByteBuffer buffer = ByteBuffer.allocate (256); while (true) {selector.select (); Still utvalgte nøkler = selector.selectedKeys (); Iterator iter = valgtKeys.iterator (); while (iter.hasNext ()) {SelectionKey key = iter.next (); if (key.isAcceptable ()) {register (selector, serverSocket); } hvis (key.isReadable ()) {answerWithEcho (buffer, nøkkel); } iter.remove (); }}} privat statisk ugyldig answerWithEcho (ByteBuffer-buffer, SelectionKey-tast) kaster IOException {SocketChannel-klient = (SocketChannel) key.channel (); client.read (buffer); hvis (ny streng (buffer.array ()). trim (). er lik (POISON_PILL)) {client.close (); System.out.println ("Godtar ikke klientmeldinger lenger"); } annet {buffer.flip (); client.write (buffer); buffer.clear (); }} privat statisk ugyldig register (Selector selector, ServerSocketChannel serverSocket) kaster IOException {SocketChannel client = serverSocket.accept (); client.configureBlocking (false); client.register (selector, SelectionKey.OP_READ); } offentlig statisk prosessstart () kaster 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 = ny ProcessBuilder (javaBin, "-cp", classpath, className); return builder.start (); }}

Dette er hva som skjer; vi lager en Velger objekt ved å ringe det statiske åpen metode. Vi oppretter en kanal også ved å kalle den statiske åpen metode, spesielt en ServerSocketChannel forekomst.

Dette er fordi ServerSocketChannel er valgbar og bra for en strømorientert lyttekontakt.

Vi binder den deretter til en port du ønsker. Husk at vi sa tidligere at før vi registrerer en valgbar kanal til en velger, må vi først sette den til ikke-blokkerende modus. Så neste gang gjør vi dette og registrerer deretter kanalen til velgeren.

Vi trenger ikke SelectionKey forekomst av denne kanalen på dette stadiet, så vi vil ikke huske det.

Java NIO bruker en annen bufferorientert modell enn en strømorientert modell. Så socketkommunikasjon skjer vanligvis ved å skrive til og lese fra en buffer.

Vi skaper derfor et nytt ByteBuffer som serveren skal skrive til og lese fra. Vi initialiserer den til 256 byte, det er bare en vilkårlig verdi, avhengig av hvor mye data vi planlegger å overføre frem og tilbake.

Til slutt utfører vi utvelgelsesprosessen. Vi velger de ferdige kanalene, henter ut valgtastene deres, gjentas over tastene og utfører operasjonene som hver kanal er klar for.

Vi gjør dette i en uendelig løkke siden servere vanligvis trenger å fortsette å kjøre, enten det er en aktivitet eller ikke.

Den eneste operasjonen a ServerSocketChannel orker er en AKSEPTERER operasjon. Når vi godtar forbindelsen fra en klient, får vi en SocketChannel objekt som vi kan lese og skrive på. Vi setter den til ikke-blokkerende modus og registrerer den for en LES-operasjon til velgeren.

Under en av de påfølgende valgene blir denne nye kanalen leseklar. Vi henter den og leser innholdet i bufferen. Tro mot det som ekkoserver, må vi skrive dette innholdet tilbake til klienten.

Når vi ønsker å skrive til en buffer som vi har lest fra, må vi ringe snu () metode.

Vi setter endelig bufferen i skrivemodus ved å ringe snu metode og bare skrive til den.

De start() metoden er definert slik at ekkoserveren kan startes som en egen prosess under enhetstesting.

8.2. Klienten

Her er koden vår for EchoClient.java:

offentlig klasse EchoClient {privat statisk SocketChannel-klient; privat statisk ByteBuffer-buffer; privat statisk EchoClient-forekomst; offentlig statisk EchoClient start () {if (forekomst == null) forekomst = ny EchoClient (); returinstans; } offentlig statisk ugyldig stopp () kaster IOException {client.close (); buffer = null; } privat EchoClient () {prøv {client = SocketChannel.open (ny InetSocketAddress ("localhost", 5454)); buffer = ByteBuffer.allocate (256); } fange (IOException e) {e.printStackTrace (); }} public String sendMessage (String msg) {buffer = ByteBuffer.wrap (msg.getBytes ()); Strengrespons = null; prøv {client.write (buffer); buffer.clear (); client.read (buffer); respons = ny streng (buffer.array ()). trim (); System.out.println ("respons =" ​​+ respons); buffer.clear (); } fange (IOException e) {e.printStackTrace (); } returnere svar; }}

Klienten er enklere enn serveren.

Vi bruker et singleton mønster for å instantiere det inne i start statisk metode. Vi kaller den private konstruktøren fra denne metoden.

I den private konstruktøren åpner vi en forbindelse på samme port som serverkanalen var bundet til og fremdeles på samme vert.

Vi lager deretter en buffer som vi kan skrive og hvorfra vi kan lese.

Endelig har vi en sende melding metoden som leser bryter hvilken som helst streng vi overfører til den i en bytebuffer som overføres over kanalen til serveren.

Vi leser deretter fra klientkanalen for å få meldingen sendt av serveren. Vi returnerer dette som ekkoet av vårt budskap.

8.3. Testing

Inne i en klasse som heter EchoTest.java, skal vi lage en testtilfelle som starter serveren, sender meldinger til serveren og bare passerer når de samme meldingene er mottatt tilbake fra serveren. Som et siste trinn stopper testsaken serveren før den er fullført.

Vi kan nå kjøre testen:

offentlig klasse EchoTest {Prosessserver; EchoClient-klient; @Før offentlige ugyldig oppsett () kaster IOException, InterruptedException {server = EchoServer.start (); klient = EchoClient.start (); } @Test offentlig ugyldig givenServerClient_whenServerEchosMessage_thenCorrect () {String resp1 = client.sendMessage ("hallo"); String resp2 = client.sendMessage ("verden"); assertEquals ("hei", resp1); assertEquals ("verden", resp2); } @Etter offentlig annullering av nedbrytning () kaster IOException {server.destroy (); EchoClient.stop (); }}

9. Konklusjon

I denne artikkelen har vi dekket grunnleggende bruk av Java NIO Selector-komponenten.

Den komplette kildekoden og alle kodebiter for denne artikkelen er tilgjengelig i GitHub-prosjektet mitt.


$config[zx-auto] not found$config[zx-overlay] not found