SIK03.rst

Sieci Komputerowe

Programowanie socketów. Protokół TCP *

Protokół TCP

Protokół TCP/IP to zestaw standardowych reguł używanych do ustanawiania połączeń między komputerami w sieciach. TCP jest protokołem działającym w trybie klient-serwer. Serwer oczekuje na nawiązanie połączenia na określonym porcie, natomiast klient inicjuje połączenie do serwera. TCP ma zapewnić pewny kanał transmisji, w którym dotarcie danych do celu jest potwierdzane przez odbiorcę. W razie potrzeby dane są retransmitowane. Innymi słowy aplikacja, która używa TCP do przesłania danych do odbiorcy może się już nie martwic, czy poszczególne pakiety IP dotarły do odbiorcy. O kontrole tego zadba TCP i tylko zbiorczo, na koniec, poinformuje aplikacje, czy transmisja zakończyła się sukcesem.

Reasumując protokół TCP charakteryzuje się następującymi cechami:

* jest zorientowany na połączenie: oznacza to, że program użytkowy, który chce skorzystać z protokółu TCP musi najpierw zwrócić się do odbiorcy z prośbą o uzyskanie połączenia i uzyskać jego zgodę;
* jest protokółem typu punkt-punkt: oznacza to, że każde połączenie TCP ma dokładnie dwa końce;
* zapewnia niezawodność: oznacza to, że protokół TCP zapewnia pełną niezawodność w dostarczaniu pakietów;
* zapewnia dwukierunkową komunikację: oznacza to, że komunikacja w połączeniu TCP odbywa się w dwu kierunkach, czyli zarówno od nadawcy do odbiorcy jak i od odbiorcy do nadawcy;
* zapewnia strumieniowy interfejs: oznacza to, że program może wysyłać połączeniem całą sekwencję bajtów, w konsekwencji prowadzi to do tego, że dane nie muszą być dostarczane do odbiorcy w kawałkach tych samych wielkości, w których zostały wysłane;
* zapewnia łagodne kończenie połączenia: oznacza to, ze protokół gwarantuje niezawodne dostarczenie pakietów przed zamknięciem połączenia.

Retransmisja

Jednym z mechanizmów zapewniających niezawodność transportu danych jest retransmisja. Polega on na tym, ze odbiorca po odebraniu danych zobowiązany jest do przesłania do nadawcy potwierdzenia odebrania danych. Jeżeli potwierdzenie nie nadejdzie w określonym czasie, to nadawca wysyła dane ponownie.

Komunikacja

TCP jest protokołem zorientowanym połączeniowo. Zanim rozpocznie się transmisja danych, dwa komunikujące się hosty przechodzą przez proces synchronizacji wirtualnego połączenia. Synchronizacja zapewnia, że obydwie strony są gotowe do transmisji danych i pozwala urządzeniom ustalić początkowe numery sekwencyjne. Proces ten jest znany jako "trzykrotny uścisk dłoni" (ang. three way handshake). Jest to trzy etapowy proces ustanawiania wirtualnego połączenia między nadawcą i odbiorcą.

> W pierwszym kroku host inicjujący (klient) wysyła segment synchronizacji (z ustawioną flagą SYN). Segment ten zawiera początkowy numer sekwencyjny dla tej sesji (ozn. x). Ustawienie bitu SYN w polu "kod" nagłówka TCP oznacza żądanie nawiązania połączenia. Numer sekwencyjny jest wartością 32-bitową.
> W pierwszym kroku host inicjujący (klient) wysyła segment synchronizacji (z ustawioną flagą SYN). Segment ten zawiera początkowy numer sekwencyjny dla tej sesji (ozn. x). Ustawienie bitu SYN w polu "kod" nagłówka TCP oznacza żądanie nawiązania połączenia. Numer sekwencyjny jest wartością 32-bitową.

Schemat:

a2

W trzecim kroku host inicjujący nawiązanie połączenia odpowiada prostym potwierdzeniem z wartością y+1, która jest równa numerowi sekwencyjnemu hosta B powiększonemu o 1. Oznacza to, że klient otrzymał potwierdzenie wysłane przez serwer i kończy proces nawiązywania połączenia.

Ważne! Początkowe numery sekwencyjne są używane do inicjalizacji komunikacji pomiędzy dwoma urządzeniami. Pełnią one rolę punktów odniesienia. Numery sekwencyjne pozwalają na precyzyjne informowanie nadawcy o odebraniu konkretnego segmentu danych lub żądania nawiązania połączenia.

Zakończenie i zamknięcie połączenia wygląda następująco:

a3

Flagi TCP

A, ACK- (Potwierdzenie) Odbiorca wysyła flagę ACK, która jest równa numerowi sekwencji nadawcy plus parametr Len lub liczba danych w warstwie TCP.

