Skip to content

Obiekty, klasy i metody w Objective-C

February 24, 2009

Programowanie zorientowane obiektowo w czystym C, mimo że możliwe, rzadko kiedy jest proste i przyjemne, a powstały kod jest zwykle bardzo zagmatwany. Dlatego też na bazie C powstały kompatybilne z nim języki dodające wygodne w użyciu wsparcie dla kodu zorientowanego obiektowo. Pierwszy z nich to niezwykle popularny C++, w którym większość tych rozszerzeń to różnego rodzaju dodatkowe operacje wykonywane podczas kompilacji. Drugi to niszowy Objective-C, który (oprócz składni) od C++ różni się tym, że dodatkowe elementy jakie zostały do niego wprowadzone są odpowiednimi operacjami wykonywanymi podczas działania programu. Dzięki temu udaje się uzyskać w języku kompilowanym do kodu natywnego część udogodnień znanych z technologii takich jak .NET czy Java.

Klasy

W języku Objective-C każda klasa jest także obiektem zawierające podstawowe informacje wymagane przez pozostałe funkcje wsparcia runtime. Najważniejsze z danych przechowywanych przez obiekty opisujące klasy to:

  • nazwa klasy
  • rozmiar instancji klasy (z uwzględnieniem klas bazowych)
  • lista zmiennych składowych klasy
  • lista funkcji składowych klasy
  • wskaźnik na klasę bazową
  • lista klas pochodnych
  • realizowane protokoły

Traktowanie klasy jako obiektu (inspirowane Smalltalkiem) pozwala znacząco poszerzyć możliwości języka szczególnie jeżeli chodzi o refleksyjność. Struktury opisujące klasy najczęściej są przechowywane w wewnętrznych danych runtime. Odpowiednie informacje są wydobywane przy okazji tworzenia instancji danej klasy, a następnie wiązane nowym obiektem. Dlatego też poniższy kod:

array = [NSArray new]

zostanie w większości implementacji, już przez kompilator zamieniony na konstrukcję podobną do tej:

array = [(objc_get_class("NSArray")) new]

Funkcja objc_get_class przegląda listę wszystkich klas w poszukiwaniu żądanej i w przypadku jej odnalezienia zwraca wskaźnik do niej. Cała operacja ma miejsce już podczas działania programu, dlatego też istotna jest szybkość działania takich podstawowych operacji. Jednym ze sposobów na przyśpieszenie kodu jest cachowanie wskaźników na obiekty opisujące klasy. W rezultacie funkcja objc_get_class jest wykonywana jeden raz dla każdej klasy, a w każdym kolejnym przypadku używany jest zwrócony przez nią wskaźnik.

Obiekty

Wszystkie klasy w Objective-C dziedziczą po NSObject. Jest to klasa która realizuje większość operacji pozwalających na korzystanie przez programistę z informacji jakie są przechowywane o każdej klasie. Oferowana funkcjonalność to między innymi:

  • sprawdzenie czy klasa odpowiada na selektor
  • sprawdzenie czy klasa realizuje protokół
  • podanie funkcji odpowiadającej na selektor
  • wykonanie podanego selektora
  • podanie nazwy klasy

Selektory

Selektory identyfikują wysyłaną do obiektu wiadomość, czyli innymi słowy, nazywają funkcję składową która powinna zostać wywołana. W większości jest to po prostu ciąg znaków odpowiadających nazwie funkcji, chociaż w niektórych implementacjach zawiera także unikalny numer identyfikacyjny. W celu usprawnienia operacji tłumaczenia ciągu znaków na selektor i odwrotnie istnieją odpowiednie tablice przechowujące listy wszystkich zarejestrowanych danych. Jako że mimo tego, może to być kosztowna operacja, umożliwiane jest (wręcz zalecane) tłumaczenie nazw do selektorów podczas kompilacji.

Wywołanie funkcji składowych

Wywołanie funkcji składowych, a właściwie zgodnie z nomenklaturą Objective C: wysyłanie wiadomości obiektom jest przeprowadzany w całkowicie dynamiczny sposób. Warto także zaznaczyć, że ponieważ klasy także są traktowane jako obiekty, statyczne funkcje składowe wywoływane są w taki sam sposób. Oto przykładowy kod wysyłania komunikatu do obiektu:

[obiekt metoda]

zostaje zamieniony na:

objc_msgSend(obiekt, @selector(metoda))

