sik04.rs

Sieci Komputerowe

Programowanie socketów. Protokół UDP *

Protokół UDP

a1

Protokół UDP jest pozbawiony wszystkich funkcji TCP. Oferuje usługę w której mogą wystąpić straty pakietów.

Protokół UDP jest bezpołączeniowy. Nie wymaga istnienia żadnego połączenia. Klient UDP może utworzyć gniazdo i wysłać datagram do jakiegoś serwera, po czym może natychmiast przez to samo gniazdo wysłać kolejne datagramy do różnych innych serwerów. Podobnie serwer przez jedno gniazdo może przyjmować datagramy od różnych klientów.

Należy podkreślić, że wiadomość zostanie odebrana tylko wtedy, gdy adresat oczekuje na odbiór datagramu, w przeciwnym wypadku wiadomość jest ignorowana.

Protokół jest zorientowany transakcyjnie, a dostarczenie wiadomości nie jest gwarantowane. Nie mamy żadnej informacji na temat tego, czy wysłane pakiety dotarły do celu. Nie ma też mechanizmy retransmisji (jak to było w przypadku TCP).

Komunikaty UDP mogą być gubione, duplikowane lub przychodzić w innej kolejności niż były wysłane, ponadto pakiety mogą przychodzić szybciej niż odbiorca może je przetworzyć.

Jest to protokół bezpołączeniowy, więc nie ma narzutu na nawiązywanie połączenia i śledzenie sesji (w przeciwieństwie do TCP). Nie ma też mechanizmów kontroli przepływu i retransmisji. Korzyścią płynącą z takiego uproszczenia budowy jest większa szybkośćtransmisji danych i brak dodatkowych zadań, którymi musi zajmować się host posługujący siętym protokołem. Z tych względów UDP jest często używany w takich zastosowaniach jak wideokonferencje, strumienie dźwięku w Internecie i gry sieciowe, gdzie dane muszą byćprzesyłane możliwie szybko, a poprawianiem błędów zajmują się inne warstwy modelu OSI.

Oprócz wysyłanych danych, każdy komunikat UDP zawiera numer portu odbiorcy i numer portu nadawcy, dzięki czemu oprogramowanie UDP odbiorcy może dostarczyć komunikat do właściwego adresata oraz umożliwia wysłanie odpowiedzi.

a5

Funkcje dla UDP

Przy korzystaniu z interfejsu UDP program, wysyłając każdy komunikat, musi wskazywać adresata. Program użytkowy korzystający z UDP może wysyłać ciąg komunikatów, z których każdy będzie skierowany do innego odbiorcy.

W przypadku komunikacji połączeniowej używamy funkcji recv() oraz send(). Ich odpowiedniki dla UDP, w których jawnie należy podać adres adresata lub wysyłającego to: sendto() i recvfrom().

int sendto ( int socket , void *buffer , size_t nbytes , int flags , struct sockaddr *receiver , int addrlen ) (prototyp w sys/socket.h)

Funkcja ta wysyła nbytes bajtów z bufora wskazywanego przez buffer za pomocą gniazda socket pod adres wskazany przez receiver (addrlen jest wielkością struktury sockaddr). Zwraca błąd (-1) praktycznie tylko w przypadku, gdy socket jest nieprawidłowy (np. nieprzydzielony) lub adres nie jest prawidłowo wypełniony. Nie ma kontroli błędów transmisji. Parametr flags ma wartość 0.

int recvfrom ( int socket , void *buffer , size_t nbytes , int flags , struct sockaddr *sender , int *addrlen ) (prototyp w sys/socket.h)

Funkcja ta jest blokująca - odbiera co najwyżej nbytes bajtów za pośrednictwem gniazda socket i umieszcza je w buforze buffer. Adres nadawcy umieszczany jest w strukturze wskazywanej przez sender (addrlen to wskaźnik do długości struktury). flags, podobnie jak poprzednio, ma wartość 0. Jeżeli nadawaca wyśle wiadomość o rozmiarze większym niż nbytes, nadmiarowe bajty zostaną utracone.

