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.
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.
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.
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ł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;
}
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_setFD_ISSET - Check if a descriptor is in an fd_setFD_SET - Add a descriptor to an fd_setFD_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ć program netcata
Serwer tcp :
nc -l -p 5555
Serwer udp :
nc -l -u -p 5555
Klient tcp:
nc localhost 5555
Klient udp:
nc -u localhost 5555
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 połącz się za pomoca netcata z adresem 150.254.68.149 port 5555 i podaj swój numer indeksu numer portu oraz adres IP swojego serwera.
Program prowadzącego automatycznie odpyta Twój serwer.
Uwaga! Zadanie musi być wykonywane wewnątrz sieci wydziałowej - przez komputer wydziałowy lub przez połączenie SSH.
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ń tekstowych proszę przesłać do 25.X.2019 godz. 19.59 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/