Flagi SYN i FIN są liczone jako 1 bajt. Flaga ACK może również oznaczać numer sekwencji następnego oktetu oczekiwanego przez odbiorcę.

S, SYN- Synchronizowanie jest używane w czasie konfigurowania sesji do uzgodnienia początkowych numerów sekwencji. Numery sekwencji są losowe.
F, FIN- Zakończenie jest używane podczas bezpiecznego zamknięcia sesji i oznacza, że nadawca nie ma już danych do wysłania.
R, RST- Zresetowanie to natychmiastowe przerwanie w dwóch kierunkach (nieprawidłowe rozłączenie sesji).
P, PSH- Pchnięcie wymusza dostarczenie bez oczekiwania na wypełnienie się buforów. Dane zostaną dostarczone również do aplikacji w miejscu docelowym bez buforowania.
U, URG- Pilne - Dane są wysyłane poza pasmem.

Interfejs gniazd BSD

Począwszy od wersji 4.1 systemu UNIX BSD, wprowadzono do niego obsługę protokołów TCP/IP wraz z interfejsem dostępu dla programów o nazwie gniazda (ang. sockets). Interfejs ten jest także dostępny w systemach Linux, a jego zadaniem jest pośredniczenie pomiędzy programami użytkowników, a implementacją stosu TCP/IP.

W procesie systemowym gniazdo jest traktowane jak plik specjalny, po jego utworzeniu programista otrzymuje deskryptor, co pozwala na realizację funkcji odczytu i zapisu, w sposób analogiczny jak na pliku (np. read() i write()).

Przeczytaj informacje na temat socketów (do miejsca na temat Socketów Windows) na stronie:

a51

Przykłady

Przykład obrazuje różnice w reprezentacji liczb całkowitych w lokalnym formacie hosta i w formacie sieciowym.

#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <arpa/inet.h>

int main(void) {
        char abcd[512];
        unsigned long adrsiec;
        unsigned char *jako_bajty =
        (unsigned char*) &adrsiec;

        printf("Podaj adres IP w formacie a.b.c.d: ");

        scanf("%s",abcd);
        adrsiec = inet_addr(abcd);

        if (adrsiec == 0xffffffff) {
                printf("To nie jest prawidlowy adres IP!\n");
                return 1;
        }
        printf("Adres w formacie sieci to %u, hex=%X\n",adrsiec, adrsiec);

        printf("Adres bajt po bajcie (hex): %X %X %X %X\n",
                jako_bajty[3], jako_bajty[2], jako_bajty[1], jako_bajty[0]);

        printf("Adres bajt po bajcie (dec): %u %u %u %u\n",
                jako_bajty[3], jako_bajty[2], jako_bajty[1], jako_bajty[0]);
        return 0;
}

Przykład pokazuje sposób wykonywania konwersji z adresu w formacie a.b.c.d na adres sieciowy

#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <arpa/inet.h>

int main(void) {
        char abcd[512];
        unsigned long adrsiec;
        unsigned char *jako_bajty =
        (unsigned char*) &adrsiec;

        printf("Podaj adres IP w formacie a.b.c.d: ");

        scanf("%s",abcd);
        adrsiec = inet_addr(abcd);

        if (adrsiec == 0xffffffff) {
                printf("To nie jest prawidlowy adres IP!\n");
                return 1;
        }
        printf("Adres w formacie sieci to %u, hex=%X\n",adrsiec, adrsiec);

        printf("Adres bajt po bajcie (hex): %X %X %X %X\n",
                jako_bajty[0], jako_bajty[1], jako_bajty[2], jako_bajty[3]);

        printf("Adres bajt po bajcie (dec): %u %u %u %u\n",
                jako_bajty[0], jako_bajty[1], jako_bajty[2], jako_bajty[3]);
        return 0;
}

Przykład pokazuje sposób wykonywania konwersji z adresu w postaci sieciowym na adres postaci a.b.c.d.

#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(void) {
        unsigned long adr;
        unsigned char *jako_bajty =
        (unsigned char *) &adr;
        char *abcd;
        struct in_addr tmp;

        printf("Podaj adres IP jako liczbe dziesietna w formacie sieci: ");
        scanf("%u",&adr);
        printf("Adres (hex): %X\n", adr);

        printf("Adres jako bajty (hex): %X %X %X %X\n",
                jako_bajty[0], jako_bajty[1],jako_bajty[2], jako_bajty[3]);

        printf("Adres jako bajty (dec): %u %u %u %u\n",
                jako_bajty[0], jako_bajty[1], jako_bajty[2], jako_bajty[3]);

        tmp.s_addr = adr;
        abcd = inet_ntoa(tmp);

        printf("Adres w formacie a.b.c.d: %s\n", abcd);

        return 0;
}

Serwer

Bardzo prosta aplikacja klient-serwer oparta na protokole TCP. Serwer nasłuchuje na porcie 8888;

