Michał Boroń

Sieci Komputerowe 2

Ogłoszenia

---

Spis treści

Zasady zaliczenia przedmiotu

Informacje na temat zasad zaliczenia znajdują się w prezentacji z zajęć organizacyjnych (tutaj) oraz w dokumencie opisującym zasady zaliczania projektów (tutaj). Odrobienie nieobecności jest możliwe przez wykonanie zadań opisanych tutaj.

Listy tematów projektów u innych prowadzących:

Możliwe jest też zaproponowanie własnego tematu.

Materiały dodatkowe

Krótkie wprowadzenie do debuggera gdb (eng) https://www.youtube.com/watch?v=sCtY--xRUyI

Krótkie wprowadzenie do narzędzia valgrind - wykrywanie wycieków pamięci (eng) http://valgrind.org/docs/manual/quick-start.html

Dokumentacja gdb (eng) https://www.gnu.org/software/gdb/documentation/

Dokumentacja valgrind (eng) http://valgrind.org/docs/manual/manual.html

BSD sockets: klient TCP

Prezentacja: link.

Szablon

Do wykonania zadań w ramach tego laboratorium, możesz skorzystać z poniższego szablonu.

BSD sockets: iteracyjny serwer TCP i aplikacja UDP

Prezentacja: link.

BSD sockets + pthread

Prezentacja

Prezentacja: link.

Wprowadzenie

Pthread jest biblioteką udostępniającą możliwość tworzenia i zarządzanie wątkami. Wykorzystanie wątków jest wielokrotnie wydajniejsze od tworzenia procesów funkcją fork(). Kolejną, po wydajności, różnicą jest współdzielenie pamięci przez wątki. Wszystkie wątki (działające w tym samym procesie) mają dostęp do tej samej przestrzeni adresowej. W konsekwencji, modyfikacje zmiennych dokonane przez jeden wątek, są widoczne w pozostałych wątkach. Co więcej, współbieżna modyfikacja zmiennych przez wiele wątków, może mieć niepożądane skutki. Na przykład: dwa wątki (A i B) zajmują się kupowaniem na wyprzedażach, współdzielą zmienną opisującą stan konta (100 zł). W tym samym momencie, wątek A decyduje się na zakup produktu p1 (100zł) i wątek B decyduje się na zakup produktu p2 (100zł). Obydwa odczytują bieżący stan konta, odejmują 100, zapisują nowy stan konta (0 zł) i zlecają przelew na rzecz odpowiednich sklepów, co spowoduje wydanie zbyt dużej ilości pieniędzy. Można uniknąć problemów związancych ze współbieżną modyfikacją kontrolując dostęp do współdzielonych zasobów przez wykorzystanie abstrakcji, które oferuje biblioteka pthread.

W celu skorzystania z biblioteki pthread należy dołączyć odpowiedni plik nagłówkowy:

#include <pthread.h>

Przy kompilacji konieczne jest dodanie przełącznika pthread:
gcc -pthread program.c -o program
Do utworzenia wątku służy funkcja pthread_create.
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

Pierwszy argument jaki należy przekazać to uchwyt na wątek (thread), który zostanie związany z nowo utworzonym wątkiem i poźniej będzie można go wykorzystać do interakcji z tym wątkiem (np. poczekać na zakończenie jego przetwarzania). Drugi argument (attr) określa atrybuty wątku, takie jak polityka szeregowania na procesorze, rozmiar stosu, itp. Na potrzeby laboratorium wystarczy przekazać wartość NULL. Interesującym atrybutem jest Detach state. Jeżeli zostanie ustawiony na PTHREAD_CREATE_DETACHED, po zakończeniu pracy wątku jego zasoby zostaną automatycznie zwolnione (normalnie dzieje się to dopiero gdy inny wątek wykona pthread_join łącząc się z zakończonym wątkiem lub przy końcu wykonania programu). Trzeci argument (start_routine) to funkcja, która zostanie wywołana przez wątek zaraz po jego utworzeniu. Należy zwrócić uwagę na typ argumentu i zwracanej wartości - w obydwu przypadkach (void *). Czwarty argument (arg) to argument, który zostanie przekazany do funkcji podanej jako start_routine. Jeżeli chcemy przekazać wiele argumentów, należy utworzyć strukturę i umieścić argumenty w jej polach. Poniżej został zamieszczony schemat korzystania z pthread_create.

//struktura zawierająca dane, które zostaną przekazane do wątku
struct thread_data_t
{
    //pola
};

//funkcja opisującą zachowanie wątku - musi przyjmować argument typu (void *) i zwracać (void *)
void *ThreadBehavior(void *t_data)
{
    //ciało funkcji
    ...
}

