markTable of Contents
Obsługa sieci w Javie
Java posiada dwa API do obsługi sieci: java.io oraz java.nio.
Obsługa wielu gniazd, operacje blokujące / nieblokujące:
java.iopozwala 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 typuselect/poll).
W praktyce oznacza to, że dla każdego gniazda potrzebny jest osobny wątek.java.niooferuje mechanizm analogiczny w działaniu do mechanizmuepoll. Przy jego wykorzystaniu narzucana jest nieblokująca obsługa gniazd.
Odczyt / zapis danych:
java.ioużywa strumieni - wysyłanie i odbieranie realizuje się przez obiekty z klasInputStreamiOutputStreamjava.nioudostę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.io
Java I/O – proste i ładne blokujące opakowanie obiektowe gniazd.
Klient TCP
//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();
Serwer TCP
ServerSocket ssock = new ServerSocket(1313); Socket sock = ssock.accept(); ssock.close(); String s = new Date().toLocaleString() + '\n'; sock.getOutputStream().write(s.getBytes()); sock.close();
UDP
Klient
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();
Serwer
Serwer UDP od klient różni się tym, że używa konstruktora przyjmującego numer portu lub metody bind
Multicast
Multicast używa klasy MulticastSocket która jest klasą DatagramSocket wzbogaconą o kilka metod, m. inn. dołączenie do grupy:
MulticastSocket socket = new MulticastSocket(1313); socket.joinGroup(InetAddress.getByName("239.255.123.45"));
Zadania
Przypomnienie wątków i synchronizacji w Javie:
synchronized (czyli zamki)
java.util.concurrent
nowy wątek, używając lambdy:
new Thread(() -> { // Kod uruchamiany wewnątrz wątku }).start();
Zadanie 1. Napisz klienta TCP z użyciem java.io i wątków.
(Możesz skorzystać z szablonu kodu z TODO's do zrobienia: tcpclienttemplate.java)
Zadanie 2. Napisz program odbierający i wysyłający wiadomości UDP od/do grupy multicastowej z użyciem java.io i wątków.
IP multicast - przypomnienie i komendy do testowania
Zadanie 3. Napisz serwer czatu z użyciem java.io. (Jako klienta użyj np. programu z zadania 1 lub netcat lub socat.)
java.nio
Java New I/O – wydajne opakowanie obiektowe gniazd.
http://tutorials.jenkov.com/java-nio/index.html
Klasy obsługujące gniazda (kanały)
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.
Praca z selektorem
Aby używać kanału NIO z selektorem, należy:
- stworzyć selektor i kanały gniazd
- przestawić kanały w tryb nieblokujący:
configureBlocking(false) - dodać kanały gniazd do selektora – metoda
register()na kanale - w pętli
- wykonywać metodę
select()selektora - metodą
selectedKeys()pobierać kolekcjęSelectionKeyopisujących zdarzenia - obsłużyć zdarzenia
- wyczyścić kolekcję kluczy
/* 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();
Bufory
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; bytesRead = channel.read(buffer); // |ala.............| assert bytesRead == 3; // ^pos ^limit bytesRead = channel.read(buffer); // |alamakota.......| assert bytesRead == 6; // ^pos ^limit buffer.flip(); // |alamakota.......| // ^pos ^limit int bytesWritten; // vlimit bytesWritten = channel2.write(buffer); // |alamakota.......| assert bytesWritten == 9; // ^pos 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(); // |alamakota.......| // ^pos ^limit
SelectionKey
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). Kanał można wyciągnąć z klucza metodą channel().
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);
"klient tcp"
java.nio nie pozwala na współpracę ze standardowym wejściem.
Serwer czatu
Zadania
Zadanie 4. 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:
- komunikaty diagnostyczne – pojawił się nowy klient, klient się rozłączył, zmieniono wartość
- obsługę błędów – tzn. usuń
throws Throwablei dodaj kod łapiący i obsługujący wyjątki - buforowanie wejścia – obsłuż sytuację, w której read zwróci tylko część linii
Testuj używając jako klienta:
''socat tcp:host:port,ignoreeof - ''
Wpisanie EOF (ctrl+d) spowoduje wysłanie dotychczas wpisanych danych - buforowanie wyjścia – obsłuż sytuację, w której write zapisze tylko część żądanych danych