Sieci Komputerowe 2
Ogłoszenia
---
Spis treści
- Zasady zaliczenia przedmiotu
- BSD sockets: klient TCP
- BSD sockets: iteracyjny serwer TCP i aplikacja UDP
- BSD sockets + pthread
- BSD sockets c.d.
- Sockets + Java
- Sockets + Qt
- BSD sockets: gniazda asynchroniczne i nieblokujące (epoll)
- Blok zajęć konfiguracyjnych
- Strony innych prowadzących
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:
- http://www.cs.put.poznan.pl/mkalewski/files/zadania.pdf
- http://www.cs.put.poznan.pl/ddwornikowski/courses/projekty_sieci2_2015.html
- http://www.cs.put.poznan.pl/jkonczak/sk2:projekt
- http://www.cs.put.poznan.pl/jkonczak/misc/lukasz_tematy_projektow.pdf
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:read
zwróci 0, kiedy połączenie zostanie zamknięte przezclose
- możesz skorzystać z funkcji
fgets
do pobrania komunikatu ze standardowego wejścia.fgets([referencja na bufor], [rozmiar bufora], stdin)
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:- wykorzystaj mutex
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
.
- możesz wypisać wynik na konsolę używając
System.out.write([tablica z odczytanymi bajtami], 0, [liczba odczytanych bajtów])
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:(new Date()).toString()
Zadanie 4
Napisz w Javie wielowątkowego klienta do serwera czatu zrealizowanego w drugim zadaniu na poprzednich zajęciach.
Wskazówki:- wczytywanie tekstu ze standardowego wejścia można zrealizować używając:
Scanner scanner = new Scanner(System.in);
String inputText = scanner.nextLine();
Materiały dodatkowe
- I/O w Javie - więcej o klasach służących do obsługi strumieni (eng): http://docs.oracle.com/javase/tutorial/essential/io/index.html
- Tutorial o programowaniu sieciowym w Javie (eng): http://tutorials.jenkov.com/java-networking/index.html
- Oficjalny tutorial o programowaniu sieciowym w Javie (eng): http://docs.oracle.com/javase/tutorial/networking/sockets/index.html
- Wielowątkowość w Javie (eng): http://docs.oracle.com/javase/tutorial/essential/concurrency/
- Java new I/O (nio) (eng): http://tutorials.jenkov.com/java-nio/index.html
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
.
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:QListWidget
Materiały dodatkowe
Więcej o Qt (eng) http://doc.qt.io/
BSD sockets: gniazda asynchroniczne i nieblokujące (epoll)
Prezentacja: link.