int main() {
    ....
    //uchwyt na wątek
    pthread_t thread1;
    //utworzenie wątku
    pthread_create(&thread1, NULL, ThreadBehavior, (void *)&t_data);
    ....
}

Wątek należy zakończyć wywołaniem funkcji pthread_exit.

void pthread_exit(void *retval);

Argument retval zostanie przekazany do innego wątku, który spróbuje połączyć się z naszym wątkiem używając funkcji pthread_join. Jeżeli nie chcemy przekazać żadnej wartości, jako retval podajemy NULL.

Można poczekać na zakończenie działania innego wątku, używając funkcji pthread_join.

int pthread_join(pthread_t thread, void **retval);

thread to uchwyt wątku, na który zamierzamy czekać. retval określa miejsce, w które zostanie skopiowana wartość retval zwrócona przez zakończony wątek (z użyciem funkcji pthread_exit).

Kontrolę dostępu do współdzielonych zasobów można realizować wykorzystując mutex (ang. mutual exclusion - wzajemne wykluczanie). Mutex może być "zajęty" przez wyłącznie jeden wątek w danym momencie. Jeżeli dwa wątki w tym samym czasie spróbują "zająć" mutex, jeden z nich przerwie przetwarzanie do czasu aż mutex zostanie zwolniony. W bibliotece pthread, mutex jest zmienną typu pthread_mutex_t. Zajmowanie i zwalnianie jest realizowane przez funkcje pthread_mutex_lock i pthread_mutex_unlock. Poniżej został zamieszczony schemat korzystania z mutex'u.


...
//uchwyt na mutex
pthread_mutex_t example_mutex = PTHREAD_MUTEX_INITIALIZER;
...
int main() {
    ...
    //zajęcie
    pthread_mutex_lock(&example_mutex);
    ...
    //zwolnienie
    pthread_mutex_unlock(&example_mutex);
    ...
}

Szablony

Do wykonania zadań w ramach tego laboratorium, możesz skorzystać z poniższych szablonów.

Zadanie 1

Stwórz aplikację pozwalającą wymieniać komunikaty tekstowe (wpisywane przez standardowe wejście) między klientem a serwerem. Utwórz tylko jedno połączenie (TCP). Zapewnij możliwość "ładnego" wyłączenia klienta (nie ctrl-c).

Wskazówki:

Zadanie 2

Stwórz aplikację pozwalającą wymieniać komunikaty między wieloma klientami. Po podłączeniu do serwera, klient zaczyna otrzymywać wiadomości wysyłane przez pozostałych klientów. Między serwerem, a dowolnym klientem może być tylko jedno połączenie (TCP). Ogranicz maksymalną liczbę równolegle połączonych klientów do 3. Możesz wykorzystać klienta z poprzedniego zadania.

Wskazówki:

Materiały dodatkowe

Więcej o bibliotece pthread (eng) https://computing.llnl.gov/tutorials/pthreads/

Rozdziały 29-33 w książce The Linux Programming Interface (Kerrisk). API pthread, dyskusja wątek vs. proces, mechanizmy synchronizacji itp.

BSD sockets c.d.

Prezentacja: link.

Sockets + Java

Wprowadzenie

W Javie, do zarządzania połączeniem z serwerem, klient używa klasy Socket. Jednym ze sposobów utworzenia instancji, jest podanie do konstruktora napisu zawierającego nazwę domenową (lub adres IP) oraz liczbę z numerem portu.

Socket clientSocket = new Socket("localhost", 1234);

Odczyt danych jest realizowany przez instancję klasy InputStream, która jest zwracana przez metodę getInputStream. Na instancji klasy InputStream można wywołać metodę read, przekazując jako argument tablicę bajtów. Tablica zostanie wypełniona danymi, a zwróconą wartością będzie liczba odczytanych bajtów (lub -1 gdy połączenie zostanie zamknięte).

InputStream is = clientSocket.getInputStream();
byte[] buffer = new byte[100];
is.read(buffer);

Na końcu trzeba zamknąć gniazdo sieciowe, korzystając z metody close.

clientSocket.close();

Zapisywanie danych do gniazda sieciowego jest realizowane przez instancję klasy OutputStream, która jest zwracana przez metodę getOutputStream. Na instancji klasy OutputStream można wywołać metodę write, przekazując jako argument tablicę bajtów.

OutputStream os = clientSocket.getOutputStream();
String msg = "some text";
os.write(msg.getBytes());

