background image

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 

background image

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

};

background image

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

background image

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

background image

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

background image

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()

 

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

);

background image

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

background image

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