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 mechanizmu epoll
. 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 OutputStream
java.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();
Serwer UDP od klient różni się tym, że używa konstruktora przyjmującego numer portu lub metody bind
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"));
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 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; 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
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);
java.nio nie pozwala na współpracę ze standardowym wejściem.
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:
throws Throwable
i dodaj kod łapiący i obsługujący wyjątkisocat tcp:host:port,ignoreeof -
mark