#define _XOPEN_SOURCE #include <unistd.h> /* crypt() */ #include <sys/types.h> #include <sys/socket.h> /* socket(), connect(), recv(),send(),bind() */ #include <linux/in.h> /* struct sockaddr_in */ #include <stdio.h> /* perror() */ #include <sys/stat.h> #include <fcntl.h> /* open() */ #define SRVPORT 1111 /* numer portu serwera */ #define CLIPORT 31337 /* numer portu gniazda klienta */ #define PASSWD "sikret" /* hasło wymagane do połączenia z serwerem */ #define CSALT "Zz" /* dwuznakowy tzw. salt dla funkcji crypt() */ #define PLIK "/proc/meminfo" /* plik, który wyślemy klientowi */Tylko jeden komentarz: wszystkie makra oprócz ostatniego muszą być tak samo zdefiniowane zarówno w programie serwera, jak i klienta. W przeciwnym wypadku klient nie będzie w stanie komunikować się z serwerem. Dlaczego tak będzie zobaczymy w dalszej części programu.
int main (void)
{
int sd, /* deskryptor gniazda */
fd, /* deskryptor pliku */
ret, /* pomocnicza */
struct sockaddr_in saddr, /* adres lokalnego gniazda */
caddr; /* adres zdalnego gniazda */
char buf[1024], /* bufor */
hash[20]; /* bufor na zahashowane hasło */
sd = socket (PF_INET, SOCK_DGRAM, 0);
if (sd < 0)
{
perror ("socket()");
exit (1);
}
saddr.sin_family = PF_INET;
saddr.sin_port = htons (SRVPORT);
saddr.sin_addr.s_addr = INADDR_ANY;
if (bind (sd, (struct sockaddr *) &saddr, sizeof (saddr)) < 0)
{
perror ("bind()");
exit (1);
}
Do tej pory postępowaliśmy identycznie, jak w przypadku serwera działającego
na bazie TCP. Otworzyliśmy więc lokalne gniazdo (PF_INET, SOCK_DGRAM) )i
przypisaliśmy mu adres żeby zdalni klienci byli w stanie wysyłać do nas
datagramy (gniazdo musi mieć przypisany adres aby możliwa była komunikacja z
nim).
sprintf (hash, "%s", crypt (PASSWD, CSALT));
printf ("Czekam na datagramy ...\n");
Następnie obliczyliśmy tzw. one way hash na podstawie hasła (PASSWD) i tzw.
salt. To ostatnie jest prawie dowolnym dwuliterowym ciągiem znaków
([a-zA-Z0-9./]) i jest stosowane przez algorytm liczenia funkcji hashującej.
Ta sama funkcja (crypt()) jest używana w większości systemów unixowych do
weryfikacji haseł użytkowników. Użyliśmy tej funkcji aby uniemożliwić przejęcie
hasła w czystej postaci poprzez podsłuchiwanie sieci.
W następnej kolejności wchodzimy do pętli, w której będziemy obsługiwali nadchodzące żądania od klientów:
while (1)
{
bzero (buf, sizeof (buf));
len = sizeof (caddr);
recvfrom (sd, buf, sizeof (buf), 0, (struct sockaddr *) &caddr, &len);
// Widzimy wspomnianą jakiś czas temu funkcję recvfrom() ale że nie przyglądaliśmy się jej wtedy dokładnie zrobimy to teraz ({\sl man 2 recvfrom}):
#include <sys/types.h>
#include <sys/socket.h>
int recvfrom(int s, void *buf, int len, unsigned int flags
struct sockaddr *from, int *fromlen);
Funkcja ta jest bardziej uniwersalnym odpowiednikiem recv(). Bardziej
uniwersalnym ponieważ recv() to po prostu recvfrom(s,buf,len,flags,NULL,NULL).
W praktyce jest tak że, dla połączeń opartych na datagramach używa się
recvfrom(), a dla połączeń strumieniowych recv(). Pierwsze cztery parametry
oraz zwracana wartość mają takie samo znaczenie, jak dla recv().
Pozostają jeszcze dwa parametry:
from - Wskaźnik do struktury sockaddr. fromlen - Wskaźnik do wartości przechowującej wielkość parametru from.
Przypomina się funkcja accept() ... Nieprzypadkowo, ponieważ oba parametry mają to samo zadanie, co w tej funkcji. Wywołanie recvfrom() blokuje do czasu, aż nadejdą jakieś dane do odczytania. Kiedy tak się stanie struktura wskazywana przez *from zostanie wypełniona adresem gniazda, z którego te dane nadeszły. Teraz już pewnie domyślamy się, dlaczego do jednego rodzaju gniazd używa się recv(), a do innego recvfrom(). Połączenia strumieniowe można określić mianem połączeń punkt-punkt (ang. point-to-point, nie mylić z PPP ;) i z chwilą kiedy wywołanie accept() zwróci adres zdalnego gniazda mamy pewność, że dalsza wymiana danych będzie przebiegała tylko z jednym konkretnym gniazdem zdalnym. Dlatego też nie musimy za każdym razem przy odbiorze danych wypełniać od nowa struktury z adresem klienta. Inaczej wygląda komunikacja korzystająca z datagramów (celowo nie używamy słowa "połączenie") - paczki danych wędrują sobie, każdy swoją drogą od klienta do serwera. Nie następuje więc logiczne skojarzenie pary gniazd. Z tego powodu zawsze gdy odczytujemy otrzymane dane musimy wiedzieć, kto je do nas przysłał. Informację tą podaje nam recvfrom() w strukturze sockaddr klienta.
Oczywiście jest i odpowiednik send(). Nazywa się sendto() i wygląda tak (man 2 sendto):
#include <sys/types.h>
#include <sys/socket.h>
int sendto(int s, const void *msg, int len, unsigned int flags,
const struct sockaddr *to, int tolen);
Wszystko, co powiedzieliśmy na temat podobieństwa recv() i recvfrom() odnosi
się także do pary send()/sendto(). Struktura sockaddr w tym wywołaniu zawiera
oczywiście adres gniazda, do którego zamierzamy wysłać datagram.
Jak widać recvfrom()/sendto() są funkcjami raczej mało wygodnymi w użyciu. Dużo mniej pisania wymaga para recv()/send(). Żeby było ciekawiej, istnieje możliwość używania recv()/send() na gniazdach bezpołączeniowych ... W jaki sposób tego dokonać zobaczymy w kodzie klienta.
Tymczasem popatrzmy, co dalej dzieje się w pętli:
printf ("Datagram od %s:%i ... ", inet_ntoa (caddr.sin_addr.s_addr),
ntohs (caddr.sin_port));
if (!strncmp (buf, hash, strlen (hash))
&& ntohs (caddr.sin_port) == CLIPORT)
{
printf ("Przyjęty.\n");
sendto (sd, "OK\n", 3, 0, (struct sockaddr *) &caddr,
sizeof (caddr));
Ten fragment dokonuje sprawdzenia, czy klient jest upoważniony do komunikacji z
serwerem. Wybraliśmy procedurę autentyfikacji klienta opartą na dwóch
elementach: haśle i porcie źródłowym (porcie, z którego nadaje klient). Serwer
oczekuje od klienta datagramu zawierającego ciąg (hash) utworzony na maszynie
klienta w ten sam sposób, co na serwerze (funkcja crypt()). Jeśli wyniki
działania funkcji hashujących na serwerze i na kliencie pokrywają się oraz
jeśli port źródłowy zgadza się z wcześniej ustalonym po obu stronach (CLIPOR)
to serwer wysyła w stronę klienta datagram zawierający odpowiedź 'OK'.
fd = open (PLIK, O_RDONLY);
if (fd < 1)
{
perror ("open()");
sendto (sd, "Błąd: open()\n", 13, 0, (struct sockaddr *) &caddr,
sizeof (caddr));
}
bzero (buf, sizeof (buf));
while (read (fd, buf, sizeof (buf)) > 0)
{
sendto (sd, buf, strlen (buf), 0, (struct sockaddr *) &caddr,
sizeof (caddr));
bzero (buf, sizeof (buf));
}
close (fd);
sendto (sd, "END\n", 4, 0, (struct sockaddr *) &caddr,
sizeof (caddr));
}
Następnie otwieramy wcześniej zdefiniowany plik i po kawałku przesyłamy go do
klienta. Kiedy cały plik zostanie przetransmitowany wysyłamy klientowi jeszcze
ciąg 'END' aby poinformować, że transmisja się powiodła.
else
{
printf ("Odrzucony.\n");
sendto (sd, "Błąd: złe hasło albo port źródłowy\n", 4, 0,
(struct sockaddr *) &caddr, sizeof (caddr));
}
}
}
Blok else wykonuje się jeśli któryś z elementów autentyfikacji (hasło, port
źródłowy) jest niepoprawny. Tutaj też kończy się pętla while oraz cały kod
serwera.
Jakie różnice w stosunku do serwera TCP ? Nie używalismy funkcji listen() oraz accept(). Wystarczyło otworzyć gniazdo korzystające z UDP (PF_INET, SOCK_DGRAM) i odbierać po kolei wszystkie datagramy, które nadchodzą na to gniazdo. Nie zachodziła też konieczność tworzenia oddzielnego procesu dla każdego klienta. Dlaczego ? Ponieważ lokalne gniazdo nie było skojarzone z pojedyńczym gniazdem zdalnym. Wystarczyło jedno gniazdo aby obsłużyć wszystkich chętnych. Pamiętajmy jednak, że mieliśmy do czynienia z bardzo prostą wymianą informacji opartą na systemie pytanie-odpowiedź. W bardziej skomplikowanych przypadkach uzasadnione może stać się przydzielenie każdemu klientowi osobnego procesu. Nasz przykładowy serwer ma przynajmniej jedną sporą wadę - nie gwarantuje, że dane dotrą w niezmienionej postaci do klienta i, że w ogóle tam dotrą ! Używając funkcji send() na serwerze TCP mieliśmy pewność, że pakiety docierają do klienta. Troszczył się o to protokół TCP. Wysyłał on klientowi po jednym pakiecie i oczekiwał od niego potwierdzenia, że pakiet pomyślnie dotarł do celu. Jeśli w pewnym przedziale czasu nie otrzymał takiego potwierdzenia to próbował jeszcze kilka razy aż do skutku. Jeśli mimo to klient nie dawał znaku życia to warstwa TCP sygnalizowała błąd naszemu serwerowi. Mechanizmem takim nie dysponuje niestety protokół UDP. Jeśli chcielibyśmy mieć pewność, że dane przesyłane za jego pośrednictwem pomyślnie dotarły do celu musielibyśmy osobiście zaprogramować podobną procedurę weryfikacyjną, którą ma już wbudowaną protokół TCP.
Kod klienta niewiele różni się w zasadniczych kwestiach od kodu serwera:
#define _XOPEN_SOURCE
#include <unistd.h> /* crypt() */
#include <sys/types.h>
#include <sys/socket.h> /* socket(), connect(), recv(), send() */
#include <linux/in.h> /* struct sockaddr_in */
#include <stdio.h> /* perror() */
#include <netdb.h> /* gethostbyname() */
#define SRVPORT 1111 /* numer portu serwera */
#define CLIPORT 31337 /* numer portu gniazda klienta */
#define SRVADDR "localhost" /* adres serwera */
#define PASSWD "sikret" /* haslo wymagane do polaczenia z serwerem */
#define CSALT "Zz" /* dwuznakowy tzw. salt dla funkcji crypt() */
// Doszło jedno nowe makro zawierające nazwę maszyny, na której uruchomiony
// jest serwer.
int main (void)
{
inti sd, /* analogicznie */
len; /* jak */
struct sockaddr_in saddr, /* w kodzie */
caddr; /* klienta */
struct hostent *sent; /* struktura opisująca host-serwer */
char buf[1024]; /* bufor ogólnego przeznaczenia ;) */
sd = socket (PF_INET, SOCK_DGRAM, 0);
if (sd < 0)
{
perror ("socket()");
exit (1);
}
caddr.sin_family = PF_INET;
caddr.sin_port = htons (CLIPORT);
caddr.sin_addr.s_addr = INADDR_ANY;
if (bind (sd, (struct sockaddr *) &caddr, sizeof (caddr)) < 0)
{
perror ("bind()");
exit (1);
}
Do pierwszej czynności już się przyzwyczailiśmy - utworzenie lokalnego gniazda.
Drugim krokiem w tym przypadu jest ręczne przypisanie adresu do lokalnego
gniazda. Z tym jeszcze nie mieliśmy do czynienia w przypadku klienta. Jak
pamiętamy system zrobiłby to za nas ale wybrałby pierwszy z dostępnych portów.
Nie możemy pozwolić sobie na taki luksus ponieważ nasz serwer nie zgodzi się na
sesję jeśli nie będziemy nadawali ze ściśle określonego gniazda.
printf ("Szukam adresu IP serwera %s ...\n", SRVADDR);
sent = gethostbyname (SRVADDR);
if (!sent)
{
herror ("gethostbyname()");
exit (1);
}
saddr.sin_family = PF_INET;
saddr.sin_port = htons (SRVPORT);
bcopy (sent->h_addr, (char *) &saddr.sin_addr, sent->h_length);
Kolejnym krokiem jest wypełnienie struktury sockaddr gniazda zdalnego. Tutaj
wszystko powinno być zrozumiałe.
bzero (buf, sizeof (buf));
sprintf (buf, "%s", crypt (PASSWD, CSALT));
Do bufora kopiujemy wynik działania funkcji hashującej. Już za chwilę wyślemy
tą informację do serwera w celu udowodnienia, że jesteśmy uprawnieni do sesji.
//connect (sd, (struct sockaddr *)\&saddr,sizeof(saddr));Kolejna, pewnie zaskakująca, nowość. Otóż możliwe jest użycie funkcji connect() na gnieździe bezpołączeniowym. Jej działanie jest jednak inne niż dla gniazd strumieniowych. Funkcja connect() użyta w takim kontekście kojarzy po prostu adres zdalnego gniazda z gniazdem lokalnym i nic więcej. W żadnym razie nie próbuje nawiązywać połączenia z serwerem ! Jej działanie ma znaczenie jedynie dla naszej wygody ponieważ od tej pory moglibyśmy (jeśli linia nie była by zakomentowana oczywiście) używać funkcji recv()/send() w miejsce recvfrom()/sendto(). Po prostu system automatycznie wypełniałby za nas parametry *from i *fromlen tej drugej pary funkcji wartościami podanymi dla wywołania connect(). W tym przypadku, jak widać nie skorzystaliśmy z tej możliwości ale zawsze warto wiedzieć, że można to zrobić.
printf ("Wysyłam hasło do %s:%i ...\n", inet_ntoa (saddr.sin_addr.s_addr),
SRVPORT);
sendto (sd, buf, strlen (buf), 0, (struct sockaddr *) &saddr,
sizeof (saddr));
Wysyłamy do serwera zakodowane hasło i oczekujemy na przyzwolenie do dalszej
komunikacji.
printf ("Czekam na odpowiedź ...\n");
do
{
bzero (buf, sizeof (buf));
len = sizeof (caddr);
recvfrom (sd, buf, sizeof (buf), 0, (struct sockaddr *) &caddr, &len);
}
while (caddr.sin_addr.s_addr != saddr.sin_addr.s_addr);
W pętli do-while odbieramy po kolei wszystkie datagramy, które przychodzą na
nasz adres. Wychodzimy z pętli tylko wtedy, jeśli otrzymany datagram pochodzi
od serwera. Pamiętajmy o tym, że istnieje możliwość przyjścia na nasz adres
jakiegoś przypadkowego datagramu nie związanego zupełnie z sesją między nami
a serwerem. Tutaj mała uwaga odnośnie użytych zmiennych: adresy źródłowe
przybywającyh datagramów przechowujemy w strukturze wskazywanej przez caddr.
Skorzystaliśmy z faktu, że struktura ta chwilę po wywołaniu bind() dla
lokalnego gniazda może być bez problemów używana do innych celów. Jeszcze jedna
uwaga: w żadnym wypadku nie należy korzystać z adresu źródłowego datagramu w
celach weryfikacji uprawnień ! Adres ten można spreparować (ang. spoof) w kilka
sekund i jest to o niebo prostsze niż w przypadku fałszowania adresu źródłowego
pakietów obsługiwanych przez TCP.
if (strncmp (buf, "OK", 2))
{
fprintf (stderr,
"Nieprawidlowe haslo albo zly port zrodlowy. Koncze ...\n");
exit (1);
}
else
printf ("Polaczenie przyjete.\n\n");
Jeśli doszliśmy do tego warunku if to znaczy, że dotarł do nas jakiś datagram
od serwera. Najprawdopodobniej (pamiętajmy o wadach UDP) jest to odpowiedź na
wysłany wcześniej przez nas datagram z hasłem. Sprawdzamy więc, czy serwer
zgodził się na obsłużenie nas (ciąg 'OK' na początku datagramu).
bzero (buf, sizeof (buf));
len = sizeof (caddr);
while (recvfrom (sd, buf, sizeof (buf), 0, (struct sockaddr *) &caddr, &len)
> 0)
{
if (caddr.sin_addr.s_addr == saddr.sin_addr.s_addr)
{
if (!strncmp (buf, "END", 3))
break;
else if (!strncmp (buf, "Błąd:", 5))
{
printf ("%s", buf);
break;
}
else
printf ("%s", buf);
}
bzero (buf, sizeof (buf));
len = sizeof (buf);
}
return 0;
}
Ostatnim fragmentem programu jest pętla, która ponownie przegląda wszystkie
nadchodzące datagramy, sprawdza, czy pochodzą one od serwera, a następnie
podejumuje jedno z trzech działań: