======= 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 [[https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/DatagramSocket.html#%3Cinit%3E(int)|konstruktora przyjmującego numer portu]] lub metody ''[[https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/DatagramSocket.html#bind(java.net.SocketAddress)|bind]]'' ==Multicast== Multicast używa klasy ''[[https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/MulticastSocket.html|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| MulticastSocket socket = new MulticastSocket(1313); Enumeration netIfEnum = NetworkInterface.getNetworkInterfaces(); while(netIfEnum.hasMoreElements()){ NetworkInterface netIf = netIfEnum.nextElement(); if(netIf.supportsMulticast()) try { socket.joinGroup(new InetSocketAddress(InetAddress.getByName("239.255.123.45"), 1313), netIf); } catch(IOException e) { } } ++++ ==== Zadania ==== Przypomnienie wątków i synchronizacji w Javie: [[https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html|synchronized (czyli zamki)]] \\ [[https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/package-summary.html|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: {{:sk2:TcpClientTemplate.java|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. [[sk2:qt#ip_multicast_-_przypomnienie| 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): * ''[[https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/channels/SocketChannel.html|SocketChannel]]'' – gniazdo TCP (//connect//, klient) * ''[[https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/channels/ServerSocketChannel.html|ServerSocketChannel]]'' – gniazdo TCP (//listen//, serwer) * ''[[https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/channels/DatagramChannel.html|DatagramChannel]]'' – gniazdo UDP * ''[[https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/channels/Selector.html|Selector]]'' – 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ę ''SelectionKey'' opisują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. ''[[https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/ByteBuffer.html|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'')((oraz znacznik, ''mark'')). 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 | 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(); } } ++++ ==== Serwer czatu ==== ++++ Przykład serwera czatu | 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 clients = new HashSet(); 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(); } } ++++ ==== Zadania ==== //Zadanie 4.// Pobierz kod prostego serwera key-value store: {{:sk2:simplekv.java|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