Funkcja objc_msgSend, na podstawie informacji zawartych w opisie klasy której instancją jest object, wywołuje odpowiednią metodę. Każda klasa zawiera listę selektorów oraz przypisanych im adresów funkcji. Procedura przekazująca wiadomość przeszukuje tę listę (z uwzględnieniem klas bazowych) w poszukiwaniu adresu odpowiedniej metody, która następnie zostaje wywołana.

Często, w celu przyśpieszenia tej bardzo częstej operacji, wykorzystywane są tak zwane dispatch tables. Właściwie pod każdym względem przypominają one tablice funkcji wirtualnych wykorzystywane np. w C++. Każda taka tablica zawiera jedynie adresy kolejnych funkcji składowych klasy. W tej implementacji unikalny numer identyfikacyjny selektora służy także jako indeks wskazujący na odpowiedni adres w tej tablicy. Najważniejszą różnicą między dispatch table a tablicami funkcji wirtualnych jest fakt, że dispatch tables najczęściej są tworzone już podczas działania programu na podstawie listy funkcji składowych zawartej w obiekcie opisującym klasę.

Pozostałe dane dotyczące klasy

Często zdarza się, że wpisy z adresami funkcji składowych i odpowiadającymi im selektorami zawierają także informację o przyjmowanych przez metodę argumentach. Jest to przydatne przy debuggingu. Podobnie rzecz się ma z wpisami dotyczącymi zmiennych składowych. Obiekt opisujący klasę przechowuję listę struktur zawierających nazwę każdej takiej zmiennej, jej typ (dla ułatwienie debuggingu) i przesunięcie w stosunku do bazowego adresu instancji.

Podsumowanie

Objective-C prezentuje inne podejście co do sposobu implementacji rozszerzeń związanych z OOP w C niż C++. Opiera się bardziej na wykonywaniu operacji podczas działania programu, co zmniejsza jego wydajność, ale z drugiej strony oferuje szereg dodatkowych możliwości. Niech za przykład posłużą tutaj choćby systemy rozproszone. W Objective-C przy odrobinie wysiłku ze strony osoby implementującej taki system, można tworzyć obiekty rozproszone w banalnie prosty sposób. Możliwe jest to właśnie dzięki dużej ilości informacji które zostają w kodzie po kompilacji, oraz wielu możliwościach wstawienia dodatkowego kodu podczas działania programu. Oczywiście takie rozwiązania są też możliwe w C++ czy innych językach, ale nie zawsze jest to aż takie proste.

Główną wadą Objective-C jest jego niewielka popularność. Przed śmiercią chroni go właściwie jedynie OpenStep. Warto jednak zwrócić uwagę na jego możliwości, ponieważ jest on gdzieś pomiędzy innymi językami kompilowanymi do kodu natywnego a kompilowanymi do kodu pośredniego. Z jednej strony wsparcie runtime jest bardzo rozbudowane, ale wciąż istnieje możliwość uzyskania maksymalnej możliwej wydajności w krytycznych miejscach, przez ograniczenie używania niektórych z jego funkcji.

Quarn OS 0.0.90

February 22, 2009

Po długim okresie prac nad Quarnem (niestety nie obyło się bez przerw), w końcu zdecydowałem się na wydanie wersji 0.0.90. Warto zaznaczyć, że nie jest to wersja nadająca się do użytkowania. Jej głównym zadaniem jest wyznaczenie osiągniętego milestone.

Zainteresowani mogą ściągnąć Quarna tutaj lub bezpośrednio tutaj. Możliwe jest także ściągnięcie gotowego obrazu dyskietki w tym miejscu. Uprzedzam, że w wielu częściach kod jest jeszcze niedopracowany i czasami wręcz razi błędami, jednak głównym celem tego wydania było ustabilizowanie pewnej podstawy na bazie której będą prowadzone dalsze prace. W najbliższej przyszłości planuję po kolei zająć się poszczególnymi elementami systemu znacząco je dopracowując. Obecnie, są one jedynie zalążkami, których głównym zadaniem jest wykorzystanie możliwości podsystemu Manes.

Quarn w wersji 0.0.90 posiada zaimplementowane między innymi:

  • Managed Execution System
  • Execution Flow Controller
  • podstawowe sterowniki (klawiatura, dma, fdc, pic, pit, rs232, pci)
  • wielozadaniowość z planistą round-robin (wywłaszczanie)
  • alokator pamięci O(1)
  • podstawowe elementy interfejsu Hydra
  • port Hydry do POSIX
  • podstawowe wsparcie dla FAT12
  • wsparcie dla RTTI w C++
  • wsparcie dla plików ELF, dynamiczny konsolidator
  • ładowanie modułów kernela
  • wykonywanie zewnętrznych plików
  • systemu zasobów, urządzeń i usług

