Komunikacja z wykorzystaniem TCP/IP TCP jest protokołem
Transkrypt
Komunikacja z wykorzystaniem TCP/IP TCP jest protokołem
Komunikacja z wykorzystaniem TCP/IP TCP jest protokołem połączeniowym, czyli przed przesłaniem danych przez użytkownika konieczne jest zestawienie połączenia, potem następuje faza przesyłania danych, a następnie połączenie jest zamykane rys.1. Serwer socket() bind() Klient listen() socket() accept() connect() recv() send() send() recv() close() close() Rys 1. Schemat blokowy przykładowego programu klient-serwer Aby stworzyć takie połączenie - gniazdo internetowe, należy wywołać funkcję socket(),w tym celu potrzebne są dwa pliki nagłówkowe: #include <sys/types.h> #include <sys/socket.h> int socket ( int family, int type, int protocol ); Jest to funkcja systemowa, która zwraca deskryptor gniazda, bądź wartość -1 w przypadku błędu utworzenia. Parametry funkcji to: family – jest przestrzenią nazw dla rodziny protokołów, który można wybrać zamiennie jako: AF_INET lub PF_INET, gdy mamy doczynienia z protokołami TCP/IP type – określa typ gniazda, SOCK_STREAM gniazdo strumieniowe, pakiety będą odbierane sekwencyjnie w kolejności w jakiej zostały wysłane. protocol – typ protokołu, jeśli parametr będzie miał wartość 0 funkcja socket() wybierze właściwy protokół bazując na parametrze type. mainSock = socket ( AF_INET, SOCK_STREAM, 0 ); Gniazdo należy powiązać z adresem na konkretnej maszynie i do tego służy funkcja systemowa bind(). int bind ( int mainSock, struct sockaddr *myAddr, int addrLen ); Dowiązuje ona do gniazda adres i port lokalny. Pierwszy argument jest deskryptorem gniazda zwróconym przez funkcję socket(). Drugi argument jest wskaźnikiem do struktury zawierającej adres IP oraz port serwera, który ma być powiązany z gniazdem. Deklaracja i wypełnienie struktury: #include <netdb.h> struct sockaddr_in mainAddr; mainAddr.sin_family = AF_INET; mainAddr.sin_port = htons(5150); mainAddr.sin_addr.s_addr = htonl(INADDR_ANY); //rodzina adresów //numer portu //adres IP memset(&(mainAddr.sin_zero), 0, 8); //dla zachowania odpowiedniego rozmiaru struktury Struktura ta ułatwia dostęp do elementów adresu gniazda. Parametr struktury sin_zero ustawia się na 0 poprzez funkcję memset(). Trzeci argument funkcji bind() określa rozmiar struktury zawierającej adres. W przypadku błędu funkcja bind() zwraca -1. bind ( mainSock, ( struct sockaddr *) &mainAddr, sizeof(mainAddr)); Aby połączyć się z innym hostem serwer musi oczekiwać (nasłuchiwać) na przychodzące połączenia. Do nasłuchiwania przeznaczona jest funkcja listen(). int listen ( int mainSock, int backlog ); Pierwszy argument jest deskryptorem gniazda zwróconym przez funkcję socket(). Drugi argument jest ilością dozwolonych żądań nawiązania połączenia do obsługi. Serwer jest wstanie nawiązywać w danej chwili jedno połączenie. W tym czasie mogą inne hosty mogą żądać obsługi swoich połączeń, które są wówczas ustawiane w kolejce. Funkcja listen() zwraca -1 w przypadku błędu. listen ( mainSock, 10 ); Funkcja connect() tworzy połączenie między dwoma gniazdami (klient-serwer). int connect( int mainSock, struct sockaddr *servAddr, int addrLen ); Pierwszy argument jest deskryptorem gniazda zwróconym przez funkcję socket(). Drugi argument jest wskaźnikiem do struktury zawierającej adres IP oraz port serwera, z którym ma zostać nawiązane połączenie. Trzeci argument określa rozmiar struktury zawierającej adres. Funkcja connect() zwraca -1 w przypadku błędu. connect ( mainSock, (struct sockaddr *) &servAddr, sizeof ( struct sockaddr ) ); Deklaracja i wypełnienie struktury sockaddr_in w kliencie ma postać: struct sockaddr_in servAddr; servAddr.sin_family = AF_INET; //rodzina adresów servAddr.sin_port = htons(5150); //numer portu servAddr.sin_addr.s_addr = inet_addr(argv[1]); //adres IP przekazywany przez parametr argv[1] funkcji main() memset(&(servAddr.sin_zero), 0, 8); //dla zachowania odpowiedniego rozmiaru struktury Aby zaakceptować przychodzące połączenie serwer musi wywołać funkcję accept(), która blokuje wykonywanie dalszych instrukcji programu dopóki nie pojawi się żądanie nawiązania połączenia. int accept(int mainSock, struct sockaddr *clientAddr, int addrLen ); Pierwszy argument funkcji jest deskryptorem gniazda głównego zwróconym przez funkcję socket().Drugi jest wskaźnikiem do struktury, w której zostanie zapisany adres obsługiwanego klienta. Trzeci argument określa rozmiar struktury przechowującej adres połączenia. Funkcja accept() tworzy nowe gniazdo do obsługi połączenia z danym klientem i zwraca jego deskryptor. Obsługa połączenia powinna odbywać się w osobnym procesie (fork()). int addrLen = sizeof ( clientAddr ); clientSock = accept ( mainSock, &addrLen ); (struct sockaddr *) &clientAddr, Wysyłanie danych jest realizowane przez funkcje send(). int send ( int clientSock, void *msg, int len, int flags ); Parametry tej funkcji to: clientSock – jest deskryptorem gniazda, do którego są wysyłane dane zwróconym przez funkcję accept(), *msg – jest wskaźnikiem na wiadomość, która ma być przesłana, len – odpowiada długości przesyłanej wiadomości, flags – flaga, gdy jest przypisana jej wartość 0 to powoduje zablokowanie funkcji send() do momentu aż wszystkie dane zostaną wysłane Funkcja send() zwraca ilość wysłanych bajtów, bądź -1 w przypadku wystąpienia błędu send ( clientSock, bufor, 100, 0 ); Odbieranie danych jest realizowane przez funkcje recv(). int recv ( int clientSock, void *buf, int len, int flags ); Parametry tej funkcji to: clientSock – jest deskryptorem gniazda, z którego są odbierane dane zwróconym przez funkcję accept(), *buf – jest buforem, do którego zostaną zapisane odebrane dane, len – odpowiada wielkości bufora, flags – flaga, gdy jest przypisana jej wartość 0 to powoduje zablokowanie funkcji send() do momentu aż wszystkie dane zostaną wysłane Funkcja recv() zwraca ilość otrzymanych bajtów, bądź -1 w przypadku wystąpienia błędu, bądź 0 jeśli zostało zamknięte połączenie. recv ( clientSock, buf, sizeof(buf), 0 ); Funkcja systemowa służąca do zamykania gniazda ma postać: int close ( int mainSock ); Parametrem funkcji jest deskryptor zamykanego gniazda. close ( mainSock ); W celu ustalenia przyczyny błędu można posłużyć się globalną zmienną errno ustawianą przez każdą z opisanych funkcji. Przykład obsługi błędu dla funkcji socket(): #include <errno.h> if( (mainSock = socket ( AF_INET, SOCK_STREAM, 0 ) ) == -1 ) { perror(“socket”); return 0; } Zadania do wykonania: 1. Wzorując się na rysunku 1. oraz fragmentach kodu zawartego w instrukcji napisać aplikację klient-serwer. Serwer ma za zadanie odsyłać wszystko co odbierze od klienta, natomiast klient ma wysyłać dane o długości 10, 100, 1000 oraz 10000 bajtów. Należy zmierzyć czas transmisji przesyłanych danych, przy założeniu że czas pomiaru powinien przekraczać 10 sekund. Aby sprawdzić z kim serwer nawiązał połączenie można posłużyć się poniższą instrukcją, wpisując ją po wywołaniu przez serwer funkcji accept(). printf(“Server otrzymal polaczenie inet_ntoa(clientAddr.sin_addr)); od %s\n”, 2. Wzorując się na rysunku 1. oraz fragmentach kodu zawartego w instrukcji napisać aplikację klient-serwer. Proces klienta ma za zadanie wysyłać do serwera znaki pobrane z klawiatury, natomiast serwer wyświetla wszystko co otrzymał od klienta i odsyła mu z powrotem. Wysyłanie i odbieranie danych przez klienta powinno się odbywać w odrębnych procesach, aby nie blokować działania programu. Aby pobrać znaki z klawiatury należy użyć funkcji: char tablica_znakow[rozmiar]; scanf(„%s”, tablica_znakow); W procesie kompilacji programów konieczne jest dodanie biblioteki „gniazd” (ang. socket) umożliwiających wykorzystanie protokołu TCP/IP do komunikacji. cc –o nazwa nazwa.c /lib/libsocket.so