Tutorial 8 - Gniazda sieciowe i epoll #
W tym tutorialu do czekania na wielu deskryptorach używamy funkcji z rodzinyepoll
, które nie są częścią standardu POSIX, ale rozszerzeniem Linuxa. Przy pisaniu kodu przenośnego między systemami należy użyć funkcji z rodzinyselect
lubpoll
, które jednak cechują się gorszą wydajnością a ich użycie jest mniej wygodne.
Uwagi wstępne:
- W trakcie tych zajęć przydatny jest program netcat
- Obowiązują wszystkie materiały z SOP1 i SOP2 jakie były do tej pory, szczególnie ważne są te dotyczące wątków i procesów!
- Szybkie przejrzenie tutoriala prawdopodobnie nic nie pomoże, należy samodzielnie uruchomić programy, sprawdzić jak działają, poczytać materiały dodatkowe takie jak strony man. W trakcie czytania sugeruję wykonywać ćwiczenia a na koniec przykładowe zadanie.
- Materiały i ćwiczenia są ułożone w pewną logiczną całość, czasem do wykonania ćwiczenia konieczny jest stan osiągnięty poprzednim ćwiczeniem dlatego zalecam wykonywanie ćwiczeń w miarę przyswajania materiału.
- Większość ćwiczeń wymaga użycia konsoli poleceń, zazwyczaj zakładam, ze pracujemy w jednym i tym samym katalogu roboczym więc wszystkie potrzebne pliki są “pod ręką” tzn. nie ma potrzeby podawania ścieżek dostępu.
- Czasem podaję znak $ aby podkreślić, że chodzi o polecenie konsolowe, nie piszemy go jednak w konsoli np.: piszę “$make” w konsoli wpisujemy samo “make”.
- To co ćwiczymy wróci podczas kolejnych zajęć. Jeśli po zajęciach i teście coś nadal pozostaje niejasne proszę to poćwiczyć a jeśli trzeba dopytać się u prowadzących.
- W wielu miejscach w kodzie dodano obsługę przerwania sygnałem (makro TEMP_FAILURE_RETRY), nawet jeśli w danym programie to nie ma uzasadnienia bo brak w nim obsługi sygnałów. Duża część prezentowanego kodu będzie przez studentów przenoszona do ich rozwiązań, zwłaszcza gotowe funkcję biblioteczne, w których to rozwiązaniach mogą się pojawiać sygnały. Jest to zabieg zwiększający przenośność kodu.
- Podczas planowania w jakiej postaci dane będą przesyłane przez sieć bardzo ważne jest aby zawsze pamiętać o tym, że komunikują się programy działające (być może) na różnych architekturach sprzętowych, trzeba mieć wzgląd na następujące kwestie:
- byte order - czyli w jakiej kolejności w pamięci są przechowywane liczby całkowite, jeśli przesyłamy liczby binarnie pomiędzy programami działającymi na różnych architekturach to zmiana tej kolejności będzie katastrofalna, równoznaczna ze zmianą liczby, np: 0x00FF zmienia się nam na 0xFF00! Nie musimy znać byte orderu architektury na której działamy, wystarczy pamiętać o zasadzie, że do sieci wysyłamy wszystkie liczby całkowite zakodowane w tzw. network byte order. Do konwersji do tej postaci mamy makra htons i htonl (dla 16 bitowego shorta i 32 bitowego integera). Po odebraniu danych z sieci następuje odwrotna konwersja z byte orderu sieciowego na lokalny, makra ntohs i ntohl.
- Nie ma uniwersalnego kodowania dla liczb zmiennopozycyjnych, różne mogą być ich implementacje na różnych platformach więc albo musimy przesłać je jako tekst lub jako liczby o ustalonej precyzji (czyli jako liczby całkowite tak naprawdę).
- Pamiętamy oczywiście, że problem byte order nie dotyczy danych jedno-bajtowych w tym przede wszystkim tekstu. Przesłanie danych w formie tekstowej jest prawie zawsze poprawnym rozwiązaniem.
- Różne mogą być rozmiary typów int,short czy long na różnych architekturach. Aby uniknąć problemów najlepiej jest używać typów o ustalonym wymiarze takich jak np.: int32_t czy uint16_t
- Niezbyt wygodne będzie też przesyłanie struktur ze względu na różne ułożenie ich pól w pamięci na różnych platformach . Występują różne przerwy pomiędzy polami wynikające z optymizacji adresowania - adresy podzielne przez różne zależne od architektury potęgi 2 są szybciej pobierane z pamięci. Aby to obejść, należy zadbać aby kompilator nie optymizował tych struktur tylko “pakował” je bez żadnych przerw. Nie zawsze da się to osiągnąć z poziomu kodu, zazwyczaj konieczne są dyrektywy kompilatora co czyni to rozwiązanie mniej przenośnym. Jak się da to unikamy przesyłania całych struktur binarnie, możemy przesyłać strukturę pole po polu.
Zadanie local + TCP #
Napisz prosty sieciowy kalkulator liczb całkowitych. Dane przesyłane pomiędzy klientami a serwerem mają postać:
- operand 1
- operand 2
- wynik
- operator (+,-,*,/)
- status
Wszystko przekonwertowane do postaci 32 bitowych liczb w tablicy.
Serwer wylicza wynik i odsyła go do klienta. Jeśli wyliczenie przebiegło pomyślnie pole status przyjmuje wartość 1, jeśli nie (np. dzielenie przez zero) wartość 0. Komunikacja z serwerem jest możliwa na 2 sposoby:
- gniazda lokalne
- gniazda sieciowe tcp
Serwer ma być jednoprocesowy i jednowątkowy, przyjmuje następujące argumenty:
- nazwa lokalnego gniazda
- port
Należy napisać 2 programy klientów, po jednym dla każdego typu połączenia, programy te pobierają następujące parametry:
- adres gniazda serwera (nazwa domenowa dla tcp, nazwa pliku specjalnego dla połączenia lokalnego)
- numer portu
- operand 1
- operand 2
- operator (+,-,*,/)
Jeśli wyliczenie się uda należy wyświetlić wynik. Wszystkie 3 programy można przerwać C-c, nie wolno po sobie zostawiać pliku połączenia lokalnego.
Rozwiązanie #
Co student musi wiedzieć:
man 7 socket
man 7 epoll
man 7 unix
man 7 tcp
man 3p socket
man 3p bind
man 3p listen
man 3p connect
man 3p accept
man 2 epoll_create
man 2 epoll_ctl
man 2 epoll_wait
man 3p freeaddrinfo (obie funkcje, getaddrinfo też)
man 3p gai_strerror
Zwróć uwagę zwłaszcza na sekcję Q&A w man 7 epoll
. Ponieważ jest bardzo dobrze przygotowana, nie będziemy jej tutaj powtarzać.
Wspólna biblioteka dla wszystkich kodów w tym tutorialu:
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#ifndef TEMP_FAILURE_RETRY
#define TEMP_FAILURE_RETRY(expression) \
(__extension__({ \
long int __result; \
do \
__result = (long int)(expression); \
while (__result == -1L && errno == EINTR); \
__result; \
}))
#endif
#define ERR(source) (perror(source), fprintf(stderr, "%s:%d\n", __FILE__, __LINE__), exit(EXIT_FAILURE))
int sethandler(void (*f)(int), int sigNo)
{
struct sigaction act;
memset(&act, 0, sizeof(struct sigaction));
act.sa_handler = f;
if (-1 == sigaction(sigNo, &act, NULL))
return -1;
return 0;
}
int make_local_socket(char *name, struct sockaddr_un *addr)
{
int socketfd;
if ((socketfd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
ERR("socket");
memset(addr, 0, sizeof(struct sockaddr_un));
addr->sun_family = AF_UNIX;
strncpy(addr->sun_path, name, sizeof(addr->sun_path) - 1);
return socketfd;
}
int connect_local_socket(char *name)
{
struct sockaddr_un addr;
int socketfd;
socketfd = make_local_socket(name, &addr);
if (connect(socketfd, (struct sockaddr *)&addr, SUN_LEN(&addr)) < 0)
{
ERR("connect");
}
return socketfd;
}
int bind_local_socket(char *name, int backlog_size)
{
struct sockaddr_un addr;
int socketfd;
if (unlink(name) < 0 && errno != ENOENT)
ERR("unlink");
socketfd = make_local_socket(name, &addr);
if (bind(socketfd, (struct sockaddr *)&addr, SUN_LEN(&addr)) < 0)
ERR("bind");
if (listen(socketfd, backlog_size) < 0)
ERR("listen");
return socketfd;
}
int make_tcp_socket(void)
{
int sock;
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock < 0)
ERR("socket");
return sock;
}
struct sockaddr_in make_address(char *address, char *port)
{
int ret;
struct sockaddr_in addr;
struct addrinfo *result;
struct addrinfo hints = {};
hints.ai_family = AF_INET;
if ((ret = getaddrinfo(address, port, &hints, &result)))
{
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
exit(EXIT_FAILURE);
}
addr = *(struct sockaddr_in *)(result->ai_addr);
freeaddrinfo(result);
return addr;
}
int connect_tcp_socket(char *name, char *port)
{
struct sockaddr_in addr;
int socketfd;
socketfd = make_tcp_socket();
addr = make_address(name, port);
if (connect(socketfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) < 0)
{
ERR("connect");
}
return socketfd;
}
int bind_tcp_socket(uint16_t port, int backlog_size)
{
struct sockaddr_in addr;
int socketfd, t = 1;
socketfd = make_tcp_socket();
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, &t, sizeof(t)))
ERR("setsockopt");
if (bind(socketfd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
ERR("bind");
if (listen(socketfd, backlog_size) < 0)
ERR("listen");
return socketfd;
}
int add_new_client(int sfd)
{
int nfd;
if ((nfd = TEMP_FAILURE_RETRY(accept(sfd, NULL, NULL))) < 0)
{
if (EAGAIN == errno || EWOULDBLOCK == errno)
return -1;
ERR("accept");
}
return nfd;
}
ssize_t bulk_read(int fd, char *buf, size_t count)
{
int c;
size_t len = 0;
do
{
c = TEMP_FAILURE_RETRY(read(fd, buf, count));
if (c < 0)
return c;
if (0 == c)
return len;
buf += c;
len += c;
count -= c;
} while (count > 0);
return len;
}
ssize_t bulk_write(int fd, char *buf, size_t count)
{
int c;
size_t len = 0;
do
{
c = TEMP_FAILURE_RETRY(write(fd, buf, count));
if (c < 0)
return c;
buf += c;
len += c;
count -= c;
} while (count > 0);
return len;
}
serwer l8-1_server.c
:
#include "l8_common.h"
#define BACKLOG 3
#define MAX_EVENTS 16
volatile sig_atomic_t do_work = 1;
void sigint_handler(int sig) { do_work = 0; }
void usage(char *name) { fprintf(stderr, "USAGE: %s socket port\n", name); }
void calculate(int32_t data[5])
{
int32_t op1, op2, result = -1, status = 1;
op1 = ntohl(data[0]);
op2 = ntohl(data[1]);
switch ((char)ntohl(data[3]))
{
case '+':
result = op1 + op2;
break;
case '-':
result = op1 - op2;
break;
case '*':
result = op1 * op2;
break;
case '/':
if (!op2)
status = 0;
else
result = op1 / op2;
break;
default:
status = 0;
}
data[4] = htonl(status);
data[2] = htonl(result);
}
void doServer(int local_listen_socket, int tcp_listen_socket)
{
int epoll_descriptor;
if ((epoll_descriptor = epoll_create1(0)) < 0)
{
ERR("epoll_create:");
}
struct epoll_event event, events[MAX_EVENTS];
event.events = EPOLLIN;
event.data.fd = local_listen_socket;
if (epoll_ctl(epoll_descriptor, EPOLL_CTL_ADD, local_listen_socket, &event) == -1)
{
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
event.data.fd = tcp_listen_socket;
if (epoll_ctl(epoll_descriptor, EPOLL_CTL_ADD, tcp_listen_socket, &event) == -1)
{
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
int nfds;
int32_t data[5];
ssize_t size;
sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigprocmask(SIG_BLOCK, &mask, &oldmask);
while (do_work)
{
if ((nfds = epoll_pwait(epoll_descriptor, events, MAX_EVENTS, -1, &oldmask)) > 0)
{
for (int n = 0; n < nfds; n++)
{
int client_socket = add_new_client(events[n].data.fd);
if ((size = bulk_read(client_socket, (char *)data, sizeof(int32_t[5]))) < 0)
ERR("read:");
if (size == (int)sizeof(int32_t[5]))
{
calculate(data);
if (bulk_write(client_socket, (char *)data, sizeof(int32_t[5])) < 0 && errno != EPIPE)
ERR("write:");
}
if (TEMP_FAILURE_RETRY(close(client_socket)) < 0)
ERR("close");
}
}
else
{
if (errno == EINTR)
continue;
ERR("epoll_pwait");
}
}
if (TEMP_FAILURE_RETRY(close(epoll_descriptor)) < 0)
ERR("close");
sigprocmask(SIG_UNBLOCK, &mask, NULL);
}
int main(int argc, char **argv)
{
int local_listen_socket, tcp_listen_socket;
int new_flags;
if (argc != 3)
{
usage(argv[0]);
return EXIT_FAILURE;
}
if (sethandler(SIG_IGN, SIGPIPE))
ERR("Seting SIGPIPE:");
if (sethandler(sigint_handler, SIGINT))
ERR("Seting SIGINT:");
local_listen_socket = bind_local_socket(argv[1], BACKLOG);
new_flags = fcntl(local_listen_socket, F_GETFL) | O_NONBLOCK;
fcntl(local_listen_socket, F_SETFL, new_flags);
tcp_listen_socket = bind_tcp_socket(atoi(argv[2]), BACKLOG);
new_flags = fcntl(tcp_listen_socket, F_GETFL) | O_NONBLOCK;
fcntl(tcp_listen_socket, F_SETFL, new_flags);
doServer(local_listen_socket, tcp_listen_socket);
if (TEMP_FAILURE_RETRY(close(local_listen_socket)) < 0)
ERR("close");
if (unlink(argv[1]) < 0)
ERR("unlink");
if (TEMP_FAILURE_RETRY(close(tcp_listen_socket)) < 0)
ERR("close");
fprintf(stderr, "Server has terminated.\n");
return EXIT_SUCCESS;
}
l8-1_client_local.c
:#include "l8_common.h"
int make_socket(char *name, struct sockaddr_un *addr)
{
int socketfd;
if ((socketfd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
ERR("socket");
memset(addr, 0, sizeof(struct sockaddr_un));
addr->sun_family = AF_UNIX;
strncpy(addr->sun_path, name, sizeof(addr->sun_path) - 1);
return socketfd;
}
int connect_socket(char *name)
{
struct sockaddr_un addr;
int socketfd;
socketfd = make_socket(name, &addr);
if (connect(socketfd, (struct sockaddr *)&addr, SUN_LEN(&addr)) < 0)
{
ERR("connect");
}
return socketfd;
}
void usage(char *name) { fprintf(stderr, "USAGE: %s socket operand1 operand2 operation \n", name); }
void prepare_request(char **argv, int32_t data[5])
{
data[0] = htonl(atoi(argv[2]));
data[1] = htonl(atoi(argv[3]));
data[2] = htonl(0);
data[3] = htonl((int32_t)(argv[4][0]));
data[4] = htonl(1);
}
void print_answer(int32_t data[5])
{
if (ntohl(data[4]))
printf("%d %c %d = %d\n", ntohl(data[0]), (char)ntohl(data[3]), ntohl(data[1]), ntohl(data[2]));
else
printf("Operation impossible\n");
}
int main(int argc, char **argv)
{
int fd;
int32_t data[5];
if (argc != 5)
{
usage(argv[0]);
return EXIT_FAILURE;
}
fd = connect_socket(argv[1]);
prepare_request(argv, data);
if (bulk_write(fd, (char *)data, sizeof(int32_t[5])) < 0)
ERR("write:");
if (bulk_read(fd, (char *)data, sizeof(int32_t[5])) < (int)sizeof(int32_t[5]))
ERR("read:");
print_answer(data);
if (TEMP_FAILURE_RETRY(close(fd)) < 0)
ERR("close");
return EXIT_SUCCESS;
}
l8-1_client_tcp.c
:#include "l8_common.h"
void prepare_request(char **argv, int32_t data[5])
{
data[0] = htonl(atoi(argv[3]));
data[1] = htonl(atoi(argv[4]));
data[2] = htonl(0);
data[3] = htonl((int32_t)(argv[5][0]));
data[4] = htonl(1);
}
void print_answer(int32_t data[5])
{
if (ntohl(data[4]))
printf("%d %c %d = %d\n", ntohl(data[0]), (char)ntohl(data[3]), ntohl(data[1]), ntohl(data[2]));
else
printf("Operation impossible\n");
}
void usage(char *name) { fprintf(stderr, "USAGE: %s domain port operand1 operand2 operation \n", name); }
int main(int argc, char **argv)
{
int fd;
int32_t data[5];
if (argc != 6)
{
usage(argv[0]);
return EXIT_FAILURE;
}
fd = connect_tcp_socket(argv[1], argv[2]);
prepare_request(argv, data);
if (bulk_write(fd, (char *)data, sizeof(int32_t[5])) < 0)
ERR("write:");
if (bulk_read(fd, (char *)data, sizeof(int32_t[5])) < (int)sizeof(int32_t[5]))
ERR("read:");
print_answer(data);
if (TEMP_FAILURE_RETRY(close(fd)) < 0)
ERR("close");
return EXIT_SUCCESS;
}
Uruchomienie:
$ ./l8-1_server a 2000&
$ ./l8-1_client_local a 2 1 +
$ ./l8-1_client_local a 2 1 '*'
$ ./l8-1_client_local a 2 0 /
$ ./l8-1_client_tcp localhost 2000 234 17 /
$ killall -s `SIGINT` prog23a_s
W tym rozwiązaniu (a także następnego zadania) wszystkie programy korzystają ze wspólnej biblioteki - inaczej każdy z nich musiałby implementować funkcje w rodzaju bulk_read
co bardzo zwiększyłoby objętość kodu.
Może zastanawiać czemu stała BACKLOG
jest ustalona na 3 a nie 5, 7 czy 9? To może być dowolna mała liczna, to tylko wskazówka dla systemu, ten program nie będzie obsługiwał dużego ruchu i kolejka czekających połączeń nie będzie nigdy duża, w praktyce połączenia są tu od razu realizowane. Przy większym ruchu trzeba empirycznie sprawdzać jaka wartość tego parametru dobrze się spisze i niestety będzie ona inna na różnych systemach.
W programie używamy makra SUN_LEN
, czemu nie sizeof? Oba rozwiązania działają poprawnie. Warto wiedzieć, że użycie zwykłego sizeof zwróci większy rozmiar niż makro a to dlatego, że rozmiar liczony przez makro to suma pól struktury (czyli typ i string) a rozmiar podany przez sizeof dodaje jeszcze kilka bajtów przerwy pomiędzy tymi polami. Implementacja oczekuje mniejszej z tych dwóch wartości ale podanie większej nic nie zepsuje ponieważ sam adres jest w postaci ciągu znaków zakończonego zerem. Co zatem wybrać? W materiałach wybieramy zgodność ze standardem czyli makro zatem oszczędzamy te kilka bajtów kosztem nieco dłuższego wyliczania rozmiaru. Jeśli użyjesz sizeof to nie będzie to traktowane jako błąd.
Standard POSIX mówi, że nawiązanie połączenia sieciowego nie może być przerwane (co jest dość logiczne ponieważ biorą w nim udział dwie strony). Zatem jeśli funkcja obsług sygnału przerwie connect
to tak naprawdę jego nawiązywanie nadal trwa asynchronicznie względem głównego kodu. Próba restartu funkcji np. makrem TEMP_FAILURE_RETRY
nie jest już możliwa (spowoduje błąd EALREADY
). W kodzie naszego klienta nie obsługujemy sygnałów, co jednak, gdybyśmy to robili? Należy sprawdzić, czy errno
jest równe EINTR
i w takim wypadku użyć funkcji select
, poll
albo epoll*
żeby poczekać aż będzie możliwy zapis.
Można od razy zażyczyć sobie asynchronicznego połączenia, wystarczy przed wywołaniem connect ustawić na deskryptorze gniazda flagę O_NONBLOC
. Nawiązywanie połączenia jest dość czasochłonne i jest to sposób aby w czasie oczekiwania program mógł coś wykonać.
W programie przepływ danych jest trywialny, klient łączy się, wysyła zapytanie i od razy dostaje odpowiedź po czym się rozłącza. Możliwe są oczywiście znacznie bardziej złożone scenariusze wymiany komunikatów (protokoły). W danym momencie komunikacji pomiędzy programami to co zostało przesłane wcześniej tworzy “kontekst” dla tego co wysyłamy. W zależności od protokołu, kontekst może być bardziej lub mniej złożony. W tym programie zapytanie jest kontekstem dla odpowiedzi, czyli odpowiedź wynika z kontekstu komunikacji (pytania).
Warto zapytać czemu służą makra ntohl i htonl użyte do konwersji byte orderu liczb skoro połączenie jest lokalne? W tym programie sporo kodu w drugim etapie będzie współdzielone z połączeniem sieciowym tcp, które takiej konwersji wymaga. Połączeniu lokalnemu taka konwersja nie przeszkadza. Warto też dodać, że nic w standardzie POSIX nie zabrania aby powstały w przyszłości gniazda lokalne działające na sieciowym systemie plików, czyli takie które będą tej konwersji jednak wymagać, nasz program jest na to gotowy.
W kodzie używana jest funkcja bulk_read
, trzeba wiedzieć, że w takiej postaci jak powyżej ta funkcja nie potrafi sobie poradzić z deskryptorem w trybie nieblokującym - zwróci błąd EAGAIN
. Czy w tym przypadku mamy taki deskryptor? Nowo otwarte (przez f. accept) połączenie dostaje nowy deskryptor, jego flagi nie muszą być dziedziczone (na Linuksie nie są) z deskryptora gniazda nasłuchującego. W tym programie nie będzie problemu bo wiemy, że dane już czekają na odbiór i pod Linuksem mamy tryb blokujący ale warto może przerobić funkcję bulk_read tak aby czekała na dane gdy gniazdo jest w trybie nieblokującym.
Obowiązuje Państwa użycie funkcji getaddrinfo, starsza funkcja gethostbyname jest w dokumentacji oznaczona jako przestarzała i ma nie być używana w pracach studenckich.
Jak po uruchomieniu serwera można podejrzeć plik gniazda?
Odpowiedź:
$ls -l a
Czym jest w programie wywołanie epoll_pwait
?Odpowiedź:
Jest punktem w którym program czeka na dostępność danych na wielu deskryptorach oraz na nadejście sygnału SIGINT
Czy można użyć tu epoll_wait
a zamiast epoll_pwait
?Odpowiedź:
Można ale nie warto bo dodanie poprawnej obsługi SIGINT
będzie wtedy bardziej pracochłonne
Czemu gniazdo sieciowe jest w trybie nieblokującym?Odpowiedź:
Bez tego trybu mogłoby się zdarzyć, że klient który chce się połączyć “zginie” pomiędzy epoll_pwait
, które potwierdzi gotowość do połączenia a accept
, które faktycznie przyjmie to połączenie. Wtedy na blokującym gnieździe program zatrzymałby się aż do nadejścia kolejnego połączenia nie reagując na sygnał SIGINT
.
Czemu użyto int32_t
(stdint.h) a nie zwykły int?Odpowiedź:
Ze względu na różne rozmiary int’a na różnych architekturach.
Czemu ignorujemy SIGPIPE
w serwerze?Odpowiedź:
Łatwiej obsłużyć błąd EPIPE
niż sygnał, zwłaszcza, że informacja o przedwczesnym zakończeniu się klienta nie może prowadzić do zamknięcia serwera.
Czemu w programie użyto bulk_read i bulk_write? Nie ma obsługi sygnałów poza SIGINT
, który i tak kończy działanie programu.Odpowiedź:
Z tego samego powodu dla którego tak często EINTR
jest obsłużony - przenośność kodu do innych rozwiązań.
Czemu służy unlink w kodzie serwera?Odpowiedź:
Usuwamy gniazdo lokalne tak samo jak usuwamy plik - funkcja porządkowa.
Czemu służy opcja gniazda SO_REUSEADDR?Odpowiedź:
Dodanie tej opcji pozwala na szybkie ponowne wykorzystanie tego samego portu serwera, jest to ważne gdy chcemy szybko poprawić jakiś błąd w kodzie i ponownie uruchomić program. Bez tej opcji system blokuje dostęp do portu na kilka minut.
Czy powyższa opcja nie naraża nas na błędy komunikacji wynikające z możliwości otrzymania jakichś pozostałości po poprzednim połączeniu?Odpowiedź:
Nie, protokół tcp jest na takie “śmieci” uodporniony. Gdyby połączenie było typu udp wtedy odpowiedź byłaby twierdząca.
Co to za adres INADDR_ANY i czemu jest często używany jako adres lokalny?Odpowiedź:
To adres specjalny postaci 0.0.0.0, który ma szczególne znaczenie w adresacji. Oznacza dowolny adres. Jeśli gniazdu serwera (nasłuchującemu) damy taki adres to oznacza, że jakiekolwiek połączenie nadejdzie do serwera, niezależnie jak zaadresowane to będzie przyjęte. To oczywiście nie oznacza, że przechwycimy wszystkie połączenia w sieci, to oznacza, że nie jest ważne jaki adres w sieci ma serwer (może mieć nawet kilka) jeśli tylko ustawienia sieci spowodują, że połączenie będzie kierowane na dany serwer to nasz program na nim będzie mógł je przyjąć nie znając swojego faktycznego lokalnego adresu!
Kod klienta jest bardzo podobny do klienta lokalnego, jako ćwiczenie proponuję zintegrowanie tych kodów do jednego programu z przełącznikiem -p local|tcp
Zadanie 2 - UDP #
Cel:
Napisz dwa programy pracujące w architekturze klient-serwer poprzez połączenie UDP. Zadanie programu klienta polega na wysłaniu pliku podzielonego na datagramy. Zadanie programu serwera polega na odbieraniu plików przesyłanych przez socket i wypisywaniu ich na ekran (bez informacji o pliku z którego dane pochodzą).
Każdy wysłany do serwera pakiet musi być potwierdzony odpowiednim komunikatem zwrotnym, w razie braku takiego zwrotnego komunikatu (czekamy 0,5s) należy ponawiać wysłanie pakietu. W razie 5 kolejnych niepowodzeń program klienta powinien zakończyć działanie. Potwierdzenia też mogą zaginąć w sieci, ale program powinien sobie i z tym radzić - serwer nie może dwa razy wypisać tego samego fragmentu tekstu.
Wszystkie dodatkowe dane (wszystko poza tekstem z pliku) przesyłane między serwerem i klientem mają mieć postać liczb typu int32_t. Należ przyjąć, że rozmiar przesyłanych jednorazowo danych (tekst z pliku i dane sterujące) nie może przekroczyć 576B. Naraz serwer może odbierać maksymalnie 5 plików, 6 jednoczesna transmisja ma być zignorowana.
Program serwer jako parametr przyjmuje numer portu na którym będzie pracował, program klient przyjmuje jako parametry adres i port serwera oraz nazwę pliku.
Co student musi wiedzieć:
- man 7 udp
- man 3p sendto
- man 3p recvfrom
- man 3p recv
- man 3p send
rozwiązanie l8-2_server.c
:
#include "l8_common.h"
#define BACKLOG 3
#define MAXBUF 576
#define MAXADDR 5
struct connections
{
int free;
int32_t chunkNo;
struct sockaddr_in addr;
};
int make_socket(int domain, int type)
{
int sock;
sock = socket(domain, type, 0);
if (sock < 0)
ERR("socket");
return sock;
}
int bind_inet_socket(uint16_t port, int type)
{
struct sockaddr_in addr;
int socketfd, t = 1;
socketfd = make_socket(PF_INET, type);
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, &t, sizeof(t)))
ERR("setsockopt");
if (bind(socketfd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
ERR("bind");
if (SOCK_STREAM == type)
if (listen(socketfd, BACKLOG) < 0)
ERR("listen");
return socketfd;
}
int findIndex(struct sockaddr_in addr, struct connections con[MAXADDR])
{
int i, empty = -1, pos = -1;
for (i = 0; i < MAXADDR; i++)
{
if (con[i].free)
empty = i;
else if (0 == memcmp(&addr, &(con[i].addr), sizeof(struct sockaddr_in)))
{
pos = i;
break;
}
}
if (-1 == pos && empty != -1)
{
con[empty].free = 0;
con[empty].chunkNo = 0;
con[empty].addr = addr;
pos = empty;
}
return pos;
}
void doServer(int fd)
{
struct sockaddr_in addr;
struct connections con[MAXADDR];
char buf[MAXBUF];
socklen_t size = sizeof(addr);
int i;
int32_t chunkNo, last;
for (i = 0; i < MAXADDR; i++)
con[i].free = 1;
for (;;)
{
if (TEMP_FAILURE_RETRY(recvfrom(fd, buf, MAXBUF, 0, &addr, &size) < 0))
ERR("read:");
if ((i = findIndex(addr, con)) >= 0)
{
chunkNo = ntohl(*((int32_t *)buf));
last = ntohl(*(((int32_t *)buf) + 1));
if (chunkNo > con[i].chunkNo + 1)
continue;
else if (chunkNo == con[i].chunkNo + 1)
{
if (last)
{
printf("Last Part %d\n%s\n", chunkNo, buf + 2 * sizeof(int32_t));
con[i].free = 1;
}
else
printf("Part %d\n%s\n", chunkNo, buf + 2 * sizeof(int32_t));
con[i].chunkNo++;
}
if (TEMP_FAILURE_RETRY(sendto(fd, buf, MAXBUF, 0, &addr, size)) < 0)
{
if (EPIPE == errno)
con[i].free = 1;
else
ERR("send:");
}
}
}
}
void usage(char *name) { fprintf(stderr, "USAGE: %s port\n", name); }
int main(int argc, char **argv)
{
int fd;
if (argc != 2)
{
usage(argv[0]);
return EXIT_FAILURE;
}
if (sethandler(SIG_IGN, SIGPIPE))
ERR("Seting SIGPIPE:");
fd = bind_inet_socket(atoi(argv[1]), SOCK_DGRAM);
doServer(fd);
if (TEMP_FAILURE_RETRY(close(fd)) < 0)
ERR("close");
fprintf(stderr, "Server has terminated.\n");
return EXIT_SUCCESS;
}
rozwiązanie l8-2_client.c
:
#include "l8_common.h"
#define MAXBUF 576
volatile sig_atomic_t last_signal = 0;
void sigalrm_handler(int sig) { last_signal = sig; }
int make_socket(void)
{
int sock;
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock < 0)
ERR("socket");
return sock;
}
void usage(char *name) { fprintf(stderr, "USAGE: %s domain port file \n", name); }
void sendAndConfirm(int fd, struct sockaddr_in addr, char *buf1, char *buf2, ssize_t size)
{
struct itimerval ts;
if (TEMP_FAILURE_RETRY(sendto(fd, buf1, size, 0, &addr, sizeof(addr))) < 0)
ERR("sendto:");
memset(&ts, 0, sizeof(struct itimerval));
ts.it_value.tv_usec = 500000;
setitimer(ITIMER_REAL, &ts, NULL);
last_signal = 0;
while (recv(fd, buf2, size, 0) < 0)
{
if (EINTR != errno)
ERR("recv:");
if (SIGALRM == last_signal)
break;
}
}
void doClient(int fd, struct sockaddr_in addr, int file)
{
char buf[MAXBUF];
char buf2[MAXBUF];
int offset = 2 * sizeof(int32_t);
int32_t chunkNo = 0;
int32_t last = 0;
ssize_t size;
int counter;
do
{
if ((size = bulk_read(file, buf + offset, MAXBUF - offset)) < 0)
ERR("read from file:");
*((int32_t *)buf) = htonl(++chunkNo);
if (size < MAXBUF - offset)
{
last = 1;
memset(buf + offset + size, 0, MAXBUF - offset - size);
}
*(((int32_t *)buf) + 1) = htonl(last);
memset(buf2, 0, MAXBUF);
counter = 0;
do
{
counter++;
sendAndConfirm(fd, addr, buf, buf2, MAXBUF);
} while (*((int32_t *)buf2) != (int32_t)htonl(chunkNo) && counter <= 5);
if (*((int32_t *)buf2) != (int32_t)htonl(chunkNo) && counter > 5)
break;
} while (size == MAXBUF - offset);
}
int main(int argc, char **argv)
{
int fd, file;
struct sockaddr_in addr;
if (argc != 4)
{
usage(argv[0]);
return EXIT_FAILURE;
}
if (sethandler(SIG_IGN, SIGPIPE))
ERR("Seting SIGPIPE:");
if (sethandler(sigalrm_handler, SIGALRM))
ERR("Seting SIGALRM:");
if ((file = TEMP_FAILURE_RETRY(open(argv[3], O_RDONLY))) < 0)
ERR("open:");
fd = make_socket();
addr = make_address(argv[1], argv[2]);
doClient(fd, addr, file);
if (TEMP_FAILURE_RETRY(close(fd)) < 0)
ERR("close");
if (TEMP_FAILURE_RETRY(close(file)) < 0)
ERR("close");
return EXIT_SUCCESS;
}
Zwróć uwagę, że w protokole UDP nie nawiązujemy połączenia, gniazda komunikują się ze sobą “ad hoc”. Nie ma gniazda nasłuchującego. Możliwe są straty, duplikaty i zmiany kolejności datagramów. W przykładzie występują kolejne przydatne do biblioteki wariacje funkcji: make_socket, bind_inet_socket, ponieważ mają te same nazwy co funkcje użyte w poprzednim zadaniu trzeba je inaczej ponazywać.
W tym zadaniu kontekst połączenia jest ważny i wymaga wysiłku aby go utrzymać. Co jest kontekstem połączenia?Odpowiedź:
Kontekstem jest ilość poprawnie przesłanych pakietów do danej chwili.
Jakie dane są przesyłane w pojedynczym datagramie? Czemu służą przesyłane metadane?Odpowiedź:
Pakiet składa się z (1) 32 bitowego numeru fragmentu, (2) 32 bitowej informacji czy to ostatni fragment oraz (3) z fragmentu pliku. Metadane służą do kontroli kontekstu (1) oraz do zakończenia transmisji (2).
Czemu i na jakich deskryptorach są używane funkcje bulk_read i bulk_write, czy nie powinno się rozszerzyć tego użycia na wszystkie deskryptory?/br>Odpowiedź:
Funkcje są potrzebne do restartowania read i write w sytuacji przerwania w trakcie operacji IO ( w odróżnieniu od EINTR
czyli przerwania przed operacją). Funkcje te są używane tylko do działań na plikach ponieważ przesyłanie datagramów jest ATOMOWE i nie może być przerwane w trakcie. W tym programie występuje obsługa sygnałów ale tam gdzie się ich spodziewamy nie dokonujemy operacji na plikach. To zabezpieczenie jest nadmiarowe, dodane z myślą o przenoszeniu tego kodu do innych programów.
Czy może wystąpić sytuacja zerwania połączenia? Czy nie powinniśmy tego rozpoznawać?Odpowiedź:
Nie może, udp nie wytwarza połączenia, które mogłoby być zerwane.
Przeanalizuj jak działa findIndex w serwerze, zwłaszcza jak są porównywane adresy. W jakim byte orderze są? Jak zachowa się ta funkcja jeśli adres jest nowy?Odpowiedź:
Porównywane adresy są w byte order sieci, nie mamy potrzeby ich konwertować skoro jedynie je porównujemy a nie np. wyświetlamy. Funkcja dla nowego adresu zakłada nowy rekord (o ile ma jeszcze wolne miejsce w tablicy).
Jak sobie poradzimy z duplikatami datagramów?Odpowiedź:
Trzymamy tablice stanu połączeń “struct connections”, wiemy, który fragment już wypisaliśmy i nie powtarzamy go.
Jak sobie poradzimy, z odwrotną kolejnością datagramów, czyli gdy otrzymamy fragment dalszy niż aktualnie oczekiwany?Odpowiedź:
Odwrócenie nie może się zdarzyć, bo klient nie prześle dalszych części dopóki nie potwierdzimy wcześniejszych.
Jak sobie poradzimy z ginącymi pakietami ?Odpowiedź:
Obsługuje to retransmisja po stronie klienta.
Co się stanie jeśli zaginie potwierdzenie pakietu a nie sam pakiet?Odpowiedź:
Klient uzna, że pakiet nie dotarł i prześle go ponownie. Serwer nie wyświetli pakietu drugi raz ale odeśle potwierdzenie po raz kolejny.
Co zawierają potwierdzenia?Odpowiedź:
Odsyłamy to co dostaliśmy, cały pakiet bez zmiany.
Jak jest zaimplementowany timeout na odpowiedź od serwera?Odpowiedź:
W funkcji sendAndConfirm najpierw ustawiamy alarm na 0.5 sekundy (setitimer) następnie program stara się odebrać potwierdzenie. Brak restartu funkcji recv makrem nie jest przypadkowy, po ew. przerwaniu musimy móc sprawdzić czy to nie był oczekiwany timeout.
Czemu konwertujemy tylko byte order numeru fragmentu i znacznika ostatniego elementu a reszta danych nie jest odwracana?Odpowiedź:
Tylko te dwie dane są przesyłane jako liczby binarne, reszta to tekst, który nie wymaga tego zabiegu.
Przeanalizuj jak działa limitowanie do 5 połączeń, zwróć uwagę na pole free w strukturze i znaczenie znacznika ostatniego fragmentu przesyłanego przez klienta.
Przykładowe zadanie #
Wykonaj przykładowe zadania. Podczas laboratorium będziesz miał więcej czasu oraz dostępny startowy kod, jeśli jednak wykonasz poniższe zadania w przewidzianym czasie, to znaczy że jesteś dobrze przygotowany do zajęć.