Dydaktyka:
FeedbackTo jest stara wersja strony!
Java posiada dwa API do obsługi sieci: java.io oraz java.nio.
Obsługa wielu gniazd, operacje blokujące / nieblokujące:
java.io pozwala tylko na blokującą obsługę gniazd i nie ma możliwości sprawdzenia czy możliwe jest wykonanie żądanej operacji bez blokowania (tzn. nie ma możliwości skorzystania z funkcji typu select/poll). java.nio oferuje mechanizm analogiczny w działaniu do funkcji poll. Przy jego wykorzystaniu narzucana jest nieblokująca obsługa gniazd.Odczyt / zapis danych:
java.io używa strumieni - wysyłanie i odbieranie realizuje się przez obiekty z klas InputStream i OutputStreamjava.nio udostępnia metody wysyłające dane z i odbierające dane do buforów – obiektowo opakowanych tablic bajtów.
java.io jest prostszym API, które często utrudnia bądź nawet uniemożliwia napisanie wydajnego kodu.
Dłuższe porównanie IO/NIO: http://tutorials.jenkov.com/java-nio/nio-vs-io.html
Java I/O – proste i ładne blokujące opakowanie obiektowe gniazd.
//Socket sock = new Socket(); //sock.connect(new InetSocketAddress("example.com", 13)); Socket sock = new Socket("example.com", 13); InputStream is = sock.getInputStream(); byte[] bytearr = new byte[16]; while(true){ int len = is.read(bytearr); if(len == -1) break; System.out.write(bytearr, 0, len); } sock.close();
ServerSocket ssock = new ServerSocket(1313); Socket sock = ssock.accept(); ssock.close(); String s = new Date().toLocaleString() + '\n'; sock.getOutputStream().write(s.getBytes()); sock.close();
DatagramSocket sock = new DatagramSocket(); InetSocketAddress addr = new InetSocketAddress("example.com", 13); DatagramPacket outpacket = new DatagramPacket(new byte [0], 0, addr); sock.send(outpacket); DatagramPacket inpacket = new DatagramPacket(new byte[1024], 1024); sock.receive(inpacket); System.out.write(inpacket.getData(), 0, inpacket.getLength()); sock.close();
Napisz serwer czatu z użyciem java.io. (Jako klienta użyj programu netcat lub socat.)
synchronized (czyli zamki)
java.util.concurrent
nowy wątek, używając lambdy:
new Thread(() -> { // Kod uruchamiany wewnątrz wątku }).start();
Java New I/O – wydajne opakowanie obiektowe gniazd.
http://tutorials.jenkov.com/java-nio/index.html
Ważne klasy (sieciowe):
SocketChannel – gniazdo TCP (connect, klient)ServerSocketChannel – gniazdo TCP (listen, serwer)DatagramChannel – gniazdo UDPSelector – multiplekser
Obiekty z tych klas są tworzone metodą statyczną open(), np:
ServerSocketChannel ssc = ServerSocketChannel.open()
Kanały wprawdzie pozwalają na pracę w trybie blokującym i nieblokującym, ale przy użycie selektora wymusza pracę w trybie nieblokującym.
Aby używać kanału NIO z selektorem, należy:
configureBlocking(false)register() na kanaleselect() selektoraselectedKeys() pobierać kolekcję SelectionKey opisujących zdarzenia/* 1. */ Selector selector = Selector.open(); /* 1. */ DatagramChannel channel = DatagramChannel.open(); /* 1. */ channel.bind(new InetSocketAddress(12345)); /* 2. */ channel.configureBlocking(false); /* 3. */ selector.register(channel, SelectionKey.OP_READ); /* 4. */ … /* 4.a. */ selector.select(); /* 4.b. */ for(SelectionKey key : selector.selectedKeys()) /* 4.c. */ … /* 4.d. */ selector.selectedKeys().clear();
NIO używa dedykowanych klas buforów – np. ByteBuffer. Klasy te służą opakowaniu tablic (np. tablicy bajtów) w sposób nie ograniczający wydajności.
Bufor można tworzyć metodami statycznymi, np. ByteBuffer.allocate() i ByteBuffer.wrap().
Każdy bufor ma stałą pojemność. Poza pojemnością każdy bufor ma kursor (position) i limit (limit)1). Pisanie i czytanie odbywa się zawsze od kursora i przesuwa kursor o ilość przeczytanych/zapisanych bajtów.
Do zmiany pozycji i limitu służą np. flip, clear.
ByteBuffer buffer = ByteBuffer.allocate(16); // |................| // ^pos(ition) ^limit int bytesRead = channel.read(buffer); // |alamakota.......| assert bytesRead == 9; // ^pos ^limit buffer.flip(); // |alamakota.......| // ^pos ^limit short useless = buffer.getShort(); // |alamakota.......| assert useless == 0x616c; // ^pos ^limit Charset utf8 = Charset.forName("UTF-8"); // vlimit String str = utf8.decode(buffer).toString(); // |alamakota.......| assert str.equals(new String("amakota")); // ^pos buffer.clear(); // |................| // ^pos ^limit
W selektorze jeden kanał gniazdo może być zarejestrowane co najwyżej raz do oczekiwania na podane zdarzenia przy pomocy metody register(). Metoda ta zwraca klucz – obiekt z klasy SelectionKey. Ten sam obiekt po wywołaniu na selektorze metody select() jest kopiowany do selectedKeys() selektora. Klucz można też wydobyć wywołując na kanale metodę keyFor(selector).
Aby zmienić listę oczekiwanych zdarzeń, należy wykonać na kluczu metodę interestOps() z nową listą zdarzeń.
Po wywołaniu metody select() selektora każdy klucz ma ustawiane readyOps() - listę gotowych operacji. Dla ułatwienia klasa SelectionKey ma też metody isReadable(), isWriteable(), isAcceptable(), …
Do usunięcia klucza z selektora należy użyć metody cancel() na kluczu (lub zamknąć kanał metodą close()).
SelectionKey key = selector.register(channel, SelectionKey.OP_READ); selector.select(); assert key == selector.selectedKeys().iterator().next(); if(key.isValid() && key.isReadable()) … key.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE);
Każdy klucz może mieć dołączony jeden załącznik, albo podany przy rejestracji kanału w selektorze, albo dołączony metodą attach(object). Można go później pobrać metodą attachment().
MyClass myObj = … selector.register(myObj.myChannel(), SelectionKey.OP_READ, myObj); selector.select(); SelectionKey key = selector.selectedKeys().iterator().next(); assert key.attachment() instanceof MyClass; ((MyClass)key.attachment()).myFunction(key);
java.nio nie pozwala na współpracę ze standardowym wejściem.
Przykład obsługujący jedno gniazdo
package sk2; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; public class NioCli { private SocketChannel sock; // gniazdo w NIO private Selector sel; // selektor – opakowuje mechanizm typu 'select' private SelectionKey sockKey; private ByteBuffer bb = ByteBuffer.allocate(8); // bufor – odpowiednio opakowana tablica bajtów public NioCli(String[] args) throws Throwable{ // Selektory, gniazda, etc. są tworzone przez metody statyczne, używające dostarczanej // przez VM implementacji klasy SelectorProvider sel = Selector.open(); sock = SocketChannel.open(new InetSocketAddress(args[0], Integer.parseInt(args[1]))); sock.configureBlocking(false); // Używanie Selectora wymaga nieblokującego I/O sockKey = sock.register(sel, SelectionKey.OP_READ); // Tak każe się czekać na zdarzenie } private void select() throws Throwable{ sel.select(); // oczekiwanie na zdarzenie // selectedKeys() zawiera listę kluczy dla których można wykonać żądaną operację // i ustawia im dostępne zdarzenia (readyOps, isReadable, is…) assert sel.selectedKeys().size() == 1; assert sel.selectedKeys().iterator().next() == sockKey; bb.clear(); // przygotowanie bufora do pracy int count = sock.read(bb); if(count == -1) { // -1 oznacza EOF sockKey.cancel(); // cancel usuwa klucz z selektora return; } System.out.write(bb.array(), 0, bb.position()); sel.selectedKeys().clear(); // klucze są w zbiorze do momentu usunięcia } public static void main(String[] args) throws Throwable{ NioCli cli = new NioCli(args); while(cli.sockKey.isValid()) cli.select(); } }
package sk2; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.HashSet; public class NioServ { ServerSocketChannel ssc; Selector sel; HashSet<SocketChannel> clients = new HashSet<SocketChannel>(); ByteBuffer commonReadBuffer = ByteBuffer.allocate(256); public NioServ(int port) throws Throwable { sel = Selector.open(); ssc = ServerSocketChannel.open(); ssc.bind(new InetSocketAddress(port)); ssc.configureBlocking(false); ssc.register(sel, SelectionKey.OP_ACCEPT); } private void loop() throws Throwable { while(true){ select(); } } private void select() throws Throwable { sel.select(); for(SelectionKey key : sel.selectedKeys()){ if(key.isAcceptable()){ accept(); } if(key.isReadable()){ SocketChannel sc = (SocketChannel) key.channel(); read(key, sc); } } sel.selectedKeys().clear(); } private void accept() throws Throwable { SocketChannel sc = ssc.accept(); sc.configureBlocking(false); sc.register(sel, SelectionKey.OP_READ); sendAll("Connected:" + sc.getRemoteAddress().toString() + "\n"); clients.add(sc); } private void read(SelectionKey key, SocketChannel sc) throws Throwable { commonReadBuffer.clear(); commonReadBuffer.put(sc.getRemoteAddress().toString().getBytes()); commonReadBuffer.putChar(':'); commonReadBuffer.putChar(' '); int count = sc.read(commonReadBuffer); if(count == 0) return; if(count == -1){ clients.remove(sc); sendAll("Disconnected:" + sc.getRemoteAddress().toString() + "\n"); key.cancel(); sc.close(); return; } sendAll(commonReadBuffer); } private void sendAll(String msg) throws Throwable { ByteBuffer bb = ByteBuffer.wrap(msg.getBytes()); bb.position(bb.limit()); sendAll(bb); } private void sendAll(ByteBuffer bb) throws Throwable { bb.flip(); for(SocketChannel sc : clients){ sc.write(bb); bb.rewind(); } } public static void main(String[] args) throws Throwable{ NioServ s = new NioServ(Integer.parseInt(args[0])); s.loop(); } }
Pobierz kod prostego serwera key-value store: SimpleKV.java.
(https://en.wikipedia.org/wiki/Key-value_database)
Przeczytaj kod, połącz się programem netcat / socat i przetestuj działanie.
Następnie dodaj do programu brakujące:
throws Throwable i dodaj kod łapiący i obsługujący wyjątkisocat tcp:host:port,ignoreeof - mark