Uwaga: Z każdym socketem UDP związany jest niejawny bufor odbiorczy zrealizowany w postaci kolejki FIFO.

Klient UDP

Rysunek pokazuje sposób oprogramowania aplikacji klienta i serwera UDP. Zauważmy, że w przeciwieństwie do TCP klient nie nawiązuje połączenia z serwerem. Zamiast tego od razu przesyłany jest komunikat przy wykorzystaniu komendy sendto, której parametrem jest adres docelowy. Podobnie w przypadku serwera nie akceptujemy przychodzącego połączenia tylko od razu przechodzimy do odbioru komunikatów. recvfrom zwraca komunikat i adres IP klienta.

a2

Skanowanie portów

Ze względu na prostotę budowy protokołu UDP, nie jest możliwe wykrycie braku "odpowiedzi" zwrotnej od serwera (stan braku połączenia). Wysłany pakiet UDP na port, który jest zamknięty powoduje wysłanie do nadawcy pakietu ICMP typu 3 (nie można osiągnąć miejsca przeznaczenia). Port otwarty nie odpowie żadnym pakietem, lub odpowie pakietem "niepoprawnie sformatowany pakiet" w przypadku niektórych usług. Zawsze w przypadku braku odpowiedzi możemy mieć do czynienia z filrowaniem pakietu. Należy o tym pamiętać zanim potwierdzi się informację o otwartym porcie.

Przykłady

Przykład serwera UDP, działającego na porcie 8888 i odsyłającego do klienta otrzymany od niego komunikat.

/*
        Simple udp server
        Silver Moon (m00n.silv3r@gmail.com)
*/
#include<stdio.h> //printf
#include<string.h> //memset
#include<stdlib.h> //exit(0);
#include<arpa/inet.h>
#include<sys/socket.h>

#define BUFLEN 512  //Max length of buffer
#define PORT 8888   //The port on which to listen for incoming data

void die(char *s)
{
        perror(s);
        exit(1);
}

