2009 10 Programowanie przy użyciu gniazd sieciowych [Programowanie]

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

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

);

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


Wyszukiwarka

Podobne podstrony:
Programowanie sieciowe przy użyciu gniazdek w?lphi 3 MPLMRFGCOQC4VOMKHU5DAT5YKUDWUHLWUTINXRI
Nauka o polityce - program wykładu - 2009-10, Politologia, 1 rok UJ
2009 10 OpenCV – systemy wizyjne [Programowanie]
Program FP WSZOP 2009 10 dz cw
Metodyka wychowania.. PROGRAM 2009-10, opracowania i notatki, metodyka wychowania nauczania i rehabi
PROGRAM WYKŁADU 2009-10 MiBM i En, Polibuda, Materiałoznastwo
program zajęć 2009.10, ^v^ UCZELNIA ^v^, ^v^ Pedagogika, promocja zdrowia z arteterapią i socjoterap
Tworzenie szkiców miejsca wypadku przy użyciu programu PLAN
Jak scrapować przy użyciu programu Corel Photo
Biblioteki Qt Zaawansowane programowanie przy uzyciu C
Biblioteki Qt Zaawansowane programowanie przy uzyciu C 2
Aktualizacja kart przy użyciu tunera na Linuxie i programu The Last Drakkar
Programowanie aplikacji dla Sklepu Windows w C Projektowanie innowacyjnych aplikacji sklepu Windows
Biblioteki Qt Zaawansowane programowanie przy uzyciu C
J Bieliński, K Iwińska, A Rosińska Kordasiewicz ANALIZA DANYCH JAKOŚCIOWYCH PRZY UŻYCIU PROGRAMÓW K
Biblioteki Qt Zaawansowane programowanie przy uzyciu C bibqtc
Jak programować PATS przy użyciu FORScan
BCT1630, Programowanie przy użyciu LPT COM
10 Programowa obsługa sygnałów analogowych materiały wykładowe

więcej podobnych podstron