5. Programowanie klient-serwer z użyciem gniazd i protokołów sieciowych


5.1. Architektura klient-serwer i sieć.

Architektura klient-serwer jest technologią budowy systemów informatycznych, polegającą na podziale systemu na współdziałające ze sobą dwie kategorie programów lub procesów: klientów i serwerów.

Klient to program lub proces, który - oprócz wykonywania swoistych dla niego działań -  łączy się z innym programem lub procesem, zwanym serwerem i poprzez kanały komunikacyjne zleca mu wykonanie określonych działań; w szczególności dostarczenia jakichś danych lub wyników przetwarzania danych


Serwer jest programem lub procesem, który - na zlecenie klientów - świadczy określone usługi - np. dostarcza im dane lub wyniki przetwarzania jakichś danych



Pojęcie "klient-serwer" jest natury software'owej, a nie hardware'owej.
Mechanizmy IPC (semafory, potoki, dzielone obszary pamięci) - różne implementacje.

Środowiska rozproszone: Wraz z rozpowszechnieniem środowisk rozproszonych (sieci) pojawiła się potrzeba swoistej unifikacji mechanizmów IPC, tak by mogły być stosowane w jednolity sposób do komunikacji  klientów i serwerów działających na dowolnych komputerach w sieci (również na jednym i tym samym komputerze).

Oczywiście, komunikacja wymaga przestrzegania pewnych reguł. Reguły te są definiowane przez protokoły.

Protokół - to swoisty język i zasady za pomocą których komunikują się programy i procesy, w szczególności serwery i klienci


Protokoły interakcji sieciowej są podzielone na warstwy, które wyznaczają poziomy komunikacji. Dzięki temu złożony problem interakcji sieciowej jest podzielony na podproblemy, za rozwiązywanie których odpowiadają protokoły poszczególnych warstw.

Ogólny standard tego podziału  tzw.  model  ISO-OSI (OSI -Open Standards Interconnect).

Model OSI składa się z  siedmiu warstw:

Komputer 1

 

Komputer 2

daneWa rstwa aplikacji Warstwa aplikacjidane
|daneWarstwa prezentacji Warstwa prezentacji|dane
||daneWarstwa sesjir Warstwa sesji||dane
|||daneWarstwa transportowa Warstwa transportowa|||dane
||||daneWarstwa sieci Warstwa sieci||||dane
|||||daneWarstwa łącza danych Warstwa łącza danych|||||dane
||||||daneWarstwa fizycznaWarstwa fizyczna||||||dane

  1. aplikacje, działające na dwóch komputerachw  sieci (klient i serwer) wymieniają dane.
  2. kolejne protokoły w kolejnych warstwach dodają nagłówki do danych otrzymywanycyh z warstwy powyżej. Dane opakowane w nagłówki są transmitowane w tzw.  Protocol Data Units (PDUs). 
  3. PDU są przesyłane w dół sekwencji warstw i transmitowane fizycznie przez warstwę fizyczną, która reprezentuje fizyczne połączenie w sieci.
  4. Dane otrzymane są przesyłane "w górę" warstw i "odpakowywane" z kolejnych nagłówków.
Jedną z możliwych, a przy tym obecnie często występującą realizację tego modelu przedstawia poniższy rysunek.

r


5.2. Gniazda.


Jak widać z poprzedniego rysunku  komunikacja pomiędzy procesami w sieci może  odbywać się za pomocą jednego z wysokopoziomowych protokołów sieciowych (HTTP, FTP itp.), a  także bardziej zaawansowanych mechanizmów takich jak zdalne wywołanie metod (RMI, CORBA), zdalne wywołanie procedur (RPC), czy SOAP (pozwalający na zdalne wolanie metod i procedur - inaczej: zlecanie usług - za pomocą przekazywania odpowiednio ustrukturyzowanych plików XML z zapisem "zadań do wykonania").

U podstaw wymiany informacji za pomocą protokołów najwyższego poziomu leży obecnie (najczęściej) komunikacja za pomocą gniazd.

Gniazdo (socket) - to abstrakcja programistyczna, oznaczająca punkt docelowy dwustronnej komunikacji dwóch procesów działających równolegle w sieci


To właśnie koncepcja gniazd, wprowadzona w Uniksie w latach 70-tych, stanowi wspomnianą wcześniej, sieciowo-zorientowaną, unifikację mechanizmu IPC.
Naturalne było uczynienie  komunikacji za pomocą gniazd w idei podobnej do dobrze znanego programistom paradygmatu operacji wejścia-wyjścia. Zatem mamy to naturalną sekwencję: otwarcie kanału komunikacji (gniazda), zapis lub odczyt (przesłanie lub otrzymanie danych za pomocą gniazda), zamknięcie kanału komunikacyjnego (zamkniecie gniazda).

Zwykle komunikacja za pomocą gniazd implementowane jest na bazie protokołu TCP lub protokołu UDP.

Protokół TCP (Transport Control Protocol) jest protokołem połączeniowym (co znaczy, że ustanawiana jest dwustronne połączenie pomiędzy klientem i serwerem). Zapewnia, że dane posyłane poprzez gniazda docierają w całości i w odpowiedniej kolejności. Inaczeje możemy powiedzieć, że realizowana jest tu strumieniowa koncepeja wymiany danych, co oznacza, że po ustanowieniu połaczenia można przesłać dane o dowolnym rozmiarze i - jak zobaczymy - operacje wymiany danych możemy wykonywać tutaj za pomocą dobrze nam znanych środków strumieniowych.

Protokół UDP (User Datagrams Protocol) jest protokołem bezpołączeniowym. Dane przesyłane są pomiędzy procesami jako datagramy (pakiety danych o określonej maksymalnej wielkości np. 64 kB), przy czym z każdym datagramem posyłany jest "adres" odbiorcy. Datagramy mogą więc przybywac na miejsce przeznaczenia (do innego procesu) w dowolnje kolejności (a niektóre nawet mogą w ogóle nie dotrzeć).

Oba protokoły (TCP i UDP) są protokołami typu "point-to-point", czyli każdorazowo zapewniającymi komunikację tylko pomiędzy dwoma procesami (w szczególności na dwóch różnych maszynach w sieci).
Istnieje również możliwość użycia tzw. multicastingu. Ten rodzaj protokołów oznacza dystrybucje informacji z serwera od razu do wielu klientów. Oparty jest on na protokole UDP.

Identyfikacji maszyn biorących udział w komunikacji (tzw. hostów) służy protokól IP (Internet Protocol). Adresy IP mają ogólnie formę 32-bitowych (lub 128-bitowych w wersji IPv6) liczb i mogą być zapisywane jako sekwencja czterech (ośmiu)  liczb rozdzielonych kropkami (np. 192.33.71.12) lub - w formie "strawniejszej", wykorzystującej DNS (Domain Name Service) jako: nazwa_hosta.nazwa_domeny (np. boulder.ibm.com).

Identyfikacja hosta jest jednak nie wystarczająca dla komunikacji miedzy procesami: na danym komputerze może się przecież wykonywać równolegle wiele procesów.

Termin port używany jest rownież w innych znaczeniach, dotyczących fizycznego łączenia urządzeń np. port szeregowy czy USB
Po to, by dane dotarły do określonego procesu protokoły TCP i UDP posługują się tzw. portami.
Porty są identyfikowane przez 16-bitowe liczby - numery portów. Numery te są używane przez TCP lub UDP do przesyłanie danych do odpowiedniego procesu.

W prokołach połączeniowych (takich jak TCP) proces-serwer przydziela sobie port o określoneym numerze i poprzez ten właśnie port procesy klienckie mogą ustanawiać połączenia z serwerem.

r



W protokołach opartych na datagramach, pakiety datagramów zawierają numer portu dzięki któremy dane kierowane są do właściwego procesu (aplikacji).

r



Teraz zajmiemy się tylko najprostszym TCP.
Inne:
Typowa interakcja pomiędzy klientem i serwerem opiera się na następującym modelu.

  1. Serwer tworzy gniazdo związane z określonym portem i na tym kanale komunikacyjnym czeka na prośbę połączenia od klienta.
  2. Inicjatywa połączenia wychodzi od klienta; klient musi znać host serwera oraz numer portu otwartego do przyjmowania połączeń i podaje tę informację tworząc "u siebie" gniazdo związane z tak określonym adresem
  3. Serwer akceptuje połaczenie od klienta i - aby pozostać dostępnym dla innych klientów na "kanale połączeniowym" - tworzy inne gniazdo do komunikacji z danym klientem;
  4. Z punktu widzenia klienta jest to (zazwyczaj) to samo gniazdo na którym zainicjowano połączenie; strumienie wejściowy i wyjściowy związane z tym gniazdem służą do komunikacji pomiędzy klientem i serwerem zgodnie z protokołem serwera.

Ilustruje to poniższy rysunek.
r

Jest to oczywiście ogólny model. Niektóre protokoły mogą wprowadzać pewne odstępstwa
od niego. Przykładem jest protokól FTP, gdzie do komunikacji z serwerem wykorzystywane są dwa gniazda: do posyłania poleceń i do posyłania/odczytywania danych.

Zatem posługujemy się dwoma rodzajami gniazd:

5.3. Klienci. Protokoły i porty

W Javie oprogramowanie klienta, komunikującego sie z serwerem - w większości przypadków - jest niezwykle proste i polega na:
  1. Utworzeniu gniazda - obiektu klasy Socket, dającego połączenie  z serwerem.
  2. Uzyskaniu od tego obiektu strumieni wyjściowego i wejściowego, związanych z gniazdem.
  3. Posyłanie zleceń dla serwera poprzez zapis do strumienia wyjściowego gniazda.
  4. Odczytywanie odpowiedzi serwera poprzez odczyt ze strumienia wejściowego.
  5. Zamknięcie strumieni.
  6. Zamknięcie gniazda

Najprostszy schemat komunikacji klienta z serwerem przez gniazda 

try {
  // Utworzenie gniazda
  String serverHost = ... ; // adres IP serwera ("cyfrowo" lub z użyciem DNS)
  int serverPort = ... ;      // numer portu na którym nasłuchuje serwer 

  Socket socket = new Socket(serverHost, serverPort)
 
  // Uzyskanie strumieni do komunikacji
  OutputStream sockOut = socket.getOutputStream();
  InputStream  sockIn = socket.getInputStream();

  // Komunikacja (zależna od protokołu)
 
  // Wysłanie zlecenia do serwera
  sockOut.write(...);
    ...

  // Odczytanie odpowiedzi serwera
  sockIn.read(...);
   ...

  // Po zakończeniu komunikacji - zamkniecie strumieni i gniazda
  sockOut.close();
  sockIn.close();
  socket.close(); 

} catch (UnknownHostException exc) {
    // nieznany host
} catch (SocketException exc) {
    // wyjątki związane z komunikacją przez gniazda
} catch (IOException exc) {
    // inne wyjątki we/wy
}

Uwaga: zazwyczaj strumienie związane z gniazdem będziemy opakowywać, zapewniając określone rodzaje przetwarzania (np. buforowanie, kodowanie-dekodowanie, odczyt/zapis danych binarnych itp.).   

 
W komunikacji pomiędzy klientem i serwerem kluczową sprawą jest znajomość protokołu (czyli rodzajów i formatów zleceń, które można posłać do serwera oraz treści i formatów odpowiedzi, które serwer przyszyła w reakcji na konkretne zlecenia).