W tym momencie należą się pewne wyjaśnienia co do niektórych elementów Quarna. Co prawda w przyszłości zamierzam napisać o nich dużo więcej, jednak teraz przynajmniej podam ogólny zarys ich działania.

Manes

Managed Execution System (Manes) jest systemem rozproszonej dystrybucji obiektów i komponentów (przypomina systemy typu CORBA czy COM). Jest sercem Quarna, rozwiązującym wszelkie kwestie związane z modułowością i komunikacją pomiędzy poszczególnymi elementami systemu. W przyszłośći Manes wspierać będzie także szereg dodatkowych technologii jak na przykład programowanie aspektowe.

Kolejnym elementem bardzo mocno związanym z Manes jest Execution Flow Controller. Wykorzystuje on tablice metod wirtualnych do wstrzykiwania kodu, który zostanie wykonany przed wywołaniem takiej funkcji składowej. Pozwala to na implementację większości funkcji dostarczanych przez Manes.

Hydra

Innym istotnym elementem Quarna jest Hydra czyli główne API jakie jest dostarczane aplikacjom. Obejmuje między innymi interfejs użytkownika (obecnie tylko w trybie tekstowym, w przyszłości także i w graficznym), operacje wejścia-wyjścia, wielowątkowość itp. Możliwe jest uruchomienie programów napisanych przy wykorzystaniu Hydry na każdym systemie zgodnym z POSIX dzięki specjalnej warstwie pośredniczącej. W Quarnie 0.0.90 jest to zaprezentowane za pomocą domyślnego shella.

Podsumowanie

Quarn jest projektem amatorskim w związku z czym prace nad nim czasowo ulegają większemu spowolnieniu. Nie skupiam się w nim na uzyskiwaniu jak największej funkcjonalności lecz na zastosowaniu ciekawych, nowych rozwiązań. Dlatego też obecna wersja nie powala swoimi możliwościami i służy głównie ułatwieniu w utrzymaniu porządku w kodzie.

Programowanie oparte na komponentach

February 17, 2009

Programowanie zorientowane obiektowo jest niewątpliwie jednym z najpopularniejszych obecnie paradygmatów. Powstało bardzo wiele opracowań na jego temat wprowadzających chociażby techniki znane jako wzorce projektowe. Nie jest to jednak rozwiązanie idealne i w pewnych zastosowaniach wiążą się z nim pewne trudności. Chodzi tutaj o tworzenie gier komputerowych gdzie coraz częściej można się spotkać z programowaniem opartym na komponentach.

Uzasadnienie

Praktycznie w każdej grze występuje główna baza obiektów w niej występujących. Zwykle są to instancje klas ściśle ze sobą powiązanych odpowiednią hierarchią. Wspólna funkcjonalność, zgodnie z duchem OOP, jest w miarę możliwości przenoszona do klas znajdujących się wyżej w hierarchii. Wszelkie odstępstwa od domyślnego zachowania są implementowane przy użyciu polimorfizmu.

Wraz ze wzrostem stopnia skomplikowania projektu coraz trudniej jest unikać dziedziczenia wielobazowego i wszystkich związanych z nim niedogodności, w trakcie pozbywania się duplikowanego kodu. Innym problemem który się pojawia jest powstanie bardzo dużych klas bazowych, które są zwykle wysoce niezalecane. Dochodzi do sytuacji gdzie programowanie zorientowane obiektowo najwyraźniej nie jest najlepszy sposobem na rozwiązanie danego problemu.

Podstawowe założenia

W powyższych sytuacjach bardziej istotnym od zarządzania obiektami jest wygodne zarządzanie logiką kodu. Realizowane jest to za pomocą dwóch podstawowych elementów programowania opartego na komponentach:

  • jednostka (entity) – identyfikuje to co w OOP zostałoby nazwane obiektem. Nie posiada pól ani metod, jest jedynie czymś w rodzaju nazwy. Swoim przeznaczeniem przypomina klasę, ponieważ definiuje konkretne zachowanie, lecz ilościowo odpowiada obiektom.
  • komponent (component) – definiują logikę operacji. Każdy komponent opisuje inne fragmenty zachowania jednostki. Najważniejszą różnicą między klasami a komponentami jest to, że klasy opisują pełną funkcjonalność, podczas gdy pojedynczy komponent tylko jej część. W większości implementacji komponenty są obiektami globalnymi realizowanymi przy pomocy wzorca singleton, jednakże nie jest to sztywną regułą.

