May 19, 2024, Sunday, 139

SK Zadanie 3 remote

From Łukasz Graczykowski

Jump to: navigation, search

Contents

Zadania

Zadanie 1

Stworzyć serwer działający na porcie 8000, który wyśle słowo HELLO! do każdego procesu, który się do niego podłączy (należy napisać skrypt hello.sh oraz użyć polecenia ncat). Należy przetestować jego działanie przy użyciu programu telnet.

Zadanie 2

Uruchomić program w środowisku eclipse. Następnie, należy połączyć się z serwerem cern.ch na porcie 80. Na ekran należy wypisać tylko te linijki które zawierają słowo body.

Zadanie 3

Uruchomić program w środowisku eclipse. Przerobić program w taki sposób, by do tekstu wysyłanego przez użytkownika dopisywał znak !. Należy przetestować wprowadzone zmiany za pomocą narzędzia telnet. Proszę zasymulować połączenie się do powyższego programu dwóch różnych klientów (należy odpalić program telnet dwukrotnie, w różnych terminalach). Co się wtedy dzieje?

Zadanie 4

Uruchomić powyższy program w środowisku eclipse. Przy użyciu narzędzia telnet przetestować możliwość łączenia się wielu klientów do powyższego programu.

Zadanie 5

Proszę w parach napisać prosty program typu “chat” do porozumiewania się pomiędzy dwoma użytkownikami.


Protokół TCP

Protokół TCP jest jedną z możliwych implementacji warstwy transportu w modelu OSI, ma on następujące cechy:

  • jeden-do-jeden

Pozwala na uwtorzenie dwukierunkowego kanału danych między dwoma hostami w sieci.

  • niezawodność (reliability)

Dane wysłane za pomocą protokołu TCP muszą dotrzeć do systemu docelowego. Jeśli nie jest możliwe dostarczenie danych (np. z powodu braku łączności z internetem systemu docelowego) implementacja protokołu TCP musi zwrócić błąd.

  • kontrolę błędów

Dane wysłane przez protokół TCP posiadają sumy kontrolne, które pozwalają wykryć błędy w transmisji.

  • kolejność

Jeśli jakaś wiadomość TCP jest wysłana jako wiele oddzielnych pakietów IP, wiadomość dotrze w tej samej kolejności w jakiej była wysłana.

Implementacja TCP

Na poziomie protokołu IP operuje się pojedyńczymi pakietami, pakiety te mogą po drodze między dwoma systemami się zawieruszyć.

Na poziomie protokolu TCP operuje się strumiueniami danych, strumień tych danych jest niezawodny.

W praktyce programowanie oparte na strumieniach (znane z basha) i programowanie TCP nie różni się zbytnio.

Pojęcie portu

Adresy IP jednoznacznie idenyfikują komputery, my chcielibyśmy jeszcze móc zaadresować kilka usług na jednym komputerze (jest to niemożliwe na poziomie protokołu IP!). Dlatego TCP (oraz UDP) używają dodatkowego numeru zwanego numerem portu.

Na komputerze docelowym numer portu jest najczęściej powiązany z usługą z jaką komputer chce się połączyć (port 80 to HTTP) – pełna lista portów dostępna jest w Internecie.

Na komputerze inicjującym port jest wybierany losowo.

Handshake TCP

Połączenie TCP jest jednoznacznie identyfikowane przez cztery liczby:

  • destination-port

Numer portu na komputerze do którego jest nawiązywane połączenie.

  • destination-host

Numer IP hosta do którego jest nawiązywane połączenie.

  • source-port

Numer portu na komputerze z którego jest nawiązywane połączenie.

  • source-host

Numer IP hosta z którego jest nawiązywane połączenie.

By nawiązać połączenie należy wymienić tzw. handshake TCP, polega on na wysłaniu trzech pakietów:

  • SYN

Wysyła go klient, zawiera on losową liczbę x.

  • SYN+ACK

Wysyła go serwer, zawiera on losową liczbę y, oraz liczbę x+1.

  • ACK

Wysyła go klient zawiera on liczbę y+1.

Narzut TCP

Użycie protokółu TCP powoduje powstanie pewnego narzutu (wysyłane są dodatkowe dane, związane z samym protokołem, a nie wysyłaną informacją), którego oszacowanie jest trudne, a sam rozmiar narzutu zależny od rozmiaru poszczególnych pakietów.

Dla długich połączeń narzut TCP jest w granicach 5%, dla wysłania pojedyńczej wiadomości może wynosić kilkaset procent (koszt wykonania handshake).

Gniazda sieciowe TCP w BASH

Gniazda sieciowe