Istnieją  pewne standardowe protokoły, dzięki którym uzyskujemy dostęp do różnorodnych usług w Internecie. Do najbardziej znanych (a czasem i  użytecznych) należą protokoły:
Protokoły te (a zatem format zleceń klienta i odpowiedzi serwera, kolejność wymiany informacji itp.) są szczegółowo opisane w dokumentach RFC (Request For Comments). Dokumenty RFC opisują technologiczne i organizacyjne aspekty Internetu. Od 1969 roku opublikowano prawie 4000 dokumentów RFC. Wiele z nich uzyskało status standardu (m.in. prawie wszystkie opisujące w/w protokoly). Standartyzacją protokołów internetowych zajmują się następujące organizacje:
IETF - The Internet Engineering Task Force - http://www.ietf.org/home.html
IESG - The Internet Engineering Steering Group
IANA - Internet Assigned Numbers Authority (http://www.iana.org/), która m.in. ustala różnego rodzaju parametry protokołów, np. numery portów, czy nazwy zbiorów znaków.

Przyjęte jako standardy dokumenty RFC (Official Internet Protocol Standards), w szczególności opisujące protokoły, można znaleźć na stronie: http://www.rfc-editor.org/rfc.html. Uzyskamy tam również dostęp do bazy danych wszystkich dokumentów RFC.

Tworząc program kliencki, który ma komunikować się z serwerem, musimy  (oczywiście) wiedzieć na jakim hoście działa serwer i przez jaki port możemy się z nim skontaktować.

Z serwerami udostępniającymi opisane wcześniej protokoły-usługi (oprócz serwerów DICT) łączymy się przez tzw. dobrze-znane-porty. "Dobrze-znane-porty" mają zarezerwowane numery od 0 do 1023 (tych numerów nie powinniśmy przydzielać swoim serwerom). Porty o numerach 1024-49151 są natomiast tzw. portami zarejestrowanymi. Tych numerów możemy używać, ale należy liczyć się z tym, że wybrany przez nas numer może być "zarejestrowany" - czyli jego użycie opisane  publicznie jako skojarzone z jakimś protokołem. Przykładem jest protokól DICT, któremu przypisano port 2628.
Pozostały zakres numerów portów 49152-65535 określa tzw. porty dynamiczne lub prywatne. Możemy ich używac.

Każdy ze standardowych protokołów (a ściślej każdy z serwerów realizujących dany protokół) ma ściśle określony port kontaktowy, za pomocą którgo klient nawiązuje połączenie. Numery portów określa standard IANA - można je znaleźć na stronie: http://www.iana.org/assignments/port-numbers.

W poniższej tabeli pokazano niektóre standardowe numery portów.


Numer portu Protokół
7ECHO
20FTP -- Dane
21FTP -- Polecenia
Uwaga: protokól FTP posluguje się dwoma portami (a w konsekwencji dwoma gniazdami): jednym dla przekazywania poleceń, drugim - dla transferu plików). W zależności od trybu p[rort danych może mieć numer 20, albo inny. 
22SSH Remote Login Protocol
23Telnet
25Simple Mail Transfer Protocol (SMTP)
37Time
70Gopher ("przodek" HTTP)
Uwaga: jest jeszcze trochę działających serwerów Gopher m.in. w Kanadyjskim Urzędzie Statystycznym
80HTTP
110POP3
119Newsgroup (NNTP)
123
Network Time Protocol (NTP)
143Internet Mail Access Protocol (IMAP)
194Internet Relay Chat (IRC)
443HTTPS (HTTP w oparciu o TSL/SSL)
989
FTPS (w oparciu o TSL/SSL) - dane
990
FTPS (w oparciu o TSL/SSL) - polecenia
992
Telnets (Telnet w oparciu o TSL/SSL)
993
IMAPS (IMAP4 w oparciu o TSL/SSL)
994
IRCS (IRC w oparciu o TSL/SSL)
995
POP3S (POP3 w oparciu o TSL/SSL)
2628DICT

Zanim przeanalizujemy nieco bardziej rozbudowane przykłady programów klienckich warto przyjrzeć najprostszym ilustracjom, i na ich  przykładach poznać pewne cechy gniazd. Zaczniemy od protokołu DAYTIME, zgodnie z którym - zaraz po połączeniu - serwer przesyła klientowi aktualną date i czas, po czym zamyka połączenie. Format odpowiedzi nie jest przez RFC określony, jednak każdy serwer dostarcza  informacji o składni komunikatu, zawierającego datę i czas.

W najprostszej postaci  program odczytujący datę i czas z serwera czasu  może wyglądać tak.
import java.net.*;
import java.io.*;

class DateTime1 {

  public static void main(String[] args) {

    String host = null;
    int port = 13;

    try {
      host = args[0];  // host - jako argument wywołania

      // Utworzenie gniazda
      Socket socket = new Socket(host, port);

      // Pobranie strumienia wejściowego gniazda
      // Nakładamy dekodowanie i buforowanie
      BufferedReader br = new BufferedReader(
                            new InputStreamReader(
                             socket.getInputStream()
                             )
                          );

      // Odczyt odpowiedzi serwera (data i czas)
      String line;
      while ((line = br.readLine()) != null) {
        System.out.println(line);
      }

      // Zamknięcie strumienia i gniazda
      br.close();
      socket.close();
    } catch (UnknownHostException exc) {
        System.out.println("Nieznany host: " + host);
    } catch (Exception exc) {
         exc.printStackTrace();
    }
  }

}

Po uruchomieniu w wierszu poleceń np. z argumentem time.nist.gov uzyskamy odpowiedź w następujacej postaci:

52943 03-10-31 22:06:50 00 0 0 223.2 UTC(NIST) *

Uwaga: wyjaśnienie szczegółów składni tej odpowiedzi można znaleźć na stronie www.time.nist.gov.

8.4. Adresowanie i wyjątki przy próbie połączenia z serwerem

Przy tworzeniu gniazda adresy hostów można podawać w postaci tekstowej (w formie DNS lub w formie "liczbowego" adresu IP).
Np.

Socket socket = new Socket("time.nist.gov", 13);
lub
Socket socket = new Socket("192.43.244.18", 13);

Te formy konstruktorów klasy Socket zapewniają nie tylko utworzenie gniazda, ale również próbę połączenia z serwerem.
Podany serwer może nie istnieć, może istnieć ale nie zgodzić się na połączenie, może się też zdarzyć że przekroczony zostanie maksymalny czas oczekiwania na uzyskanie połączenia. Wszystkie te sytuacje są sygnalizowane jako odpowiednie wyjątki.

Pokazuje to poniższy przykład, w którym staramy się połączyć z różnymi potencjalnymi serwerami ECHO (serwery takie przyjmują jako zlecenie dowolny tekst, a ich funkcją jest zwrócenie tego samego tekstu - były kiedyś powszechnie wykorzystywane do testowania, teraz jednak, ze względów bezpieczeństwa, porty są blokowane i raczej trudno jest znaleźć jakiś działający echo-serwer).

import java.io.*;
import java.net.*;

public class EchoClient {

  final static int ECHO_PORT = 7;
  private Socket sck;
  private PrintWriter out;
  private BufferedReader in;

  public EchoClient() {}

  public void connect(String host)
              throws UnknownHostException, IOException   {
    sck = new Socket(host, ECHO_PORT);
    in = new BufferedReader (
            new InputStreamReader(sck.getInputStream()));
    out = new PrintWriter (
            new OutputStreamWriter(sck.getOutputStream()), true);
    System.out.println("Połączony z hostem:" + sck.getInetAddress() );
  }

  public void echoMsg(String msg) throws IOException {
    out.println(msg);
    String response = in.readLine();
    System.out.println("Klient: " + msg);
    System.out.println("Serwer: " + response);
  }

  public void disconnect() throws IOException  {
    in.close();
    out.close();
    sck.close();
  }



  public static void main(String[] args) {
    String[] hosts = { "aeneas.mit.edu",
                       "cs.toronto.edu",
                       "cs.utah.edu",
                       "web.mit.edu",
                       "boulder.ibm.com",
                       "somethin"
                     };
    int i = 0;
    EchoClient ec = new EchoClient();
    for (; i<hosts.length; i++) {
      try {
        ec.connect(hosts[i]);
        ec.echoMsg("Dzień dobry!");
        ec.disconnect();
      } catch(UnknownHostException exc) {
          System.out.println("Nieznany host: " + hosts[i]);
      } catch(IOException exc) {
          System.out.println(hosts[i] + " - " + exc);
      }
    }
  }


}
Program wyprowadzi następującą informację:

aeneas.mit.edu - java.net.ConnectException: Connection refused: connect
cs.toronto.edu - java.net.ConnectException: Connection refused: connect
cs.utah.edu - java.net.ConnectException: Connection timed out: connect
web.mit.edu - java.net.ConnectException: Connection timed out: connect
boulder.ibm.com - java.net.ConnectException: Connection timed out: connect
Nieznany host: somethin


Zauważmy:
Istnieje jeszcze inny sposób tworzenia gniazd i użycia adresów internetowych.
Możemy w tym celu wykorzystać klasy InetAddress i InetSocketAddress.

Obiekty klasy InetAddress reprezentują adresy IP (podklasa Inet4Address w wersji 4, a podklasa Inet6Adrres w wersji 6 protokołu IP) i zapewniają translację adresów z postaci tekstowej do binarnej i odwrotnie. Adresy IP - obiekty klasy uzyskujemy  stosując statyczne metody klasy m.in.
Uwagi:
Referencje do obiektu klasy InetAddress możemy podać przy tworzeniu gniazda np.

    String host = "time.nist.gov";
    int port = 13;

    try {
      // Utworzenie adresu
      InetAddress inetadr = InetAddress.getByName(host);

      // Utworzenie gniazda
      Socket socket = new Socket(inetadr, port);
     ....
    }

Oprócz bardziej zaawansowanych działań na adresach sieciowych, klasa InetAdress jest wykorzystywana przy tworzeniu "adresów gniazd" (adres IP + port), które są obiektami klasy InetSocketAddress. Te z kolei mogą być zastosowane do odroczenia (odseparowania) łączenia z serwerem od aktu utworzenia gniazda.

Możemy mianowicie najpierw utworzyć "niezwiązane" gniazdo:

    Socket socket = new Socket();


odpowiednie adresy:

    InetAdress inetadr = InetAddress.getByName(host);
    InetSocketAddress conadr = new InetSocketAddress(inetadr, port);

po czym w odpowiednim, wybranym momencie połączyć się z serwerem:

    socket.connect(conadr);

Dokładniej obrazuje to poniższy fragment programu:
  public static void main(String[] args) {

    String host = "time.nist.gov";
    int port = 13;
    Socket socket = new Socket(); // utworzenie niezwiązanego gniazda

    try {
      // Utworzenie adresów
      InetAddress inetadr = InetAddress.getByName(host);
      InetSocketAddress conadr = new InetSocketAddress(inetadr, port);

      // Połaczenie z serwerem
      socket.connect(conadr);

      // Pobranie strumienia wejściowego gniazda
      // Nakładamy buforowanie
      BufferedReader br = new BufferedReader(
                            new InputStreamReader(
                             socket.getInputStream()
                             )
                          );

      // Odczyt odpowiedzi serwera (data i czas)
      String line;
      while ((line = br.readLine()) != null) {
        System.out.println(line);
      }

      // Zamknięcie strumienia i gniazda
      br.close();
      socket.close();
    } catch (UnknownHostException exc) {
        System.out.println("Nieznany host: " + host);
    } catch (Exception exc) {
         exc.printStackTrace();
    }
  }
Ten sposób programowania może sprzyjać lepszemu izolowaniu fragmentów kodu, okaże się także przydatny wtedy, gdy chcemy precyzyjnie określić maksymalny czas oczekiwania na połączenie.

5.5.  Limity czasu łączenia i odczytu

Czasami przydatne jest określenie maksymalnego czasu oczekiwania na połączenie z serwerem jak również masymalnego czasu oczekiwania na odczyt kolejnych danych z serwera. Na przykład - nie chcemy uzyskać informacji o czasie od serwera czasu po upływie kilku sekund - chcemy mieć raczej dokładny, aktualny pomiar.

Limit czasu na połączenie (w milisekundach) specyfikujemy jako drugi argument przeciążonej metody connect. Gdybyśmy np. w poprzednim programie napisali:
socket.connect(conadr, 100);
to nie uzyskanie połączenia w ciągu 100 milisekund będzie skutkowało powstaniem wyjątku SocketTimeoutException np.

java.net.SocketTimeoutException: connect timed out


Innym limitem czasowym jest limit odczytu. Możemy uzyskać połączenie, ale w komunikacji z serwerem (w trakcie czytania odpowiedzi serwera) mogą wystąpić jakieś opóźnienia, których nie chcemy zaakceptować. Limity czasu na odczyt specyfikujemy w milisekunadach za pomocą metody setSoTimeout(...) z klasy Socket. Ten limit określa maksymalny czas blokowania operacji czytania strumienia wejściowego gniazda. Po przekroczeniu tego czasu blokowanie na wejściu jest przerywane i powstaje wyjątek SocketTimedOutException. Gniazdo i jego strumień wejściowy mogą być ponownie użyte.
Ustalenie limitu musi poprzedzać wprowadzanie jakiejkolwiek blokującego czytania strumienia gniazda.
 
Przykładowy program pokazuje, że po uzyskaniu połaczenia z serwerem czasu odpowiedź (aktualny czas) uzyskujemy nie dość szybko (jak sobie tego życzymy)   i powstaje wyjątek SocketTimeoutException.
    try {
      // Utworzenie adresów
      InetAddress inetadr = InetAddress.getByName(host);
      InetSocketAddress conadr = new InetSocketAddress(inetadr, port);

      // Połaczenie z serwerem
      // Określenie maksymalnego czasu oczekiwania na połączenie
      socket.connect(conadr, 200);


      // Pobranie strumienia wejściowego gniazda
      // Nakładamy buforowanie
      BufferedReader br = new BufferedReader(
                            new InputStreamReader(
                             socket.getInputStream()
                             )
                          );
      // Okreslenie maksymalnego czasu oczekiwania na odczyt danych z serwera
      socket.setSoTimeout(50);

      // Odczyt odpowiedzi serwera (data i czas)
      String line;
      while ((line = br.readLine()) != null) {
        System.out.println(line);
      }

      // Zamknięcie strumienia i gniazda
      br.close();
      socket.close();
    } catch (UnknownHostException exc) {
        System.out.println("Nieznany host: " + host);
    } catch (Exception exc) {
         exc.printStackTrace();
    }
  }


java.net.SocketTimeoutException: Read timed out
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.read(SocketInputStream.java:129)
        at sun.nio.cs.StreamDecoder$CharsetSD.readBytes(StreamDecoder.java:408)
        at sun.nio.cs.StreamDecoder$CharsetSD.implRead(StreamDecoder.java:450)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:182)
        at java.io.InputStreamReader.read(InputStreamReader.java:167)
        at java.io.BufferedReader.fill(BufferedReader.java:136)
        at java.io.BufferedReader.readLine(BufferedReader.java:299)
        at java.io.BufferedReader.readLine(BufferedReader.java:362)
        at DateTime2.main(DateTime2.java:36)


5.6. Inne parametry gniazd kliencich.


Klasa Socket dostarcza wielu metod uzyskiwania informacji o stanie i opcjach gniazda.
W syntetyczny sposób możemy poznać je za pomocą poniższego programiku, który refleksyjnie wywołuje publiczne metody get...() i is...() z klasy Socket.

import java.net.*;
import java.io.*;
import java.lang.reflect.*;

class DateTime2 {

  public static void main(String[] args) {

    String host = "time.nist.gov";
    int port = 13;
    Socket socket = new Socket();

    try {
      // Utworzenie adresów
      InetAddress inetadr = InetAddress.getByName(host);
      InetSocketAddress conadr = new InetSocketAddress(inetadr, port);

      // Połaczenie z serwerem
      // Określenie maksymalnego czasu oczekiwania na połączenie
      socket.connect(conadr, 200);


      // Pobranie strumienia wejściowego gniazda
      // Nakładamy buforowanie
      BufferedReader br = new BufferedReader(
                            new InputStreamReader(
                             socket.getInputStream()
                             )
                          );
      // Okreslenie maksymalnego czasu oczekiwania na odczyt danych z serwera
      socket.setSoTimeout(200);

      // Czego możemy się dowiedzieć o stanie gniazda?
      report(socket);

      // Odczyt odpowiedzi serwera (data i czas)
      String line;
      while ((line = br.readLine()) != null) {
        System.out.println(line);
      }

      // Zamknięcie strumienia i gniazda
      br.close();
      socket.close();
    } catch (UnknownHostException exc) {
        System.out.println("Nieznany host: " + host);
    } catch (Exception exc) {
         exc.printStackTrace();
    }
  }

  // Dynamiczne wołanie metod z klasy Socket
  static void report(Socket s) throws Exception {
    Method[] methods = (java.net.Socket.class).getMethods();
    Object[] args = {};
    for (int i=0; i<methods.length; i++) {
      String name = methods[i].getName();
      if ((name.startsWith("get") || name.startsWith("is")) &&
          !name.equals("getChannel") &&
          !name.equals("getInputStream") &&
          !name.equals("getOutputStream")) {

        System.out.println(name + "() = " +
                           methods[i].invoke(s, args));
      }
    }
  }


}

Program wyprowadzi:

getPort() = 13
isClosed() = false
getInetAddress() = time.nist.gov/192.43.244.18
getKeepAlive() = false
getLocalAddress() = /61.120.87.131
getLocalPort() = 1178
getLocalSocketAddress() = /61.120.87.131:1178
getOOBInline() = false
getReceiveBufferSize() = 8192
getRemoteSocketAddress() = time.nist.gov/192.43.244.18:13
getReuseAddress() = false
getSendBufferSize() = 8192
getSoLinger() = -1
getSoTimeout() = 200
getTcpNoDelay() = false
getTrafficClass() = 0
isBound() = true
isConnected() = true
isInputShutdown() = false
isOutputShutdown() = false
getClass() = class java.net.Socket

52944 03-11-01 00:36:14 00 0 0 913.5 UTC(NIST) *


Proszę sprawdzić w dokumentacji znaczenie poszczególnych opcji.

5.7. Klient SMTP


Protokół - to jest ważne !!!

Klient posyła zlecenia w określonej formie, serwer odpowiada wierszami danych, w których na początku znajdują się numeryczne kody, określające wynik przetworzenia zlecenia, a po nich następują werbalne (różne dla różnych serwerów) informacje (np. informacja o serwerze, słówko "Ok", że wszystko w porządku, lub napis typu "go ahead"). Oczywiście, napisy mogą być w różnych językach; w przeciwieństwie do numetycznych kodów nie są one określone przez protokół SMTP i każdy serwer może opisywac wyniki swego działania w dowolny sposób.

Ważna jest również kolejność zleceń i odpowiedzi oraz to, by klient identyfikował wyniki  zleceń i odpowiednio do tego reagował (np. nie ma sensu posyłania poczty,  jeżeli serwer zwraca wynik, świadczący o tym, że transakcja nie może dojść do skutku).


klient: połączenie przez port 25,
serwer: kod potwierdzenia uzyskania połączenia 220,
klient: inicjacja komunikacji HELO lub EHLO (ESMPTP) + identyfikacja domeny,
serwer: OK? - 250, nie - kody błędów (np. brak autoryzacji). Odp: wiele wierszy (nie wiadomo ile).
klient: inicjacja transakci - MAIL, RCPT (odbiorcy)
serwer: OK? - 250
klient: posyłanie danych - polecenie DATA
serwer: 354 (go ahead),
klient: list (koniec listu: jedna kropka w wierszu, sic!)
serwer: odebrałem? - 250
i dalej następne zlecenia (inne transakcje pocztowe itp.)
Zamknięcie kanału przez klienta.


Przykładowy program wysyła listy, których zawartosć pobierana jest z plików. Przeslanie zleceń do serwera - metoda doRequest(String zlecenie, int checkCode). Zapisuje ona zlecenie do strumienia wyjściowego gniazda łączącego klienta z  serwerem SMTP, po czym wywołuje metodę readResponse(int checkCode), która odczytuje odpowiedź serwera. Przekazywany jako argument tej metody checkCode jest spodziewanym (i wymaganym) kodem wyniku. Przy jego niezgodności z kodem odczytywanym ze strumienia wejściowego gniazda (czyli przekazanym przez serwer) zgłaszamy wyjątek IOException z komunikatem "Niespodziewany kod wyniku".
W obu metodach - doRequest i readResponse - wypisuejmy informację na konsole, uzyskując w ten sposób prezentację komunikacji pomiędzy klientem i serwerem.

import java.io.*;
import java.net.*;