Niezwykle istotnym elementem całego systemu jednostek jest przyporządkowywanie im konkretnych komponentów. Może to odbywać się na dwa sposoby, zależnie od implementacji. Także umieszczenie kodu i danych jest sprawą zależną od samej implementacji. Warto jednak pamiętać, że podstawowym założeniem programowania opartego na komponentach jest utrzymanie minimalnego rozmiaru jednostki. W rezultacie jest to najczęściej liczba całkowita.

Dane

W systemach opartych na tym paradygmacie dane są umieszczane w komponentach. W zależności od możliwych zastosowań jednostki, komponenty przechowują dane potrzebne do realizowania przez nie odpowiednich funkcji. Wymaga to dodatkowego mechanizmu komunikowania się pomiędzy komponentami różnych typów w sytuacji gdy potrzebna jest modyfikacja danych innego komponentu. Powinno to być jednak unikane. Główną zaletą jest uproszczenie mechanizmów związanych z synchronizacją w przypadku systemów wielowątkowych. Każdy komponent zajmuje się tylko swoimi danymi dzięki czemu można ograniczyć ilość blokad.

Dodatkowo najczęściej to komponenty przechowują informację o przyporządkowanym im jednostką. Dzięki temu zlokalizowanie wszystkich jednostek jest proste i nie wymaga istnienia dodatkowych struktur danych. W tej kwestii wiele jednak zależy od implementacji, bo nie da się ukryć, że zdarza się także sytuacja w której każda jednostka przechowuje listę przypisanych komponentów.

Kod

W przypadku kodu najpopularniejszym rozwiązaniem jest umieszczenie go także w komponentach. Wtedy co prawda jest on powiązany z danymi, co może przypominać OOP, jednak uzyskuje się duże rozdrobnienie funkcjonalności co ułatwia wiele kwestii. Oprócz wspomnianego ominięcia problemu synchronizacji, możliwa jest dynamiczna zmiana właściwości i metod obiektu (reprezentowanego przez jednostkę) co pozwala na ograniczenie ilości zajmowanej pamięci. Istotny jest także inny sposób zapisu kodu co jest główną cechą odróżniającą od siebie paradygmaty programowania. Znacząco ułatwia to dodanie dodatkowej funkcjonalności poprzez wprowadzenie kolejnego komponentu.

Komponenty ułatwiają także masowe wykonywanie operacji. Jeżeli dana funkcja powinna być wykonana na wszystkich obiektach danego typu, komponent potrafi to zrobić korzystając z posiadanej listy przyporządkowanych mu jednostek. Kluczowe jest tutaj ustalenie odpowiedniej kolejności wykonywania komponentów w odpowiedzi na zdarzenia.

Przykład realizacji

Przykładowe zastosowanie komponentów w grze polega na stworzeniu wystarczającej ich ilości do pokrycia każdego możliwego zachowania obiektów. Oczywiście, z reguły nie stosuje się tutaj żadnej hierarchii komponentów, ponieważ zwykle nie przynosi to żadnych korzyści. Każdy obiekt obecny w grze jest zarejestrowany w komponencie Render który udostępnia procedury związane z jego wyświetlaniem. Oprócz tego łódki mogą być zarejestrowane w komponencie Boat. Po ich zniszczeniu zostają usunięte z komponentu Boat a zarejestrowane w Wreck.

Główna pętla programu w odpowiedniej kolejności, często w odpowiedzi na konkretne zdarzenia wykonuje żądane komponenty. Dla przykładu, jeżeli samochód jest sterowany przez gracza, to należy do komponentu PlayerControl, którego procedury są wywoływane w odpowiedzi na zdarzenia z urządzeń zewnętrznych.

Podsumowanie

Programowanie oparte na komponentach zwane także bardziej szczegółowo Entity System jest bez wątpienia bardzo ciekawym spojrzeniem na sposób rozwiązania problemów związanych z dużą ilością podobnych obiektów, których implementacja zawiera jednak pewne różnice. Pozwalają one w takiej sytuacji na uporządkowanie kodu i co za tym idzie ułatwienia jego dalszej rozbudowy.

