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 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 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();

Klient UDP

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();

Zadanie 1.

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:

new Thread(new Runnable() {
	@Override
	public void run() {
		// Kod uruchamiany wewnątrz wątku
	}
}).start();

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 = 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

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

Zadanie 2.

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: 2019/11/26 10:53 przez jkonczak