public class EmailClient {

  Socket smtpSocket = null;
  PrintWriter sockOut = null;
  InputStream sockIn = null;

  public void connect(String server, String myDomain) {
    try {
      smtpSocket = new Socket(server, 25);
      sockOut = new PrintWriter(
             new OutputStreamWriter(smtpSocket.getOutputStream(), "UTF-8"),
             true);
      sockIn = smtpSocket.getInputStream();

      // Czy połączenie zostało nawiązane?
      // Musi być kod 220 - wtedy Ok
      // Odczytując odpowiedż serwera,
      // sprawdzamy w metodzie readResponse kod 220

      readResponse(220);

      // Przedstawiamy się serwerowi
      // Jeśli nas zaakceptuje - poda kod 250
      doRequest("HELO " + myDomain, 250);


    } catch (UnknownHostException e) {
        System.err.println("Nieznany host: " + server);
        cleanExit(1);
    } catch (IOException exc) {
        System.err.println(exc);
        cleanExit(2);
    }
  }


  // Posyłanie maila
  public void send(String from, String to, String fname) {
    try {

      // Inicjacja transakcji
      // Kod 250 - jesli OK
      doRequest("MAIL FROM:<" + from + ">", 250);

      // Określenie adresata
      // Kod 250 - jesli OK
      doRequest("RCPT TO:<" + to + ">", 250);

      // Posyłanie danych listu
      // Odpowiedź serwera - 354 = jestem gotowy na przyjęcie danych
      doRequest("DATA", 354);

      // Teraz będziemy zapisywać treść listu
      // bezpośrednio do strumienia wyjściowego gniazda
      // Najpierw jakieś nagłówki
      sockOut.println("From: " + from);
      sockOut.println("To: " + to);

      // Czytanie treści listu z pliku
      // Ponieważ samotna kropka w wierszu kończy dane listu
      // to samotną kropkę w treści zamieniamy na dwie kropki
      BufferedReader br = new BufferedReader(
                            new FileReader(fname));
      String line;
      while ((line = br.readLine()) != null) {
        if (line.equals(".")) line += ".";
        sockOut.println(line);
      }
      // Sekwencja CRLF.CRLF oznacza koniec treści listu
      // Drugie CRLF dodane w metodzie doRequest przez println
      doRequest("\r\n.", 250);

    } catch (IOException e) {
        System.err.println(e);
        cleanExit(2);
    }
  }

  // Zamknięcie połączenia
  public void closeConnection() {
    try {
      doRequest("QUIT", 221);
    } catch (Exception exc) {
      System.err.println(exc);
    }
    cleanExit(0);
  }


  private void doRequest(String req, int checkCode)
              throws IOException {
    sockOut.println(req);
    System.out.println("Klient: " + req);
    readResponse(checkCode);
  }

  // Uwaga: nie powinniśmy tu stosować buforowania i metody
  // readLine(), ponieważ nie wiadomo ile wierszy zwrówci serwer
  // a wywołanie readLine jest blokujące
  // Zakłądamy: że każda odpowiedź zmieści się w 10000 bajtów  
  private void readResponse(int checkCode) throws IOException {
    byte[] readBytes = new byte[10000];
    int num = sockIn.read(readBytes);
    String resp = new String(readBytes, 0, num);
    System.out.println("Serwer: " + resp);
    if (!resp.startsWith(String.valueOf(checkCode)))
     throw new IOException("Niespodziewany kod wyniku od serwera");
  }

  private void cleanExit(int code) {
    try {
      sockIn.close();
      sockOut.close();
      smtpSocket.close();
    }
    catch (Exception exc) {}
    System.exit(code);
  }

  public static void main(String[] args) {
    String server = "mail.somemailer.net";
    String myDomain = "62.125.12.111";
    String from = "[email protected]";
    String to = "you@anywhere";
    EmailClient email = new EmailClient();
    email.connect(server, myDomain);
    email.send(from, to, "list1");
    email.send(from, to, "list2");
    email.closeConnection();
  }
}

Przykładowy wynik działania programu:


Serwer: 220 mail.somemailer.net ESMTP

Klient: HELO 62.125.12.111      
Serwer: 250 mail3.somemailer.net

Klient: MAIL FROM:<[email protected]>
Serwer: 250 Ok

Klient: RCPT TO:<someone@anywhere>
Serwer: 250 Ok

Klient: DATA
Serwer: 354 End data with <CR><LF>.<CR><LF>

Klient:
.
Serwer: 250 Ok: queued as 28F372E815A

Klient: MAIL FROM:<[email protected]>
Serwer: 250 Ok

Klient: RCPT TO:<someone@anywhere>
Serwer: 250 Ok

Klient: DATA
Serwer: 354 End data with <CR><LF>.<CR><LF>

Klient:
.
Serwer: 250 Ok: queued as 8FA212E81EC

Klient: QUIT
Serwer: 221 Bye



Ten przykład oprogramowania klienta ilustruje dwie ważne kwestie:
Szczegółowy opis protokołu SMTP zawiera RFC0821, a jego rozszerzeń (ESMTP) - RFC1869.

Naturalnie, omówiony przykład nie jest ani uniwersalny ani nazbyt użyteczny.  Prawdziwe klienckie programy pocztowe mogą być bardzo rozbudowane i dostarczać wielu możliwości. W środowisku Javy możemy wykorzystać do ich budowy (jak również do budowy serwerów pocztowych) Java Mail API. Zestaw klas tego pakietu umożliwia m.in. posługiwanie się protokolami SMTP, POP3, IMAP, prowadzenie folderów wiadomości, łatwą kompozycję wiadomości (m.in. dołączanie do listów różnego rodzaju plików jako załączników), a wszystko to bez eksponowania szczegółów dotyczących niskopoziomowej komunikacji poprzez gniazda.


5.8. Klient DICT

Ciekawym, choć mało popularnym protokołem jest DICT.
Jest on opisany w RFC2229.
Serwery DICT prowadzą bazy danych definicji różnych terminów i dostarczają - na zlecenie klientów - odpowiednich informacji.
Nie będziemy oczywiście szczegółowo omawiać tego protokołu. Dla potrzeb demonstracji wystarczy wiedzieć, że:
Przykładowy program pokazuje, że tym razem możemy spokojnie posługiwac się buforowanym czytaniem i metodą readLine() (serwer zwraca wiersze, a my przy czytaniu będziemy czatowac na odpowiednie kody). Ponadto - uczynimy nasz program nieco bardziej przyjaznym dostarczając GUI (to dobra idea i krok naprzód - generalnie programy tego typu powinny posiadać odpowiednie wygodne GUI).

Program przedstawiono na poniższym wydruku.
import java.io.*;
import java.net.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;

public class DictClient extends JFrame
{
   public final static int port = 2628;
   private String server;
   private Socket clientSocket;
   private PrintWriter out;
   private BufferedReader in;
   private String database = "*";  // info ze wszystkich baz

   JTextArea ta = new JTextArea(20, 40);
   Container cp = getContentPane();



   public DictClient (String server, int timeout)   {

     try {
       clientSocket = new Socket (server, port);
       in = new BufferedReader (
                new InputStreamReader(clientSocket.getInputStream(), "UTF8"));
       out = new PrintWriter (
                new OutputStreamWriter(clientSocket.getOutputStream(), "UTF8"),
                             true);

       String resp = in.readLine(); // połączenie nawiązane - info o tym
       System.out.println(resp);
       if (!resp.startsWith("220")) {
         cleanExit(1); // jeżeli dostęp niemożliwy
       }

       // Ustalenie maksymalnego czasu blokowania
       // na operacji czytania ze strumienia gniazda

       clientSocket.setSoTimeout(timeout);


     } catch(UnknownHostException exc) {
         System.err.println("Uknown host " + server);
         System.exit(2);
     } catch(Exception exc) {
         exc.printStackTrace();
         System.exit(3);
     }

     // wszystko poszło dobrze - tworzymy i pokazujemy okno wyszukiwania

    Font f = new Font("Dialog", Font.BOLD, 14);
    ta.setFont(f);
    cp.add(new JScrollPane(ta));
    final JTextField tf = new JTextField();
    tf.setFont(f);
    tf.setBorder(BorderFactory.createLineBorder(Color.blue, 2));
    cp.add(tf, "South");

    tf.addActionListener( new ActionListener() {
      public void actionPerformed(ActionEvent e) {
         doSearch(tf.getText());
      }
    });

    addWindowListener( new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        dispose();
        cleanExit(0);
      }
    });

    pack();
    show();

    // Ustalenie fokusu na polu wprowadzania szukanych słów
    SwingUtilities.invokeLater( new Runnable() {
      public void run() {
        tf.requestFocus();
      }
    });
  }

  // Wyszukiwanie

  public void doSearch(String word) {
    try {
      String resp = "",
            defin = "Uzyskano następujące definicje:\n";

      // Zlecenie dla serwera
      out.println("DEFINE " + database + " " + word);

      // Czytamy odpowiedź
      // Kod 250 na początku wiersza oznacza koniec definicji
      while (resp != null && !resp.startsWith("250")) {
        resp = in.readLine();
        defin += resp + "\n";
        if (resp.startsWith("552")) break;  // słowo nie znalezione
      }
      ta.setText(defin);
    } catch(SocketTimeoutException exc) {
       ta.setText("Za długie oczekiwanie na odpowiedź");
    } catch(Exception exc) {
          exc.printStackTrace();
    }
  }

  private void cleanExit(int code) {
    try {
      out.close();
      in.close();
      clientSocket.close();
    } catch(Exception exc) {}
    System.exit(code);
  }

  public static void main(String[] args) {

    int timeout = 0;
    String server = "dict.org";
    try {
      timeout = Integer.parseInt(args[0]);
      server = args[1];
    } catch(NumberFormatException exc) {
      server = args[0];
    } catch(ArrayIndexOutOfBoundsException exc) {}

    new DictClient(server, timeout);
  }

}
Uruchamiając program podajemy adres hosta serwera oraz maksymalne oczekiwanie na odpowiedź (wykorzystamy je później w metodzie setSoTimeout(...)). Adresy różnych serwerów słownikowych można znaleźć na stronie www.dict.org. Jesli nie podamy adresu - domyślnie zostanie przyjęty podstawowy (choć nie największy i nie najbardziej wszechstronny) serwer dict.org.

Połączenie z nim wyprowadzi na konsolę:

220 pan.alephnull.com dictd 1.8.0/rf on Linux 2.4.18-14 <auth.mime> <1094327.262
[email protected]>


i od tego momentu będziemy mogli w oknie, w polu tekstowym u dolu wprowadzać szukane słowa i poprzez wciśnięcie ENTER uzyskiwać ich definicje (co pokazują rysunki)

r


r


Programik ten ma jedną istotną wadę. Zlecenia obsługiwane są synchronicznie. Nie możemy np. pisać w edytorze (wielopolu tekstowym w górnej części okna) i jednocześnie przeszukiwać baz danych serwerów słownikowych. Zapewne moglibyśmy sobie z tym poradzić wyodrębniając w programie wątek komunikacji z serwerem.
Zaraz to zrobimy, ale chcielibyśmy również, aby wątek ten na bieżąco informował nas o tym co robi i ile czasu mu to zajmuje. Przy normalnym, blokującym wejściu/wyjściu taka precyzyjna, na bieżąco podawana informacja nie jest dostępna. Jeżeli wątek czeka na odczyt danych (jest zablokowany na operacji czytania), to nie może równocześnie przekazywać informacji np. o upływającym czasie oczekiwania.
Powinniśmy zatem zastosowac mechanizmy NIO - mianowicie kanały gniazd klienckich i nieblokujące wejście/wyjście.

5.9. Kanały gniazd klienckich i nieblokujące wejście/wyjście

Mechanizmy NIO (nowego wejścia-wyjścia) w Javie są nam już znane. Przed dalszą lekturą niewątpliwie warto odświeżyć sobie te informacje. 

W odróżnieniu od kanałów plikowych kanały gniazd umożliwiają nieblokujące (asynchroniczne) wejście-wyjście.
Przy czytaniu z takiego kanału za pomoca metody read(...), wątek nie jest blokowany, gdy brak danych. Metoda read(...) natychmiast zwraca wynik - liczbę przeczytanych bajtów (jeśli nie ma jeszcze danych - to 0). Możemy zatem w pętli odpytywać kanał czy dane już nadeszły, a jeśli nie - wykonywać inne zadania. Za chwilę zobaczymy to na przykładzie zmodyfikowanego klienta DICT.

Inną, odróżniającą i ważną cechą kanałów gniazd jest to, iż możliwe jest ich multipleksowanie czyli obsługiwanie przez jeden wątek wielu kanałów. Klasy kanałów gniazd (SocketChannel i ServerSocketChannel) pochodzą bowiem od klasy SelectableChannel, która pozwala na rejestrowanie kanałów do użycia z selektorami . Te zaś obsługują selekcję kanału (zdarzenia połączenia, czytania, pisania ...), dzięki czemu możemy fragmenty programów odpowiedzialne np. za odczytywanie danych z kanalów pisać w konwencji call-back.
Właściwość tę mają zarówno kanały gniazd klienckich jak i gniazd serwerowych, niewątpliwie jednak prawdziwie przydatna jest ona przy programowaniu serwerów, które przecież powinny obsługiwac równolegle wielu klientów. Programy klienckie zwykle korzystają z jednego kanału komunikacyjnego, w związku z tym mechanizm selektorów omówiony zostanie w dalszych punktach przy okazji programowania serwerów, teraz natomisat zajmiemy się nieblokującym wejściem-wyjściem dla kanałów klienckich.

Kanały gniazd klienckich tworzymy za pomocą statycznej metody open() klasy SocketChannel.
Po stworzeniu kanału w  ten sposób  kanał jeszcze nie jest połączony (poprzez gniazdo) z serwerem.
Połączenie uzyskujemy używając metody connect z argumentem typu SocketAddress (zwykle będzie to znana nam już  klasa InetSocketAddress, której obiekty specyfikują adresy gniazd, czyli adres hosta + numer portu).

Uwaga: w obecnej postaci kanały gniazd nie są wystarczającą abstrakcją dla obsługi połączeń. Ustalanie opcji gniazd oraz ich zamykanie odbywa się nadal za pomocą klasy Socket (dostęp do obiektu-gniazda, związanego z danym kanałem uzyskujemy za pomocą metody socket() z klasy SocketChannel).

Kanały gniazd możemy skonfigurować w dwóch trybach: blokującym lub nieblokującym. Służy do tego metoda configureBlocking(true | false).

W trybie biokującym każda operacja I/O blokuje wątek, dopóki nie zostanie zakończona.

W trybie nieblokującym operacja nie blokuje wątku (jej wywołanie wraca natychmiast) i może przekazać mniej bajtów danych niż było wymagane lub nawet wcale.

Dla nieblokujących kanałów metoda connect()  nie blokuje wątku, wynik jest zwracany natychmiast i wskazuje na to czy połączenie zostało nawiązane (true), lub, że jest w trakcie nawiązywania (false).
 Zwykle połączenie nie może być nawiązane natychmiast (ew. wyjątkiem są połączenia z lokalnym hostem), zatem potrzebny jest mechanizm uzyskania informacji o tym czy i kiedy połączenie zostało nawiązane. Służy temu metoda finishConnect(), której wywołanie zwraca true, jesli proces łączenbia został zakończony a false w przeciwnym razie.

Schemat nawiązania połączenia poprzez kanał w trybie nieblokującym przedstawiono poniżej.


     SocketChannel channel;
     String server = ...; // adres hosta serwera
     int port =         ...; // numer portu
     .... 

     try {
        channel = SocketChannel.open();
        channel.configureBlocking(false);
        channel.connect(new InetSocketAddress(server, port));
       
        System.out.print("Łącze się ...");
        while (!channel.finishConnect()) {
           // ew. pokazywanie czasu łączenia (np. pasek postępu)
           // lub wykonywanie jakichś innych (krótkotrwałych) działań
        }
     } catch(UnknownHostException exc) {
         System.err.println("Uknown host " + server);
         // ...
     } catch(Exception exc) {
         exc.printStackTrace();
         // ...
     }
     System.out.println("\nPołaczony");