Jest to technika stosowana najczęściej w przypadku produkcji gier komputerowych co nie oznacza, że w innych dziedzinach nie może znaleźć ona zastosowania. Istnieje wiele różnych podejść do takich kwestii jak umieszczenie kodu czy sposób kojarzenia komponentów z jednostkami. Dzieje się tak, ponieważ nie ma tutaj uniwersalnego rozwiązania i każdy dostosowuje to do swoich potrzeb. Podobnie rzecz się ma gdy porównać komponenty do OOP. Trudno powiedzieć żeby jeden paradygmat był lepszy od drugiego. Ich zastosowanie jest odmienne i należy tak je używać, aby wykorzystać jak najwięcej ich zalet.

Protokół SOAP

February 10, 2009

W każdym systemie rozproszonym, niezależnie od tego czy jest to CORBA, DCOM, .NET Remoting czy cokolwiek innego, niezbędny jest pewny protokół zdalnego wywoływania kodu. Java posiada swój RMI, CORBA – GIOP/IIOP, DCOM – własny protokół, ponadto w Uniksach często stosuje się RPC firmy Sun Microsystems. Ten ostatni próbowano pogodzić z językiem XML i w rezultacie stworzono XML-RPC, zwany protokołem XML pierwszej generacji. Jego następcą jest niewątpliwie jeden z ciekawszych protokołów zdalnego wywoływania procedur (i nie tylko) – SOAP.

Podstawowe założenia

SOAP zakłada, że każda wiadomość jest wysyłana od konkretnego nadawcy do przynajmniej jednego odbiorcy przy opcjonalnym udziale jednego lub więcej pośredników. Każdy z elementów uczestniczących w przekazaniu wiadomości (tj. nadawca, pośrednik i odbiorca) jest nazywany węzłem i posiada swój własny adres URI. Dodatkowo, każda przesyłana wiadomość jest całkowicie niezależna od pozostałych, a protokół nie wspiera żadnych mechanizmów synchronizacji.

Budowa wiadomości

Każda wiadomość składa się z nagłówka (header) i ciała (body). Pierwszy z tych elementów może być analizowany przez każdego z pośredników, a także przez ostatecznych odbiorców. Ciało wiadomości jest zarezerwowane jedynie dla odbiorców. Poniżej przedstawiono strukturę wiadomości:

<?xml version="1.0"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope"
        xmlns:m="http://www.example.org/timeouts"
        xmlns:xml="http://www.w3.org/XML/1998/namespace">
        <env:Header>
                ...
        </env:Header>
        <env:Body>
                ...
                <env:Fault>
                        ...
                </env:Fault>
        </env:Body>
</env:Envelope>

Nagłówek jest podzielony na bloki. Każdy blok posiada informację o roli do której jest przeznaczony, informacje czy musi być analizowany przez węzeł, informacje dotyczące warunków w których ma on być przekazany dalej oraz właściwe dane. Nagłówki służą głownie do realizacji przez pośredników dodatkowych zadań takich jak autoryzacja, logowanie czy dodatkowe mechanizmy bezpieczeństwa. Nagłówki są opcjonalne.

Ciało wiadomości jest częścią wymaganą przez standard, mimo że jej zawartość jest praktycznie dowolna. To właśnie w tym miejscu umieszczana jest treść wiadomości. Jedyną istotną rzeczą jest env:Fault jest to opcjonalny element używany do przekazywania informacji o błędach.

Cechy (features)

Niezwykle istotnym elementem protokołu SOAP są cechy (features), opisują one w dokładny sposób niemalże każdy aspekt całego procesu zdalnego wywołania procedury. Implementacje mają tutaj dość dużą dowolność, dzięki czemu mogą nadać pożądane w danym przypadku dodatkowe właściwości, które pozwolą lepiej wykorzystać możliwości protokołu SOAP.

Role (roles)

Role opisują zachowanie węzła po otrzymaniu danej wiadomości. Każdy blok w nagłówku posiada informację do jakiej roli jest przeznaczony, co decyduje czy węzeł powinien analizować zawartość danego bloku. Podobnie jak węzły i cechy, role także posiadają swój adres URI, który służy do ich identyfikacji. Wersja 1.2 standardu SOAP definiuje trzy role, lecz możliwe jest wprowadzenie dodatkowych.

  • next – każdy pośrednik oraz odbiorca musi przeanalizować nagłówek wiadomości
  • none – nagłówek wiadomości nie może być analizowany
  • ultimateReceiver – nagłówek wiadomości jest analizowany jedynie przez ostatecznych odbiorców, jest to domyślna rola

