Programowanie
Programowanie przy użyciu gniazd sieciowych
64
październik 2009
Programowanie
Programowanie przy użyciu gniazd sieciowych
65
www.lpmagazine.org
lin
ux
@
so
ftw
ar
e.
co
m
.p
l
Programowanie
gniazd sieciowych
Podstawową umiejętnością, którą musi opanować każdy programista chcący pisać aplikacje sieciowe,
jest wykorzystanie mechanizmu gniazd sieciowych (ang. network sockets). Pozwala on na wygodne
przesyłanie i odbieranie danych, niezależnie od wykorzystywanego sprzętu sieciowego. Podstawową
ideą gniazd sieciowych jest bowiem zapewnienie warstwy abstrakcji dla niskopoziomowych funkcji
sieciowych. Jeżeli chcesz dowiedzieć się, w jaki sposób nowoczesne systemy operacyjne realizują
komunikację sieciową, jakie są rodzaje gniazd sieciowych oraz w jaki sposób możesz wykorzystać je
w swoich aplikacjach, to jest to artykuł dla Ciebie. Zapraszam do lektury!
Rafał Kułaga
J
estem przekonany, że nikogo nie trzeba prze-
konywać co do znaczenia funkcji sieciowych
we współczesnych aplikacjach. Śmiało można
stwierdzić, że zdecydowana większość dostęp-
nych na rynku programów (w tym również gier kompu-
terowych), w taki czy inny sposób wykorzystuje połącze-
nia sieciowe komputera. Dotyczy to nie tylko baz danych,
aplikacji biznesowych i wspomagających zarządzanie, w
których naturalne jest zastosowanie architektury klient-
serwer, lecz również całej gamy aplikacji działających w
architekturze równy z równym (ang. P2P – Peer To Peer),
pozwalających na wymianę plików.
Kiedy mówimy o programowaniu sieciowym, z
pewnością przychodzą nam na myśl języki programo-
wania, takie jak PHP, ASP, J2EE. Rozwiązania budo-
wane przy ich użyciu niemal zawsze korzystają z funk-
cji sieciowych. Ich cechą charakterystyczną jest wyko-
rzystanie strony internetowej jako interfejsu użytkow-
nika oraz przesyłanie poleceń i efektów ich wykona-
nia za pomocą protokołu HTTP. W tym artykule skupi-
my się jednak na innym aspekcie programowania przy
wykorzystaniu sieci komputerowych – będziemy mie-
li pełną kontrolę nad reprezentacją danych wysyłanych
poprzez sieć.
Jako programista aplikacji sieciowych korzystających
jedynie z mechanizmu gniazd, udostępnianego przez sys-
tem operacyjny, będziesz odpowiedzialny za zdefiniowa-
nie protokołu transmisji danych pomiędzy dwoma proce-
sami, działającymi na oddalonych maszynach. Może się
to wydawać dość skomplikowane, szczególnie jeżeli nie
posiadasz zbyt dużej wiedzy o budowie typowych proto-
kołów sieciowych warstwy aplikacji, jednak w rzeczywi-
stości sprowadza się do odpowiedniego przemyślenia roz-
kładu danych w części pakietu przetwarzanej przez apli-
kację.
Tworzenie bezpiecznych aplikacji w języku C/C++
korzystających z gniazd sieciowych wymaga od nas jed-
nak dużej ostrożności w manipulowaniu otrzymany-
mi danymi. Pamiętajmy, że błędy w tym aspekcie mo-
gą spowodować podatność naszego programu na ataki
wykorzystujące przepełnienie bufora (ang. buffer over-
flow). Na szczególne niebezpieczeństwo zostaje narażo-
ny nasz system w przypadku, gdy program uruchamia-
ny jest z uprawnieniami użytkownika root, co często jest
Programowanie
Programowanie przy użyciu gniazd sieciowych
64
październik 2009
Programowanie
Programowanie przy użyciu gniazd sieciowych
65
www.lpmagazine.org
konieczne w celu wykorzystania niższych nu-
merów portów.
W artykule zostanie opisany sposób dzia-
łania gniazd sieciowych, ich typy oraz zasto-
sowanie. W trakcie lektury artykułu nauczysz
się, jak tworzyć proste aplikacje posiadające
możliwość bezpiecznej wymiany danych za
pomocą sieci komputerowej. Przyjrzymy się
również narzędziom, pozwalającym na testo-
wanie aplikacji korzystających z mechanizmu
gniazd. Wspomnimy również o dwóch bar-
dzo przydatnych bibliotekach – libnet i libp-
cap, pozwalających na niskopoziomowy do-
stęp do sieci.
System operacyjny
a komunikacja sieciowa
Zanim przejdziemy do praktycznej realiza-
cji komunikacji sieciowej za pomocą mecha-
nizmu gniazd, warto poznać sposób, w ja-
ki system operacyjny (a konkretnie część ją-
dra systemu, zwana stosem TCP/IP) obsługu-
je te funkcje.
Gdy nasz komputer odbiera sygnały po-
chodzące z sieci, sprzęt, a konkretnie – inter-
fejs sieciowy, usuwa nagłówki protokołów
warstw znajdujących się poniżej warstwy sie-
ciowej. Od tej chwili, za obróbkę odebranych
danych odpowiada jedynie system operacyjny
oraz aplikacje (Rysunek 1).
W czasie przetwarzania przez część stosu
TCP/IP odpowiedzialną za obsługę protoko-
łów warstwy sieciowej, obcięty zostaje nagłó-
wek protokołu IP. Otrzymany segment (jed-
nostka danych protokołów warstwy transpor-
tu) zostaje przekazany w górę stosu.
Na poziomie warstwy transportu mamy
do czynienia z numerami portów, stanowią-
cych identyfikatory, pozwalające na przeka-
zanie danych wydobytych z segmentu do od-
powiedniego procesu. W większości syste-
mów operacyjnych, obsługiwane są dwa ty-
py portów, odpowiadające dwóm najpopu-
larniejszym protokołom warstwy transportu:
TCP i UDP. Dla każdego z protokołów przy-
dzielony jest zakres portów – zwróćmy jed-
nak uwagę, że różne procesy mogą korzy-
stać z tego samego numeru portu pod warun-
kiem, że używają różnych protokołów war-
stwy transportu.
Każdy z portów posiada unikalny numer
identyfikujący z zakresu 0 – 65535 (port iden-
tyfikowany jest za pomocą 16-bitowej liczby
naturalnej). Porty o numerach 0 – 1023 okre-
ślane są jako ogólnie znane i przypisane do
najpopularniejszych usług (takich jak telnet,
WWW, e-mail). W celu utworzenia gniaz-
da i przypisania go do portu z tego zakresu,
konieczne są uprawnienia użytkownika root.
Rysunek 1.
Enkapsulacja danych w modelu TCP/IP
������������
��������������
��������
���
��������
��������
��
�������
��������
�����
����������
������
�����
Listing 1.
Podstawowe struktury
struct
addrinfo
{
int
ai_flags
;
// flagi sterujące
int
ai_family
;
// protokół: AF_INET (IPv4), AF_INET6
(IPv6), AF_UNSPEC (dowolny)
int
ai_socktype
;
// typ gniazda: SOCK_STREAM (TCP),
SOCK_DGRAM (UDP)
int
ai_protocol
;
// protokół
size_t
ai_addrlen
;
// rozmiar struktury ai_addr
struct
sockaddr
*
ai_addr
;
// wskaźnik na strukturę sockaddr_in
char
*
ai_canonname
;
// nazwa hosta
struct
addrinfo
*
ai_next
;
// następny element listy
};
struct
sockaddr
{
unsigned
short
sa_family
;
//wersja adresu: AF_INET (Ipv4),
AF_INET6 (Ipv6)
char
sa_data
[
14
];
//tablica przechowująca adres
};
struct
sockaddr_in
{
short
int
sin_family
;
//rodzina adresów: AF_INET
unsigned
short
int
sin_port
;
//numer portu
struct
in_addr
sin_addr
;
//struktura przechowująca adres IP
unsigned
char
sin_zero
[
8
];
//wypełnić zerami
};
struct
in_addr
{
uint32_t
s_addr
;
//adres IP
};
66
październik 2009
Programowanie
Programowanie przy użyciu gniazd sieciowych
67
www.lpmagazine.org
Programowanie
Programowanie przy użyciu gniazd sieciowych
Porty o wyższych numerach możemy dowol-
nie przypisywać tworzonym przez nas apli-
kacjom.
Jednak w jaki sposób system operacyjny
wie, jaki port przypisany jest do danej aplika-
cji, oraz w jaki sposób możemy dokonać takie-
go przypisania? Właśnie w tym celu stosuje się
gniazda sieciowe.
Gniazda sieciowe
Z pewnością wiesz, że w systemach unikso-
wych dostęp do urządzeń, plików, katalogów
oraz kolejek FIFO odbywa się za pomocą de-
skryptorów plików. Są one liczbami całkowi-
tymi, zapisanymi w postaci typu
int
języka
C/C++. Deskryptor pliku stanowi identyfika-
tor, przekazywany w wywołaniach systemo-
wych, informujący jądro, na jakim obiekcie
ma zostać wykonana dana operacja. Gniazda
sieciowe są kolejnym mechanizmem, do któ-
rego dostęp odbywa się za pomocą deskryp-
torów plików.
Typy gniazd sieciowych
Istnieje wiele różnych standardów siecio-
wych, jak również wiele protokołów warstwy
transportu. Z tego względu mamy do czynie-
nia z wieloma typami gniazd. W artykule opi-
sane zostały jedynie standardowe gniazda in-
ternetowe, służące do komunikacji za pomo-
cą sieci lokalnych oraz internetu (więcej in-
formacji na temat innych typów gniazd sie-
ciowych znajdziesz na stronach wymienio-
nych w tabelce W Sieci).
W obrębie gniazd internetowych wyróż-
niamy trzy najważniejsze typy:
• Gniazda połączeniowe TCP – transmisja
danych realizowana przy użyciu gniazd
tego typu odbywa się z wykorzystaniem
protokołu TCP w warstwie transportu.
Gwarantuje on dostarczenie danych, za-
pobiega odebraniu pakietów w nieodpo-
wiedniej kolejności, kosztem prędkości
przesyłania danych;
• Gniazda bezpołączeniowe UDP – trans-
misja danych realizowana przy użyciu
gniazd tego typu odbywa się z wyko-
rzystaniem protokołu UDP w warstwie
transportu. Protokół UDP jest protoko-
łem bezpołączeniowym – oznacza to,
że nie jest gwarantowane dostarczenie
pakietów, ani ich odpowiednia kolej-
ność. Protokół UDP znajduje zastoso-
wanie tam, gdzie ważna jest duża szyb-
kość przesyłania danych, a utrata czę-
ści pakietów nie stanowi dużego pro-
blemu (media strumieniowe, gry kom-
puterowe);
• Gniazda raw (ang. raw sockets)
– gniazda sieciowe, które dają aplika-
cjom bezpośredni dostęp do nagłów-
ków pakietu. Gdy korzystamy z gniazd
raw, jesteśmy odpowiedzialni za odpo-
wiednie przypisanie wartości wszyst-
kim polom. Podczas gdy w codzien-
nym zastosowaniu byłoby to co naj-
mniej niewygodne, to w pewnych
przypadkach (takich jak np. testowa-
nie firewalli oraz oprogramowania sie-
ciowego pod kątem bezpieczeństwa,
szczególnie pod względem odporności
na ataki Denial of Service) jest to nie-
zwykle przydatna możliwość;
W niniejszym artykule opiszemy zastosowa-
nie dwóch pierwszych typów gniazd siecio-
wych. Jeżeli jesteś zainteresowany bliższym
poznaniem tematyki związanej z gniazdami
raw, to polecam zapoznanie się z biblioteka-
mi libpcap i libnet, o których powiemy w dal-
szej części artykułu.
Działanie gniazd sieciowych
W celu utworzenia nowego gniazda, korzysta-
my z wywołania systemowego
socket()
, po-
dając typ gniazda jako argument. Zwraca ono
wartość typu
int
, będącą deskryptorem pli-
ku. Po utworzeniu, gniazdo nie jest przypisa-
ne do żadnego portu – jeżeli chcemy tego do-
konać, korzystamy z wywołania systemowego
Listing 2.
Sposób użycia funkcji getaddrinfo()
#include <sys/types.h>
//definicje typów danych
#include <sys/socket.h>
//obsługa gniazd
#include <netdb.h>
//operacje na sieciowej bazie danych
int
getaddrinfo
(
const
char
*
node
,
// adres IP lub nazwa domenowa
const
char
*
service
,
// port lub nazwa usługi
const
struct
addrinfo
*
hints
,
// struktura służąca jako wzór
struct
addrinfo
**
res
);
// wskaźnik na początek listy wyników
// w kodzie programu
int
errn
;
// kod błędu
struct
addrinfo
hints
;
// wzór dla wywołania getaddrinfo()
struct
addrinfo
*
nodeinf
;
// informacje o interesującym nas adresie
memset
(
&
hints
,
0
,
sizeof
(
hints
));
// czyścimy strukturę
hints
.
ai_family
=
AF_INET
;
// interesuje nas jedynie protokół Ipv4
hints
.
ai_socktype
=
SOCK_STREAM
;
// będziemy używać protokołu TCP
hints
.
ai_flags
=
AI_PASSIVE
;
// tylko, jeżeli chcemy pobrać nasz adres!
if
((
errn
=
getaddrinfo
(
NULL
,
”5000”, &hints, &nodeinf)) == -1)
{
fprintf
(
stderr
,
„
getaddrinfo
error:
%
s
\
n
", gai_strerror(errn));
exit
(
1
);
Rysunek 2.
Interfejs programu Wireshark
66
październik 2009
Programowanie
Programowanie przy użyciu gniazd sieciowych
67
www.lpmagazine.org
Programowanie
Programowanie przy użyciu gniazd sieciowych
bind()
, jako argument podając deskryptor pli-
ku gniazda oraz żądany port.
Od tej chwili możemy na danym porcie
nasłuchiwać połączeń (za pomocą wywołania
systemowego
listen()
) oraz akceptować je
przy użyciu funkcji
accept()
. Zaakceptowa-
nie połączenia powoduje utworzenie nowego
deskryptora pliku, pozwalającego na oddziel-
ną obsługę komunikacji z każdym z łączących
się komputerów.
Wysyłanie i odbieranie danych odbywa
się przy pomocy funkcji
send()
i
recv()
. Na-
leży tu pamiętać o istnieniu maksymalnej jed-
nostki transmisyjnej (MTU – ang. Maximum
Transmission Unit), określającej maksymal-
ny rozmiar datagramu. Zawsze należy spraw-
dzać, czy rzeczywisty rozmiar przesłanych da-
nych (zwracany przez funkcję
send()
) pokry-
wa się z rozmiarem żądanym – jeżeli jest ina-
czej, musimy wywołać funkcję
send()
jesz-
cze raz, tym razem odpowiednio modyfikując
wskaźnik początku obszaru w pamięci.
Znacznie prościej wygląda obsługa komu-
nikacji przy pomocy gniazd bezpołączenio-
wych UDP – przesyłać i odbierać dane mo-
żemy bezpośrednio po otrzymaniu deskrypto-
ra pliku gniazda. Wykorzystujemy w tym celu
dwie funkcje:
sendto()
i
recvfrom()
. War-
to również wspomnieć o możliwości połącze-
nia gniazd UDP – w takim przypadku możemy
korzystać ze standardowych funkcji
send()
i
recv()
. Pamiętaj jednak, że dane nadal prze-
syłane będą przy użyciu protokołu UDP – ich
dotarcie do celu nie będzie gwarantowane.
Po zakończeniu przesyłania danych, na-
leży zamknąć deskryptor pliku gniazda przy
użyciu wywołania systemowego
close()
. Je-
żeli chcemy poprawnie zakończyć połączenia
dla gniazd TCP, to powinniśmy przed tym wy-
wołać funkcję
shutdown()
.
O aktualnie otwartych gniazdach oraz sta-
nie, w jakim się znajdują, możemy dowiedzieć
się przy użyciu programu netstat. Dokładne
informacje na temat jego użycia znajdziesz w
dokumentacji (
man netstat
).
Wymagane
biblioteki i pliki nagłówkowe
Do rozpoczęcia programowania przy użyciu
gniazd sieciowych wystarczy nam dowolna
dystrybucja Linuksa z zainstalowanymi pa-
kietami klasy Development. W artykule nie
wykorzystujemy żadnych dodatkowych bi-
bliotek, jedynie standardowe wywołania sys-
temowe.
Do kompilacji polecam zastosować kom-
pilator gcc w przypadku gdy programy pisane
są w języku C i g++ dla języka C++. Zwróć
uwagę, że bardzo wygodnym rozwiązaniem
(szczególnie w przypadku większych aplika-
cji), jest utworzenie odpowiednich obiektów,
reprezentujących wykorzystywane gniazda i
ukrywające przed nami szczegóły działania
konkretnych wywołań systemowych. Roz-
wiązanie takie jest szczególnie warte polece-
nia, jeżeli swój kod zamierzasz wykorzysty-
wać wielokrotnie, w różnych aplikacjach.
Testowanie programów
wykorzystujących gniazda
Wykorzystując w praktyce informacje zawar-
te w tym artykule, z pewnością nie raz natra-
fisz na problemy i trudne do wykrycia błędy w
kodzie, uniemożliwiające poprawną wymia-
nę danych. W takim przypadku, oprócz stan-
dardowego debuggera, warto mieć również
pod ręką programy, które pozwolą nam prze-
konać się, jakie dane w rzeczywistości wysy-
łamy w sieć.
Powiedzieliśmy już, że wykorzystując
mechanizm gniazd sieciowych, jesteśmy od-
powiedzialni za zdefiniowanie zasad, na któ-
rych mają porozumiewać się ze sobą progra-
my. W tym przypadku, wykorzystanie snif-
ferów w celu rozwiązywania problemów
okazuje się wręcz niezbędne – pozwala bo-
wiem na wizualizację przesyłanych danych
oraz szybkie wykrywanie błędów w ich re-
prezentacji.
Tcpdump i tcpflow
Większość Czytelników z pewnością miała
już kiedyś doświadczenia z tymi narzędziami
– pozwalają one na monitorowanie danych od-
bieranych i wysyłanych przez nasz komputer
za pośrednictwem sieci.
Tcpdump jest standardowo dostępny w
każdej dystrybucji Linuksa. W celu przechwy-
tywania danych korzysta z niskopoziomowych
Listing 3.
Sposób użycia wywołania socket()
#include <sys/types.h>
#include <sys/socket.h>
int
socket
(
int
domain
,
// wersja protokołu IP: PF_INET (v4) lub
PF_INET6 (v6)
int
type
,
// typ gniazda: SOCK_STREAM, SOCK_DGRAM
int
protocol
);
// 0, jeżeli chcemy by protokół został wybrany
za nas
int
sock
;
// tu wywołujemy funkcję getaddrinfo() jak w Listingu 2
sock
=
socket
(
nodeinf
->
ai_family
,
nodeinf
->
ai_socktype
,
nodeinf
->
ai_
protocol
);
Listing 4.
Sposób użycia funkcji bind()
#include <sys/types.h>
#include <sys/socket.h>
int
bind
(
int
sockfd
,
// deskryptor pliku gniazda
struct
sockaddr
*
my_addr
,
// adres gniazda (IP i port) naszego komputera
int
addrlen
);
// wielkość struktury my_addr
// wywołujemy funkcje getaddrinfo() i socket() tak jak w Listingu 2 i
Listingu 3
if
(
bind
(
sockfd
,
nodeinf
->
ai_addr
,
nodeinf
->
ai_addrlen
)
== -
1
)
{
perror
(
NULL
);
exit
(
1
);
}
Listing 5.
Sposób użycia funkcji listen()
#include <sys/socket.h>
int
listen
(
int
sockfd
,
// deskryptor pliku gniazda
int
backlog
);
// dozwolona liczba połączeń w kolejce
68
październik 2009
Programowanie
Programowanie przy użyciu gniazd sieciowych
69
www.lpmagazine.org
Programowanie
Programowanie przy użyciu gniazd sieciowych
mechanizmów sieciowych systemu, do których
dostęp wymaga uprawnień użytkownika root.
Aby rozpocząć przechwytywanie na interesują-
cym nas interfejsie, z zapisem do pliku, należy
wydać polecenie:
tcpdump -i interfejs -w nazwa_
pliku.dmp filtry
Filtry to wyrażenia, informujące program tcp-
dump, jakie pakiety są dla nas interesujące z
punktu widzenia dalszej analizy. Dokładny
opis działania wszystkich opcji programu oraz
filtrów znajdziesz w dokumentacji aplikacji
(
man tcpdump
).
Program tcpflow różni się od programu tcp-
dump przeznaczeniem – za jego pomocą może-
my przekonać się o postaci danych przesyłanych
za pośrednictwem strumieni TCP, przez co może
okazać się wygodniejszym narzędziem do testo-
wania tworzonych programów. Aplikacja ta nie
jest jednak standardowo dostępna w większości
dystrybucji – możesz ją pobrać ze strony http:
//www.circlemud.org/~jelson/software/tcpflow/.
Znajdują się tam również pakiety binarne dla
najpopularniejszych dystrybucji.
Wykorzystanie aplikacji tcpflow wygląda
bardzo podobnie jak w przypadku tcpdump.
Aby rozpocząć zapisywanie strumieni do od-
powiadających im plików, należy wydać po-
lecenie:
tcpflow -i interfejs filtry
Tcpflow, podobnie jak tcpdump, korzysta z bi-
blioteki libpcap, oferując przez to taką samą
składnię wyrażeń filtrujących.
Wireshark
Przyznam, że opisując powyższe programy,
nie mogłem się doczekać, kiedy przejdziemy
do aplikacji Wireshark (Rysunek 2). Jest to
bowiem zdecydowanie najlepszy program do
analizy przechwyconego ruchu sieciowego,
oferujący wiele bardzo przydatnych i łatwych
w obsłudze funkcji.
Od razu chciałbym Cię jednak prze-
strzec przed uruchamianiem programu Wi-
reshark z uprawnieniami użytkownika ro-
ot. Może to być bardzo niebezpieczne,
szczególnie jeżeli korzystasz z dodatko-
wych parserów protokołów. Zdecydowanie
lepszym rozwiązaniem jest przechwycenie
ruchu do pliku (przy użyciu programu tcp-
dump) a następnie jego analiza w pakiecie
Wireshark.
Najnowszą wersję programu Wi-
reshark znajdziesz na stronie http:
//www.wireshark.org/ oraz w repozytoriach
większości dystrybucji. Tą drugą opcję po-
lecam szczególnie tym, którzy chcieliby
uniknąć dość czasochłonnej kompilacji i
rozwiązywania zależności.
Dokładne informacje na temat wykorzy-
stania programu Wireshark do analizy pakie-
tów znajdziesz w cyklu artykułów Analiza pa-
kietów sieciowych, opublikowanym w nume-
rach: kwietniowym, czerwcowym oraz wa-
kacyjnym Linux+. Jeżeli nie czytałeś jeszcze
tych artykułów, to gorąco Cię do tego zachę-
cam – nauczą Cię one nie tylko obsługi apli-
kacji Wireshark, lecz również zwiększą Twoją
wiedzę na temat działania sieci w ogóle, co po-
zwoli na szybsze rozwiązywanie problemów z
tworzonymi aplikacjami.
Podstawowe struktury
Wiesz już, jak działa mechanizm gniazd
sieciowych w Linuksie – przyszedł czas
na praktyczne wykorzystanie go w tworzo-
nych aplikacjach. Zanim jednak zajmiemy
się opisem poszczególnych wywołań sys-
temowych, opiszemy podstawowe struktu-
ry, których poznanie jest niezbędne w ce-
lu efektywnego wykorzystania informacji
zawartych w dalszej części artykułu. Oma-
wiane struktury przedstawione zostały na
Listingu 1.
addrinfo – informacje o adresach
Podstawową strukturą jest
addrinfo
, słu-
żąca do przechowywania informacji o adre-
sach. Zawiera informacje o wersji protoko-
łu IP, typie gniazda, protokole, adres gniazda
(struktura
sockaddr
) Najważniejszą funkcją,
operującą na strukturze
addrinfo
, jest
ge-
taddrinfo()
. Dzięki niej uzyskujemy listę
wszystkich adresów danego hosta, spośród
których możemy wybrać najbardziej nam od-
powiadający.
sockaddr, sockaddr_in, in_addr – adresy
Teraz sytuacja trochę się skomplikuje. Mamy
bowiem trzy struktury (jeżeli korzystamy je-
dynie z wersji czwartej protokołu IP), które
służą do przechowywania adresów. Skąd bie-
rze się taka różnorodność i jak sobie z nią po-
radzić?
Struktura
sockaddr
jest najogólniejszą
strukturą przechowującą adresy gniazd – nie
jest ona ograniczona w żaden sposób do kon-
kretnej rodziny protokołów (którą definiuje-
my przypisując odpowiednią wartość skła-
dowej
sa_family
). W praktyce nie będziesz
jednak korzystał z tej struktury – ręczne wpi-
sywanie danych do tablicy
sa_data
mija się
z celem.
Dla protokołu IPv4 odpowiednie jest wy-
korzystanie struktury
sockaddr_in
(istnieje
jej odpowiednik dla protokołu IPv6 –
soc-
kaddr_in6
). Zapisane są w niej pełne infor-
macje o adresie sieciowym: adres IP (w po-
staci struktury
in_addr
) oraz numer portu
docelowego (zmienna składowa
sin_port
).
Zwróć uwagę na zastosowanie tablicy obiek-
tów typu
unsigned char
– wypełniamy ją
zerami w celu zapewnienia odpowiednie-
go rozmiaru struktury
sockaddr_in
, takie-
go samego jak struktury
sockaddr
. Dzięki
temu, pomimo że większość wywołań sys-
temowych wymaga podania adresu w posta-
ci struktury
sockaddr
, możemy dokonać rzu-
towania.
Sam 32-bitowy adres IP zapisany jest w
strukturze
in_addr
w postaci zmiennej ty-
Listing 6.
Użycie funkcji accept()
#include <sys/types.h>
#include <sys/socket.h>
int
accept
(
int
sockfd
,
// deskryptor pliku gniazda
struct
sockaddr
*
addr
,
// adres gniazda
socklen_t
*
addrlen
);
// wielkość struktury addr
// wywołujemy funkcje getaddrinfo(), socket(), bind() i listen() tak jak
na wcześniejszych listingach
char
*
port
=
”5000”
;
// wykorzystywany port
const
int
backlog
=
10
;
// maksymalna liczba połączeń
struct
sockaddr
remote_addr
;
// adres komputera inicjalizującego połączenie
socklen_t
addr_size
;
// rozmiar struktury sockaddr
int
newsockfd
;
// deskryptor nowego gniazda
addr_size
=
sizeof
(
remote_addr
);
newsockfd
=
accept
(
sockfd
,
&
remote_addr
,
&
addr_size
);
// możemy rozpocząć przesyłanie danych za pomocą gniazda newsockfd
68
październik 2009
Programowanie
Programowanie przy użyciu gniazd sieciowych
69
www.lpmagazine.org
Programowanie
Programowanie przy użyciu gniazd sieciowych
pu
unsigned int
. Nie musisz jednak przej-
mować się sposobem kodowania adresu – ca-
łą niezbędną pracę wykonają odpowiednie
funkcje.
Kolejność wywołań
Kolejność, z jaką używamy wywołań syste-
mowych zależy ściśle od typu gniazda, a w
szczególności od wykorzystywanego protoko-
łu warstwy transportu.
Dla gniazd korzystających z protokołu
TCP, kolejność wywołań systemowych jest
następująca:
• Wywołujemy funkcję
getaddrinfo()
w
celu wpisania odpowiednich danych ad-
resowych do struktur;
• Wywołujemy funkcję
socket()
, zwra-
cającą deskryptor pliku gniazda. Ja-
ko parametry wywołania wykorzystu-
jemy składowe struktur, które uzupeł-
niliśmy przy pomocy funkcji
getad-
drinfo()
;
• Jeżeli nasz program ma działać jako
serwer, nasłuchujący i obsługujący po-
łączenia od klientów, korzystamy z wy-
wołania systemowego
bind()
, służące-
go do przypisania gniazda do konkret-
nego portu TCP. Nasłuchiwanie połą-
czeń odbywa się przy pomocy funk-
cji
listen()
. Połączenia przychodzą-
ce akceptujemy za pomocą funkcji
ac-
cept()
, co powoduje utworzenie nowe-
go gniazda, służącego do komunikacji z
danym klientem;
• Jeżeli nasz program ma działać jako
klient, nie potrzebujemy korzystać z wy-
wołania
bind()
. Zamiast tego, łączymy
się ze zdalnym procesem przy pomocy
funkcji
connect()
– system automatycz-
nie przydzieli połączeniu odpowiedni port
po stronie naszej maszyny;
• Wysyłamy i odbieramy dane, pamiętając
o ograniczeniach związanych z maksy-
malną ilością danych wysyłanych w jed-
nym datagramie;
• Zamykamy deskryptor pliku gniazda przy
pomocy wywołań
shutdown()
i
close()
.
Dla gniazd korzystających z protokołu UDP
przebieg typowej wymiany danych jest znacz-
nie prostszy:
• Podobnie jak w przypadku gniazd TCP,
korzystamy z wywołań
getaddrinfo()
i
socket()
;
• Możemy już wysyłać i odbierać da-
ne przy pomocy funkcji
sendto()
i
re-
cvfrom()
;
• W celu wymiany danych możemy rów-
nież korzystać ze standardowych funk-
cji
send()
i
recv()
, pod warunkiem, że
połączymy się ze zdalnym hostem przy
użyciu wywołania
connect()
. Pamię-
taj, że dane w dalszym ciągu będą prze-
syłane przy użyciu zawodnego protokołu
UDP;
• Zamykamy deskryptor pliku przy pomo-
cy wywołania
close()
.
Przygotowanie
adresów – getaddrinfo()
Zanim skorzystamy z wywołania
socket()
w celu uzyskania deskryptora pliku nowego
gniazda, powinniśmy pobrać informacje o in-
terfejsie sieciowym naszego komputera oraz
komputera, z którym nawiążemy połączenie.
Korzystamy w tym celu z funkcji
getaddrin-
fo()
, której sposób wywołania został przed-
stawiony na Listingu 2.
Bardzo ważną rolę przy wywoła-
niu funkcji
getaddrinfo()
pełni struktu-
ra
hints
– zawiera ona informacje o inte-
resującej nas wersji protokołu IP oraz typie
gniazda (TCP lub UDP). Zwróć uwagę, że
jeżeli chcemy otrzymać adresy IP naszego
komputera, powinniśmy składowej
ai_flags
przypisać wartość
AI_PASSIVE
. Jeżeli chce-
my uzyskać adresy IP innego komputera,
pozostawiamy wartość
NULL
.
Wywołanie
getaddrinfo()
zwraca
wyniki w postaci jednostronnie łączonej
listy. Możesz zatem wybrać najodpowied-
niejszy w danej sytuacji adres IP, stosując
jedynie prosty algorytm przechodzenia li-
sty. Ostatni element listy rozpoznasz bez
problemów – jego składowa
ai_next
ma
wartość
NULL
.
Jeżeli wywołanie funkcji
getaddrin-
fo()
zakończyło się sukcesem, to zwraca-
na jest wartość 0. W przeciwnym wypadku
zwracany jest kod błędu, który możesz na-
stępnie zamienić na czytelną dla użytkowni-
ka postać, przy pomocy funkcji
gai_strer-
ror()
.
Na koniec przypomnę, że nie ma różni-
cy, czy funkcji
getaddrinfo()
podamy ad-
res IP, czy nazwę domenową. W tym dru-
gim przypadku, system sam zadba o wyko-
nanie odpowiedniego zapytania DNS i ak-
tualizację odpowiedniego pola struktury
ad-
drinfo
.
Gdy uznamy, że uzyskana lista adresów
nie będzie już nam w dalszej części progra-
mu potrzebna, warto ją zwolnić, korzysta-
jąc z wywołania
freeaddrinfo()
, jako je-
dyny parametr podając wskaźnik do począt-
ku listy.
Z tego powodu nigdy nie powinieneś
zmieniać wartości tego wskaźnika, ponieważ
w takim przypadku zwolnienie pamięci było-
by niemożliwe.
Utworzenie gniazda – socket()
Po uzyskaniu niezbędnych informacji o adre-
sach, możemy użyć wywołania systemowe-
go tworzącego nowy deskryptor pliku gniaz-
da. Jako parametrów użyjemy składowych
struktury
addrinfo
z listy zwróconej przez
wywołanie
getaddrinfo()
. Sposób wywo-
łania funkcji
socket()
został przedstawiony
na Listingu 3.
Jak widać, wartością zwracaną przez
wywołanie
socket()
jest deskryptor pli-
ku gniazda. Jest to zmienna typu
int
. Jeże-
li w trakcie działania funkcji wystąpił błąd,
zwracana jest wartość -1, zaś zmienna glo-
balna
errno
przybiera odpowiednią wartość.
Można ją zamienić na postać tekstową, czy-
telną dla użytkownika, przy pomocy funkcji
perror()
.
Listing 7.
Sposób użycia funkcji connect() i getpeername()
#include <sys/types.h>
#include <sys/socket.h>
int
connect
(
int
sockfd
,
// deskryptor pliku gniazda
struct
sockaddr
*
serv_addr
,
// adres serwera
int
addrlen
);
// rozmiar struktury serv_addr
int
getpeername
(
int
sockfd
,
// deskryptor pliku gniazda
struct
sockaddr
*
addr
,
// struktura, w której zapisany
zostanie adresowych
int
*
addrlen
);
// rozmiar struktury addr
// wywołujemy funkcje getaddrinfo() i socket(), tak jak na wcześniejszych
listingach
connect
(
sockfd
,
nodeinf
->
ai_addr
,
nodeinf
->
ai_addrlen
);
70
październik 2009
Programowanie
Programowanie przy użyciu gniazd sieciowych
71
www.lpmagazine.org
Programowanie
Programowanie przy użyciu gniazd sieciowych
Przypisanie do portu – bind()
Po otrzymaniu prawidłowego deskryptora
pliku gniazda, możemy powiązać je z odpo-
wiednim portem w naszym systemie. Wyko-
rzystujemy w tym celu funkcję
bind()
, któ-
rej sposób wywołania został przedstawiony
na Listingu 4.
Działanie wywołania
bind()
nie powin-
no budzić żadnych wątpliwości. Pamiętaj jed-
nak, że porty 0 – 1023 są dostępne jedynie dla
użytkownika root.
Nasłuchiwanie
połączeń – listen()
Po powiązaniu gniazda z portem naszego sys-
temu, możemy rozpocząć nasłuchiwanie po-
łączeń przy użyciu wywołania systemowe-
go
listen()
, którego sposób użycia został
przedstawiony na Listingu 5.
Funkcja
listen()
zwraca wartość 0 w
przypadku, gdy rozpoczęcie nasłuchiwa-
nia zakończyło się powodzeniem oraz -1 w
przypadku, gdy wystąpił błąd. Komunikat o
błędzie możesz przedstawić użytkownikowi
przy pomocy funkcji
perror()
.
Akceptowanie
połączeń – accept()
Po rozpoczęciu nasłuchiwania, możemy ak-
ceptować przychodzące połączenia przy po-
mocy wywołania
accept()
. Sposób jego uży-
cia przedstawiony został na Listingu 6.
Zaakceptowanie oczekującego połącze-
nia spowoduje utworzenie nowego deskryp-
tora pliku gniazda. Za jego pomocą możemy
komunikować się ze zdalnym procesem za po-
mocą funkcji
send()
i
recv()
. Gniazdo, któ-
re przypisane jest do interfejsu naszego kom-
putera pozostaje dalej otwarte, nasłuchując na-
stępnych połączeń.
Adres komputera nawiązującego po-
łączenie z naszym serwerem przechowuje-
my w strukturze
remote_addr
, zaś zmien-
na
addr_size
zawiera jej rozmiar wyrażo-
ny w bajtach.
Duże znaczenie ma wielkość bufora po-
łączeń, określona za pomocą zmiennej
bac-
klog
– jeżeli zostanie przekroczona, dalsze
połączenia będą automatycznie odrzuca-
ne. Należy pamiętać, że nie zawsze żądana
wielkość bufora połączeń zostanie uwzględ-
niona przez system – również jądro może li-
mitować maksymalną liczbę oczekujących
połączeń.
Nawiązanie
połączenia – connect()
Jeżeli nasza aplikacja ma służyć jedynie do na-
wiązywania połączenia z serwerem, to może-
my pominąć wywołanie funkcji
bind()
i od
razu skorzystać z funkcji
connect()
, odpo-
wiadającej za nawiązanie połączenia ze zdal-
nym hostem. Sposób użycia wywołania
con-
nect()
został przedstawiony na Listingu 7.
Po wywołaniu zakończonym sukcesem
(w takim przypadku zwracana jest wartość
0), możemy już użyć funkcji
send()
i
recv()
w celu wymiany danych pomiędzy procesami
działającymi na połączonych maszynach.
Nazwę zdalnego komputera możesz odczy-
tać przy pomocy funkcji
getpeername()
, któ-
rej sposób wywołania został również przedsta-
wiony na Listingu 7. Wykorzystywana funkcja
inet_ntop()
pozwala na przedstawienie adre-
su hosta (zapisanego w strukturze
sockaddr
) w
postaci czytelnej dla użytkownika.
Transmisja
danych – send() i recv()
Gdy nawiążemy połączenie ze zdalnym ho-
stem lub zaakceptujemy żądanie połączenia,
możemy rozpocząć przesyłanie danych. Słu-
żą w tym celu funkcje
send()
i
recv()
, któ-
rych wykorzystanie przedstawione zostało na
Listingu 8.
Funkcja
send()
po wywołaniu zwraca
ilość wysłanych danych, wyrażoną w bajtach
lub -1, jeżeli wystąpił błąd. Pamiętaj, że jeżeli
przekroczysz maksymalną jednostkę transmi-
syjną sieci, to będziesz musiał ponownie wy-
wołać funkcję
send()
w celu przesłania resz-
ty danych. Aby uniknąć takiej sytuacji, war-
to wysyłać dane w porcjach nie przekracza-
jących wielkości 1 KB. Optymalny rozmiar
zależy oczywiście od interfejsu sieciowego
– możesz przekonać się o tym wywołując pro-
gram ifconfig. Pamiętaj, że pakiet, oprócz ob-
szaru danych, zawiera również nagłówki, któ-
rych rozmiar powinieneś odjąć od wielkości
MTU w celu uzyskania maksymalnego roz-
miaru pakietu, który może zostać przesłany za
pomocą danego interfejsu.
Funkcja
recv()
zwraca liczbę bajtów za-
pisaną w buforze lub -1, jeżeli wystąpił błąd.
Zwrócenie wartości 0 oznacza, iż zdalny host
zamknął połączenie. Podając prawidłowy roz-
miar
len
bufora, zgodny z wielkością zare-
zerwowanego obszaru pamięci, zapobiegamy
przypadkowemu nadpisaniu obszarów przy-
legających.
Listing 8.
Sposób użycia funkcji send() i recv()
#include <sys/types.h>
#include <sys/socket.h>
int
send
(
int
sockfd
,
// deskryptor pliku gniazda
const
void
*
msg
,
// wskaźnik na przesyłane dane
int
len
,
// rozmiar danych w bajtach
int
flags
);
// flagi kontrolne
int
recv
(
int
sockfd
,
// deskryptor pliku gniazda
void
*
buf
,
// bufor, do którego zapisane zostaną dane
int
len
,
// rozmiar bufora
int
flags
);
// flagi kontrolne
Listing 9.
Parametry wywołania funkcji sendto() i recvfrom()
#include <sys/types.h>
#include <sys/socket.h>
int
sendto
(
int
sockfd
,
// deskryptor pliku gniazda
const
void
*
msg
,
// dane do wysłania
int
len
,
// rozmiar danych wyrażony w bajtach
unsigned
int
flags
,
// flagi kontrolne
const
struct
sockaddr
*
to
,
// adres docelowy
socklen_t
tolen
);
// rozmiar struktury to
int
recvfrom
(
int
sockfd
,
// deskryptor pliku gniazda
void
*
buf
,
// bufor otrzymywanych danych
int
len
,
// rozmiar bufora
unsigned
int
flags
,
// flagi kontrolne
struct
sockaddr
*
from
,
// adres źródłowy
int
*
fromlen
);
// rozmiar struktury from
70
październik 2009
Programowanie
Programowanie przy użyciu gniazd sieciowych
71
www.lpmagazine.org
Programowanie
Programowanie przy użyciu gniazd sieciowych
Dodatkowe informacje na temat działa-
nia funkcji
send()
i
recv()
, a w szczególno-
ści flagi kontrolne, znajdziesz w ich dokumen-
tacji (
man send
,
man recv
).
Gniazda UDP
– sendto() i recvfrom()
W przypadku gniazd UDP, po otrzymaniu
deskryptora pliku gniazda, możemy od razu
przystąpić do wysyłania i odbierania danych
za pomocą funkcji
sendto()
i
recvfrom()
.
Sposób ich wykorzystania został przedstawio-
ny na Listingu 9.
Z pewnością zauważyłeś bardzo duże po-
dobieństwo do funkcji
send()
i
recv()
. Ko-
rzystając z funkcji
sendto()
i
recvfrom()
wystarczy dodatkowo podać adres docelowy
lub źródłowy. Jeżeli wysyłasz (lub odbierasz)
wiele danych do jednego hosta, to warto się z
nim połączyć przy użyciu funkcji
connect()
.
Zakończenie połączenia
Po zakończeniu wymiany danych należy za-
mknąć gniazdo przy pomocy funkcji
close()
,
jako jedyny parametr wywołania podając de-
skryptor pliku gniazda.
Dla gniazd TCP powinniśmy przed wy-
wołaniem funkcji
close()
zakończyć połą-
czenie przy pomocy funkcji
shutdown()
.
Efektywna obsługa gniazd
Wykorzystując informacje zawarte w artyku-
le, z pewnością zauważyłeś, że wywołanie du-
żej części funkcji służących do obsługi gniazd
powoduje zablokowanie wykonywania dalszej
części programu. Dzieje się tak, ponieważ w sy-
tuacji, gdy nie ma oczekujących danych lub po-
łączeń, wywołanie systemowe oczekuje na ich
nadejście. W większości programów jest to bar-
dzo niepożądane.
Podczas tworzenia nowego gniazda mamy
możliwość sprawienia, aby było ono niebloku-
jące, jednak powoduje to dość duże problemy
związane z jego obsługą – konieczne jest jego
odpytywanie, co z kolei pochłania dużą licz-
bę cykli zegarowych. Jakie jest więc optymal-
ne rozwiązanie?
Możliwość jednoczesnego monitorowania
i wykorzystania wielu gniazd sieciowych daje
nam funkcja
select()
. Jej zastosowanie zo-
stało świetnie opisane w tutorialu Beej's Guide
to Network Programming Using Internet Soc-
kets, autorstwa Briana Halla. Jest on dostępny
na stronie wymienionej w ramce W Sieci.
Definiowanie protokołów
Za pomocą gniazd możemy wysyłać dowol-
ne dane. Bardzo ważne jest jednak, aby były
one poprawnie interpretowane przez odbie-
rający je program. W celu określenia zasad,
z jakimi należy budować komunikaty, a na-
stępnie wydobywać z nich istotne dane, two-
rzy się protokoły.
Istnieje wiele typów protokołów. Jedna z
najpopularniejszych klasyfikacji dzieli je na
protokoły binarne i tekstowe (znakowe). W
protokołach binarnych, dane zapisywane są
za pomocą sekwencji bitów, stanowiących re-
prezentację przesyłanych informacji. W proto-
kołach tekstowych, dane zapisywane są za po-
mocą znaków (np. ASCII), przez co są zro-
zumiałe dla człowieka (ang. human reada-
ble protocol).
Libnet i libpcap
Omawiane w artykule techniki mają bar-
dzo szerokie zastosowanie w pisaniu stan-
dardowych aplikacji sieciowych. Nie dają
nam jednak wielkich możliwości, jeżeli cho-
dzi o manipulowanie nagłówkami warstwy
sieciowej i transportu. W większości aplika-
cji jest to zupełnie niepotrzebne, co więcej
– często może powodować spore niedogod-
ności, ponieważ w celu uzyskania niskopo-
ziomowego dostępu do mechanizmów sie-
ciowych, konieczne są uprawnienia użyt-
kownika root.
Libnet jest biblioteką bardzo szero-
ko wykorzystywaną w aplikacjach związa-
nych z bezpieczeństwem sieci. Pozwala na
wstrzykiwanie pakietów o zdefiniowanej
przez nas zawartości nagłówków warstwy
sieciowej i transportu. Wykorzystanie bi-
blioteki libnet nie jest trudne, jednak wyma-
ga znacznie większej wiedzy na temat budo-
wy i zadań warstw niższych, niż w przypad-
ku standardowego programowania przy uży-
ciu gniazd.
Libpcap to biblioteka służąca do prze-
chwytywania danych odbieranych przez inter-
fejs sieciowy naszego komputera. Pozwala na
przełączenie karty sieciowej w tryb promisco-
us, pozwalający na odbieranie danych nieprze-
znaczonych dla naszego komputera. Podobnie
jak libnet, biblioteka ta jest wykorzystywana
głównie w aplikacjach powiązanych z bezpie-
czeństwem sieci.
Podsumowanie
Jeżeli po lekturze artykułu odczuwasz niedo-
syt, to gorąco zachęcam Cię do zapoznania się
z tutorialem, wymienionym w poprzednim pa-
ragrafie. Na szczególną uwagę zasługują za-
prezentowane w nim przykłady aplikacji ko-
rzystających z gniazd, które z oczywistych po-
wodów ciężko zawrzeć w artykule.
Oprócz tutoriali, polecam Ci również
zapoznanie się ze stronami podręczni-
ka systemowego. Znajdujące się tam opi-
sy działania funkcji z pewnością są do-
kładniejsze od zawartych w tym artykule.
Głównym moim celem było przekazanie Ci
wiedzy na temat kolejności i sposobu wy-
woływania poszczególnych funkcji – nie
da się jej bowiem zdobyć, czytając jedy-
nie strony podręcznika. Początkujący pro-
gramiści często gubią się również w gąsz-
czu struktur odpowiadających za przecho-
wywanie adresów gniazd, które również
nie zostały dobrze opisane.
Gdy zdobędziesz już większą wprawę w
programowaniu sieciowym, polecam Ci za-
poznanie się z podstawowymi dokumentami
RFC, dotyczącymi najważniejszych protoko-
łów warstwy aplikacji. Po ich lekturze, spró-
buj zaimplementować niektóre z ich funk-
cji w programach – jest to bardzo pouczają-
ce doświadczenie, szczególnie jeżeli monito-
rujesz wychodzące dane przy użyciu sniffera,
a następnie analizujesz je za pomocą progra-
mu Wireshark.
Jeżeli masz jakieś wątpliwości lub proble-
my związane z programowaniem przy użyciu
gniazd, to z chęcią na nie odpowiem – mój ad-
res e-mail znajduje się w ramce O Autorze.
Autor interesuje się bezpieczeństwem
systemów informatycznych, programo-
waniem, elektroniką, muzyką rockową,
architekturą mikroprocesorów oraz za-
stosowaniem Linuksa w systemach wbu-
dowanych.
Kontakt z autorem: rkulaga89@gmail.com
O autorze
• Główna strona programu Wireshark – http://www.wireshark.org/;
• Główna strona programu tcpflow – http://www.circlemud.org/~jelson/software/tcpflow/;
• Wspomniany tutorial – http://www.beej.us/guide/bgnet/;
• Biblioteka libnet – http://sourceforge.net/projects/libnet-dev/;
• Biblioteka libpcap – http://sourceforge.net/projects/libpcap/.
W Sieci