Projektowanie oprogramowania systemów

Transkrypt

Projektowanie oprogramowania systemów
Projektowanie
oprogramowania
systemów
KOMUNIKACJA SIECIOWA I SYSTEMY RPC
plan

programowanie sieciowe


BSD/POSIX Socket API
systemy RPC


interfejsy obiektowe

CORBA

DCOM

RMI
WebServices

WSDL/SOAP

XML-RPC

REST
programowanie sieciowe

Termin „programowanie sieciowe” (network programming) odnosi się
do tworzenia aplikacji, które komunikują się ze sobą za
pośrednictwem sieci komputerowej

Komunikacja sieciowa to specyficzna forma komunikacji
międzyprocesowej (IPC), w której komunikujące się procesy znajdują
się na osobnych maszynach


Nie używamy więc obiektów IPC, których zasięg ograniczony jest do
pojedynczej maszyny (pamięci współdzielonej, semaforów, mutexów…)

Komunikacja odbywa się za pośrednictwem gniazd sieciowych (network
sockets) i protokołów komunikacyjnych
Najbardziej powszechnym API komunikacji sieciowej jest BSD (POSIX)
Sockets API (AKA Berkeley sockets)

stąd, programowanie sieciowe nazywamy również programowaniem gniazd
BSD Socket API – nazewnictwo i
historia

Tradycyjnie, API gniazd wywodzi się z dystrybucji Unixa BSD, stąd nazwa
BSD Socket (lub Berkeley sockets, bo BSD to Berkeley Software
Distribution – Unix opracowany na UCal w Berkeley)

Na bazie API BSD powstał standard gniazd POSIX (POSIX sockets)

API gniazd BSD było oryginalną implementacją protokołów TCP/IP, które
są podstawą działania Internetu (tzw. Internet Protocol Suite)

W praktyce wszystkie współczesne systemy operacyjne posiadają API
gniazd przynajmniej częściowo spójne z BSD – włącznie z Windows (tzw.
Winsock API)
BSD Socket API - alternatywy


Konkurencyjna w stosunku do BSD implementacja Unixa System V
używała innego API komunikacji sieciowej: STREAMS (Transport Layer
Interface)

TLI ściśle opiera się na modelu OSI/ISO – ścisła separacja warstw

STREAMS było wykorzystywane m.in. w Novell NetWare, Windows NT

Wiele implementacji Unixa używa równolegle TLI oraz BSD sockets
Wraz z Windows for Workgroups Microsoft promował protokół i API
NetBEUI (NetBIOS Extended User Interface) – sieć peer-to-peer
oryginalnie działająca niezależnie od TCP/IP, w tej chwili praktycznie
zawsze bazująca na TCP/IP
gniazda sieciowe – słowniczek
pojęć

gniazdo – punkt końcowy (endpoint) komunikacji IPC w oparciu o sieć
komputerową

adres gniazda – kombinacja adresu IP komputera i numeru portu (numer
usługi)

typ gniazda

gniazda datagramowe (protokół: UDP), typ SOCK_DGRAM

gniazda strumieniowe (protokół: TCP), typ SOCK_STREAM

gniazda „surowe”, SOCK_RAW – użytkownik jest odpowiedzialny za implementację
własnego protokołu
tryb nieblokujący I/O

nowoutworzone gniazda są blokujące, co oznacza że operacje odczytu/zapisu blokują
działanie programu tak długo, aż operacja się powiedzie

gniazdo można przestawić w tryb nieblokujący (nonblocking mode), wówczas
operacje odczytu/zapisu nie blokują działania programu ale zwracają kod błędu
EWOULDBLOCK w momencie, kiedy operacja nie może być wykonana natychmiast
(brak danych w buforze odczytu gniazda, przepełniony bufor zapisu gniazda)

tryb nieblokujący umożliwia obsługę wielu gniazd równocześnie w pojedynczym wątku
poprzez multipleksację I/O (i nie tylko gniazd – również plików i wszystkiego co
„wygląda jak plik”, włacznie z UI)

w trybie blokującym zwykle każde gniazdo wymaga osobnego wątku, co prowadzi do
nieefektywnego wykorzystania zasobów

tryb nieblokujący jest zalecany dla wszystkich, poza najbardziej prymitywnymi
aplikacjami sieciowymi
multipleksacja I/O

rejestrujemy deskryptory plików w specjalnej strukturze fd_set, specyfikując w jakich
zdarzeniach związanych z gniazdem/plikiem jesteśmy zainteresowani (możliwość
odczytu, możliwość zapisu, sytuacja wyjątkowa)

podajemy strukturę jako parametr funkcji systemowej select(), poll() lub podobnej