int main(void)
{
        struct sockaddr_in si_me, si_other;

        int s, i, slen = sizeof(si_other) , recv_len;
        char buf[BUFLEN];

        //create a UDP socket
        if ((s=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1)
        {
                die("socket");
        }

        // zero out the structure
        memset((char *) &si_me, 0, sizeof(si_me));

        si_me.sin_family = AF_INET;
        si_me.sin_port = htons(PORT);
        si_me.sin_addr.s_addr = htonl(INADDR_ANY);

        //bind socket to port
        if( bind(s , (struct sockaddr*)&si_me, sizeof(si_me) ) == -1)
        {
                die("bind");
        }

        //keep listening for data
        while(1)
        {
                printf("Waiting for data...");
                fflush(stdout);

                //try to receive some data, this is a blocking call
                if ((recv_len = recvfrom(s, buf, BUFLEN, 0, (struct sockaddr *) &si_other, &slen)) == -1)
                {
                        die("recvfrom()");
                }

                //print details of the client/peer and the data received
                printf("Received packet from %s:%d\n", inet_ntoa(si_other.sin_addr), ntohs(si_other.sin_port));
                printf("Data: %s\n" , buf);

                //now reply the client with the same data
                if (sendto(s, buf, recv_len, 0, (struct sockaddr*) &si_other, slen) == -1)
                {
                        die("sendto()");
                }
        }

        close(s);
        return 0;
}

Przykład klieta UDP

/*
        Simple udp client
        Silver Moon (m00n.silv3r@gmail.com)
*/
#include<stdio.h> //printf
#include<string.h> //memset
#include<stdlib.h> //exit(0);
#include<arpa/inet.h>
#include<sys/socket.h>

#define BUFLEN 512  //Max length of buffer

void die(char *s)
{
        perror(s);
        exit(1);
}

int main(void)
{
        char adres[512];
        struct sockaddr_in si_other;
        int s, i, portno, slen=sizeof(si_other);
        char buf[BUFLEN];
        char message[BUFLEN];

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

        if ( (s=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1)
        {
                die("socket");
        }

        memset((char *) &si_other, 0, sizeof(si_other));
        si_other.sin_family = AF_INET;
        si_other.sin_port = htons(portno);

        if (inet_aton(adres , &si_other.sin_addr) == 0)
        {
                fprintf(stderr, "inet_aton() failed\n");
                exit(1);
        }

        while(1)
        {
                printf("Enter message : ");
                gets(message);

                //send the message
                if (sendto(s, message, strlen(message) , 0 , (struct sockaddr *) &si_other, slen)==-1)
                {
                        die("sendto()");
                }

                //receive a reply and print it
                //clear the buffer by filling null, it might have previously received data
                memset(buf,'\0', BUFLEN);
                //try to receive some data, this is a blocking call
                if (recvfrom(s, buf, BUFLEN, 0, (struct sockaddr *) &si_other, &slen) == -1)
                {
                        die("recvfrom()");
                }

                puts(buf);
        }

        close(s);
        return 0;
}

Select

Komendy recv() i recvfrom() są komendami blokującymi (tzn. blokują wykonanie programu do czasu otrzymania danych dla odpowiadającego im deskryptora). W związku z tym nie mogą być one wykorzystywane gdy chcemy naraz obsłużyć więcej niż jednego klienta.

Jednym z rozwiązań jest w tym wypadku użycie funkcji select(), która umożliwia monitorowanie wielu socketów naraz w oczekiwaniu na aktywność któregokolwiek z nich. Np. jeżeli na jednym z nich pojawią się dane to select() nas o tym poinformuje.

fd_set

fd_set jest strukturą danych (dokładniej zbiorem sokcetów) które chcemy monitorować. Posiadamy cztery makre pozwalające obsługiwać ten zbiór : FD_CLR, FD_ISSET, FD_SET, FD_ZERO.

FD_ZERO - Clear an fd_set
FD_ISSET - Check if a descriptor is in an fd_set
FD_SET - Add a descriptor to an fd_set
FD_CLR - Remove a descriptor from an fd_set

Przykładowe użycie:

//set of socket descriptors
fd_set readfds;

//socket to set (gdzie s to socket)
FD_SET( s , &readfds);

Funkcja select() ma następującą postać:

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

Funkcja select umożliwia nadzorowanie zbioru deskryptorów pod względem możliwości odczytu, zapisu bądź wystąpienia sytuacji wyjątkowych. Funkcja przyjmuje wspomniane trzy zbiory deskryptorów, jednak nie ma obowiązku określania ich wszystkich (można w miejsce odp. zbioru deskryptorów podać NULL - wówczas dany zbiór nie będzie nadzorowany przez select).

Funkcja select jako swój wynik zwraca liczbę "gotowych" deskryptorów. Pierwszym parametrem select musi być największa wartość deskryptora ze zbiorów powiększona o 1.
Jeśli jako timeout podamy NULL, select wraca natychmiast, informując jaki jest bieżący stan deskryptorów.
Jeśli jako timeout podamy niezerowy czas, select wraca po upływie tego czasu lub po wystąpieniu zdarzenia na deskryptorze (zależy co nastąpi wcześniej).

Przykład - program czekający przez 5,8 sek. na wciśnięcie jakiegoś (i entera), inaczej kończący pracę.

/*selectcp.c - a select() demo*/
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
/* file descriptor for standard input */
#define STDIN 0

int main(int argc, char *argv[ ])
{
        struct timeval tval;
        fd_set readfds;
        tval.tv_sec = 5;
        tval.tv_usec = 800000;

        FD_ZERO(&readfds);
        FD_SET(STDIN, &readfds);
        /* don’t care about writefds and exceptfds: */
        select(STDIN+1, &readfds, NULL, NULL, &tval);

        if (FD_ISSET(STDIN, &readfds))
        printf("A key was pressed lor!\n");
        else
        printf("Timed out lor!...\n");

        return 0;
}

Zwróćmy uwagę, że jako, że interesuje nas tylko odczyt select będzie ma postać:

select(STDIN+1, &readfds, NULL, NULL, &tval);

Funkcja select blokuje wykonanie programu do uzyskania aktywności na którymś z socketów. Jeżeli któryś/eś sockety są gotowe do odczytu to struktura readfs będzie zawierać elementy gotowe do odczytu.

Przykład - program serwera odczytującego komunikaty od wielu użytkowników naraz i odsyłający im otrzymywane od nich komunikaty.

/**
Handle multiple socket connections with select and fd_set on Linux
Silver Moon ( m00n.silv3r@gmail.com)
*/

#include <stdio.h>
#include <string.h>   //strlen
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>   //close
#include <arpa/inet.h>    //close
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h> //FD_SET, FD_ISSET, FD_ZERO macros

#define TRUE   1
#define FALSE  0
#define PORT 8888

int main(int argc , char *argv[])
{
        int opt = TRUE;
        int master_socket , addrlen , new_socket , client_socket[30] , max_clients = 30 , activity, i , valread , sd;
        int max_sd;
        struct sockaddr_in address;

        char buffer[1025];  //data buffer of 1K

        //set of socket descriptors
        fd_set readfds;

        //a message
        char *message = "Witaj \r\n";

        //initialise all client_socket[] to 0 so not checked
        for (i = 0; i < max_clients; i++)
        {
                client_socket[i] = 0;
        }

        //create a master socket
        if( (master_socket = socket(AF_INET , SOCK_STREAM , 0)) == 0)
        {
                perror("socket failed");
                exit(EXIT_FAILURE);
        }

        //set master socket to allow multiple connections , this is just a good habit, it will work without this
        if( setsockopt(master_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt)) < 0 )
        {
                perror("setsockopt");
                exit(EXIT_FAILURE);
        }

        //type of socket created
        address.sin_family = AF_INET;
        address.sin_addr.s_addr = INADDR_ANY;
        address.sin_port = htons( PORT );

        //bind the socket to localhost port 8888
        if (bind(master_socket, (struct sockaddr *)&address, sizeof(address))<0)
        {
                perror("bind failed");
                exit(EXIT_FAILURE);
        }
        printf("Listener on port %d \n", PORT);

        //try to specify maximum of 3 pending connections for the master socket
        if (listen(master_socket, 3) < 0)
        {
                perror("listen");
                exit(EXIT_FAILURE);
        }

        //accept the incoming connection
        addrlen = sizeof(address);
        puts("Waiting for connections ...");

        while(TRUE)
        {
                //clear the socket set
                FD_ZERO(&readfds);

                //add master socket to set
                FD_SET(master_socket, &readfds);
                max_sd = master_socket;

                //add child sockets to set
                for ( i = 0 ; i < max_clients ; i++)
                {
                        //socket descriptor
                        sd = client_socket[i];

                        //if valid socket descriptor then add to read list
                        if(sd > 0)
                                FD_SET( sd , &readfds);

                        //highest file descriptor number, need it for the select function
                        if(sd > max_sd)
                                max_sd = sd;
                }

                //wait for an activity on one of the sockets , timeout is NULL , so wait indefinitely
                activity = select( max_sd + 1 , &readfds , NULL , NULL , NULL);

                if ((activity < 0) && (errno!=EINTR))
                {
                        printf("select error");
                }

                //If something happened on the master socket , then its an incoming connection
                if (FD_ISSET(master_socket, &readfds))
                {
                        if ((new_socket = accept(master_socket, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0)
                        {
                                perror("accept");
                                exit(EXIT_FAILURE);
                        }

                        //inform user of socket number - used in send and receive commands
                        printf("New connection , socket fd is %d , ip is : %s , port : %d \n" , new_socket , inet_ntoa(address.sin_addr) , ntohs(address.sin_port));

                        //send new connection greeting message
                        if( send(new_socket, message, strlen(message), 0) != strlen(message) )
                        {
                                perror("send");
                        }

                        puts("Welcome message sent successfully");

                        //add new socket to array of sockets
                        for (i = 0; i < max_clients; i++)
                        {
                                //if position is empty
                                if( client_socket[i] == 0 )
                                {
                                        client_socket[i] = new_socket;
                                        printf("Adding to list of sockets as %d\n" , i);

                                        break;
                                }
                        }
                }

                //else its some IO operation on some other socket :)
                for (i = 0; i < max_clients; i++)
                {
                        sd = client_socket[i];

                        if (FD_ISSET( sd , &readfds))
                        {
                                //Check if it was for closing , and also read the incoming message
                                if ((valread = read( sd , buffer, 1024)) == 0)
                                {
                                        //Somebody disconnected , get his details and print
                                        getpeername(sd , (struct sockaddr*)&address , (socklen_t*)&addrlen);
                                        printf("Host disconnected , ip %s , port %d \n" , inet_ntoa(address.sin_addr) , ntohs(address.sin_port));

                                        //Close the socket and mark as 0 in list for reuse
                                        close( sd );
                                        client_socket[i] = 0;
                                }

                                //Echo back the message that came in
                                else
                                {
                                        //set the string terminating NULL byte on the end of the data read
                                        buffer[valread] = '\0';
                                        send(sd , buffer , strlen(buffer) , 0 );
                                }
                        }
                }
        }

        return 0;
}

Zadanie (1pkt)

Co stanie się w przypadku, gdy uruchomimy klienta TCP nie uruchamiając serwera? Dlaczego tak jest?

Zadanie (1pkt)

Co stanie się w przypadku, gdy uruchomimy klienta UDP nie uruchamiając serwera? Opisz jaka jest różnica między tym przypadkiem a TCP?

Zadanie (3pkt)

Zakładając, że serwer ma otwartego socketa na porcie X dla połączeń TCP. Co stanie się gdy ten sam serwer utworzy socket UDP do odbioru komunikatów na tym samym porcie? Czy wystąpi w tym przypadku jakiś konflikt? Możesz wykorzystać przykład 1 ze strony http://www.staff.amu.edu.pl/~ttomek/sik/cwiczenia3.html oraz program netcat żeby sprawdzić. Znajdź w sieci informacje dlaczego tak jest? Co stanie się jeżeli chcielibyśmy stworzyć dwa serwery TCP na tym samym porcie?

Zadanie (4pkt)

Zmień ostatni przykład tak, by przesyłał wiadomości otrzymane od jednego klienta do wszystkich pozostałych podłączonych do niego klientów (działał na zasadzie czatu). Uwaga! serwer nie ma sam z siebie wysyłać żadnej wiadomości do klientów - w szczególności należy zakomentować wysyłanie wiadomości "Witaj!". Po uruchomieniu wyślij za pomoca netcata na adres prowadzącego swój numer indeksu, numer portu oraz adres IP swojego serwera.

Zadanie (1pkt)

Opisz do czego służy linijka

setsockopt(master_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt))

Czy bez niej serwer nadal działa?

Rozwiązania zadań proszę przesłać przez stronę:

*

Wykorzystano materiały z:

http://www.staff.amu.edu.pl/~ttomek/sik/cwiczenia2.html

http://edu.pjwstk.edu.pl/wyklady/sko/scb/

http://www.cs.put.poznan.pl/ddwornikowski/sieci/sieci2/bsdsockets.html

http://www.tenouk.com/Module41.html

http://www.binarytides.com/programming-udp-sockets-c-linux/

http://www.binarytides.com/multiple-socket-connections-fdset-select-linux/