Gniazdo (ang. socket) jest to abstrakcyjne pojęcie reprezentujące dwukierunkowy punkt końcowy sieciowego połączenia pomiędzy odległymi procesami. Dwukierunkowe, gdyż umożliwia zarówno (1) wysyłanie i (2) odbieranie danych.

Podstawowe właściwości każdego gniazda:

  • typ gniazda, związany z protokołem wymiany danych (np. Stream socket wykorzystujący TCP lub Datagram socket wykorzystujący UDP)
  • adres (np. adres IP)
  • opcjonalny numer portu

Procesy które zapewniają różne usługi przy użyciu gniazd nazywamy serwerami. W przypadku takich procesów stosowane są gniazda w trybie nasłuchiwania: czekają i nasłuchują, kiedy podłączy się do nich proces klienta.

Narzędzie telnet (klient)

Telnet jest narzędziem pozwalającym wykonywać połączenia TCP z serwerem. Jest więc programem typu “klient”.

Składnia najprostrzego połączenia:

telnet ADRES_IP PORT

Czyli by połączyć się do serwera google.pl serwującego strony internetowe (standardowym portem HTTP jest port 80), możemy napisać w terminalu:

telnet google.pl 80

Przykład: by uzyskać stronę internetową serwowaną przez serwer musimy wysłać zapytanie HTTP:

GET / HTTP/1.0

(oraz nacisnąć ENTER 2 razy!)

Alternatywnie, by połączyć się z gniazdem na własnym komputerze zamiast adresu IP można użyć słowa localhost.

Narzędzie ncat (serwer)

By tworzyć serwer (gniazdo nasłuchujące) na własnym komputerze można użyć programu ncat.

Przykładowo:

ncat -l 8000 -k -e skrypt.sh

tworzy serwer na porcie 8000, serwer ten dla każdego przychodzącego połączenia wykonuje skrypt skrypt.sh. Używane opcje:

  • -l PORT : listen - nasłuchuj na porcie
  • -k  : akceptacja wielu połączeń równocześnie w trybie nasłuchiwania (w przeciwnym wypadku najwyżej 1 połączenie na raz będzie obsługiwane)
  • -e COMMAND  : exec - uruchamia komendę/skrypt COMMAND

Możemy sprawdzić ich działanie za pomocą połączenia telnet.

Zadanie 1

Stworzyć serwer działający na porcie 8000, który wyśle słowo HELLO! do każdego procesu, który się do niego podłączy (należy napisać skrypt hello.sh oraz użyć polecenia ncat). Należy przetestować jego działanie przy użyciu programu telnet.

Programowanie gniazd sieciowych w języku JAVA

Nawiązywanie połączeń TCP (klient)

Do nawiązywania połączeń TCP służy klasa java.net.Socket.

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

public class ClientSocket {
   public static void main(String[] args) throws Exception{
       Socket socket = new Socket("google.pl", 80);
       BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
       bufferedWriter.write("GET / HTTP/1.0\n\n");
       bufferedWriter.flush();
       BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
       String line = reader.readLine();
       while (line!=null){
           System.out.println(line);
           System.out.flush();
           line = reader.readLine();
       }
   }
}

Ważniejsze części programu:

W konstruktorze socketa podajemy adres i port na który się łączymy:

Socket socket = new Socket("google.pl", 80);

Z socketem powiązane są dwa strumienie, które Państwo znacie z przedmiotu PO Java:

  • OutputStream, pobierany za pomocą wywołania socket.getOutputStream, służy do wysyłania danych do docelowego hosta
  • InputStream, pobierany za pomocą wywołania socket.getInputStream, służy do odbierania danych

Strumienie te są strumieniami binarnymi (tj. przesyłającymi nie tekst a dane binarne). Ponieważ HTTP jest protokołem tekstowym (w zasadzie) opakowujemy te strumienie do klasy BufferedReader która pozwala wygodnie pracować na tekście:

BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

