Avatar billede apocryphal Nybegynder
11. februar 2004 - 13:08 Der er 10 kommentarer og
1 løsning

En eller flere Selector's

Hey.

Jeg har skrevet en SocketChannelServer, hvor jeg har én Selector, der håndtere både nye SocketChannels og Indkommende data:

Det ser sådan her ud:
public void run()
{
    while(true)
    {
        try
        {
            selector.select();
            Set keys = selector.selectedKeys();
           
            for(Iterator i = keys.iterator(); i.hasNext();)
            {
                SelectionKey key = (SelectionKey)i.next();
                i.remove();
               
                if(key.isAcceptable())
                    acceptConnection(key);
                else if(key.isReadable())
                    processRequest(key);
            }
        }
        catch(IOException e)
        {
            e.printStackTrace();
        }
    }
}

Dette virker fint nok - men jeg spekulere lidt på performance.

I de eksempeler jeg ellers har set på brug af Selector-klassen, har der været én seperat selector til at håndtere indgående forbindelse, og én seperat til at håndtere indgående data...

Er min løsning performance mæssigt brugbar?

--
Jonas
Avatar billede dsj Nybegynder
11. februar 2004 - 18:11 #1
Der er ikke brug for mere end én tråd pr. CPU, og derfor heller ikke mere end én Selector.
Avatar billede dsj Nybegynder
11. februar 2004 - 18:15 #2
Det du for alt i verden skal sørger for er, at processRequest ikke gennemløber blokkerende kode, som f.eks. database-tilgang. I øvrigt holder din kode sandsynligvis ikke (nu ved jeg ikke hvad processRequest indeholder). Du kan aldrig være sikker på at modtage en "hel" besked, altså de data der fra klienten betrages som én sammenhængende besked. En gang imellem vil du risikere at modtage én besked over flere gange. Altså første gang du kører processRequest fordi en SelectionKey er blevet readable, modtager du første halvdel af afsendte data fra klienten, saom derfor skal gemmes til næste halvdel ankommer.
Avatar billede dsj Nybegynder
11. februar 2004 - 18:16 #3
Du gør heller ikke brug af key.isWritable(), kan jeg se. Skriver du ikke tilbage til klienten?
Avatar billede dsj Nybegynder
11. februar 2004 - 20:05 #4
NIO er i øvrigt ikke særlig ligetil at arbejde med. Hvis ikke du har erfaring med det, kommer du med 100 % sikkerhed til at kæmpe med underlige fænomener.

Et godt råd til at undgå en masse problemer er, kun at lade Selector'ens egen tråd servicere ALT i forbindelse med egne SelectionKey's. Det være sig operationerne write, read, accept og connect. Hvis du udfører funktioner der kan blokkere, som f.eks. database-tilgang, kan du med fordel oprette en pool med worker-tråde, som kan stå og gnave af en kø af færdige beskeder, som Selector'ens tråd så putter færdige de komplette beskeder i.
Avatar billede apocryphal Nybegynder
12. februar 2004 - 11:19 #5
Okey - jeg sætter noget mere kode ind så...

public void processRequest(SelectionKey key)
{
    SocketChannel incomingChannel = (SocketChannel) key.channel();
    Socket incomingSocket = incomingChannel.socket();
    try
    {
        int bytesRead = incomingChannel.read(readBuffer);
        readBuffer.flip();
        String result = asciiDecoder.decode(readBuffer).toString();
        readBuffer.clear();
        SocketConnection conn = (SocketConnection)key.attachment();
        if(result.length() <= 0 || !conn.isConnected)
            closeChannel(key);
        else
            OnData(conn, result);
    }
    catch (IOException ioe)
    {
        closeChannel(key);
    }
}

private void OnData(SocketConnection conn, String data)
{
    conn.append(data);
}

Hvor:
SocketConnection.append er:
public synchronized void append(String data)
{
    String command = buffer.append(data).toString();
   
    if(command.endsWith("\r\n"))
    {
        command = command.replaceAll("\r\n", "");
        OnData(this, command);
        clear();
    }
}

SocketConnection.OnData kalder en interfacet metode SocketServerListener.OnData(SocketConnection conn, String data)

Når jeg writer til socketten sker det med SocketConnection.writeLine(String data):
public void writeLine(String data)
{
    String toSend = data + "\r\n";
    CharBuffer chars = CharBuffer.allocate(toSend.length());
    chars.put(toSend);
    chars.flip();
       
    try
    {
        ByteBuffer buffer = ascii.newEncoder().encode(chars);
       
        channel.write(buffer);
    }
    catch(CharacterCodingException cce)
    {
        cce.printStackTrace();
    }
    catch(IOException ioe)
    {
        ioe.printStackTrace();
    }
}