Skrócową formą utworzenia kanału i połączenia jest wywołanie metody:

        SocketChannel  SocketChannel.open(InetSocketAddress)

To odwołanie jest blokujące (bowiem kanały są domyślnie tworzone w trybie blokującym), ale tryb ten możemy w dowolnym momencie zmienić na nieblokujący za pomocą metody configureBlocking(...) i od tego momenty wszetkie operacje we/wy na kanale będą nieblokujące.

Po stworzeniu kanału możemy do niego pisać (przekazywać zlecenia do serwera) lub z niego czytać (odczytywać dane przekazane przez serwer).  W sumie jest to wygodne, bowiem zamiast dwóch strumieni związanych z gniazdem mamy jeden kanał. Przy tym jednak musimy posługiwac się buforami NIO. Przypomnijmy, że:
Schemat nieblokującego czytania.


Możliwy schemat nieblokującego czytania
z kanału gniazda

SocketChannel channel;

// Utworzenie i połączenie kanału
// ...
// Ustalenie trybu nieblokującego

channel.configureBlocking(false);

// ...

// Alokowanie bufora bajtowego
// allocateDirect pozwala na wykorzystanie mechanizmów sprzętowych
// do przyspieszenia operacji we/wy
// Uwaga: taki bufor powinien być alokowany jednokrotnie
// i wielokrotnie wykorzystywany w operacjach we/wy

  ByteBuffer inBuf = ByteBuffer.allocateDirect(rozmiar_bufora);


  // pętla czytania
  while (true) {      

      inBuf.clear();                                         // opróżnienie bufora

      int readBytes = channel.read(inBuf);     // czytanie nieblokujące
                                                                   // natychmiast zwraca liczbę
                                                                   // przeczytanych bajtów

      if (readBytes == 0) {                              // jeszcze nie ma danych
          // jakieś (krótkotrwałe) działania np. info o upływającym czasie
         continue;
      }
      else if (readBytes == -1) {                     // kanał zamknięty po stronie serwera
                                                                  // dalsze czytanie niemożlwe
          // ...
          break;
      }
      else {
                                                  // dane dostępne w buforze
          inBuf.flip();                                     // przestawienie bufora
          // pobranie danych z bufora
          // ew. decyzje o tym czy mamay komplet danych - wtedy break
          // czy też mamy jeszcze coś do odebrania z serwera - kontynuacja
      }
   }

Uwaga: na schemacie nie pokazano koniecznej obsługi wyjątków; generalnei dotyczy ona połączenia i konfiguracji kanału a także operacji read.


Nowa wersja klienta DICT.
Program będzie składać się z dwóch klas:
Kod programu pokazano na poniższym wydruku:
import java.io.*;
import java.net.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.regex.*;


class DictGui extends JFrame implements ActionListener {

  public final static int port = 2628;
  private String server;
  private SocketChannel channel;

  private JTextArea ta = new JTextArea(20, 20);
  private JTextField tf = new JTextField(20);
  private JLabel infoLab = new JLabel("Nie było szukania");
  private JButton paste = new JButton("Wklej definicję");
  private Container cp = getContentPane();
  private ReadDef rd;

  public DictGui(String server) {
    this.server = server;

    // Otwarcie i połączenie kanału
    // metoda connect - zdefiniowana u końca klasy
    try {
      channel = SocketChannel.open();
      channel.configureBlocking(false);
      connect();
    } catch(UnknownHostException exc) {
        System.err.println("Uknown host " + server);
        System.exit(1);
    } catch(IOException exc) {
        exc.printStackTrace();
        System.exit(2);
    }

    // Konfiguracja GUI
    Font f = new Font("Dialog", Font.BOLD, 14);
    ta.setFont(f);
    tf.setFont(f);
    tf.setBorder(BorderFactory.createLineBorder(Color.orange, 1));
    infoLab.setPreferredSize(new Dimension(200,30));
    JPanel p = new JPanel();
    p.setBorder(BorderFactory.createLineBorder(Color.red, 2));
    p.add(tf);
    p.add(infoLab);
    p.add(paste);
    cp.add(new JScrollPane(ta));
    cp.add(p, "South");

    tf.addActionListener(this);
    paste.addActionListener(this);

    // Przy zamykaniu aplikacji
    // zamykamy kanał i gniazdo
    addWindowListener( new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        dispose();
        try {
         channel.close();
         channel.socket().close();
        } catch(Exception exc) {}
        System.exit(0);
      }
    });

    pack();
    show();
  }

  // Obsługa akcji
  public void actionPerformed(ActionEvent e) {
    // Kliknięcie w przycisk "Wklej definicję"
    // definicję przechowuje dla nas obiekr klasy ReadDef
    if (e.getSource() == paste && rd != null) {
       ta.insert(rd.getResult(), ta.getCaretPosition());
    }
    else {  // ENTER na polu tekstowym tf - start wątku komuniakcji z serwerem
     if (!channel.isConnected()) try {
       connect();
     } catch(Exception exc) {
        exc.printStackTrace();
        return;
     }
     rd = new ReadDef(this, channel, tf.getText());
     rd.start();
    }
  }

  // Łączenie kanału z serwerem
  private void connect() throws UnknownHostException, IOException {
    if (!channel.isOpen()) channel = SocketChannel.open();
    channel.connect(new InetSocketAddress(server, port));
    System.out.print("Łącze się ...");
    while (!channel.finishConnect()) {
      try { Thread.sleep(200); } catch(Exception exc) { return; }
      System.out.print(".");
    }
    System.out.println("\nPołączony.");
  }

  // Metoda wykorzystywana przez ReadDef
  // do pokazywania postepów komuniakcji z serwerem
  public void setInfo(String s) {
    infoLab.setText(s);
  }
}

class ReadDef extends Thread {

  private static Charset charset  = Charset.forName("ISO-8859-2");
  private static ByteBuffer inBuf = ByteBuffer.allocateDirect(1024);
  private static Matcher matchCode =
                 Pattern.compile("(\n250 ok)|(552 no match)").matcher("");
  private SocketChannel channel;
  private DictGui gui;
  private String word;


  public ReadDef(DictGui gui, SocketChannel ch, String wordToSearch) {
    this.gui = gui;
    channel = ch;
    word = wordToSearch;
  }

  private StringBuffer result;

  public void run() {
    result = new StringBuffer("Wyniki wyszukiwania:\n");
    int count = 0, rcount = 0;
    try {
      CharBuffer cbuf = CharBuffer.wrap("DEFINE * " + word + "\n");
      ByteBuffer outBuf = charset.encode(cbuf);
      channel.write(outBuf);

      while (true) {
        inBuf.clear();
        int readBytes = channel.read(inBuf);
        if (readBytes == 0) {
          gui.setInfo("Czekam ... " + ++count);
          Thread.sleep(200);
          continue;
        }
        else if (readBytes == -1) {
          gui.setInfo("Kanał zamknięty");
          channel.close();
          break;
        }
        else {
          inBuf.flip();
          cbuf = charset.decode(inBuf);
          result.append(cbuf);
          matchCode.reset(cbuf);
          if (matchCode.find()) break;
          else gui.setInfo("Czytam ... " + ++rcount);
        }
      }
    } catch(Exception exc) {
         exc.printStackTrace();
         return;
    }
    gui.setInfo("Czekałem: " + count + " / Czytałem: " + rcount + ". Gotowe.");
  }

  public String getResult() {
    if (result == null) return "Brak wyników wyszukiwania";
    return result.toString();
  }
}

class Main {
  public static void main(String[] args) {
    String server = "dict.org";
    new DictGui(server);
  }
}
Komentarze:
Sposób działania programu ilustruje rysunek.
r

Widzimy tu (w obszarze etykiety informacyjnej), że nasz klient czekał ok. 600 ms (3 *200) na uzyskanie pierwszej odpowiedzi serwera na zapytanie o definicję słowa "words
, a odpowiedź przeczytał w dwóch porcjach danych (pierwsza 1024B, druga co najwyżej 1024 bajty).

5.10. Serwery

Serwery dostarczają - na zlecenie klientów - określonych usług (m.in. informacji).
Typowe działanie serwera polega na:
  1. Utworzeniu gniazda serwera (klasa ServerSocket)
  2. Związaniu go z określonym adresem (adres hosta, na którym działa serwer + adres portu)
  3. Oczekiwaniu na połączenie od klienta "na tym" gnieździe
  4. Po zaakceptowaniu połączenia od klienta - utworzeniu ganiazda wymiany informacji z połączonym klientem (klasa Socket) i obsłudze zleceń tego klienta posyłanych poprzez strumień związany z tym gniazdem.
  5. Kontynuacji "nasłuchu" połączeń od innych klientów na gnieżdzie serwera.
Poniższy schemat ilustruje oprogramowanie serwera.


    
      String host = ...;  // nazwa hosta
      int port = ...;     // numer portu

      InetSocketAddress isa = new InetSocketAddress(host, port);

      // Utworzenie gniazda serwera
      ServerSocket serverSock =  new ServerSocket();

      // Związanie gniazda serwera z adresem hosta i portu
      serverSock.bind(isa);
 
      // W pętli następuje akceptacja połączeń kolejnych klientów
      // i obsługa ich zleceń
     
      boolean serverIsRunning = true;         // przy zamykaniu serwera
                                                             // ustalane na false (np. przez inny wątek)

      while (serverIsRunning) { 

        // akceptacja połączenia
        // i utworzenie gniazda komunikacji z połączonym klientem

        Socket connection = serverSock.accept();

        // obsługa zleceń klienta
        // za pośrednictwem strumieni we/wy związanych z gniazdem connection
        // ...
        // po zakończeniu interakcji z klientem
        // - zamknięcie strumieni i gniazda
        // ...
        connection.close()
       
     }
    
     // zamknięcie gniazda serwera

     serverSock.close();


Przy tworzenie adresu gniazda (InetSocketAddress) podajemy host serwera oraz port poprzez który serwer będzie dostępny dla klientów.
W naszych eksperymentach domowych jako host możemy podać "localhost" (lub adres "127.0.0.1").
Specyfikując numer portu, należy pamiętać o tym, że pierwsze 1024 porty są zarezerwowane (wspominane wcześniej "dobrze-znane-porty"). Możemy oczywiście pisać serwer FTP czy HTTP, ale użycie właściwych dla nich numerów portów wymaga uprawnień administrtora systemu (przy braku takich uprawnień, używamy dostępnych numerów portów, czasem konwencjonalnych np. dla serwera HTTP - 8080).


Metoda bind(...) wiąże gniazdo serwera z podanym adresem. Możemy podać również - jako jej drugi argument - maksymalną liczbę klientów oczekujących na połączenie (domyślnie jest to liczba 50). Wtedy przy wydłużeniu kolejki oczekujących klientów poza podaną liczbę, kolejni próbujący łączyć sie z serwerem klienci będą uzyskiwać odmowę połączenia (wyjątek "Connection refused").

Metoda accept() jest blokująca i wstrzymuje wykonanie wątku do chwili zgłoszenia chęci połączenia ze strony klienta (klient łączy się z serwerem, podając - czy to w konstruktorze klasy Socket, czy w metodzie connect() host serwera i numer portu, który przydzielił sobie serwer).  Gdy połączenie jest akceptowane, metoda accept() zwraca gniazdo (obiekt klasy Socket), poprzez które będzie odbywać się interakcja klienta z serwerem.
Metoda accept() zmoże sygnalizować m.in. wyjątek SocketTimeoutException, który powstaje, gdy przekroczony zostanie limit czasowy na uzyskanie połączenia. Dla gniazda serwera możemy ustalić ten limit za pomocą metody setSoTimeout().

Od gniazda, zwróconego przez metodę accept() można uzyskać strumienie wejściowy i wyjściowy i za ich pomocą "rozmawiać" z klientem.

Pisząc własny serwer należy przemyśleć i ustalić protokół komunikacji. Oczywiście, możemy skorzystać z gotowych protokołów (np. HTTP czy FTP). Takie serwery są jednak już gotowe - celowość pisania własnych serwerów HTTP czy FTP jest uzasadniona tylko w przypadkach jakiegoś testowania czy też specjalnego dostosowywania tych protokołów. Najczęściej zatem będziemy tworzyć całkiem nowy, własny protokół.

Zilustruje to przykład serwera - książki telefonicznej. Serwer ma dostęp do "bazy" numerów telefonów. Dla prostoty wykorzystamy mapę, w której osobom-kluczom będą przypisane numery telefonów - wartości, a dostęp do niej zrealizujemy poprzez klasę PhoneDirectory.
import java.util.*;
import java.io.*;

public class PhoneDirectory {

  private Map pbMap = new HashMap();

  public PhoneDirectory(String fileName) {
    // Inicjalna zawartość książki telefonicznej
    // jest wczytywana z pliku o formacie
    //  imię  numer_telefonu
    try {
      BufferedReader br = new BufferedReader(
                             new FileReader(fileName));
      String line;
      while ((line = br.readLine()) != null) {
        String[] info = line.split(" +", 2);
        pbMap.put(info[0], info[1]);
      }
    } catch (Exception exc) {
        exc.printStackTrace();
        System.exit(1);
    }
  }

  // Zwraca numer telefonu dla podanej osoby
  public String getPhoneNumber(String name) {
    return (String) pbMap.get(name);
  }

  // Dodaje nową osobę do książki
  // Wynik:
  // - true - dodana
  // - false - nie (przy próbie dodania osoby zapisanej już w książce)
  public boolean addPhoneNumber(String name, String num) {
    if (pbMap.containsKey(name)) return false;
    pbMap.put(name, num);
    return true;
  }

  // Zastępuje numer podanej osoby nowym
  // Wynik:
  // - true (numer zastąpiony)
  // - false (nie - próba podania nowegu numeru nieistniejącej osoby)
  public boolean replacePhoneNumber(String name, String num) {
    if (!pbMap.containsKey(name)) return false;
    pbMap.put(name, num);
    return true;
  }

}  

Serwer obsługuje zlecenia klientów:
Na każde zlecenie klienta serwer odpowiada posyłając (jako jeden wiersz) kod numeryczny wyniku przetworzenia zlecenia oraz opis tego wyniku. Przy zleceniu get posyłany jest w odpowiedzi dodatkowy, kolejny wiersz zawierający numer telefonu.
Ustalimy następujące kody:
0 - zlecenie wykonane (Ok)
1 - błędna składnia zlecenia lub nieznane zlecenie (Invalid request)
2 - nie znaleziono numeru dla podanej osoby, dotyczy tylko zlecenia get (Not found)
3 - błąd w zleceniu add - próba dodania osoby-numeru, która już istnieje w książce
4 - błąd w zleceniu replace - próba podania nowego numeru dla nie istniejącej osoby

Program serwera podano na wydruku.

import java.net.*;
import java.io.*;
import java.util.regex.*;

public class PhoneBookServer1  {

  private PhoneDirectory pd = null;  // mapa osoby-numery tel.
  private ServerSocket ss = null;
  private BufferedReader in = null;  // strumienie gniazda
  private PrintWriter out = null;    // komunikacji z klientem

  public PhoneBookServer1(PhoneDirectory pd, ServerSocket ss) {
    this.pd = pd;
    this.ss = ss;
    System.out.println("Server started");
    System.out.println("at port: " + ss.getLocalPort());
    System.out.println("bind address: " + ss.getInetAddress());

    serviceConnections();  // nasłuchiwanie połączeń
  }

  // Metoda nasłuchuje połączeń od klientów
  // po zaakceptowaniu połączenia - tworzy gniazdo komunikacyjne
  // i przekazuje obsługę zleceń metdodzie serviceRequest

  private void serviceConnections() {
    boolean serverRunning = true;   // serwer działa ciągle
    while (serverRunning) {
      try {
        Socket conn = ss.accept();  // nasłuch i akceptaccja połączeń

        System.out.println("Connection established");

        serviceRequests(conn);      // obsługa zleceń dla tego połączenia

      } catch (Exception exc) {
          exc.printStackTrace();
      }
    }                               // zamknięcie gniazda serwera
    try { ss.close(); } catch (Exception exc) {}
  }