Właściwości (properties)

Kolejnym istotnym elementem protokołu SOAP są właściwości. Przechowują one istotne informacje dotyczące wiadomości nie będące jednak jej częścią. Najczęściej dotyczą one szczegółów związanych z transmisją danych. Podobnie jak większość innych elementów SOAP właściwości także identyfikowane są adresami URI.

Istnieją dwa konteksty właściwości: kontekst węzła (environment context) i kontekst wiadomości (message exchange context). Pierwszy zawiera informacje dotyczące konkretnego węzła, takie jak jego numer IP czy lokalną datę i czas. Drugi oprócz standardowego opisu wiadomości takiego jak jej ID zawieta także informacje zależne od konkretnego MEP-a.

Wzorce wymiany wiadomości (MEP)

MEP jest wzorcem według którego węzły SOAP wymieniają miedzy sobą informację. Każdy wzorzec tego typu posiada swój własny adres URI. Ich implementacja jest ściśle zależna od rodzaju wykorzystywanego protokołu niższej warstwy. Dodatkowo w zależności od schematu wg którego przesyłane są dane transmisja jest podzielona na poszczególne etapy. Dwa schematy wymienione w standardzie (które muszą być implementowane przez obsługę HTTP) to Response Message Exchange Pattern i Request-Response Message Exchange Pattern. Zasadnicza różnica między nimi jest taka, że pierwszy schemat nie zakłada przesyłania szczegółowego żądania. Ogranicza się ono do prostego wywołania zależnego od używanego protokołu niższej warstwy (np. HTTP). W obu przypadkach odpowiedź jest pełnowartościową wiadomością SOAP.

Przetwarzanie wiadomości

Po otrzymaniu wiadomości każdy węzeł analizuje ją w poszukiwaniu informacji, które są dla niego przeznaczone. Wygląda to w następujący sposób.

  1. Nagłówek wiadomości jest przeszukiwany pod kątem istnienia bloków przeznaczonych do roli jaką spełnia węzeł.
  2. Odnajdywane są bloki nagłówka obowiązkowe do przetworzenia.
  3. Jeżeli węzeł nie jest w stanie przetworzyć któregokolwiek z bloków znalezionych w powyższych punktach wysyłana jest zwrotna informacja o błędzie.
  4. W przeciwnym wypadku węzeł przetwarza w odpowiedni dla siebie sposób znalezione bloki. Jeżeli jest jednocześnie ostatecznym odbiorcą przetwarza także ciało wiadomości.
  5. W sytuacji gdy węzeł nie jest odbiorcą oraz nie wystąpił żaden błąd wiadomość jest przekazywana dalej z pominięciem przetworzonych bloków w nagłówku.

Transmisja wiadomości

W większości przypadków SOAP wykorzystuje do przesyłania danych protokół HTTP. Dzięki temu łatwiej jest zbudować system oparty na SOAP w sieci z firewallami i serwerami proxy. Warto także zauważyć, że HTTP idealnie odpowiada specyfice SOAP. Każda wiadomość SOAP, podobnie jak każde żądanie HTTP, jest całkowicie niezależna od pozostałych. Naturalnie, możliwe jest także wykorzystywanie HTTPS do transmisji wymagających większego bezpieczeństwa. Inną alternatywą, choć dość rzadko spotykaną i omawianą, jest wykorzystanie protokołu SMTP. Spotyka się także implementacje które dla zwiększenia wydajności operują bezpośrednio na niższej warstwie – protokole TCP.

Jak zostało to wspomniane w paragrafie o wzorcach wymiany wiadomości, transmisja jest podzielona na stany. Oto przykładowy przebieg transmisji Request-Response Message Exchange Pattern z użyciem protokołu HTTP.

  • initOdpowiednia właściwość kontekstu wiadomości zawiera adres HTTP węzła do którego należy się połączyć. Wykorzystywana jest w zależności od konfiguracji i/lub implementacji metoda GET lub POST. W żądaniu HTTP umieszczone zostają wszystkie informacje dotyczące kodowania znaków. W tym czasie nasłuchujący adresat przyjmuje połączenie i jeżeli nie wystąpi żaden błąd oczekuje na zakończenie transmisji ze strony nadawcy.
  • requestingUżywając metody określonej w poprzednim etapie przesyłana jest treść żądania do adresata. W zależności od odpowiedzi algorytm może powrócić do stanu init aby ponowić próbę (ma to miejsce np. przy przekierowaniach), fail jeżeli wystąpił bład lub sending+receiving jeżeli wszystko przebiegło pomyślnie. W tym samym czasie adresat jest w stanie responding w którym oczekuje na dane od nadawcy i po ich odebraniu wysyła odpowiedź.
  • sending+receivingW tym stanie adresat przetwarza otrzymaną wiadomość. Transmisja danych jest zakończona.
  • success lub failW zależności od wyniku przeprowadzonej operacji węzeł kończy przesyłanie danych z informacją o sukcesie lub porażce.