Po wysłaniu linijki danych musimy wykonać polecenie flush które spowoduje wysłanie danych do hosta (implementacja TCP może czekać aż nie zbierze się dostateczna ilość danych by wysłać pełny pakiet:

bufferedWriter.flush();

Zadanie 2

Uruchomić powyższy program w środowisku eclipse. Następnie, należy połączyć się z serwerem cern.ch na porcie 80. Na ekran należy wypisać tylko te linijki które zawierają słowo body.

Przyjmowanie połączeń TCP (serwer)

Do tworzenia serwerów w javie służy klasa java.net.ServerSocket.

Ten program stworzy serwer odpisujący na to co mu się wysłało:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerSocketExample {

   public static void main(String[] args) throws IOException {
       ServerSocket serverSocket = new ServerSocket(12347);
       while (true){
           Socket socket = serverSocket.accept();
           BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
           BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
           bufferedWriter.write("Napisz: \"END\" by zakończyć połączenie.");
           String line = bufferedReader.readLine();
           while (!line.contains("END")){
               bufferedWriter.write("Sever says: ");
               bufferedWriter.write(line);
               bufferedWriter.write("\n");
               bufferedWriter.flush();
               line = bufferedReader.readLine();
           }
           socket.close();
       }
   }
}

Ciekawsze elementy programu:

Tworzymy serwer który będzie akceptował połączenia na porcie 12347:

ServerSocket serverSocket = new ServerSocket(12347);

Wywołanie serverSocket.accept() jest blokujące tj. metoda ta zakończy się w momencie w którym serwer otrzyma połączenie. Metoda ta zwraca zwykłego socketa pozwalającego odczytywać i zapisywać dane do zdalnego systemu.

Zadanie 3

Uruchomić powyższy program w środowisku eclipse. Przerobić program w taki sposób, by do tekstu wysyłanego przez użytkownika dopisywał znak !. Należy przetestować wprowadzone zmiany za pomocą narzędzia telnet. Proszę zasymulować połączenie się do powyższego programu dwóch różnych klientów (należy odpalić program telnet dwukrotnie, w różnych terminalach). Co się wtedy dzieje?

Wielowątkowy serwer w Javie

Serwer z poprzedniego przykładu jest jednowątkowy, tj. kiedy obsługuje jednego klienta i nie akceptuje połączeń od innych klientów.

Oczywiście jest to niepożądana cecha, w praktyce serwery oprogramowuje się tak by każde połączenie było oprogramowane w oddzielnym wątku.

Ponższy przykład jest w stanie obsłużyć wiele wątków:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MultihreadedServerSocketExample {

   public static void main(String[] args) throws IOException {
       ServerSocket serverSocket = new ServerSocket(12347);
       ExecutorService executorService = Executors.newFixedThreadPool(10);
       while (true){
           final Socket socket = serverSocket.accept();
           Runnable connection = new Runnable() {
               @Override
               public void run() {

                   try {
                       BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                       BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                       bufferedWriter.write("Napisz: \"END\" by zakończyć połączenie.");
                       bufferedWriter.flush();
                       String line = bufferedReader.readLine();
                       while (!line.contains("END")){
                           bufferedWriter.write("Sever says: ");
                           bufferedWriter.write(line);
                           bufferedWriter.write("\n");
                           bufferedWriter.flush();
                           line = bufferedReader.readLine();
                       }
                       socket.close();
                   } catch (IOException e) {
                       e.printStackTrace();
                   }

               }
           };
           executorService.submit(connection);
       }
   }
}

Informacje na temat przykładu:

Żeby socket był widoczny wewnątrz instancji Runnable musimy poprzedzić go modyfikatorem final. Pojawił się ExecutorService Połączenie obsługiwane jest nie w wątku main a w wątku zarządzanym przez ExecutorService. Musimy obsłużyć błędy w połączeniu, bo interfejs Runnable nie pozwala na propagację wyjątków z metody run.

Zadanie 4

Uruchomić powyższy program w środowisku eclipse. Przy użyciu narzędzia telnet przetestować możliwość łączenia się wielu klientów do powyższego programu. Następnie podłączyć się do uruchomionego serwera kolegi używając narzędzia telnet.

Zadanie 5

Proszę w parach napisać prosty program typu “chat” do porozumiewania się pomiędzy dwoma użytkownikami.

  • Na początku zdecydować która osoba w parze tworzy 'serwer', a która 'klienta'. Posiłkując się odpowiednimi przykładami przygotować wstępnie programy w javie. Zsynchronizować odpowiednio numer IP (/sbin/ifconfig) i numer portu.
  • Na początku warto zacząć od wersji zsynchronizowanej - na zmianę serwer i klient:
    • (a) wysyłają jedną wiadomość (write, write("\n"), flush)
    • (b) oczekują na jedną wiadomość (nextLine())
    • (c) wypisują otrzymaną wiadomość na ekran, przechodzą do punktu (a)
  • Wczytywanie linii tekstu z klawiatury:
 Scanner sc = new Scanner(System.in);
 sc.nextLine();

UWAGA:

  • należy uważać, by równocześnie klient i serwer nie czekały nawzajem na wiadomości od siebie, bo program utknie w martwym punkcie; któryś zawsze powinien wysyłać, gdy drugi czeka
  • należy pamiętać o przesłaniu również "\n", gdyż "readLine" oczekuje znaku końca linii w odczytywanej wiadomości!
  • gdy udało się wykonać wersję zsynchronizowaną, proszę spróbować wykonać wielowątkowo wersję asynchroniczną czata, która pozwala jednej osobie wysłać również kilka wiadomości z rzędu