  // wzorzec do rozbioru zlecenia (maks. trzy "słowa" rozdzielone spacjami)
  private static Pattern reqPatt = Pattern.compile(" +", 3);

  // Słowne komunikaty serwera
  // odpowidające im indeksy tablicy - kody wyniku
  private static String msg[] = { "Ok", "Invalid request", "Not found",
                                  "Couldn't add - entry already exists",
                                  "Couldn't replace non-existing entry",
                                  };


  // Obsługa zleceń od klienta
  private void serviceRequests(Socket connection)
                              throws IOException {
    try {
      in = new BufferedReader(                   // utworzenie strumieni
               new InputStreamReader(
                   connection.getInputStream()));
      out = new PrintWriter(
                connection.getOutputStream(), true);

      // Odczytywanie zleceń (line zawiera kolejne zlecenie)
      for (String line; (line = in.readLine()) != null; ) {

        String resp;                           // odpowiedź
        String[] req = reqPatt.split(line, 3); // rozbiór zlecenia
        String cmd = req[0];                   // pierwsze słowo - polecenie

        if (cmd.equals("bye")) {        // zlecenie "bye" - koniec komunikacji
          writeResp(0, null);
          break;
        }
        else if (cmd.equals("get")) {   // "get" - klient chce dostać nr tel.
          if (req.length != 2) writeResp(1, null);
          else {
            String phNum = (String) pd.getPhoneNumber(req[1]); // pobranie
            if (phNum == null) writeResp(2, null);             // numeru tel.
            else writeResp(0, phNum);                          // i zapis
          }
        }
        else if (cmd.equals("add"))  {  // "add" - klient chce dodać numer
          if (req.length != 3) writeResp(1, null);
          else {
            boolean added = pd.addPhoneNumber(req[1], req[2]); // dodany?
            if (added) writeResp(0, null);                     // tak - ok
            else writeResp(3, null);                           // nie
          }
        }
        else if (cmd.equals("replace"))  {  // klient chce zmienić nr tel.
          if (req.length != 3) writeResp(1, null);
          else {
            boolean replaced = pd.replacePhoneNumber(req[1], req[2]);
            if (replaced) writeResp(0, null);
            else writeResp(4, null);
          }
        }
        else writeResp(1, null);             // nieznane zlecenie
      }
    } catch (Exception exc) {
        exc.printStackTrace();

    } finally {
        try {                                // zamknięcie strumieni
          in.close();                        // i gniazda
          out.close();
          connection.close();
          connection = null;
        } catch (Exception exc) { }
    }
  }


  // Przekazanie odpowiedzi klientowi poprzez zapis do strumienia
  // gniazda komuniakcyjnego
  private void writeResp(int rc, String addMsg)
               throws IOException {
    out.println(rc + " " + msg[rc]);
    if (addMsg != null) out.println(addMsg);
  }

  public static void main(String[] args) {
    PhoneDirectory pd = null;
    ServerSocket ss = null;
    try {
      String phdFileName = args[0];
      String host = args[1];
      int port = Integer.parseInt(args[2]);

      pd = new PhoneDirectory(phdFileName); // utworzenie mapy numerów z pliku

      InetSocketAddress isa = new InetSocketAddress(host, port);

      ss =  new ServerSocket();             // Utworzenie gniazda serwera
      ss.bind(isa);                         // i związanie go z adresem

    } catch(Exception exc) {
        exc.printStackTrace();
        System.exit(1);
    }
    new PhoneBookServer1(pd, ss);
  }

}

Przykładowy klient, który wykonuje szereg testujących operacji może wyglądać tak jak na poniższym wydruku.

import java.net.*;
import java.io.*;

public class PhoneBookClient {

  private Socket sock = null;
  private PrintWriter out = null;
  private BufferedReader in = null;

  public PhoneBookClient(String host, int port) {
    try {
      sock = new Socket(host, port);
      out = new PrintWriter(sock.getOutputStream(), true);
      in = new BufferedReader(
               new InputStreamReader(
                   sock.getInputStream()));

      makeRequest("get Asia");
      makeRequest("get Alicja");
      makeRequest("add Adam 77777");
      makeRequest("add Adam");
      makeRequest("get Adam");
      makeRequest("add Adam 333333");
      makeRequest("replace Adam 333333");
      makeRequest("replace Alicja 202020");
      makeRequest("get Adam");
      makeRequest("add");
      makeRequest("");
      makeRequest("bye");
      in.close();
      out.close();
      sock.close();
    } catch (UnknownHostException e) {
        System.err.println("Nieznany host: "+host);
        System.exit(2);
    } catch (IOException e) {
        System.err.println("I/O err dla");
        System.exit(3);
    } catch (Exception exc) {
        exc.printStackTrace();
        System.exit(4);
    }
  }

  private boolean makeRequest(String req) throws IOException {
    System.out.println("Request: " + req);
    out.println(req);
    String resp = in.readLine();
    System.out.println(resp);
    boolean ok = resp.startsWith("0");
    if (req.startsWith("get") && ok)
       System.out.println(in.readLine());
    return ok;
  }

  public static void main(String[] args) {
    new PhoneBookClient(args[0], Integer.parseInt(args[1]));
  }
}

Po kompilacji programów, uruchamiamy serwer w sesji znakowej poleceniem i otrzymujemy potwierdzenie uruchomienia serwera:


>java PhoneBookServer1 Book.txt localhost 2300

Server started
at port: 2300
bind address: localhost/127.0.0.1


Wybraliśmy tu  port 2300 i wszyscy klienci łączący się z tym serwerem będą musieli korzystać z tego portu.
Klienta uruchamiamy w innej sesji znakowej (jako odrębny proces):


>java PhoneBookClient localhost 2300


Po uruchomieniu, klient podejmie próbę połączenia z serwerem, nasłuchująca metoda accept() serwera przyjmie połączenie, a nasz program serwera wypisze inforrmację o ustanowieniu połączenia:


Connection established



Od tego momentu zacznie się interakcja klienta i serwera. Klient będzie posyłał kolejne polecenia (zapisanie w programie przykaądowym), a serwer je realizował i odpowiadał klientowi podając wynik przetworzenia zlecenia oraz (ew.) dodatkową informację (numery telefonów). W programie klienta zapewniono wypisywanie na konsoli kolejnych kroków tej komunikacji. Wygląda to tak.

Request: get Asia
0 Ok
171717
Request: get Alicja
2 Not found
Request: add Adam 77777
0 Ok
Request: add Adam
1 Invalid request
Request: get Adam
0 Ok
77777
Request: add Adam 333333
3 Couldn't add - entry already exists
Request: replace Adam 333333
0 Ok
Request: replace Alicja 202020
4 Couldn't replace non-existing entry
Request: get Adam
0 Ok
333333
Request: add
1 Invalid request
Request:
1 Invalid request
Request: bye
0 Ok


Opisywany serwer "telefoniczny" ma jedną bardzo istotną wadę: nie jest przygotowany do tego by równolegle obsługiwać wielu klientów.

Ilustruje to fragmencik programu,  tworzący trzech klientów naszej książki telefonicznej (działających jako wątki). Każdy z klientów kilkakrotnie pyta o numer telefonu wlaściwiej dla niego osoby, nie zawłaszczając przy tym całkowicie procesora (użycie Thread.sleep).

import java.net.*;
import java.io.*;

public class PhoneBookClients extends Thread {

  private Socket sock = null;
  private PrintWriter out = null;
  private BufferedReader in = null;
  private String nameToSearch;

  public PhoneBookClients(String host, int port, String name ) {
    try {
      sock = new Socket(host, port);
      out = new PrintWriter(sock.getOutputStream(), true);
      in = new BufferedReader(
               new InputStreamReader(
                   sock.getInputStream()));

      nameToSearch = name;

    } catch (Exception exc) {
        exc.printStackTrace();
        System.exit(4);
    }
    start();
  }

  public void run() {
    try {
      for (int i=1; i <= 5; i++) {
        find(nameToSearch);
        Thread.sleep(500);
      }
      out.println("bye");
    } catch (Exception exc) {
        exc.printStackTrace();
    }
  }

  private void find(String name) throws IOException {
    out.println("get " + name);
    String resp = in.readLine();
    boolean ok = resp.startsWith("0");
    String tel = ok ? in.readLine() : " - not found";
    System.out.println(name + " - tel. " + tel);
  }

  public static void main(String[] args) {
    String host = args[0];
    int port = Integer.parseInt(args[1]);
    String[] names = { "Asia", "Adam", "Jacek" };
    for (int i=0; i<names.length; i++)
      new PhoneBookClients(host, port, names[i]);
  }
}

Zatem każdy klient jest wywłaszczany i wtedy o nuner telefonu ma szansę zapytać inny klient.
Nic takiego się jednak nie dzieje, bowiem  nasz serwer nie potrafi równolegle obsługiwać wielu klientów i musi zawsze zakończyć rozpoczetą komunikację. W efekcie klienci obsługiwani są po kolei (a nie rownolegle) co obrazuje wynik działania programu.


>java PhoneBookClients localhost 2300

Asia - tel. 171717
Asia - tel. 171717
Asia - tel. 171717
Asia - tel. 171717
Asia - tel. 171717
Adam - tel. 333333
Adam - tel. 333333
Adam - tel. 333333
Adam - tel. 333333
Adam - tel. 333333
Jacek - tel. 111111
Jacek - tel. 111111
Jacek - tel. 111111
Jacek - tel. 111111
Jacek - tel. 111111


Serwery działające w ten sposób noszą nazwę serwerów sekwencyjnych.
Ich użyteczność jest ograniczona: zwykle wymaga się bowiem by serwer obsługiwał równolegle wielu klientów.
 


5.11. Serwery wielowątkowe

Klasycznym rozwiązaniem wspomnianego przed chwilą problemu jest zastosowanie współbieżności w programowaniu serwerów.

Jednym z możliwych podejść jest:

Przyjrzyjmy się temu rozwiązaniu w praktyce na przykładzie zmodyfikowanego serwera "ksiązki telefonicznej", który teraz uczynimy wielowątkowym.

import java.net.*;
import java.io.*;
import java.util.regex.*;

public class PhoneBookServerMT1 extends Thread {

  private PhoneDirectory pd = null;
  private ServerSocket ss = null;
  private BufferedReader in = null;
  private PrintWriter out = null;

  private volatile boolean serverRunning = true; // można zakończyć wątek
                                         // metodą ustalającą
                                         // wartość tej zmiennej na false

  private String serverTID;              // identyfikator wątku

  public PhoneBookServerMT1(String serverTID, PhoneDirectory pd,
                          ServerSocket ss) {
    this.serverTID = serverTID;
    this.pd = pd;
    this.ss = ss;
    System.out.println("Server " + serverTID + " started");
    System.out.println("listening at port: " + ss.getLocalPort());
    System.out.println("bind address: " + ss.getInetAddress());

    start();    // uruchomienie wątku
  }


  public void run() {
    while (serverRunning) {
      try {
        Socket conn = ss.accept();

        System.out.println("Connection established by " + serverTID);

        serviceRequests(conn);

      } catch (Exception exc) {
          exc.printStackTrace();
      }
    }                               // zamknięcie gniazda serwera
    try { ss.close(); } catch (Exception exc) {}
  }

  // Pozoatełe metody m.in.serviceRequest jak w poprzednio pokazanej klaeie

  private void serviceRequests(Socket connection) {
     // ...
  }

  private void writeResp(int rc, String addMsg)
               throws IOException {
     // ...
  }

  public static void main(String[] args) {
    final int SERVERS_NUM = 4;   // liczba serwerów
    PhoneDirectory pd = null;
    ServerSocket ss = null;
    try {
      String phdFileName = args[0];
      String host = args[1];
      int port = Integer.parseInt(args[2]);
      pd = new PhoneDirectory(phdFileName);
      InetSocketAddress isa = new InetSocketAddress(host, port);
      ss =  new ServerSocket();
      ss.bind(isa);
    } catch(Exception exc) {
        exc.printStackTrace();
        System.exit(1);
    }

    // Start wielu wątków (serwerow) dzialających równolegle
    // na tym samym gnieżdzie serwera

    for (int i=1; i <= SERVERS_NUM; i++) {
      new PhoneBookServerMT1("serv thread " + i, pd, ss);
    }
  }

}
Tym razem nasza klasa serwera dziedziczy Thread, a jej obiekty-wątki są uruchamiane metodą start() z konstruktora. Akceptacja i obsługa połączeń (przekazywana metodzie serviceRequests) jest zapisana w metodzie run(). Zatem każdy z uruchomionych wątków-serwerów jest zdolny do równoległej z innymi wątkami-serwerami obsługi połączeń od klientów.

W metodzie main() tworzymy kilka egzemplarzy serwerów. Po uruchomieniu programu uzyskamy następujące komunikaty:

>java PhoneBookServerMT1 Book.txt localhost 2300

Server serv thread 1 started
listening at port: 2300
bind address: localhost/127.0.0.1
Server serv thread 2 started
listening at port: 2300
bind address: localhost/127.0.0.1
Server serv thread 3 started
listening at port: 2300
bind address: localhost/127.0.0.1
Server serv thread 4 started
listening at port: 2300
bind address: localhost/127.0.0.1


Cztery serwery czekają tu na połączenia klientów. Każdy z nich może je zaakceptować i obsłużyć zlecenia.

Teraz nasz poprzedni program testujący, w którym trzy wątki klienckic pytały po kilka razy o telefony (jeden - do Asi, drugi - do Adama, trzeci - do Jacka) będzie się wykonywał jak trzeba:  klienci  będą mieli praktycznie równoległy dostęp do serwera.
Pokazuje to wydruk działania programu.

>java PhoneBookClients localhost 2300
Adam - tel. 333333
Jacek - tel. 111111
Asia - tel. 171717
Jacek - tel. 111111
Adam - tel. 333333
Asia - tel. 171717
Jacek - tel. 111111
Adam - tel. 333333
Asia - tel. 171717
Jacek - tel. 111111
Adam - tel. 333333
Asia - tel. 171717
Jacek - tel. 111111
Adam - tel. 333333
Asia - tel. 171717


Widzimy tu, że żaden z wątków klienckich nie blokuje innym dostępu do serwera (w trakcie obsługi zleceń jednego klienta, obsługiwane są również - przez inny egzemplarz serwera - zlecenia innego klienta).

Inną możlwością zapewnienia współbieżnej obsługi wielu klientów jest wyodrębnienie wątków obsługi zleceń.

Ideę tą można zrealizować na wiele sposobów skladniowych. Tutaj pokazany zostanie przykład, w ktorym obsługiwać zlecenia klientów będą obiekty-wątki klasy RequestHandler (dziedziczącej Thread).
Zobaczmy najpierw co - w tej sytuacji - robi główny fragment serwera.
import java.net.*;
import java.io.*;

public class PhoneBookServerMT2  {

  private PhoneDirectory pd = null;
  private ServerSocket ss = null;

  public PhoneBookServerMT2(PhoneDirectory pd,ServerSocket ss) {
    this.pd = pd;
    this.ss = ss;
    System.out.println("Server started");
    System.out.println("listening at port: " + ss.getLocalPort());
    System.out.println("bind address: " + ss.getInetAddress());

    serviceConnections();
  }


  private void serviceConnections() {
    boolean serverRunning = true;
    while (serverRunning) {
      try {
        Socket conn = ss.accept();
        System.out.println("Connection established");

        // start wątku obsługi zleceń
        new RequestHandler(pd, conn).start();

      } catch (Exception exc) {
          exc.printStackTrace();
      }
    }
    try { ss.close(); } catch (Exception exc) {}
  }

//...

}

Klasę obsługi zleceń (RequestHandler) pokazuje poniższy wydruk.

import java.net.*;
import java.io.*;
import java.util.regex.*;

class RequestHandler extends Thread {

  private PhoneDirectory pd = null;
  private Socket connection = null;
  private BufferedReader in = null;
  private PrintWriter out = null;

  private static Pattern reqPatt = Pattern.compile(" +", 3);