ww. funkcja blokuje działanie programu do momentu aż w którymkolwiek z gniazd
wystąpi którekolwiek zdarzenie, a następnie zwraca informacje o rodzaju zdarzenia
które wystąpiło

obsługujemy zdarzenie – np. odczyt/zapis danych z gniazda, które w tej sytuacji nie
mają prawa zablokować programu

powracamy do pętli select()/poll() – oczekujemy na kolejne zdarzenie

deskryptor może również odnosić się do źródła zdarzeń UI – wówczas możliwa jest
obsługa komunikatów UI w tej samej pętli – tak działa m.in. X-Windows
gniazda serwerowe i klienckie

W przypadku gniazd typów połączeniowych (SOCK_STREAM) zwykle
wyróżnia się 2 modele ich użycia

gniazda serwerowe – powiązane z określonym portem i adresem, nasłuchują w
oczekiwaniu na połączenia przychodzące od klientów

gniazda klienckie – inicjują połączenia z nasłuchującymi gniazdami serwerowymi,
nie muszą mieć przypisanego a’priori adresu sieciowego

Nie jest możliwe nawiązanie sesji pomiędzy dwoma gniazdami
klienckimi

W przypadku gniazd typów bezpołączeniowych nie ma potrzeby
tworzenia gniazd nasłuchujących i oczekiwania na połączenie –
możliwa jest natychmiastowa komunikacja każdy-z-każdym

Podział na gniazda serwerowe i klienckie wynika ze sposobu ich użycia
(wywoływanych funkcji) – tworzymy je identycznie
BSD API – socket()

int socket(int domain, int type, int protocol);

socket() tworzy nowe gniazdo określonego rodzaju, alokuje dla niego zasoby i
zwraca deskryptor pliku

parametry

domena – określa rodzinę protokołów danego gniazda:

AF_INET – protokoły oparte na IPv4

AF_INET6 – protokoły IPv6

AF_UNIX – gniazda lokalne (domeny Unixa – tylko w systemach Unix)

typ – rodzaj gniazda (SOCK_DGRAM, SOCK_STREAM, SOCK_RAW, …)

protokół – specyficzny protokół warstwy transportowej, lub 0 np.

IPPROTO_TCP – TCP (domyślny dla SOCK_STREAM)

IPPROTO_UDP – UDP (domyślny dla SOCK_DGRAM)

IPPROTO_SCTP

IPPROTO_DCCP
bind()

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind() wiąże utworzone gniazdo z adresem sieciowym lokalnego
komputera

nowoutworzone gniazdo nie jest związane z żadnym adresem, za
pomocą bind() możemy mu przypisać adres, dzięki czemu będziemy
mogli oczekiwać na przychodzące połączenia na określonym
adresie/porcie

parametry

sockfd - deskryptor gniazda

addr – struktura określająca adres do którego przypisujemy gniazdo (adres IP +
numer portu). Jeśli nie podamy adresu IP, gniazdo zostanie powiązane ze wszystkimi
adresami IP komputera. Jeśli nie podamy numeru portu, zostanie on wybrany losowo
przez OS

addrlen – rozmiar w bajtach struktury addr (kompatybilność IPv4/IPv6 itp.)
listen()

int listen(int sockfd, int backlog);

listen() rozpoczyna „nasłuchiwanie” przychodzących połączeń na
danym gnieździe serwerowym o określonym adresie – tylko dla
gniazd typu strumieniowego (połączeniowych)

parametry

sockfd – deskryptor gniazda

backlog – rozmiar kolejki nadchodzących połączeń; przychodzące
połączenia są umieszczane w kolejce, z której są usuwane poprzez ich
zaakceptowanie funkcją accept(); połączenia nadmiarowe będą
automatycznie odrzucane
accept()

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

accept() akceptuje przychodzące połączenie na nasłuchującym
gnieździe serwerowym

dla zaakceptowanego połączenia przychodzącego tworzone jest
nowe gniazdo, które jest połączone w sesję ze zdalnym gniazdem

zwraca deskryptor nowego gniazda klienckiego

parametry

sockfd – deskryptor nasłuchującego gniazda serwerowego

cliaddr – struktura, w której zostanie zapisany adres zdalnego połączonego
gniazda

addrlen – rozmiar struktury cliaddr
connect()

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t
addrlen);

connect() inicjuje połączenie gniazda klienckiego ze zdalnym,
nasłuchującym gniazdem serwerowym

w przypadku gniazd bezpołączeniowych jedynie określa domyślny cel
(endpoint) dla komunikacji

parametry

sockfd – deskryptor klienckiego gniazda, które łączymy ze zdalnym systemem

serv_addr – adres zdalnego, nasłuchującego gniazda serwerowego

addrlen – rozmiar struktury serv_addr
łączenie gniazd serwerowych i
klienckich typu połączeniowego
odczyt/zapis gniazd