Do odczytu i zapisu danych z/do gniazda sieciowego nie jest konieczne korzystanie wyłącznie z niskopoziomowych klas InputStream i OutputStream. Dla danych tekstowych, prostszymi abstrakcjami są klasy BufferedReader oraz PrintWriter. BufferedReader oferuje między innymi metodę readLine, która odczytuje znaki ze strumienia, aż do napotkania znaku nowej linii ('\n', '\r', '\r''\n'). PrintWriter oferuje między innymi metodę println, która zapisuje do strumienia podany w argumencie String oraz dodaje znak nowej linii. Podając true jako drugi argument w konstruktorze kalsy PrintWriter zapewniamy, że tekst będzie wysyłany zaraz po wywołaniu println (w przeciwnym wypadku może być buforowany). Sposób użycia wymienionych klas do komunikacji sieciowej zaprezentowano poniżej.

BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String serverMessage = reader.readLine();
...
String clientMessage = "hello";
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);
writer.println(clientMessage);

W Javie, do utworzenia gniazda sieciowego dla serwera TCP, używa się klasy ServerSocket. Jako argument konstruktora wystarczy przekazać liczbę z numerem portu. Na instancji tej klasy można wywołać metodę accept, która zwróci obiekt klasy Socket umożliwiający komunikację z klientem. Przykładowy kod zamieszczono poniżej.

ServerSocket serverSocket = new ServerSocket(1234);
Socket connectionSocket = serverSocket.accept();
...
connectionSocket.close();
serverSocket.close();

W celu utworzenia nowego wątku można stworzyć klasę, która implementuje interfejs Runnable (i co za tym idzie, zawiera metodę run). Nic nie stoi na przeszkodzie by taka klasa posiadała też wieloargumentowy konstruktor, własne metody i prywatne pola. Przykład takiej klasy zamieszczono poniżej.

public class MyRunnable implements Runnable {
	
	private boolean someField;
	
	public MyRunnable(boolean someValue) {
		this.someField = someValue;
	}

	public void run() {
		...
	}
	
	public void someMethod() {
		...
	}
}

W miejscu, w którym planujemy wystartować wątek, należy utworzyć instancję klasy Thread przekazując jako argument konstruktora instancję wcześniej zdefiniowanej klasy implementującej Runnable. Następnie trzeba wywołać metodę start.

MyRunnable mr = new MyRunnable(true);
Thread thread = new Thread(mr);
thread.start();

Zadanie 1

Napisz w Javie klienta, który połączy się z serwerem TCP, odbierze i wyświetli napis. Użyj klasy BufferedReader. Serwer wysyłający napis stwórz w C. Pamiętaj, by serwer zakończył napis znakiem nowej linii.

Zadanie 2

Zmodyfikuj klienta z poprzedniego zadania tak, by operował bezpośrednio na instancji klasy InputStream.

Wskazówki:

Zadanie 3

Napisz serwer daytime - po przyjęciu połączenia od klienta, serwer wysyła napis zawierający bieżącą datę i godzinę.

Wskazówki:

Zadanie 4

Napisz w Javie wielowątkowego klienta do serwera czatu zrealizowanego w drugim zadaniu na poprzednich zajęciach.

Wskazówki:
Scanner scanner = new Scanner(System.in);
String inputText = scanner.nextLine();

Materiały dodatkowe

Sockets + Qt

Wprowadzenie

Qt jest narzędziem zaprojektowanym z myślą o tworzeniu aplikacji z graficznym interfejsem użytkownika. Domyślnie wspieranym językiem programowania jest C++. Wśród dowiązań do innych języków programowania dobrą opinią cieszy się m. in. dowiązanie do języka Python. Qt posiada własne środowisko deweloperskie o nazwie Qt Creator. Aby utworzyć nowy projekt w Qt Creator wybieramy File->New File or Project->Qt Widgets Application (na potrzeby zajęć). Po utworzeniu, struktura projektu wygląda następująco:

Plik (*.pro) zawiera informacje dotyczące projektu, w tym m. in. które części Qt są wykorzystywane. Pliki nagłówkowe (headers) odpowiadają plikom źródłowym (sources). Katalog Forms zawiera definicje elementów graficznych, które można modyfikować w trybie projektowania (design) lub ręcznie wprowadzając zmiany do pliku xml.