  private static String msg[] = { "Ok", "Invalid request", "Not found",
                                  "Couldn't add - entry already exists",
                                  "Couldn't replace non-existing entry",
                                  };

  public RequestHandler(PhoneDirectory pd, Socket connection) {
    this.pd = pd;
    this.connection = connection;
    try {
      in = new BufferedReader(
               new InputStreamReader(
                   connection.getInputStream()));
      out = new PrintWriter(
                connection.getOutputStream(), true);
    } catch (Exception exc) {
        exc.printStackTrace();
        try { connection.close(); } catch(Exception e) {}
        return;
    }
  }

  public void run() {
    try {
      for (String line; (line = in.readLine()) != null; ) {
        String resp;
        String[] req = reqPatt.split(line, 3);
        String cmd = req[0];
        if (cmd.equals("bye")) {
          writeResp(0, null);
          break;
        }
        else if (cmd.equals("get")) {
          if (req.length != 2) writeResp(1, null);
          else {
            String phNum = (String) pd.getPhoneNumber(req[1]);
            if (phNum == null) writeResp(2, null);
            else writeResp(0, phNum);
          }
        }
        else if (cmd.equals("add"))  {
          if (req.length != 3) writeResp(1, null);
          else {
            boolean added = pd.addPhoneNumber(req[1], req[2]);
            if (added) writeResp(0, null);
            else writeResp(3, null);
          }
        }
        else if (cmd.equals("replace"))  {
          if (req.length != 3) writeResp(1, null);
          else {
            boolean replaced = pd.replacePhoneNumber(req[1], req[2]);
            if (replaced) writeResp(0, null);
            else writeResp(4, null);
          }
        }
        else writeResp(1, null);
      }
    } catch (Exception exc) {
        exc.printStackTrace();

    } finally {
        try {                  // by nie być zbyt drobiazgowym:
          connection.close();  // przy zamknięciu gniazda
          connection = null;   // strumienie są zamykane automatycznie
        } catch (Exception exc) { }
    }
  }

  private void writeResp(int rc, String addMsg)
               throws IOException {
    out.println(rc + " " + msg[rc]);
    if (addMsg != null) out.println(addMsg);
  }
}

Przy programowaniu serwerów wielowątkowych musimy zadbać o właściwą synchronizację dostępu do wspóldzielonych zasobów.
W przypadku omawianych tu serwerów "książki telefonicznej" takim zasobem jest mapa zdefiniowana w klasie PhoneDirectory.
Nie jest ona synchronizowana, ale możemy ( i powinniśmy) zastąpić ją wersją synchronizowaną:

public class PhoneDirectory {

  private Map pbMap = Collections.synchronizedMap(new HashMap());
  // ...
}
Uwaga: to rozwiązanie nie zapewnia synchronizacji dostępu do większych fragmentów mapy (np. operacje zbiorcze lub iterowanie).

Ale synchronizacja zmniejsza efektywność dzialania (przypomnijmy  programowanie wspólbieżne). Przy tym,  wszystkie rodzaje dostępu będą synchronizowane,  a przecież nie jest to konieczne przy dostępie "tylko do odczytu" (pobieraniu numerów telefonów).
Może więc moglibyśmy zrobić tak: nie synchronizowac mapy, a jedynie te metody w klasie PhoneDirectory, które modyfikują jej zawartość. Niestety, to nie jest dobry pomysł.
Przecież, jeśli nie zsynchronizujemy również metody odczytującej numer telefonu, to jej wyniki mogą być niespójne z aktualnym stanem mapy (odczyt może pokazać np. brak numeru, który był akurat zapisywany przez wywłaszczony wątek).

Rozwiązaniem tego dylematu są tzw. read-write locks, semafory, które pozwalają wątkom czytającym pobierać informacje bez narzutu synchronizacji (o ile akurat zasób nie jest zajęty przez wątek modyfikujący). Krótkie wprowadznie do tej problematyki można znaleźć w artukule Amandeep Singha "Implementig Read/Write Locks in Java" (http://www.asingh.net/technical/rwlocks.html).

W Javie 1.5 mamy w pakiecie java.util.concurrent interfejs ReadWriteLock oraz jefo implementację w klasie ReentrantReadWriteLock.


5.12. Nieblokujące kanały i  użycie selektorów przy programowaniu serwerów

Oba przedstawione w poprzednim punkcie rozwiązania serwerów wielowątkowych mają swoje wady.

Pierwsze (wiele egzemplarzy serwera) jest praktycznie nieskalowalne (uruchamiamy zadaną liczbę wątków-serwerów i nie wiadomo czy jest to liczba za duża czy za mała wobec nie znanych a priori, dynamicznie zmieniających sie, potrzeb).

W drugim rozwiązaniu - po akceptacji połączenia z klientem - uruchamiamy odrębny dla każdego klienta wątek obsługi zleceń. Przy bardzo dużej liczbie klientów, łączących się i kończących połączenia z dużą czestotliwością, powstaje bardzo dużo wątków i bardzo dużo wątków kończy działanie. Maszyna wirtualna Java może nie nadążać w tej sytuacji z odśmiecaniem pamięci.

Z problemami skali - częściowo - można radzić sobie  prowadząc dynamiczną pulę wątków-połączeń.
W każdym przypadku jednak będziemy mieli do czynienia z pelnowymiarowymi problemami synchronizacji dostępu do wspólnych zasobów, które to problemy nie tylko utrudniają programowanie, ale często obniżają efektywność działania serwera.

Pewnym rozwiązaniem i ułatwieniem jest użycie nieblokujących kanałów gniazd (zob. też punkt 9 tego wykładu oraz - ogólnie o NIO).

Dla nieblokujacych kanałów możliwe jest ich multipleksowanie czyli obsługiwanie przez jeden wątek wielu kanałów, bowiem klasy kanałów gniazd (SocketChannel i ServerSocketChannel) pochodzą od klasy SelectableChannel, która pozwala na rejestrowanie kanałów do monitorowania przez selektory.

Selektor jest obiektem monitorującym zarejestrowane kanały, reagującym na zlecenia przychodzące od klientów przez te kanały i przekazującym serwerowi sygnały o gotowości do wykonania konkretnych operacji na poszczególnych kanałach


"Sygnały gotowości" są przekazywane w kluczach selekcji (obiektach klasy SelectionKey). Obiekty te zawierają informację o kanale i o rodzaju operacji, która aktualnie może być na kanale wykonana (jest "gotowa do wykonania").

Ideowy schemat działania slektora przedstawia poniższy rysunek.
r

 
  1. Najpierw kanały są rejestrowane do monitorowania przez selektor (obiekt klasy Selector). Rejestracja kanału tworzy klucz selekcji dla tego kanału (obiekt klasy SelectionKey).
  2. W kluczach zapisywane są stany gotowości kanałów do wykonania określonych operacji.
  3. Selektor reaguje na zmiany stanów kanałów, wynikające z żądań klientów.
  4. Gdy jakiś klient łączy się z serwerem przez kanał, związany z gniazdem serwera, selektor ustala stan gotowości klucza identyfikującego ten kanał, tak by klucz wskazywał , że istnieje połączenie gotowe do zaakceptowania przez serwer.
  5. Gdy klient pisze do kanału komunikacyjnego, selektor ustala stan gotowości klucza  identyfikującego ten kanał, tak by wskazywał na  to, że w kanale są gotowe dane do odczytania   
Wkażdym momencie może być dostępnych wiele kluczy w zbiorze kluczy, opisujących gotowe do wykonania operacje.
Serwer przegląda ten (zmieniający się) zbiór kluczy i odpowiednio do rodzaju gotowych do wykonania operacji wykonuje je na identyfikowanych przez klucze kanalach. Ponieważ kanały są w trybie nieblokującym, to operacje kanałowe nie blokują wątku.

W tej sytuacji obsługa zleceń klientów może być wykonywana synchronicznie (!!!).
W przeciwieństwie do także synchronicznie dzialającego serwera sekwencyjnego uzyskujemy przy tym efekt rownoległej obsługi klientów (o ile obsługa zlecenia nie trwa długo).
Serwer sekwencyjny musiał bowiem obsłużyć jednego klienta niejako "od początku do końca" (w naszym przypadku serwera telefonicznego od połączenia, poprzez wszystkie zlecenia odczytywania czy zapisywania książki telefonicznej, aż do kończącego komunikację zlecenia "bye").
Natomiast przy nieblokujących kanałach selektory rozbijają komunikację na pojedyńcze zlecenia (np. odczytu konkretnego numeru telefonu) i każde takie zlecenie (przychodzące od różnych klientów przez różne kanały) jest obsługiwane przez serwer niezależnie od innych.

Przykładowy schemat programowaniu serwerów z użyciem selektorów
 

1

Utworzenie kanału gniazda serwera
i związanie go z konkretnym adresem (host+port)


String host = ...;
int port = ...;
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(host, port));

Uwaga: metoda socket() z klasy ServerSocketChannel zwraca skojarzone z kanałem gniazdo klasy ServerSocket. Używamy tu jednej z wersji metody bind tej klasy.


2

Ustalenie trybu nieblokującego
dla kanału serwera gniazda


serverChannel.configureBlocking(false);

Uwaga: kanały rejestrowane z selektorami (a więc przeznaczone do multipleksowania) muszą być skonfigurowane w trybie nieblokującym i nie można zmienić tego trybu na blokujący, zanim kanał nie zostanie wyrejestrowany.

.

3

Utworzenie selektora

Selector selector = Selector.open();

Statyczna metoda open() z klasy Selector zwraca refencję do obiektu-selektora, który jest tworzony przez domyślnego, systemowego kreatora selektorów. Można jednak  stworzyć własną klasę "dostawcy selektorów" dostarczającą selektory niejako "własnej konstrukcji".

4

Rejestracja kanału gniazda serwera u selektora

SelectionKey sscKey = serverChannel.register(selector,                                                                                                       SelectionKey.OP_ACCEPT);

Drugi argument wywolania określa rodzaje operacji na danym kanale, którymi jesteśmy zainteresowani przy wykorzystaniu tego selektora do wyboru operacji.
Rodzaje operacji są określane przez bity liczby całkowitej i możemy je podawać jako bitową alternatywę następujących stałych statycznych typu int z klasy SelectionKey:
OP_ACCEPT
          Akceptacja połaczenia.
OP_CONNECT
          Operacja łączenie (kanały gniazd klienckich).
OP_READ
          Operacja czytania
OP_WRITE
          Operacja pisania
Na kanale gniazda serwera interesują nas tylko gotowe do zaakceptowania połączenia - dlatego podajemy SelectionKey.OP_ACCEPT.
Uwaga: można specyfikować tylko operacje właściwe dla danego rodzaju kanału. Dopuszczalen operacje możemy uzyskać od kanału za pomocą metody validOps(). Rodzaje operacji są właściwoscią tworzonego przez rejestrację klucza. Możemy je w każdej chwili zmienić za pomocą metody interestOps(int) z klasy SelectionKey.

Metoda zwraca klucz selekcji dla zarejestrowanego kanału.

Kanał zostaje wyrejestrowany, gdy wobec klucza reprezentującego jego rejestrację (zwróconego przez metodę register) zastosujemy odwołanie cancel, lub gdy kanał zostanie zamknięty.


5

Selekcja gotowych operacji do wykonania i ich obsługa
w pętli dzialania serwera

for(;;) {                                     // Nieskończona pętla działania serwera

  // Selekcja gotowej operacji
  // To wywolanie jest blokujące
  // Czeka aż selektor powiadomi
  // o gotowości jakiejś operacji na jakimś kanale
  selector.select();
 
  // Teraz jakieś operacje są gotowe do wykonania
  // Zbiór kluczy opisuje te operacje (i kanały
)
  Set keys = selector.selectedKeys();

 
  // Przeglądamy "gotowe" klucze
  Iterator iter = keys.iterator();
  while(iter.hasNext()) {  
    // pobranie klucza
    SelectionKey key = (SelectionKey) iter.next();
    // musi być usunięty ze zbioru (nie ma autonatycznego usuwania)
    // w przeciwnym razie w kolejnym kroku pętli "obsłużony" klucz
    // dostalibyśmy do ponownej obsługi
    iter.remove();                                                  
    // Wykonanie operacji opisywanej przez klucz
    if (key.isAcceptable()) { // połaczenie klienta gotowe do akceptacji
      // Uzyskanie kanału do komunikacji z klientem
      // accept jest nieblokujące, bo już klient czeka
      SocketChannel cc = serverChannel.accept();
      // Kanał nieblokujący, bo będzie rejestrowany u selektora
      cc.configureBlocking(false);
      // rejestrujemy kanał komunikacji z klientem
      // do monitorowania przez ten sam selektor
      cc.register(selector, SelectionKey.OP_READ | OP_WRITE);
      continue;
    }

    if (key.isReadable()) {  // któryś z kanałów gotowy do czytania
      // Uzyskanie kanału na którym czekają dane do odczytania
      SocketChannel cc = (SocketChannel) key.channel();
      // obsługa zleceń klienta
      // ...
      continue;
    }
    if (key.isWriteable()) {  // któryś z kanałów gotowy do pisania
      // Uzyskanie kanału
      SocketChannel cc = (SocketChannel) key.channel();
      // pisanie do kanału
      // ...

      continue;
    }
  }
}


Pokazaną pętlę działania serwera można bliżej wyjasnić w następujący sposób..
Metoda select() wstrzymuje dzialanie pętli dopóki selektor moniturający zarejestrowane u niego kanały nie określi gotowych do wykonania operacji. Gotowe operacje są reprezentowane przez podzbiór wszystkich kluczy (tzw. selected-keys set) i podzbiór ten przy każdorazowym wywołaniu select() jest przez selektor modyfikowany zgodnie z aktualnym stanem kanałów. W każdym kroku pętli przeglądany jest aktualny zbiór "selected-keys" (metoda selectedKeys()) i dla każdego klucza z tego zbioru wykonywane są na kanale identyfikowanym przez ten klucz (kanał uzyskujemy od klucza za pomocą metody channel()) określone przez klucz operacje (rodzaj operacji uzyskujemy wołając metody isAccepteble(), isReadable(), isWriteable()).
Sczególną operacją jest akceptacja połączenia nowego klienta. Jak wspomniano na schemacie wywolanie metody accept() tym razem nie jest blokujące (bo selektor już określił, że na kanale gniazda serwera czeka kolejny klient). Wywołując metodę accept() natychmiast więc tworzymy kanał komunikacji z klientem (SocketChannel). Ten kanał rejestrujemy u tego samego selektora który monitoruje kanał gniazda serwera i inne kanały klienckie (zwrócmy uwagę - mamy tu jeden selektor). W ten sposób ta sama pętla, w sposób jednolity, obsługuje wszystkie zlecenia.

Tę właściwość pokazanego podejścia ilustruje poniższy rysunek.
r

Mamy już teraz wystarczające wiadomości, aby napisać omawiany w poprzednich punktach serwer "książki telefonicznej" w konwencji nieblokującego we/wy i selektorów.
Jego kod może wyglądać tak.
import java.net.*;
import java.io.*;
import java.util.*;
import java.util.regex.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

public class PhoneBookServerNB {

  private PhoneDirectory pd = null;
  private ServerSocketChannel ssc = null;
  private Selector selector = null;

  public PhoneBookServerNB(PhoneDirectory pd, String host, int port ) {
    this.pd = pd;
    try {
      // Utworzenie kanału dla gniazda serwera
      ssc = ServerSocketChannel.open();

      // Tryb nieblokujący
      ssc.configureBlocking(false);

      // Ustalenie adresu (host+port) gniazda kanału
      ssc.socket().bind(new InetSocketAddress(host, port));

      // Utworzenie selektora
      selector = Selector.open();

      // Zarejestrowanie kanału do obsługi przez selektor
      // dla tego kanału interesuje nas tylko nawiązywanie połączeń
      // tryb - OP_ACCEPT
      ssc.register(selector,SelectionKey.OP_ACCEPT);

    } catch(Exception exc) {
        exc.printStackTrace();
        System.exit(1);
    }
    System.out.println("Server started and ready for handling requests");
    serviceConnections();
  }

