Narzędzia użytkownika

Narzędzia witryny


Pasek boczny

sk2:java

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.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).
    W praktyce oznacza to, że dla każdego gniazda potrzebny jest osobny wątek.
  • 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.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"));

Od Javy 14 metoda MulticastSocket.joinGroup(InetAddress) jest oznaczona jako deprecated i żeby dodać się na wszystkie interfejsy należy ręcznie dodać się do każdego po kolei

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):

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:

  1. stworzyć selektor i kanały gniazd
  2. przestawić kanały w tryb nieblokujący: configureBlocking(false)
  3. dodać kanały gniazd do selektora – metoda register() na kanale
  4. w pętli
    1. wykonywać metodę select() selektora
    2. metodą selectedKeys() pobierać kolekcję SelectionKey opisujących zdarzenia
    3. obsłużyć zdarzenia
    4. 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.

Przykład obsługujący jedno gniazdo

Serwer czatu

Przykład serwera 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 Throwable i 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
1) oraz znacznik, mark
sk2/java.txt · ostatnio zmienione: 2023/12/01 13:08 przez jkonczak