W Qt za powiązanie zdarzeń i reakcji na zdarzenia odpowiadają mechanizmy o nazwach signal i slot. Są one implementacją wzorca projektowego Obserwator. Obserwowany, czyli element informujący o zdarzeniach, wysyła sygnały opisujące zdarzenie do zainteresowanych obserwatorów (którzy zdefiniowali funkcję obsługującą nadejście sygnału - slot). W celu powiązania zdarzenia kliknięcia przycisku, należy kliknąć PPM na przycisku, wybrać "Go to slot", po czym kliknąć pozycję "clicked". Deklaracja funkcji obsługującej sygnał zostanie automatycznie umieszczona w pliku nagłówkowym, w sekcji private slots.

Wypisywanie komunikatów na konsolę można zrealizować używając strumienia udostępnianego przez funkcję qInfo. Uruchamiając program w Qt Creator, komunikaty będą widoczne w zakładce "Application Output".
qInfo() << "informacja";

Aby odwołać się do komponentu graficznego (np. etykiety) z kodu, należy sprawdzić jego nazwę. W trybie projektowania należy zaznaczyć wybrany element, następnie w oknie właściwości odczytać wartość QObject->objectName. W kodzie możemy odwołać się do elementu w następujący sposób (zakładając, że nazwa elementu to dataLabel):

ui->dataLabel->...;

W Qt, programowanie sieciowe jest wspierane przez moduł network. Konieczne jest dołączenie tego modułu w pliku (*.pro).

QT	+= network

Można też dodać obsługę wyrażeń z C++11:

CONFIG += c++11

Dodatkowo, należy dołączyć następującą dyrektywę do odpowiedniego pliku nagłówkowego:

#include <QtNetwork>

Do komunikacji przez TCP potrzebne jest utworzenie instancji klasy QTcpSocket oraz funkcji nasłuchującej na transmisje danych. W pliku nagłówkowym umieszczamy następujące wpisy:

...
private slots:
	void readData();
...
private:
	QTcpSocket *tcpSocket;
...

Następnie w odpowiadającym pliku źródłowym inicjalizujemy socket i rejestrujemy funkcję nasłuchującą na sygnał (transmisje danych):

MainWindow::MainWindow(QWidget *parent) :
	...
	tcpSocket(new QTcpSocket(this)),
	...
	{
	connect(tcpSocket, &QIODevice::readyRead, this, &MainWindow::readData);
	...
	}
...

Nawiązanie połączenia z serwerem jest realizowane przez metodę connectToHost klasy QTcpSocket.

tcpSocket->connectToHost("127.0.0.1", 1234);

Zapis danych do gniazda sieciowego jest możliwy przez wywołanie metody write. Za odczyt odpowiadają metody read, readAll, readLine.

Do zamknięcia gniazda sieciowego służą metody abort (rozłącza od razu) i disconnectFromHost (czeka aż zbuforowane dane zostaną wysłane).

Aby umożliwić obsługę błędów, należy obsłużyć odpowiedni sygnał. W pliku nagłówkowym:

private slots:
	...
	void displayError(QAbstractSocket::SocketError socketError);

W pliku źródłowym, w ciele konstruktora:

typedef void (QAbstractSocket::*QAbstractSocketErrorSignal)(QAbstractSocket::SocketError);
connect(tcpSocket, static_cast<QAbstractSocketErrorSignal>(&QAbstractSocket::error), this, &MainWindow::displayError);

Inne przydatne sygnały to connected i disconnected, zdefiniowane w klasie QAbstractSocket.

Klasa QTcpServer odpowiada za realizację serwera. Instancję tworzy się analogicznie do QTcpSocket. Konieczne jest obsłużenie sygnału newConnection klasy QTcpServer oraz rozpoczęcie nasłuchiwania przez jednokrotne wywołanie metody listen. W funkcji obsługi zdarzenia newConnection nawiązujemy połączenie wywołując metodę nextPendingConnection, która zwraca wskaźnik na obiekt typu QTcpSocket.

Zadanie 1

Napisz w Qt klienta, który połączy się z serwerem TCP, odbierze i wyświetli napis. Użyj metody readLine() klasy QTcpSocket. Serwer wysyłający napis stwórz w C. Pamiętaj, by serwer zakończył napis znakiem nowej linii.

Zadanie 2

Napisz w Qt serwer, który przyjmie połączenie od klienta i odeśle napis.

Zadanie 3

Napisz w Qt klienta do serwera czatu zrealizowanego w drugim zadaniu na zajęciach z pthread.

Wskazówki:

Materiały dodatkowe

Więcej o Qt (eng) http://doc.qt.io/

BSD sockets: gniazda asynchroniczne i nieblokujące (epoll)

Prezentacja: link.

Szablon

Do wykonania zadań w ramach tego laboratorium, możesz skorzystać z poniższego szablonu.

Blok zajęć konfiguracyjnych

Prezentacje:

Strony innych prowadzących