Wprowadzenie do gniazd BSD¶
Wstęp¶
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.
Do dyspozycji programistów oddano zestaw funkcji bibliotecznych, które służą do obsługi komunikacji
pomiędzy procesami działającymi na różnych węzłach w sieci (możliwa jest także komunikacji pomiędzy
procesami działającymi lokalnie), które składają się na interfejs sieci. Funkcje te ukrywają przed
programistą warstwę transportową oraz wymagają utworzenia specjalnego punktu końcowego kanału
komunikacji, które nazywany jest gniazdem. 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()
).
Model oraz trybu komunikacji¶
Nawiązanie połączenia i rozpoczęcie komunikacji wymaga aby jeden z uczestniczących procesów oczekiwał na zgłoszenia; wówczas inny proces może nawiązać z nim komunikację, w dowolnym, obsługi przez siebie momencie.
Proces, który oczekuje na połączenie nazywany jest serwerem (ang. server). Serwer może działać według dwóch schematów:
- po odebraniu połączenia od innego procesu następuje przetwarzanie przyjętego zgłoszenia oraz ewentualna odpowiedź, po tym serwer przechodzi w stan dalszego oczekiwania na nowe zgłoszenia, schemat taki nazywany jest iteracyjnym,
- odbieranie połączenia od klientów obsługiwane są przez serwer jednocześnie, np. poprzez wykorzystanie procesów potomnych lub wątków, główny proces serwera w tym czasie może dalej nasłuchiwać nowych połączeń; model ten nazywany jest współbieżnym.
Proces, który nawiązuje połączenie z serwerem nazywany jest klientem. Proces ten musi posiadać informacje o adresie IP serwera oraz porcie TCP lub UDP, na którym serwer nasłuchuje. Proces serwera może uzyskać informacje o kliencie, dopiero po nawiązaniu przez tego połączenia.
Interfejs gniazd obsługuje dwa tryby komunikacji: połączeniowy, za pomocą protokołu transportowego TCP (RFC 793), oraz bezpołączeniowy, z wykorzystaniem protokołu transportowego UDP (RFC 768).
Interfejs gniazd¶
Korzystanie z mechanizmu komunikacji TCP/IP wymaga utworzenia obiektu zwanego gniazdem, które
traktowane jest przez system jako plik specjalny. Gniazdo tworzone jest przy pomocy funkcji
bibliotecznej socket()
, która w wyniku zwraca deskryptor utworzonego gniazda. Oba
komunikujące się procesy muszą utworzyć gniazda, jedno gniazdo może służyć zarówno do odbioru jak i
do wysyłania danych. Przez użyciem gniazda należy określić adres oraz port, a także wybrać tryb
komunikacji.
Informacja
Na jednym komputerze można utworzyć dwa gniazda z tym samym numerem portu, jednak muszą mieć różne tryby komunikacji.
Główne funkcje interfejsu gniazd¶
Aby wykorzystać omawiane poniżej funkcje, należy skorzystać z następujących plików nagłówkowych:
<sys/types.h>
, <sys/socket.h>
, a dodatkowo aby korzystać z pozostałych funkcji,
należy użyć: <netinet/in.h>
, <arpa/inet.h>
, <netdb.h>
.
-
int socket(int domain, int type, int protocol)
Funkcja tworzy nowe gniazdo do komunikacji sieciowej, w wyniku zwraca deskryptor gniazda lub wartość -1 w przypadku błędu, strona man: socket(7)
int domain
określa domenę komunikacyjną i służy do wyboru rodziny protokołów: AF_UNIX i AF_LOCAL dla komunikacji wewnątrzsystemowej, AF_INET oraz AF_INET6 dla komunikacji zdalnej protokołem IPv4 oraz IPv6.int type
określa tryb komunikacji: SOCK_STREAM dla gniazd TCP, SOCK_DGRAM dla UDP oraz SOCK_RAW dla gniazd surowych.int protocol
określa protokół, najcześciej parametr ten przyjmuje wartość 0
-
int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen)
Funckja pozwala na związanie wcześniej utworzonego gniazda z adresem lokalnej maszyny i jest używana głównie przez serwery i klientów UDP. Poprawnie wykonana funkcja zwraca 0, błąd oznaczany jest wartością -1.
int sockfd
to deskryptor gniazda utworzony przez funckjęsocket()
struct sockaddr * addr
struktura adresu opisana poniżejsocklen_t addrlen
rozmiar drugiego parametru
-
int listen(int sockfd, int backlog)
Funkcja pozwala na przygotowanie gniazda do obierania zgłoszeń i umozliwia określenie rozmiaru kolejki żądań, oczekujących na wykonanie funkcji
accept()
. Wynikiem poprawnie wykonanej funkcji jest wartość 0, błąd oznaczany jest przez wartość -1.int sockfd
to deskryptor gniazda utworzonego przez funckjęsocket()
int backlog
to rozmiar kolejki dla nadchodzących żądań
-
int accept(int sockfd, struct sockaddr *addr, socklen_t * addrlen)
Funkcja oczekuje na zgłoszenie lub pobiera takowe z kolejki dla gniazd strumieniowych. W wyniku otrzymujemy nowy deskryptor gniazda, w przypadku błędu -1
int sockfd
to deskryptor gniazda utworzonego przez funckjęsocket()
struct sockaddr *addr
wskaźnik na strukturę, która zostanie wypełniona danymi połączenia, adresem i numerem portu klientasocklen_t *addrlen
wskaźnik na rozmiar struktury adresowej
-
int connect(int sockfd, const struct sockaddr *srvaddr, socklen_t addrlen)
Funkcja użwana przez klientów, wiąże stworzone gniazdo z adresem serwera. Jeśli gniazdo zostało uwtworzone w trybie połączeniowym, funkcja nawiązuje połączenie z serwerem, jeśli w trybie bezpołączeniowym, funkcja jedynie przypisuje gniazdu adres i numer portu zdalnego serwera.
int sockfd
to deskryptor gniazda utworzonego przez funckjęsocket()
const struct sockaddr *srvaddr
adres zdalnej maszynysocklen_t addrlen
rozmiar struktury drugiego patrametru
-
int close(int fd)
Funkcja zamyka gniazdo i usuwa jego deskryptor, jako parametr podajemy deskryptor zamyknaego gniazda.
Do odbierania i wysyłania danych należy używać “tradycyjnych” funkcji read()
oraz
write()
. W przypadku komunikacji bezpołączeniowej należy używać funkcji recv()
oraz
send()
. Istnieją także ich odpowiedniki, w których jawnie należy podać adres adresata lub
wysyłającego: sendto()
i recvfrom()
.
Poniżej przedstawiono struktury używane przez funkcję interfejsu gniazd.
struct sockaddr_in {
u_short sin_family; // AF_INET
u_short sin_port; // numer portu
struct in_addr sin_addr; //adres węzła
char sin_zero[8]
}
struct in_addr {
u_long s_addr; // 32 bitowy adres
}
Informacja
W domenie protokołów internetowych, zamiast sockaddr
używamy sockaddr_in
,
wykonując rzutowanie (struct sockaddr*)
Poniższy rysunek przedstawia kolejność wywołań kolejnych funkcji interfejsu gniazd dla serwera i klienta TCP:
W przypadku klienta i serwera UDP należy wywołać metodę bind.
Funkcje pomocnicze¶
Dla protokołów TCP/IP przyjęto standard określający sieciowy porządek bajtów [Wiki20131], zgodnie
z którym kolejność bajtów reprezentujących liczbę liczony jest od najmniej znaczącego bajtu (czyli
od “prawej” strony). Niektóre parametry funkcji interfejsu gniazd należy podawać zgodnie z tym
właśnie porządkiem, np. numer portu w strukturze sockaddr_in
. Dostępne są zatem funkcje,
które umożliwiają konwersję z porządku systemu operacyjnego do porządku sieciowego i odwrotnie.
Istnieją dwa zestawy tych funkcji, odpowiednio dla 16 oraz 32 bitowych liczb.
-
uint32_t htonl(uint32_t hostlong)
Konwersja z reprezentacji lokalnek do sieciowej dla liczb 16 bitowych
-
uint16_t htons(uint16_t hostshort)
Konwersja z reprezentacji lokalnek do sieciowej dla liczb 32 bitowych
-
uint32_t ntohl(uint32_t netlong)
Konwersja z reprezentacji sieciowej do lokalnej dla liczb 32 bitowych
-
uint16_t ntohs(uint16_t netshort)
Konwersja z reprezentacji lokalnej do sieciowej dla liczb 16 bitowych
-
inet_addr
(const char *cp)¶ Zamiana adresu IP zapisanego w formacie dziesiętnym (XXX.YYY.ZZZ.VVV) na jego reprezentację sieciową
Pozostałe funkcje katalogowe i odwzorowujące:
-
struct hostent *gethostbyaddr(const char *addr, int len, int type)
Zamiana adresu IP na nazwę domenową
-
struct hostent *gethostbyname(const char *name)
Zamiana nazwy domenowej na adres IP
-
int getpeername(int sockfd, struct sockaddr *name, socklen_t *namelen)
Zwraca adres zdalnego komputera, z którym połączone jest gniazdo.
-
struct protent *getprotobyname(const char *name)
Pobiera informacje o protokole z
/etc/protocols
na podstawie nazwy i zwracastrukturęprotent
, zawierającą między innymi numer protokołu (getprotobyname(3)).
-
struct servent *getservbyname(const char *name)
Pobiera informacje o usłudze z
/etc/services
na podstawie nazwy i zwraca strukturęservent
, zawierającą numer między innymi numer portu (getservbyname(3))
Informacja
Dokładny opis wszystkich funkcji można znaleźć w manualu !!
Interfejs gniazd w systemach Windows¶
Interfejs gniazd dostępny jest także dla systemów Windows - nosi on nazwę Windows Sockets lub
WinSock. Interfejs ten udostępnia dwie grupy poleceń: pierwsza obejmuje funkcje niemal całkowicie
zgodne z interfejsem BSD, druga grupa natomiast jest specyficzna sla systemów Windows i
charakteryzuje się przedrostkiem WSA_
. Definicje funkcji ujęte są w pliku nagłówkowym
winsock2.h
lub w starszych systemach w winsock.h
. Podstawowe różnice to:
- przed rozpoczęciem pracy należy zainicjować bibliotekę WinSock funkcją
WSAStartup()
, funkcja ta przyjmuje dwa argumenty wywołania: minimalną wymaganą wersję biblioteki oraz strukturęWSADATA
, której postać jest następująca:
typedef struct WSADATA {
WCRD wVersion; //oczekiwany nr wersji
WCRD wHighVersion; //najwyższy akceptowany wersji
char szDescription[WSADESCRIPTION_LEN+1] //opis implementacji
char szSystemStatus[WSASYS_STATUS_LEN+1] //informacje konfiguracyjne
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR* lpVendorInfo;
} WSADATA, *LPWSADATA
- zakończenie pracy z biblioteką wymaga wywołania bezargumentowej funkcji
WSACleanup()
- część funkcji w wyniku przekazuje wartości innych typów, np.
socket()
iaccept()
przekazują w wyniku wartości typuSOCKET
- zamknięcie gniazda realizowane jest przez funkcję
closesocket()
- odczyt i zapis z gniazda realizowany jest funkcjami
recv()/recvfrom()
orazsend()/sendto()
Więcej: [WinSock]
Przykłady¶
Klient usługi daytime¶
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#define BUFSIZE 10000
char *server = "127.0.0.1"; /* adres IP pętli zwrotnej */
char *protocol = "tcp";
short service_port = 13; /* port usługi daytime */
char bufor[BUFSIZE];
int main ()
{
struct sockaddr_in sck_addr;
int sck, odp;
printf ("Usługa %d na %s z serwera %s :\n", service_port, protocol, server);
memset (&sck_addr, 0, sizeof sck_addr);
sck_addr.sin_family = AF_INET;
inet_aton (server, &sck_addr.sin_addr);
sck_addr.sin_port = htons (service_port);
if ((sck = socket (PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
perror ("Nie można utworzyć gniazdka");
exit (EXIT_FAILURE);
}
if (connect (sck, (struct sockaddr*) &sck_addr, sizeof sck_addr) < 0) {
perror ("Brak połączenia");
exit (EXIT_FAILURE);
}
while ((odp = read (sck, bufor, BUFSIZE)) > 0)
write (1, bufor, odp);
close (sck);
exit (EXIT_SUCCESS);
}
Przykład użycia funkcji katalogowych¶
// deklaracje
struct sockaddr_in sck_adr;
struct hostent *host_ptr;
struct protoent *protocol_ptr;
struct servent *service_ptr;
host_ptr = gethostbyname (ADRES_IP);
protocol_ptr = getprotobyname("tcp");
service_ptr = getservbyname("daytime", "tcp");
// wypełnienie struktury
memset(&sck_addr, 0, sizeof sck_addr);
memcpy ((char*) &sck_addr.sin_addr, (char*) host_ptr -> h_addr, host_ptr -> h_length);
sck_addr.sin_port = service_ptr -> s_port;
// tworzymy gniazdo
sock = socket (PF_INET, SOCK_STREAM, protocol_ptr->p_proto)
Klient UDP - szkielet¶
...
struct sockaddr_in myaddr, serv_addr;
// struktura adresu własnego
memset(&myaddr, 0, sizeof(struct sockaddr));
myaddr.sin_family = AF_INET;
myaddr.sin_addr.s_addr = INADDR_ANY; // jakikolwiek adres
myaddr.sin_port = 0; // -> jakikolwiek port wybrany przez system
// struktura adresu serwera
memset(&serv_addr, 0, sizeof(struct sockaddr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("1.2.3.4");
serv_addr.sin_port = htons(atoi("23000"));
sock = socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP);
socklen_t serv_addr_size = sizeof(struct sockaddr);
bind(sock, (struct sockaddr*)&myaddr, sizeof(struct sockaddr));
sendto(sock, buf, strlen(buf+1), 0, (struct sockaddr*)&serv_addr, &serv_addr_size);
recvfrom(sock, buf, strlen(buf+1), 0, (struct sockaddr*)&serv_addr, &serv_addr_size);
close(sock);
...
Serwer UDP - szkielet¶
...
struct sockaddr_in myaddr, serv_addr;
int nFoo = 1
// struktura adresu serwera
memset(&myaddr, 0, sizeof(struct sockaddr));
myaddr.sin_family = AF_INET;
myaddr.sin_addr.s_addr = INADDR_ANY;
myaddr.sin_port = htons(atoi("PORT SERVERA"));
sock = socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP);
setsockopt(nSocket, SOL_SOCKET, SO_REUSEADDR, (char*)&nFoo, sizeof(nFoo));
bind(sock, (struct sockaddr*)&myaddr, sizeof(struct sockaddr));
while(1) {
...
recvfrom(
sendto
...
}
close(sock);
...
Serwer daytime - TCP¶
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <time.h>
#define SERVER_PORT 1234
#define QUEUE_SIZE 5
int main(int argc, char* argv[])
{
int nSocket, nClientSocket;
int nBind, nListen;
int nFoo = 1;
socklen_t nTmp;
struct sockaddr_in stAddr, stClientAddr;
/* address structure */
memset(&stAddr, 0, sizeof(struct sockaddr));
stAddr.sin_family = AF_INET;
stAddr.sin_addr.s_addr = htonl(INADDR_ANY);
stAddr.sin_port = htons(SERVER_PORT);
/* create a socket */
nSocket = socket(AF_INET, SOCK_STREAM, 0);
if (nSocket < 0)
{
fprintf(stderr, "%s: Can't create a socket.\n", argv[0]);
exit(1);
}
setsockopt(nSocket, SOL_SOCKET, SO_REUSEADDR, (char*)&nFoo, sizeof(nFoo));
/* bind a name to a socket */
nBind = bind(nSocket, (struct sockaddr*)&stAddr, sizeof(struct sockaddr));
if (nBind < 0)
{
fprintf(stderr, "%s: Can't bind a name to a socket.\n", argv[0]);
exit(1);
}
/* specify queue size */
nListen = listen(nSocket, QUEUE_SIZE);
if (nListen < 0)
{
fprintf(stderr, "%s: Can't set queue size.\n", argv[0]);
}
while(1)
{
/* block for connection request */
nTmp = sizeof(struct sockaddr);
nClientSocket = accept(nSocket, (struct sockaddr*)&stClientAddr, &nTmp);
if (nClientSocket < 0)
{
fprintf(stderr, "%s: Can't create a connection's socket.\n", argv[0]);
exit(1);
}
printf("%s: [connection from %s]\n",
argv[0], inet_ntoa((struct in_addr)stClientAddr.sin_addr));
time_t now;
struct tm *local;
time (&now);
local = localtime(&now);
char buffer[50];
int n;
n = sprintf(buffer, "%s\n", asctime(local));
write(nClientSocket, buffer, n);
close(nClientSocket);
}
close(nSocket);
return(0);
}
Wątki¶
Kompilacja z biblioteką pthread
z przełącznikiem -lpthread.
#include <pthread.h>
...
sock = socket(...);
bind(sock, ... );
listen(..);
while(is_running) {
tmp_sck = accept(...)
pthread_t id;
pthread_create(&id, NULL, client_loop, sck);
}
...
void* client_loop(void *arg) {
int rcvd;
char buffer[1204];
int sck = *((int*) arg);
while(rcvd = recv(sck, buffer, 1024, 0) {
send(sck, buffer, rcvd, 0);
close(sck);
pthread_exit(NULL);
}
Blokowanie w celu dostępu równoczesnego za pomocą zamka (pthread_mutex_lock(3p)).
pthread_mutex_lock(&sock_mutex)
.. operacje na współdzielonej strukturze np. client_threds
pthread_mutex_unlock(&sock_mutex)
Przykład klienta z WinSock¶
#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
...
WORD WRequiredVersion;
WSADATA WData;
SOCKET SSocket;
WRequiredVersion = MAKEWORD(2, 0);
WSAStartup(WRequiredVersion, &WData)
SSocket = socket(..);
connect(..);
read(..);
closesocket(SSocket);
WSACleanup();
Zadania¶
- Używając programu telnet lub netcat (komenda nc) przetestuj serwery usług echo oraz daytime uruchomionych na wskazanym przez prowadzącego adresie oraz porcie.
- Uruchom program nc w trybie nasłuchiwania na dowolnym porcie. Poproś koleżankę lub kolegę o podłączenie się do ciebie. Wykonaj to także dla protokołu UDP.
- Skompiluj klienta (z Klient usługi daytime) i zmień tak, aby połączyć się do usługi daytime
(
src/daytime_tcp.c
). - Przerób klienta aby używał funkcji katalogowych, skorzystać z pomocy z Przykład użycia funkcji katalogowych.
- Przerób klienta daytime aby używał protokołu UDP, skorzystaj z pomocy z Klient UDP - szkielet.
- Napisz iteracyjny serwer usługi echo lub daytime, sprawdź jak zachowa się przy 2 i więcej połączeniach jednoczesnych.
- Przerób serwer tak aby obsługiwał równoczesne połączenia za pomocą wątków, zobacz przykład
Wątki oraz przykładowy program
src/thread_example.c
, race conditionsrc/race_example.c
,src/mutex_example.c
. - Napisz prostego klienta z pomocą biblioteki WinSock, zobacz przykład winsock.
- Napisz klienta w bibliotece Qt.
Literatura¶
[Wiki20131] | http://en.wikipedia.org/wiki/Endianness#Endianness_in_networking |
[Tanen] | A.S. Tannenbaum: Computer Networks, Prentice Hall PTR, 2003. |
[Steve] |
|
[WinSock] | http://www.sockets.com/winsock2.htm |