w zależności od typu gniazda istnieją 2 zestawy funkcji służących do
odczytu/zapisu danych z/do gniazd


połączeniowe (SOCK_STREAM)

odczyt: ssize_t recv(int sockfd, void *buf, size_t len, int flags);

zapis: ssize_t send(int sockfd, const void *buf, size_t len, int flags);
bezpołączeniowe (SOCK_DGRAM)

odczyt: ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr,
socklen_t *addrlen);

zapis: ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr
*dest_addr, socklen_t addrlen);
odczyt/zapis gniazd

API połączeniowe jest tożsame z wywołaniem standardowych funkcji
read()/write() używanych z plikami – kiedy parametr flags jest równy 0
(można ich używać zamiennie)

Parametr flags umożliwia określenie dodatkowych, zaawansowanych
opcji odczytu/zapisu (najczęściej nieużywanych)

API połączeniowe może być używane również z gniazdami typu
bezpołączeniowego, których domyślny adres określono za pomocą
connect()

API bezpołączeniowe recvfrom()/sendto() umożliwia podanie
docelowego adresu datagramu lub uzyskanie informacji o adresie
źródłowym datagramu odebranego
użycie API bezpołączeniowego
zamykanie gniazd

gniazda zamykamy i zwalniamy zasoby za pomocą standardowej
funkcji close() (na Windows closesocket())

w przypadku połączonych sesją gniazd połączeniowych powoduje to
również zerwanie połączenia – jest to tzw. abrupt (non-graceful)
shutdown i jest nieeleganckie (zwykle oznacza błąd)

zaleca się przeprowadzenie procedury graceful shutdown poprzez
użycie funkcji shutdown() (co jednak wydłuża czas zamknięcia i
komplikuje maszynę stanów)
graceful shutdown

shutdown(SD_RDWR)

recv() zwraca 0

close()

recv() zwraca 0 – połączenie
zamknięte

shutdown(SD_RDWR)

close()
odnajdywanie adresów sieciowych

maszyny w sieci identyfikowane są za pomocą adresów IP – liczb 32bitowych (IPv4) lub 128-bitowych (IPv6)

adresy IP są trudne do zapamiętania i mogą się zmieniać w czasie,
dlatego stworzono system nazw i serwerów nazw (domain name
system, DNS) będący rozproszoną bazą danych wiążącą nazwy z
adresami

API BSD umożliwia wyszukiwanie adresów i nazw maszyn w sieci poprzez
odpytywanie serwerów DNS i innych baz danych

funkcjonalność ta nosi nazwę resolver

podstawowy resolver w API BSD jest blokujący – nie istnieje
standardowy, przenośny mechanizm tworzenia nieblokujących zapytań
systemu DNS
resolver

struct hostent *gethostbyname(const char *name);

gethostbyname() zwraca listę znanych adresów dla hosta o podanej
nazwie

struktura hostent zawiera w sobie pola określające typ adresu (np.
AF_INET, AF_INET6), adres hosta, jego długość oraz nazwę
kwalifikowaną powiązaną z tym adresem; wskaźnik do następnego
elementu listy

z jedną nazwą może być powiązane kilka adresów różnych typów oraz
kilka nazw (aliasów)

aby poznać wszystkie adresy/nazwy danego hosta należy
trawersować listę struktur hostent aż kolejny element będzie pusty
gethostbyaddr()

struct hostent *gethostbyaddr(const void *addr, int len, int
type);

gethostbyaddr() odnajduje inne znane adresy oraz nazwy hosta
o podanym adresie i zwraca ich listę identycznie jak
gethostbyname()

parametry

addr – adres hosta, którego wyszukujemy

len – rozmiar adresu

type – type (np. AF_INET) adresu
resolver POSIX

funkcje gethostbyname() i gethostbyaddr() są bardzo
rozpowszechnione, ale generalnie są przestarzałe i nie powinny być
używane

w standardowym API POSIX zostały zastąpione nowszymi funkcjami
getaddrinfo() i getnameinfo(), które są bardziej elastyczne, (m.in.
umożliwiają wyszukiwanie usług o określonych nazwach) i
niezależne od domeny/protokołu

w nowych aplikacjach zaleca się korzystanie z nowego API (które
jest bardziej rozbudowane ale równocześnie mniej wygodne do
prostych zastosowań)
getaddrinfo()

int getaddrinfo(const char *hostname, const char *service, const
struct addrinfo *hints, struct addrinfo **res);

getaddrinfo() umożliwia wyszukanie hosta o określonej nazwie lub
sformatowanym w tekst adresie oraz usługi o określonej nazwie lub
numerze portu

hint pozwala dodatkowo określić kryteria wyszukiwania – protokół,
etc.