  private void serviceConnections() {
    boolean serverIsRunning = true;

    while(serverIsRunning) {
      try {
        // Wywołanie blokujące
        // czeka na zajście  zdarzenia związanego z kanałami
        // zarejestrowanymi do obslugi przez selektor
        selector.select();

        // Coś się wydarzyło na kanałach
        // Zbiór kluczy opisuje zdarzenia
        Set keys = selector.selectedKeys();

        Iterator iter = keys.iterator();
        while(iter.hasNext()) {   // dla każdego klucza

          SelectionKey key = (SelectionKey) iter.next(); // pobranie klucza
          iter.remove();                                 // usuwamy, bo już
                                                         // go zaraz obsłużymy

          if (key.isAcceptable()) { // jakiś klient chce się połączyć

            // Uzyskanie kanału do komunikacji z klientem
            // accept jest nieblokujące, bo już klient się zgłosił
            SocketChannel cc = ssc.accept();

            // Komunikacja z klientem - nieblokujące we/wy
            cc.configureBlocking(false);

            // rejestrujemy kanał komunikacji z klientem
            // do obsługi przez selektor
            // - typ zdarzenia - dane gotowe do czytania przez serwer
            cc.register(selector, SelectionKey.OP_READ);
            continue;
          }

          if (key.isReadable()) {  // któryś z kanałów gotowy do czytania
            // Uzyskanie kanału na którym czekają dane do odczytania
            SocketChannel cc = (SocketChannel) key.channel();
            serviceRequest(cc);    // obsluga zlecenia
            continue;
          }
        }
      } catch(Exception exc) {
          exc.printStackTrace();
          continue;
      }
    }
  }

  private static Pattern reqPatt = Pattern.compile(" +", 3);

  private static String msg[] = { "Ok", "Invalid request", "Not found",
                                  "Couldn't add - entry already exists",
                                  "Couldn't replace non-existing entry",
                                  };

  // Strona kodowa do kodowania/dekodowania buforów
  private static Charset charset  = Charset.forName("ISO-8859-2");
  private static final int BSIZE = 1024;

  // Bufor bajtowy - do niego są wczytywane dane z kanału
  private ByteBuffer bbuf = ByteBuffer.allocate(BSIZE);

  // Tu będzie zlecenie do pezetworzenia
  private StringBuffer reqString = new StringBuffer();

  // Obsługa (JEDNEGO) zlecania
  private void serviceRequest(SocketChannel sc) {
    if (!sc.isOpen()) return; // jeżeli kanał zamknięty - nie ma nic do roboty

    // Odczytanie zlecenia
    reqString.setLength(0);
    bbuf.clear();
    try {
      readLoop:                    // Czytanie jest nieblokujące
      while (true) {               // kontynujemy je dopóki
        int n = sc.read(bbuf);     // nie natrafimy na koniec wiersza
        if (n > 0) {
          bbuf.flip();
          CharBuffer cbuf = charset.decode(bbuf);
          while(cbuf.hasRemaining()) {
            char c = cbuf.get();
            if (c == '\r' || c == '\n') break readLoop;
            reqString.append(c);
          }
        }
      }
      // Analiza zlecenia (jak poprzednio) i wołanie nowej metody
      // writeResp zapisującej odpowiedź do kanału
      String[] req = reqPatt.split(reqString, 3);
      String cmd = req[0];

      if (cmd.equals("bye")) {             // koniec komunikacji
          writeResp(sc, 0, null);          // - zamknięcie kanału
          sc.close();                      // i gniazda
          sc.socket().close();
      }
      else if (cmd.equals("get")) {
        if (req.length != 2) writeResp(sc, 1, null);
        else {
          String phNum = (String) pd.getPhoneNumber(req[1]);
          if (phNum == null) writeResp(sc, 2, null);
          else writeResp(sc, 0, phNum);
        }
      }
      else if (cmd.equals("add"))  {
        if (req.length != 3) writeResp(sc, 1, null);
        else {
          boolean added = pd.addPhoneNumber(req[1], req[2]);
          if (added) writeResp(sc, 0, null);
          else writeResp(sc, 3, null);
        }
      }
      else if (cmd.equals("replace"))  {
        if (req.length != 3) writeResp(sc, 1, null);
        else {
          boolean replaced = pd.replacePhoneNumber(req[1], req[2]);
          if (replaced) writeResp(sc, 0, null);
          else writeResp(sc, 4, null);
        }
      }
      else writeResp(sc, 1, null);             // nieznane zlecenie

    } catch (Exception exc) {                  // przerwane polączenie?
        exc.printStackTrace();
        try { sc.close();
              sc.socket().close();
        } catch (Exception e) {}
    }
  }

  private StringBuffer remsg = new StringBuffer(); // Odpowiedź

  private void writeResp(SocketChannel sc, int rc, String addMsg)
                         throws IOException {
    remsg.setLength(0);
    remsg.append(rc);
    remsg.append(' ');
    remsg.append(msg[rc]);
    remsg.append('\n');
    if (addMsg != null) {
      remsg.append(addMsg);
      remsg.append('\n');
    }
    ByteBuffer buf = charset.encode(CharBuffer.wrap(remsg));
    sc.write(buf);
  }

  public static void main(String[] args) {
    try {
      String phdFileName = args[0];
      String host = args[1];
      int port = Integer.parseInt(args[2]);

      PhoneDirectory pd = new PhoneDirectory(phdFileName);
      new PhoneBookServerNB(pd, host, port);
    } catch(Exception exc) {
        exc.printStackTrace();
        System.exit(1);
    }
  }

}
W programie tym warto zwrócić uwagę na zastosowanie buforów  i na sposób radzenia sobie z niepewnością co do liczby odczytanych w jednej operacji read() bajtów przy nieblokującym wejściu-wyjściu. Na uwagę zasługuje również staranie by nie tworzyć zbyt wielkiej liczby obiektów. Dlatego bufor bajtowy, wykorzystywany do czytania z kanału utworzono i alokowano jednorazowo na poziomie pól klasy. Takie podejście jest uzasadnione, gdy nie ma wielu wątków, które mogłyby dzielić ten zasób.
No, właśnie, nasza nowy serwer w ogóle nie wykorzystuje wątków i nie mamy rownież problemów z synchronizacją odwołań do klasy PhoneDirectory (bowiem wszystkie operacje na zawartej w niej mapie są wykonywane synchronicznie, kolejno).
A jego działanie jest tak samo efektywne przy równoległej obsłudze wielu klientów, jak działanie serwerów wielowątkowych (ale: z mutexami, w środowiskach jednoprocesorowych).

Trzeba jednak wyraźnie podkreślić, że w praktycznym programowaniu "poważnych" serwerów stosowane są zarówno selektory jak i wspólbieżność (pule wątków). Tylko w ten sposób można uzyskać wymaganą efektywność działania, a zastosowanie nieblokującego wejścia-wyjścia i selektorów pomaga przy tym rozwiązywać problemy skalowalności i syncjronizacji.
W takich zastosowanich używane są jeszcze inne środki klas SelectableChannel,  Selector i SelectionKey. Na koniec więc krótko o nich:


5.13. Klasy Javy wspomagające programowanie aplikacji klienckich

Tworząc programy klienckie nie zawsze musimy sięgać do programownaia na poziomie gniazd.
W Javie istnieją zestawy klas, które ukrywają przed programistą szczegóły komunikacji poprzez gniazda. O jednym takim zestawie już wspominano -  jest nim bogato oprzyrządowany pakiet Java Mail API.
Inne to:
Poniższy program łączy sie z  serwerem FTP, w kolejnych dialogach wejściowych pyta użytkownika o pliki do pobrania i zapisuje je na lokalnym dysku.
import java.io.*;
import sun.net.ftp.FtpClient;    // podstawowa klasa pakietu sun.net.ftp
import java.text.*;

import javax.swing.*;

public class FtpFileRetriever {

  public static void main(String args[]) {
    FtpClient client = null;
    String userName = null;
    try {
     String host = args[0];
     userName = args[1];
     String password = args[2];
     client = new FtpClient(host);
     client.login(userName, password);  // logowanie
     client.binary();                   // tryb transmisji - binarny
    } catch(Exception exc) {
        exc.printStackTrace();
        System.exit(1);
    }

    String dir = null;
    String file = "";
    String dialogMsg = "Podaj nazwę pliku do pobrania:";
    while((file = JOptionPane.showInputDialog(dialogMsg)) != null) {
      try {
        String fileName = file;
        int lastSlash = file.lastIndexOf('/');

        // jeżeli w podanej nazwie pliku występuje ścieżka katalogowa
        // zmieniamy biezący katalog, jesli nie - pobierany jest plik
        // z aktualnego bieżącego katalogu
        if (lastSlash != -1) {
           fileName = file.substring(lastSlash+1);
           dir = file.substring(0,lastSlash);
           client.cd(dir);                     // zmiana biezącego katalogu
        }
        BufferedInputStream in = new BufferedInputStream(
                                  client.get(fileName)  // zwraca strumień
                                 );                     // do czytania pliku

        BufferedOutputStream out = new BufferedOutputStream(
                                     new FileOutputStream(fileName)
                                   );
        System.out.println("Pobieram: " + fileName);

        byte[] data = new byte[1024];
        int bytesRead, totalBytes = 0;
        long start = System.currentTimeMillis();
        while( (bytesRead = in.read(data)) > 0) {
          out.write(data, 0, bytesRead);
          totalBytes += bytesRead;
          System.out.print("\r... " + totalBytes);
        }
        out.close();
        double sec = (System.currentTimeMillis() - start)/1000.0;
        double kb =   totalBytes/1024.0;
        double kbps = kb/sec;
        DecimalFormat dn = new DecimalFormat("#.#");
        System.out.println("\nPobieranie ukonczone");
        System.out.println("Pobrano " + dn.format(kb) + "KB" +
                            " - " + dn.format(kbps) + " KB/sek.");
      } catch (IOException exc) {
         exc.printStackTrace();
      }
      dialogMsg = "Ostatnio pobrany plik: " + file +'\n' +
                  "Podaj nazwę pliku do pobrania:";
    }
    // Zamkniecie połaczenia
    try {client.closeServer(); } catch(Exception exc) {}
    System.exit(1);
  }
}
Działanie tego programiku ilustruje rysunek.

r


Jak wspomniano, pakiet sun.net.ftp nie należy do standardu Javy.
W pakiecie java.net znajdziemy natomiast standardowe klasy przeznaczone do pracy z zasobami sieciowymi, m.in.  klasy URL i URI oraz  URLConnection i HttpURLConnection.

Obiekty klasy URL (Uniform Resource Locator) reprezentują - dobrze chyba znane wszystkim - adresy zasobów sieciowych (jednolite lokalizatory zasobów).
Składniowo lokalizator taki składa się z kilku części, m.in.:
Przykład:
http://www.java.sun.con/j2ee/index.html

Konkretne znaczenie treści zasobu zależy od protokołu i hosta (np. http://www.java.sun.com/j2ee/index.html może oznaczac konkretny dokument lub treść dokumentu generowaną dynamicznie). Zasoby sieciowe - to nie tylko pliki czy katalogi. mogą oznaczać również inne rodzaje informacji (np. wynik zapytania kierowanego do bazy danych).

O lokalizatorach zasobów sieciowych bedziemy czasem mówić krótko: url.

URI (Uniform Resource Identifier) jest uogólnieniem pojęcia URL - opisuje dowolne (fizyczne i abstrakcyjne) zasoby w sieci, które mogą być w jakikolwiek sposób zidentyfikowane.
Lokalizatory (URLe) są podzbiorem identyfikatorów (URI).
 O ile lokalizator (url) opisuje zasób w  taki sposób, że jednoznaczny jest mechanizm dostępu do niego (np. położenie pliku w sieci i protokół), to identyfikator (URI) może oznaczać zasób o nieznanej lub niejednoznacznej konkretnej lokalizacji i mechanizmie dostępu, charakteryzujący sie jedynie unikalną, jednoznaczną nazwą (Uniform Resource Name - URN).

Więcej informacji na ten temat można znaleźć w dokumencie  RFC 2396: Uniform Resource Identifiers (URI): Generic Syntax.

URI - identyfikacja, parsowanie
URL - fizyczny dostęp

Klasa URI w Javie nie dostarcza metod dostępu do zasobów (znajdziemy tu głównie metody służące do rozbioru identyfikatorów na części składowe, normalizacji, relatywizacji i rozwikływania idnetyfikatorów - o rozwikływaniu zob. w następnym przykładowym programie).

Natomiast poprzez obiekty klasy URL możemy uzyskać dostęp do zasobów sieciowych i to co najmniej na dwa sposoby.

Najprostszy sposób (zarazem jednak najmniej elastyczny) uzyskiwania dostępu do zasobow sieciowych polega na wykorzystaniu strumienia wejściowego, związanego z lokalizatorem zasobu (URLem - obiektem klasy URL).

Kolejne kroki są następujące:
Przykładowy program wykorzystujący ten sposób wczytuje podaną stronę WWW i dokonuje jej analizy składniowej w celu wyróżnia znajdujących się na stronie plików graficznych, po czym ładuje te pliki i pokazuje w "przeglądarce obrazków", zbudowanej jako własna klasa ImageViewer.

import java.net.*;
import java.io.*;
import java.util.*;
import java.util.regex.*;

public class SimpleGet {

  public SimpleGet(String urlString) {
    try {
      URL url = new URL(urlString);
      BufferedReader in = new BufferedReader(
                          new InputStreamReader(
                               url.openStream() // zwraca InputStream
                                                // związany z URLem
                          ));
      // Częsci URL-a
      String protocol = url.getProtocol();
      String host = url.getHost();
      String file = url.getFile();
      System.out.println("Protocol: " + protocol);
      System.out.println("Host: " + host);
      System.out.println("File: " + file);

      // Zapiszemy dokument za pomocą strumienia StringWriter
      StringWriter sw = new StringWriter(10240);
      String line;
      while ((line = in.readLine()) != null) {
         sw.write(line);
      }
      in.close();

      // Matcher do wyodrebniania referencji do plików graficznych na stronie
      Matcher matcher = Pattern.compile("img src=\"(.+?)\"",
                            Pattern.CASE_INSENSITIVE).matcher(sw.toString());

      // Obrazy będziemy przechosywać na liście
      java.util.List imgList = new ArrayList();

      while (matcher.find()) {
        String imgRef = matcher.group(1); // wyodrębniamy zapisany na stronie
                                          // url obrazka
        // zwykle będzie relatywny
        // za pomocą konstruktora URL(kontekst, referencja)
        // uzyskamy właściwy lokalizator wskazujący zasób
        URL imgUrl = new URL(url, imgRef);
        System.out.println(imgRef + " ==> " + imgUrl);
        // utworzenie obiektu Image i dodanie do listy
        imgList.add(Toolkit.getDefaultToolkit().createImage(imgUrl));
      }
      System.out.println("Preparation complete... wait for viewer init");

      // Listę obrazów przekażemy utworzonemu obiektowi przeglądarki obrazków
      new ImageViewer(imgList);

    } catch (MalformedURLException exc) {
        exc.printStackTrace();
        System.exit(1);
    } catch (Exception exc) {
        exc.printStackTrace();
    }

  }

  public static void main(String[] args) {
    String url = JOptionPane.showInputDialog("Adres");
    new SimpleGet(url);
  }
}
W programie wczytujemy stronę, której adres podano w dialogu i wyodrębniamy na niej wszystkie zapisane w znacznikach <img src ... > odniesienia do obrazków.
Takie odniesienie może być relatywne (w stosunku do lokalizacji wczytanej strony). Dlatego urle obrazków tworzymy za pomoca konstruktora URL(URL context, String spec). Konstruktor ten potrafi:
Przykładowo, jezeli nasz program uruchomimy podając jako adres:

http://bugbog.com/beaches/beach_pictures_thailand/beach_pictures_ko%20samet_4.html;

to na konsoli uzyskamy najpierw wydruk składników urla (ilustracja metod get... pobierających różne cześci lokalizatora);

Protocol: http
Host: bugbog.com
File: /beaches/beach_pictures_thailand/beach_pictures_ko%20samet_4.html


a  z części programu wyodrębniającj adresy obrazków uzyskamy wydruk tych adresów  w literalnie zapisanej na stronie postaci, z następującym po znakach ==> adresem rozwikłanym i znormalizowanym przez konstruktor URL(URL context, String spec):


../../images/beaches/thailand_beaches_west/thailand_beaches_ko_samet_9.jpg ==> http://bugbog.com/images/beaches/thailand_beaches_west/thailand_beaches_ko_samet_9.jpg

../../images/bugbog/bugbog.gif ==> http://bugbog.com/images/bugbog/bugbog.gif

../../images/beaches/thailand_beaches_west/thailand_beaches_ko_samet_7.jpg ==> http://bugbog.com/images/beaches/thailand_beaches_west/thailand_beaches_ko_samet_7.jpg

../../images/beaches/thailand_beaches_west/thailand_beaches_ko_samet_8.jpg ==> http://bugbog.com/images/beaches/thailand_beaches_west/thailand_beaches_ko_samet_8.jpg

../../images/bugbog/line.gif ==> http://bugbog.com/images/bugbog/line.gif


Program z uzyskanych urli obrazków za pomocą metody createImage z klasy Toolkit tworzy obiekty klasy Image i dodaje je do listy. Ta lista jesr przekazywana przeglądarce obrazków - ImageViewer.

Klasy ImageViewer i wykorzystaywana przez nią ImagePanel (stanowiąca obszar prezentacji obrazu z pliku) przedstawiono - dla porządku -  na poniższym wydruku (warto na nie spojrzeć choćby dla przypomnienia wiadomości o kolekcjach i programowaniu GUI):
import java.util.*;
import java.util.regex.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

class ImagePanel extends JPanel {

  private Image img;
  private boolean correct;
  private Dimension defd = new Dimension(200,200);
  private String msg;

  public ImagePanel(String msg) {
    this.msg = msg;
    setPreferredSize(defd);
    setBorder(BorderFactory.createLineBorder(Color.blue, 2));
  }

  public void setImage(Image img) {
    this.img = img;
    int w = img.getWidth(this);       // szerokość obrazka
    int h = img. getHeight(this);     // wysokość obrazka
    if (w != -1 && w != 0 && h != -1 && h != 0) {
      correct = true;
      setPreferredSize(new Dimension(w, h));
    }
    else setPreferredSize(defd);
    repaint();
  }

  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    if (img != null && correct)
      g.drawImage(img, 0, 0, getWidth(), getHeight(), this);
    else
      g.drawString(msg, 10, 10);
  }

}