Jeg har faktisk ikke overvejet at bruge en ThreadPool til at håndtere dataen der skal sendes tilbage - det er nok en god idé.
Jeg er ikke super erfaren med NIO, en kommentar på det her ville være kærkommen.

Det sjov med at jeg modtager en besked over flere gange omgås jeg ved at bestemme at en besked sluttes med \r\n - og så bare appende alt hvad der kommer fra en socket til et SocketConnection-objekt (der holder information om hvilken klient der er tale om). SocketConnection-objektet kaster så et ActionEvent når den har modtaget en hel besked.

Er der andre måder at håndtere det på?

--
Jonas

--
Jonas
Avatar billede dsj Nybegynder
12. februar 2004 - 11:40 #6
Man kan groft sagt bestemme på to hvordan en hel besked er modtaget.
1. Som du gør ved at append'e indkomne data til du modtager en terminater.
2. Anvende en fixed length protokol, hvor headeren fortæller hvor mange bytes hele beskeden fylder, og så måle ud fra det.

Noget andet er, at du ikke kan regne med isConnected(). Du kan i ganske få tilfælde risikere, at den returnerer true, selvom forbindelsen er død. Nogle gange dør netværksforbindelser på en underlige måder som gør, at data ikke kan sendes eller modtages, men uden at nogen fejl opstår, data når bare aldrig frem. I disse tilfælde kan det tage op til et kvarter, før OS opdager at forbindelsen er død og dermed at isConnected() returnerer false. Det sker sjældent, men det sker. Her er man nødt til at lave nogle beregninger på output-bufferen, for at bestemme om det overhovedet er muligt at sende via en SocketChannel - nu ved jeg ikke hvor meget forstand du har på buffere, hvordan de virker med pointer, limit osv.?

Jeg kan se at du kalder SocketChannel.write() én gang, når du sender data retur til klienterne. Imidlertid kan man aldrig være sikker på hvor meget write() får skrevet af de data der ligger i 'buffer'. Faktisk kan du slet ikke være sikker på at den skriver noget, og derfor kan du blive nødt til at udføre write(buffer) flere gange, for at få skrevet alle data. Hvis de datamængder du sender retur til klienterne er meget små, er det ikke sandsynligt, at du oplever nogen problemer.

Hvor meget det er muligt at sende i ét kald af write() bestemmes ud fra den ledige plads i operativsystemets, socket's outputbuffer. Som default er denne buffer 8192 bytes under Linux og Windows, men du kan rekvirere værdien med channel.socket().getSendBufferSize(). Når man sender data i Java, sender man faktisk slet ingen ting, man overfører blot data til denne buffer, og så sørger OS for den faktiske forsendelse. write() returnerer hvor bytes det lykkedes at skrive, og så må du forholde dig til, hvis ikke det var lige så mange, som 'buffer' indeholdt.

Her kommer noget tricky, som rigtig mange ikke forstår, fordi Sun aldrig har skrevet det nogen steder:
Hvis du oplever ikke at kunne sende alle data, fordi socket-bufferen er fyldt, skal du sætte interessen OP_WRITE på den tilhørende SelectionKey. Det gør nemlig, at Selector'en fortæller dig hvornår der igen er plads i socket-bufferen.

De fleste har fundet ud af, at registrerer man en nyoprettet SocketChannel for OP_WRITE, vil den altid blive returneret som klar af Selector'en, og forstår derfor ikke hvad den skal gøre godt for. Det den fortæller er nemlig, om der er ledig plads i OS'ets socket-outputbuffer.
Avatar billede dsj Nybegynder
12. februar 2004 - 11:50 #7
Du skal også huske, at hvis incomingChannel.read(readBuffer) returnerer -1, er det fordi forbindelsen er blevet afbrudt, og så skal du have afregistreret incomingChannel i Selector'en.
Avatar billede apocryphal Nybegynder
12. februar 2004 - 12:13 #8
Jeg ved ikke rigtigt noget om buffers i detaljer. Jeg bruger dem blot (ignorent af mig) :/

Angående write:
Hvis jeg ønsker at sende mere end 8192 bytes af gangen, skal jeg implementerer OP_WRITE, og når den bliver selected, og sende eventuel data der skal sendes?

Angående isConnected():
Jeg kan nøjes med at tjekke om read(buffer) returnerer < 0?

Har du et link til nogle dybdegående artikler om NIO (gerne Selectorens måde at virke på, i detaljer)?

--
Jonas
Avatar billede dsj Nybegynder
12. februar 2004 - 12:56 #9
Write:
-----------------------------
Ja, når den bliver selected, kører du write(buffer) endnu en gang. Det tricky er så her, at hvis du i mellemtiden har flere data der skal sendes til klienten, vil det være en god ide først at fylde 'buffer' op, før write() kaldes igen - men så skal du også til at gemme den del af en eventuel besked, som der ikke var plads til i din egen ByteBuffer. I øvrigt kan du lette tingene for dig selv, og samtidig optimere lidt, hvis buffer's størrelse sættes til channel.socket().getSendBufferSize() - dette gælder også for din input-buffer.