wynik zwracany jest w postaci listy dynamicznie alokowanych struktur
addrinfo, które muszą być zwolnione za pomocą freeaddrinfo()
plan

programowanie sieciowe


BSD/POSIX Socket API
systemy RPC


interfejsy obiektowe

CORBA

DCOM

RMI
WebServices

WSDL/SOAP

XML-RPC

REST
mechanizmy RPC

RPC – Remote Procedure Call – zdalne wywołanie procedury

oryginalnie termin RPC to nazwa protokołu stworzonego przez firmę Sun
do budowy aplikacji rozproszonych (standard RFC1057), m.in.
implementacji systemu plików NFS

współcześnie terminem tym określa się wszelkie rozwiązania, służące do
uruchamiania usług w środowiskach rozproszonych, bez konieczności
zaprogramowania niskopoziomowych szczegółów komunikacji

w idealnym przypadku wywołanie funkcji/procedury poprzez RPC
wygląda z punktu widzenia programisty identycznie jak wywołanie jej na
lokalnie, zaś cała komunikacja niezbędna w tym celu odbywa się
niejawnie
typowy scenariusz RPC

program-klient RPC wywołuje lokalną funkcję „wydmuszkę” (stub) z
poziomu języka programowania

stub zamienia przekazane mu parametry wywołania w wiadomość
zgodną ze stosowanym protokołem RPC (serializacja parametrów)

wiadomość jest wysyłana do serwera RPC oferującego daną usługę

po odebraniu wiadomość jest deserializowana poprzez server stub

server stub wywołuje lokalną funkcję z odczytanymi parametrami z
poziomu języka programowania, odczytuje odpowiedź

odpowiedź jest serializowana w wiadomość…
tworzenie „wydmuszek”

interfejs wywołań RPC jest opisywany za pomocą standardowego
języka IDL – interface description language

na podstawie opisu IDL generator kodu systemu RPC tworzy wiązanie
(binding) pomiędzy wydmuszkami a biblioteką RPC w danym języku
programowania, dostarczając niezbędnego kodu
serializacji/deserializacji parametrów

kod wydmuszki jest kompilowany i łączony do kodu klienta i serwera

implementacja wydmuszek po stronie klienta i serwera może być w
innym języku programowania, dopóki zachowany jest wspólny
protokół RPC (heterogeniczna)
przykład IDL
implementacja serwera w Javie
użycie wydmuszki w C++
interfejsy obiektowe

współczesne implementacje systemów RPC nie modelują
pojedynczych funkcji tylko interfejsy w modelu obiektowym

podejście zorientowane obiektowo umożliwia tworzenie „zdalnych”
obiektów przechowujących określony stan oraz ułatwia ukrywanie
szczegółów implementacyjnych systemu RPC

obiektowo-zorientowane systemy RPC to m.in.

CORBA – Common Object Request Broker Architecture (standard
zdefiniowany przez Object Management Group)

DCOM – Distributed Component Object Model (Microsoft – adaptacja modelu
komponentowego COM do środowisk rozproszonych)

RMI – Remote Method Invocation (Java – podsystem wbudowany w każdej
implementacji języka)
CORBA jako przykład OO-RPC
usługi internetowe

usługi internetowe (Web Services) to podejście opierające się na zastosowaniu
standardowych protokołów i formatów danych internetowych (HTTP, XML, JSON) do
realizacji wywołań RPC

zaletą jest łatwość wdrożenia i debugowania – znane, powszechnie stosowane
formaty i narzędzia


nie ma konieczności wdrażania pełnej, skomplikowanej infrastruktury serwerowej jak w
przypadku CORBA – aplikacje serwerowe WebServices działają na standardowym serwerze
WWW

mniejsza szansa że wywołania HTTP zostaną zablokowane przez firewalle, możliwość użycia
standardowych serwerów proxy
podobnie jak w przypadku „tradycyjnego” RPC istnieją narzędzia, umożliwiające
generowanie wydmuszek z języka IDL lub specjalnego języka WSDL (Web Service
Definition Language – dialekt XML)
podejścia web services


sformalizowane

opis interfejsu WSDL

protokół komunikacyjny SOAP

wsparcie narzędzi, generatorów – Java, .NET

przerost formy nad treścią – inwokacja naszego „sayHello()” i przesłanie kilkunastu znaków
wymaga kilku tysięcy znaków w formacie XML
luźne, ad hoc – „wszystko co wygląda i działa jak web service to jest web service”

XML-RPC – proste fragmenty XML przesyłane za pomocą HTTP

JSON-RPC – analogicznie jak wyżej, ale dane w formacie JSON

RESTful

interoperatybilność najczęściej jedynie pomiędzy wydmuszkami generowanymi przez tą samą
bibliotekę

wsparcie języków skryptowych – Python, PHP, JavaScript