Non-blocking sockets w Javie
Wysłany dnia 09 września 2011 o godzinie 23:47. Skomentuj!, kategorie: Programowanie, tagi: , .

Ostatnimi czasy dość dużo siedzę w Javie, programując na Androida(Kingdoms Clash.NET zostało niestety porzucone, ale o tym kiedy indziej (może)). Rozwijam swój stary projekt(w końcu!), który od początku miał być dość mocno związany z platformami mobilnymi. Mam tylko telefon z Androidem, więc na nim się skupiłem. Java nie jest moim ulubionym językiem, ale niestety zostałem „zmuszony” do poznania go ciut głębiej. Suma sumarum zły nie jest, lecz kilka rzeczy naprawdę mnie wkurza. Przy moim pierwszym spotkaniu z nim miałem problem z ogarnięciem obsługi nieblokujących gniazd – nie mogłem trafić na sensowne informacje o tym, tym razem miałem ciut więcej samozaparcia i udało mi się rozgryźć ten problem i stwierdziłem, że warto bym to, nawet jeśli będzie tylko dla mnie, zapisał. Mimo iż Java jest językiem bardzo wysokopoziomowym, obsługa gniazd jest… niskopoziomowa jak w C(C# też ma to dość niskopoziomowo zrobione, lecz nie odbiega bardzo od reszty)!

Połączenie

By użyć nieblokujących gniazd musimy skorzystać z klasy SocketChannel. I od razu napotykamy na coś, czego nie lubię w językach pokroju Javy/C# – metoda statyczna zamiast konstruktora(ja rozumiem, polimorfizm, stringly-typed language ;) , ale w .NET można to było sensowniej zaimplementować). Zajęło mi kilka dni zorientowanie się, że jeśli ustawimy socket na nieblokujący za pomocą configureBlocking(false) PRZED połączeniem, musimy w pętli wywoływać metodę finishConnect(nie wystarczy ją wywołać raz), albo czekać na selektor(o których za chwilę) OP_ACCEPT(kolejny bubel – stała zamiast wyliczenia), gdy zrobimy to po wywołaniu connect, mamy spokój – zostanie ona wywołana synchronicznie.

By odczytywać dane tylko wtedy, gdy nadejdą(i nie czekać na ich nadejście), musimy utworzyć Selector(i znowu open…) oraz zarejestrować go w kanale za pomocą register(pierwszy parametr to nasz selektor, drugi to kombinacja flag z SlectionKey, dla czytania będzie to OP_READ). Po rejestracji możemy cieszyć się nieblokującym gniazdem i możliwością odczytywania z niego.

Odczyt

Odczytywanie danych już jest stosunkowo proste – wystarczy odpytywać selektor, czy nadeszły dane za pomocą select oraz iterować(choć dla OP_READ nie jest to chyba potrzebne, ale pewien nie jestem) po zwróconej kolekcji kluczy(przy rejestracji selektora dla tylko jednego OP_* nie musimy nic sprawdzać) i czytać dane z kanału.

Zakończenie połączenia przez drugą stronę

By dowiedzieć się, kiedy Sockety został zamknięty, jak przystało na C, trzeba sprawdzić, co zwróciła metoda read/write. Jeśli jest to -1 – gniazdo zostało zamknięte przez drugą stronę, i jedyne, co możemy zrobić to wywołać close oraz przestać czytać, bo wpadniemy w nieskończoną pętle – selektor OP_READ nadal będzie wysyłany(też mi zajęło chwilę dojście do tego, myślałem, że zostanie rzucony wyjątek).

Cały kod(łączenie + odczyt) będzie wyglądał mniej-więcej tak:

// Łączenie
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("localhost", 80));
channel.configureBlocking(false);
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_READ);

// Odczyt
ByteBuffer buffer = ByteBuffer.allocate(1024); // I zamiast konstruktora...
int read = 0;
while (read != -1) // Póki nie zamknięto połączenia
{
    if(selector.select(100) > 0) // Mamy co czytać
    {
        Set<SelectionKey> s = selector.selectedKeys();
        for(SelectionKey k : s)
        {
            read = channel.read(buffer);
        }
        s.clear();
    }
}
channel.close();
Dodaj komentarz

XHTML: Możesz użyć: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>