1
Laboratorium Systemów Operacyjnych
Ćwiczenie 10. Protokół TCP-IPProtokół TCP - IP został opracowany w latach 70 w ramach projektu, którego celem było opracowanie sieci komputerowej odpornej na uszkodzenia węzłów oraz połączeń między nimi. Zastosowana sieć posiada pewną nadmiarowość połączeń, wybór trasy w sieci o takiej strukturze, wymagał opracowania specjalnych algorytmów.
Protokół TCP – IP składa się z dwóch oddzielnych protokołów: TCP oraz IP. Protokół IP odpowiedzialny jest za adresowanie węzłów sieci. Protokół ten nie gwarantuje, że pakiety dotrą do adresata, że nie zostaną pofragmentowane, bądź zdublowane lub dotrą do odbiorcy w odmiennej kolejności. Za uporządkowanie danych odpowiada protokół z wyższej warstwy (UDP oraz TCP).
Protokół UDP jest protokołem bezpołączeniowym. Dane wymieniane są pomiędzy komputerami w ramkach, a nadawca nie wie czy odbiorca odebrał wysłane przez niego dane. Ze względu na obecność sum kontrolnych, odebrane dane zawsze są poprawne. Dane mogą przychodzić do odbiorcy w innej kolejności niż zostały wysłane.
Protokół TCP jest protokołem połączeniowym. Dostarcza dane w takiej samej kolejności jak zostały wysłane. W razie konieczności dokonuje retransmisji danych. Z uwagi na konieczność zapewniania informacji zwrotnych pomiędzy nadawcą i odbiorcą (potwierdzenie odebrania danych) jest on wolniejszy od protokołu UDP.
Model protokołu TCP-IP nawiązuje do modelu OSI i składa się z kilku warstw, z których każda odpowiada za inne zagadnienie komunikacji pomiędzy węzłami sieci:
warstwa aplikacji – jest to warstwa w której znajdują się programu użytkownika, warstwa transportowa – jest to warstwa odpowiedzialna za kierowanie pakietów do
odpowiednich aplikacji (każdy komputer posiada zestaw adresowalnych portów internetowych),
warstwa Internetu - w tej warstwie przetwarzane są pakiety posiadające adres IP, które są kierowane do odpowiednich komputerów,
warstwa dostępu do sieci – jest to warstwa odpowiedzialna za przesyłanie danych pomiędzy węzłami sieci.
Z punktu widzenia programu komunikacja z wykorzystaniem gniazd internetowych nie różni się w znaczącym stopniu od komunikacji z wykorzystaniem dwukierunkowego potoków lub plików. Opracowane na potrzeby systemu BSD API stało się nieformalnym standardem i z niewielkimi modyfikacjami jest dostępne pod większością współczesnych systemów operacyjnych (Linux, Windows).
Nowe gniazdo internetowe tworzymy za pomocą funkcji socket(), zwracającej deskryptor połączenia:
int socket(int domain, int type, int protocol);
Do inicjalizacji nasłuchującego gniazda (serwera) wykorzystujemy następujące funkcje (w kolejności ich wywołania):
2
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen); int listen(int sockfd, int backlog);
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
Poniżej zamieszczono przykład inicjalizacji serwera nasłuchującego na nowe połączenia: #include <sys/types.h>
#include <sys/socket.h> #include <sys/ioctl.h> #include <netinet/in.h> #include <arpa/inet.h>
struct sockaddr_in serv_addr, cli_addr;
int socServer = socket(AF_INET, SOCK_STREAM, 0); if (socServer < 0)
{
perror("ERROR opening socket"); return 1; }
bzero((char *) &serv_addr, sizeof(serv_addr));
int portno = 1111; //nr portu na którym nasłuchujemy na połączenia serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(portno);
if (bind(socServer, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
perror ("ERROR on binding"); return 1; }
listen(socServer,5);
socklen_t clilen = sizeof(cli_addr);
int newsockfd = accept(socServer, (struct sockaddr *) &cli_addr, &clilen); if (newsockfd < 0)
{
perror ("ERROR on accept"); return 1; }
Funkcja accept() oczekuje na przychodzące połączenia i zwraca dla każdego nowego klienta nowe gniazdo służące do komunikacji z nowym klientem. Pierwotne gniazdo serwera jest wykorzystywane wyłącznie do nawiązywania nowych połączeń z klientami. Komunikacja z klientami następuje poprzez nowo utworzone gniazda zwrócone przez funkcję accept(). Do inicjalizacji nowego połączenia klienta z serwerem wykorzystujemy funkcję:
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
Poniżej zamieszczono przykład nawiązania połączenia przez klienta z serwerem:
int conSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (conSocket < 0 )
{
3
}
struct sockaddr_in clientService; clientService.sin_family = AF_INET;
clientService.sin_addr.s_addr = inet_addr( "127.0.0.1"); //ip komputera z którym się łączymy
clientService.sin_port = htons( 1111 ); //nr portu na który chcemy się połączyć if( connect( conSocket, (struct sockaddr *) &clientService, sizeof(clientService) ) <0) {
perror("Error at connect:"); return 1; }
Do wysyłania oraz odbierania danych zarówno po stronie klienta i serwera wykorzystuje się funkcje:
ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t send(int sockfd, const void *buf, size_t len, int flags);
Po zakończeniu komunikacji przez gniazdo należy je zamknąć z wykorzystaniem funkcji:
ssize_t close(int sockfd);
Zadania do samodzielnego wykonania
1. Napisz program klienta wysyłający komunikat tekstowy do serwera (np.: login) i wyświetlający odpowiedź. Serwer nasłuchuje pod adresem ip 157.158.48.103 na porcie 1111.
2. Napisz program serwera zmieniający w odebranym komunikacie wszystkie małe znaki na duże i wysyłający odpowiedź do klienta.