Serializacja danych

Niezbędnym elementem w każdym systemie zdalnych wywołań procedur jest mechanizm serializacji przekazywanych danych, takich jak argumenty funkcji. Nazwy występujące w programie wykorzystującym SOAP są modyfikowane w taki sposób aby mogły zostać użyte w każdym elemencie języka XML. Dlatego niedozwolone znaki zastępuje się ich wartością szestnastkową w UTF-16 (lub UTF-8) dla przykładu Hello world zostanie zamienione na Hello_x0020_world.

Wywołanie jest przedstawiane w ciele wiadomości jako element o nazwie takiej jak nazwa funkcji (oczywiście po zakodowaniu znaków niedozwolonych w XML-u) zawierający wszystkie wejściowe i wejściowo-wyjściowe argumenty. Każdy argument jest przedstawiany jako element o tej samej nazwie zawierający jego wartość (także po przekształceniu w formę akceptowalną dla XML-a). Odpowiedzią jest element o dowolnej nazwie zawierający wszystkie argumenty wyjściowe oraz wejściowo-wyjściowe. Sposób ich zapisu jest taki sam jak w przypadku wywołania. Do RPC wykorzystywane są wzorce Response Message Exchange Pattern i Request-Response Message Exchange Pattern, chociaż standard zezwala także na inne.

Zastosowanie

SOAP jest jednym z elementów pozwalającym na rozproszoną dystrybucję komponentów. Razem z technologiami takimi jak WSDL pozwala on na stworzenie rozbudowanego systemu podobnego do CORBA i DCOM. W rezultacie zostało to uczynione w Java 2 Enterprise Edition (gdzie SOAP występuje obok Java RMI) oraz platformie .NET, gdzie SOAP ma za zadanie całkowicie wyprzeć DCOM. Często też spotyka się tę technologię w aplikacjach webowych z racji wykorzystania typowych dla tej dziedziny technologii jak XML i HTTP. Trwają także prace nad standardem XUP, który jest czymś w rodzaju połaczenia SOAP i XUL. Zakłada on wykorzystanie SOAP do przekazywania informacji o zdarzeniach dotyczących interfejsu opartego na XML (na przykład wspomniany XUL).

Podsumowanie

SOAP jest niewątpliwie ciekawym protokołem, mimo że oparcie się na XML-u ogranicza jego wydajność. Co prawda trwają pracę nad technologiami takimi jak Binary XML, który pozwolą przyśpieszyć pracę z XML-em, lecz jak na razie w starciu wydajnościowym CORBA czy DCOM bez problemu są w stanie pokonać SOAP.

To co jest wadą jest także i zaletą, fakt, że SOAP opiera się na XML-u pozwala na wykorzystanie w nim wielu istniejących już technologii (chociażby XSLT czy algorytmy serializacji danych). Stawia on na modułowość i łatwość dostępu co niejednokrotnie jest ważniejsze od wydajności. Jednak niezależnie od tego, jest to bez wątpienia bardzo interesująca technologia.

Komunikacja międzyprocesowa w QNX

February 3, 2009

Na desktopach oraz serwerach niewątpliwie królują jądra monolityczne. Kernele systemów takich jak *BSD, (Open)Solaris czy Linux z grubsza opierają się na tej samej architekturze. Podobnie rzecz się ma w stosunku do Windowsa, mimo że w jego budowie jest już parę ciekawych różnic. Właściwie jedynym popularnym na tego typu maszynach systemem bazującym na mikrojądrze jest Mac OS X korzystający z jądra Mach.