I øvrigt viser ByteBuffer.remaining() hvor mange bytes der efter en write()-operation mangler at blive sendt. Mange tror derfor at man kan gøre følgende for at sikre at alle data sendes:

while(buffer.remaining() > 0) {
  channel.write(buffer);
}

Godt nok sikrer den, at ALLE data med sikkerhed bliver sendt, men den går bare ikke, da det er en effektiv busy-wait. Man kan kun udføre write() én gang, og må så registrere for OP_WRITE, og først udføre write() igen, når du modtager din SelectionKey, hvor isWritable() returnerer true.

isConnected():
-----------------------------
Nej, du kan ikke nøjes, du skal også. Hvis read() returnerer -1 betyder det, at OS har konstateret forbindelse død, hvorfor du skal blot reagere på det. De tilfælde jeg snakker om er døde forbindelse, som OS (og dermed din server) tror er i live. Her er der ikke rigtig en 100 % sikker metode at finde ud af, om forbindelsen er død eller ej.

Jeg blev selv opmærksom på problemet, da jeg havde en server der på eget initiativ disconnectede klienterne, efter endt arbejde. Da serveren tog initiativet gik jeg ud fra, at forbindelsen stadig var åben, og så ville jeg godt sikre at alle data i min send-queue og buffere kom frem til klienten før serveren disconnectede klienten endeligt. Derfor flushede jeg ved at køre write()og registerere for OP_WRITE, hvis ikke alle data var blevet flushed. Problemet var så, at Selector'en returnerede min SelectionKey som writable med det samme, da der var plads til overs i OS' socket-buffer, men alle write()-operationerne sendte ikke noget og returnerede blot 0 (bytes afsendt). Dette resulterede i en rigtig effektiv busy-wait. Vi snakker en kommerciel server, der pludselig stod og brugte 100% CPU i 15-30 minutter, før OS fandt ud af at forbindelsen faktisk var død.

De underlige, døde forbindelser kan altså kendetegnes ved:
1. write() returnerer 0, da data ikke kan sendes.
2. Hvis bufferen ikke var fuld da forbindelsen døde, vil der være plads i OS' outputbuffer, selv om write() returnerer 0 - dette er ellers ikke muligt.

Det man kan gøre, og som jeg har gjort for at løse problemet, er at se på ByteBuffer.position(). Jeg har så sagt, at hvis jeg efter 3 kald af write() (og dermed to OP_WRITE registreringer) ikke kan se at bufferens position (pointer) har flyttet sig (der er ikke blevet sendt data), stopper jeg og lukker forbindelsen.
Avatar billede apocryphal Nybegynder
12. februar 2004 - 14:30 #10
Okey - jeg kommer nok ikke udenom at rewrite noget kode her så :/

Jeg tænker lidt på:
Hvis jeg registrerer interesse i OP_WRITE - vil det så ikke få Selectoren til at returnere på select() hvergang man kalder den, og man så skal tjekke alle de sockets man nu har registeret for, om de har data? - går det stærkt nok hvis der f.eks. er 4000 socketchannels registreret på selectoren?

--
Jonas
Avatar billede dsj Nybegynder
12. februar 2004 - 15:08 #11
Hvis du registrerer alle SocketChannel's for OP_WRITE, jo så vil Selector'en som jeg har nævnt selecte dem alle. Derfor skal du kun registrere for OP_WRITE så længe alle data ikke er skrevet, men OS' socket-buffer fyldt op. Du får lige skrive-proceduren som seudo:

while (dataTilbage > 0) {
  if (der er registreret for OP_WRITE) {
    afregistrer for OP_WRITE
  }

  dataSkrevet = channel.write(data);
  dataTilbage -= dataSkrevet;
 
  if (dataTilbage > 0) {
    registrér for OP_WRITE
  }
}
Avatar billede Ny bruger Nybegynder

Din løsning...

Tilladte BB-code-tags: [b]fed[/b] [i]kursiv[/i] [u]understreget[/u] Web- og emailadresser omdannes automatisk til links. Der sættes "nofollow" på alle links.

Loading billede Opret Preview
Kategori
Kurser inden for grundlæggende programmering

Log ind eller opret profil

Hov!

For at kunne deltage på Computerworld Eksperten skal du være logget ind.

Det er heldigvis nemt at oprette en bruger: Det tager to minutter og du kan vælge at bruge enten e-mail, Facebook eller Google som login.

Du kan også logge ind via nedenstående tjenester