class ImageViewer extends JFrame implements ActionListener {

  private ListIterator lit;
  private ImagePanel imagePanel = new ImagePanel("Loading images ...");
  private JButton left = new JButton(new ImageIcon("arrow1.gif"));
  private  JButton right = new JButton(new ImageIcon("arrow2.gif"));
  private JButton os = new JButton("<html><b>Oryginalna<br>skala</b></html>");

  public ImageViewer(java.util.List imgList) {

    lit = imgList.listIterator();

    getContentPane().add(imagePanel);
    JPanel p = new JPanel(new FlowLayout(FlowLayout.CENTER));
    left.addActionListener(this);
    right.addActionListener(this);
    os.addActionListener(this);
    p.add(left); p.add(right); p.add(os);
    getContentPane().add(p, "South");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    pack();
    show();


    MediaTracker mt = new MediaTracker(this);
    int i=0;
    while(lit.hasNext()) mt.addImage((Image) lit.next(), ++i);
    try {
      mt.waitForAll();
    } catch (InterruptedException exc) { }
    System.out.println("Image viewer ready");

    SwingUtilities.invokeLater(new Runnable() {
      public void run() { getNext(); }
    });
  }

  public void actionPerformed(ActionEvent e) {
    Object src = e.getSource();
    if (src == right) getNext();
    else if (src == left) getPrevious();
    else pack();
  }

  private boolean lastOperWasNext = true;

  private void getNext() {
    if (!lastOperWasNext) lit.next();
    if (!lit.hasNext()) while (lit.hasPrevious()) lit.previous();
    imagePanel.setImage( (Image) lit.next() );
    lastOperWasNext = true;
  }

  private void getPrevious() {
    if (lastOperWasNext) lit.previous();
    if (!lit.hasPrevious()) while (lit.hasNext()) lit.next();
    imagePanel.setImage( (Image) lit.previous() );
    lastOperWasNext = false;
  }

}

Wynik dzialania programu ilustruje rysunek (dwa przyciski - strzałki, w programie oznaczane jako left i right) służą do przechodzenia do kolejnych obrazków; warto przy okazji zwrócić uwage na zastosowanie w tym celu iteratora listowego).

r


Przedstawiony bardzo prosty sposób pobierania zasobów (za pomocą strumienia uzyskanego do URLa) ma jednak co najmniej dwie wady:
Klasa URLConnection i pochodna od niej HttpURLConnection usuwają te wady, a co więcej pozwalają nie tylko na odczytywanie treści zasobów, ale również na posyłanie do serwera innych zleceń HTTP.

No, właśnie - jak w każdym protokole - komunikacja pomiędzy klientem i serwerem HTTP polega na wymianie zleceń (od klienta) i odpowiedzi (od serwera).  Rodzaje zleceń HTTP podaje poniższa tabela.


Zlecenie
(request method)
Znaczenie
GET uzyskanie zasobu identyfikowanego przez URL (odczytanie treści zasobu)
HEAD Uzyskanie nagłówków
POST Wysłanie danych o nielimiotowanej długości
PUT Zapisanie zasobu
DELETE Usunięcie zasobu
OPTIONS Zwraca metody HTTP podtrzymywane przez serwer
TRACEZwraca nagłówki wysłane z leceniem TRACE (do celów testowania_

Każde zlecenie składa się m.in. z W odpowiedzi serwera również występują nagłówki, opisujące "parametry" treści odpowiedzi ( np. wielkośc, datę modyfikacji, typ treści)

W odróżnieniu od komunikacji z serwerem HTTP za pomocą gniazd, gdzie do/z strumieni gniazda zapisuje się/czyta całą wymaganą przez protokół informację   przy zastosowaniu klas URL i URLConnection nagłówki nie mogą być zapisywane do strumieni i nie mogą być z tych strumieni odczytane.
Analizę nagłówków odpowiedzi serwera umożliwją metody klasy URLConnection. Klasa ta (wraz z pochodną od niej HttpURLConnection)  pozwala również na wysyłanie do serwera innych od najprostszego GET zleceń HTTP  i ustalanie nagłówków tych zleceń.

Komunikację z serwerem HTTP za pomocą klasy URLConnection można przedstawić w nastepujących krokach.

1
Tworzymy obiekt klasy URLConnection, wiążąc go z podanym URLem:
URL url = new URL(...);
URLConenction conn = URLConnection.open(url);

2
Opcjonalnie:
  • Za pomocą metody setRequestProperty(String key, String value) ustalamy zawartość nagłówków zlecenia (np. nagłówek "accept-language")
  • Za pomocą wywolania metody conn.doOutput() ustalamy, że zlecenie będzie zleceniem POST, służącym do przesyłania do serwera  dowolnych danych, najczęściej danych formularza HTTP, zapisanych jako parametry zlecenia.
Domyślnie zlecnie jest typu GET (a do ustalenia tego trybu służy metoda doInput).
3
Nawiązujemy połączenie:
conn.connect()
4
Możemy teraz - jeśli tego potrzebujemy - odczytać nagłówki przesłane przez serwer za pomocą metod getHeader(...), getHeaders(...) oraz metod przeznaczonych do odczytywania często używanych nagłówków (np. getContentType()).
5
Jeżeli zlecenie jest typu POST (ustalone metodą doOutput()) to za pomoca metody conn.getOutputStream() uzyskujemy dostęp do strumienia wyjściowego połączenia i zapisujemy do niego treśc zlecenia (np. parametry formularza)
6
Uzyskujemy strumień wejściowy do czytania odpowiedzi serwera (conn.getInputStream()) i za jego pomoca czytamy tę odpowiedź


Spójrzmy najpierw na bardzo prosty program, odczytujący z serwera dokument HTML za pomocą obiektu klasy URLConnection (domyślnie posyłane zlecenie jest typu GET).
Przy okazji zapoznamy sie ze sposobem uzyskiwania całego zestawu nagłówków odpowiedzi (w mapie zawierajacej jako klucze nazwy nagłówków a jako wartości - ich zawartość). Dla często używanych nagłówków (np. contnet-type, content-length) istnieją dodatkowe odrębne metody pobierania (np. getContentType(), getVontentLength()).

import java.net.*;
import java.io.*;
import java.util.*;
import javax.swing.*;

public class Connection1 {

  public Connection1(String urlString) {
    try {
      URL url = new URL(urlString);
      URLConnection conn = url.openConnection();
      conn.setUseCaches(false);
      conn.connect();

      Map map = conn.getHeaderFields();
      for (Iterator it = map.keySet().iterator(); it.hasNext(); ) {
        String header = (String) it.next();
        System.out.println(header + ":" + map.get(header));
      }

      BufferedReader in = new BufferedReader(
                           new InputStreamReader(
                            conn.getInputStream() ) );

      for ( String line; (line = in.readLine()) != null; ) {
        System.out.println(line);
      }
      in.close();
    } catch (MalformedURLException exc) {
        exc.printStackTrace();
        System.exit(1);
    } catch (FileNotFoundException exc) {
        System.out.println("Podanego zasobu nie ma na serwerze");
        System.exit(2);
    } catch (Exception exc) {
        exc.printStackTrace();
    }
  }

  public static void main(String[] args) {
    String url = JOptionPane.showInputDialog("Adres");
    new Connection1(url);
  }
}

Po wprowaddzeniu jakiegoś (prawidłowego) adresu dokumnetu na początku wydruku uzyskamy nagłówki np.

null:[HTTP/1.1 200 OK]
Content-Language:[pl]
Date:[Mon, 17 Nov 2003 02:29:38 GMT]
Accept-Ranges:[bytes]
Server:[Apache]
Content-Type:[text/html; charset=iso-8859-2]
Transfer-Encoding:[chunked]
...
// dalej treśc dokumentu html


Zwróćmy uwagę, interpretacja nagłówków może sie nam przydać do prawidłowego kodowania treści dokumentu (nagłówek Content-Type będzie często, choć nie zawsze, podawał stronę kodową).
Zauważmy też, że w pierwszesj linii wydruku podany jest kod odpowiedzi serwera (200) i związany z nim komunikat (OK - zlecenie było poprawne i uzyskaliśmy odpowiedź).
Gdyby np. podać nieistniejący plik, to uzyskalibyśmy nagłówki np.:

null:[HTTP/1.1 404 Not Found]
Connection:[Keep-Alive]
Expires:[Tue, 01 Jan 1980 12:00:00 GMT]
Date:[Mon, 17 Nov 2003 02:39:46 GMT]
Keep-Alive:[timeout=15, max=100]
Pragma:[no-cache]
Server:[Apache]
Content-Type:[text/html]
Last-Modified:[Thu, 01 Jan 1970 00:00:00 GMT]
Transfer-Encoding:[chunked]
Cache-control:[no-cache, must-revalidate]


ale przy czytaniu treści odpowiedzi wystąpiłby wyjątek FileNotFoundException.

Na marginesie nalezy dodać tu, że wygodniejszym i  bardziej szczegółowym sposobem identyfikacji wyniku zlecenia jest użycie metod getResponseCode() i getResponseMessage() z klasy HttpURLConnection (obiekt tej klasy dla URLi z protokołem HTTP jest zwracany z metody URLConnection.openConnectuin(), musimy tylko dokonać zawężajacej konwesrji referencyjnej z formalnego typu URLConncetion do  HttpURLConnection).

Drugi przykladowy programik kliencki, używający klasy URLConenction jest nieco ciekawszy, pokazuje bowiem jak zapisywać zlecenie typu POST. W tym celu ustalamy właściwość połączenia jako "do pisania" - metodą doOutput(). Możemy także ustalić nagłówki naszego zlecenia. Ustalimy tu język, dzięki czemu aplikacje dzialajace po stronie serwera bedą mogły wykorzystać informacje o lokalizacji (np. przesyłać informacje po polsku).
Nastepnie piszemy treść zlecenia do strumienia wyjściowego. Może to być dowolna informacja, którą umieją przetworzyć aplikacje serwerowe. Najcześciej będziemy tu zapisywać parametry formularzy HTTP w formie: nazwa_parametru=wartośc_ parametru. Ponieważ przy przekazywaniu parametrów przyjęto konwencje, że muszą one być specjalnie kodowane (tzw. url-kodowanie), to parametry będziemy kodować za pomocą klasy URLEncoder (o sczegółach url-kodowania zobacz w dokumentacji tej klasy).
Po przekazaniu parametrów odczytujemy odpowiedź serwera. Umówimy się, że   aplikacja obsługująca po stronie serwera zlecenia naszego przykładowego klienta  zwraca informacje tekstową o rodzaju zlecenia, lokalizacji, kodowaniu,  jego nagłówkach oraz przekazanych wartościach parametrów.
Klient wypisze te informacje na konsoli.

import java.io.*;
import java.net.*;

public class Client1 {

    public static void main(String[] args) throws Exception {

     // Z tego pliku przeczytamy zlecnie do posłania
     // bedą to pary "nazwa_parametru=wartość"
      BufferedReader br = new BufferedReader(
                              new FileReader(args[0])
                            );

      URL url = new URL("http://....");                // adres aplikacji
                                                       // po stronie serwera
                                                       // obsługującej klienta
      URLConnection connection = url.openConnection();

      // Ustalenie nagłówka "accept-language" zlecenia
      // Po stronie serwera da to informację o lokalizacji
      // Tutaj jej nie używamy, ale może sie przydac w innych okolicznościach  
      connection.setRequestProperty("accept-language", "pl");

      // Będziemy pisać treść zlecenia
      connection.setDoOutput(true);

      String charset = "UTF-8";          // kodowanie treści
      PrintWriter out = new PrintWriter(
                             new OutputStreamWriter(
                              connection.getOutputStream(), charset)
                           );
      // Czytamy z pliku i zapisujemy jako treść zlecenia
      // pary: nazwa_parametru=wartośc parametru
      String line;
      while ((line = br.readLine()) != null)
       out.println(URLEncoder.encode(line, "UTF-8"));
      br.close();
      out.close();

      System.out.println("Zapis dokonany");

      // Odczytujemy odpowiedź serwera
      BufferedReader in = new BufferedReader(
                            new InputStreamReader(
                              connection.getInputStream(), charset));

      while ((line = in.readLine()) != null)
        System.out.println(line);

      in.close();
   }


}
Jesli w pliku podanym jako argument znajdują się zapisy:
parametr1=Pies
parametr2=Kot
parametr3=Wróbel ćwierkający > 20

to program wyprowadzi na konsoli:


Zapis dokonany
Klient wysłał zlecenie
Metoda: POST                    
Locale: pl
Character encoding: UTF-8

Nagłówki:
accept-language: pl
user-agent: Java/1.4.2
host: localhost:8080
accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
connection: keep-alive
content-type: application/x-www-form-urlencoded
content-length: 90

Parametry:
parametr1=Pies
parametr2=Kot
parametr3=Wróbel ćwierkający > 20


Warto zauważyć, że do url-kodowania przyjęliśmy bezpieczną stronę kodową UTF-8. W związku z tym i treści kodujemy w tej stronie kodowej.

Oczywiście, tajemnicze jest nieco skąd akurat bierze sie taka odpowiedź serwera. Napisano, że tworzy ją aplikacja dzialająca po stronie serwera. Pora więc na zapoznanie się z takimi aplikacjami i temu poświęcony będzie następny wykład. Jednocześnie można tam znaleźć dodatkowe infomacje o protokole HTTP, zleceniach i ich obsłudze, a także kwestiach związanych z formularzami HTTP  i ich parametrami.