Zupełnie inaczej rzecz się ma w przypadku systemów wbudowanych, a także wielu systemów czasu rzeczywistego. Tam mikrojądra odgrywają znacznie większą rolę. Jednym z popularniejszych systemów opartych na mikrojądrze jest QNX korzystający z jądra QNX Neutrino. Jego wewnętrzna architektura jest zupełnie inna od tej znanej z Linuksa czy też Uniksów. Wynika to głównie z tego, że nacisk jest położony na zupełnie elementy systemu. Jedną z najistotniejszych kwestii we wszystkich mikrojądrach jest komunikacja międzyprocesowa.

Wiadomości (messages)

Podobnie jak w innych systemach opartych na mikrojądrach, także w QNX tym co łączy wszystkie elementy systemu w jedną całość jest IPC. Podstawowym mechanizmem IPC udostępnianym przez QNX Neutrino są wiadomości (messages). Są to synchroniczne komunikaty na które adresat zawsze odpowiada. Warto przyjrzeć się sposobowi w jaki Neutrino przekazuje wiadomości.

  • Wątek będący klientem wykonuje procedurę MsgSend(). Przechodzi on w stan SEND blocked, co jest równoważne jego wstrzymaniu. Jeżeli wątek będący adresatem czeka na wiadomość, jest on wznawiany automatycznie, bez udziału planisty.
  • W momencie w którym wątek będący serwerem wywoła procedurę MsgReceive() dostanie on informację o otrzymanej wiadomości, a nadawca przejdzie w stan REPLY blocked. Jeżeli procedura MsgReceive() zostanie wywołana w sytuacji, kiedy nie ma żadnych wiadomości w kolejce do przetworzenia, wątek przejdzie w stan RECEIVE blocked i zostanie wstrzymany do momentu otrzymania wiadomości.
  • Po wykonaniu operacji związanych z obsługą otrzymanego komunikatu, serwer wykonuję procedurę MsgReply(), która przekazuje klientowi odpowiedź lub MsgError(), która przekazuje informacje o błędzie. Obie te funkcje powodują przejście klienta w stan READY, a więc wznowienie jego działania.

Przesyłanie danych

Obok synchronizacji działania realizowanej przez przechodzenie wątków w różne stany drugą kwestią jest sam sposób przesłania danych. Jeżeli ilość przesyłanych danych jest niewielka (najczęściej poniżej 256 bajtów) są one buforowane w przestrzeni jądra i w ten sposób przenoszone między przestrzeniami adresowymi dwóch wątków. W przeciwnym razie Neutrino tymczasowo mapuje fragment przestrzeni adresowej serwera w przestrzeni klienta co pozwala na przeniesienie danych między nimi w bardzo wydajny sposób. Dzięki temu prędkość kopiowania wiadomości jest zależna jedynie od przepustowości pamięci operacyjnej urządzenia. Neutrino pozwala także na wysyłanie wiadomości złożonych z bloków o różnych rozmiarach rozmieszczonych w różnych obszarach przestrzeni adresowej.

Kanały (channels) i połączenia

W QNX Neutrino wątek, który chce otrzymywać wiadomości musi utworzyć kanał (channel). Nadawca, przed wysłaniem danych nawiązuje połączenie z tym kanałem. W przestrzeni użytkownika zarówno kanały jak i połączenia są reprezentowane przez liczby całkowite. Dodatkowo liczby identyfikujące połączenia używa się zamiennie z deskryptorami plików. Do każdego kanału przyporządkowane są trzy struktury danych przechowujące informacje o klientach.

  • receive – bufor LIFO wątków oczekujących na wiadomość
  • send – kolejka (FIFO) wątków, które wysłały jeszcze nieodebrane wiadomości
  • reply – nieuporządkowana lista wątków, które wysłały wiadomość na którą nie udzielono jeszcze odpowiedzi

Podsumowanie

Oprócz wyżej wymienionych messages QNX wspiera także wiele innych rodzajów komunikacji międzyprocesowej. Są to sygnały, kolejki, pipes, pamięć dzielona, itp. Ich implementacja opiera się jednak głównie na messages, które to w najbardziej dobitny sposób przedstawiają cechy charakterystyczne QNX jeżeli chodzi o przesyłanie komunikatów. Wykorzystany został w nich przemyślany system synchronizacji, pozwalający uniknąć opóźnienia związanego z wywołaniem planisty CPU oraz mechanizm kopiowania danych wybierający optymalną metodę dla określonej ich ilości. Nie dziwi nacisk jaki położono właśnie na IPC, skoro jest to jeden z najważniejszych elementów mikrojądra QNX Neutrino.

Follow

Get every new post delivered to your Inbox.