/*
        C socket server example
*/

#include<stdio.h>
#include<string.h>    //strlen
#include<sys/socket.h>
#include<arpa/inet.h> //inet_addr
#include<unistd.h>    //write

int main(int argc , char *argv[])
{
        int socket_desc , client_sock , c , read_size;
        struct sockaddr_in server , client;
        char client_message[2000];

        //Create socket
        socket_desc = socket(AF_INET , SOCK_STREAM , 0);
        if (socket_desc == -1)
        {
                printf("Could not create socket");
        }
        puts("Socket created");

        //Prepare the sockaddr_in structure
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = INADDR_ANY;
        server.sin_port = htons( 8888 );

        //Bind
        if( bind(socket_desc,(struct sockaddr *)&server , sizeof(server)) < 0)
        {
                //print the error message
                perror("bind failed. Error");
                return 1;
        }
        puts("bind done");

        //Listen
        listen(socket_desc , 3);

        //Accept and incoming connection
        puts("Waiting for incoming connections...");
        c = sizeof(struct sockaddr_in);

        //accept connection from an incoming client
        client_sock = accept(socket_desc, (struct sockaddr *)&client, (socklen_t*)&c);
        if (client_sock < 0)
        {
                perror("accept failed");
                return 1;
        }
        puts("Connection accepted");

        //Receive a message from client
        while( (read_size = recv(client_sock , client_message , 2000 , 0)) > 0 )
        {
                //Send the message back to client
                write(client_sock , client_message , strlen(client_message));
        }

        if(read_size == 0)
        {
                puts("Client disconnected");
                fflush(stdout);
        }
        else if(read_size == -1)
        {
                perror("recv failed");
        }

        return 0;
}

Przetestuj działanie serwera łącząc się z nim korzystając z netcata (nc twój_IP 8888).

Klient

Prosta implementacja klienta.

#include <stdio.h>
#include <stdlib.h>

#include <netdb.h>
#include <netinet/in.h>

#include <string.h>

int main(int argc, char *argv[]) {
        char abcd[512];
        int sockfd, portno, n;
        struct sockaddr_in serv_addr;
        struct hostent *server;

        char buffer[256];

        printf("Podaj adres IP odbiorcy: ");
        scanf("%s", abcd);
        printf("Podaj numer portu odbiorcy: ");
        scanf("%u", &portno);

        /* Create a socket point */
        sockfd = socket(AF_INET, SOCK_STREAM, 0);

        if (sockfd < 0) {
                perror("ERROR opening socket");
                exit(1);
        }

        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(portno);
        serv_addr.sin_addr.s_addr = inet_addr(abcd);

        /* Now connect to the server */
        if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
                perror("ERROR connecting");
                exit(1);
        }

        /* Now ask for a message from the user, this message
                * will be read by server
        */

        printf("Please enter the message: ");
        bzero(buffer,256);
        scanf("%s", buffer);

        /* Send message to the server */
        n = write(sockfd, buffer, strlen(buffer));

        if (n < 0) {
                perror("ERROR writing to socket");
                exit(1);
        }

        /* Now read server response */
        bzero(buffer,256);
        n = read(sockfd, buffer, 255);

        if (n < 0) {
                perror("ERROR reading from socket");
                exit(1);
        }

        printf("%s\n",buffer);
        return 0;
}

Zadanie (1 pkt)

Podaj adres witryny amu.edu.pl w formacie sieciowym.

Zadanie (5 pkt)

Stwórz serwer, który będzie w pętli odczytywał komunikaty od klienta (będące liczbami naturalnymi z przedziały 1-100000) i odsyłał do klienta liczbę o jeden większą.

URUCHOM SWÓJ SERWER

Po uruchomieniu połącz się za pomocą netcata z adresem 150.254.78.69 port 5055 i podaj swój numer indeksu numer portu oraz adres IP swojego serwera.

Program prowadzącego automatycznie odpyta Twój serwer w celu sprawdzenia poprawności.

Podejrzyj przez stronę

, czy Ci się udało.

Uwaga! Zadanie to musi być wykonywane wewnątrz sieci wydziałowej - komputer wydziałowy lub przez połączenie SSH.

Pomocne Państwu mogą być funkcje atoi oraz sprintf.

W moodle prześlij jako odpowiedź swój numer indeksu.

Zadanie (2 pkt)

Sprawdź i opisz jak zachowa się serwer TCP (na podstawie serwera napisanego w C i klienta w postaci netcata) przy 2 i więcej połączeniach jednoczesnych (dwóch klientów stara się połączyć). Czy widzisz jakieś problemy (niepożądane własności) w działaniu serwera, dlaczego tak jest?

Zadanie (2 pkt)

Napisz co stanie się gdy w bind(), server.sin_port podam jako numer portu 0.