Przeglądając plik źródłowy programu w dowolnym niemal języku, gdzieś bardzo blisko początku znajdziemy zawsze region z importami. Niekoniecznie będą one oznaczone słowem kluczowym import
– czasem to będzie using
, być może do spółki z #include
– ale zawsze będą robiły zasadniczo to samo. Chodzi o poinformowanie kompilatora lub interpretera, że w tym pliku z kodem używamy takich-a-takich funkcji/klas/itp. z takich-a-takich modułów/pakietów. Dzięki temu “obce” nazwy użyte w dalszej części będą mogły być połączone z symbolami zdefiniowanymi gdzie indziej.
Każdy import w jakiś sposób rozszerza więc przestrzeń nazw danego modułu i zazwyczaj wszystko jest w porządku, dopóki dokładnie wiemy, jak to robi. Dlatego też powszechnie niezalecane są “dzikie” importy (wild imports), które nie wyliczają jawnie wszystkich dodawanych nazw, zwykle ukrywając je za gwiazdką (*
). Ale nawet jeśli ich nie używamy, to nie oznacza to, że żadne problemy z importowanymi nazwami nas nie spotkają. Oto kilka innych potencjalnych źródeł kłopotów:
import foo.bar.baz;
wprowadza do przestrzeni modułu nazwę baz
(czyli niekwalifikowaną) w przypadku Javy. W przypadku Pythona ten sam efekt wymaga z kolei instrukcji from foo.bar import baz
, a zwykła instrukcja import
da nam jedynie kwalifikowaną nazwę foo.bar.baz
– która z kolei w Javie i C# jest dostępna bez żadnych importów, a w C++ po dodaniu dyrektywy #include
… Całkiem intuicyjne, czyż nie? ;-) Skoro tak, to dodajmy do tego jeszcze fakt, iż…Podsumowując, importy – chociaż często zarządzane prawie całkowicie przez IDE – to w sumie dość poważna sprawa i warto zwrócić na nie uwagę przynajmniej od czasu do czasu.
Rozszerzalność od dawna jest w modzie: praktycznie żadna poważniejsza aplikacja nie obywa się bez jakiegoś systemu pluginów, czyli “wtyczek” zwiększających jej funkcjonalność. Niektóre robią to przy okazji (acz ze słusznych powodów), inne czynią z elastyczności i rozszerzalności swój główny oręż (patrz np. uniwersalne komunikatory w typie Mirandy). Wszystko to kojarzy się trochę linuksiarsko, jednakże obecnie także wiele programów stricte pod Windows zachęca (a przynajmniej umożliwia) swoich użytkownika do zakasania rękawów i zakodowania im nowych funkcji.
Dotyczy to także aplikacji na platformę .NET. Co więcej, sama jej natura ułatwia budowanie programów wspierających koncepcję rozszerzalności. Rzecz opiera się na pojęciu assembly, czyli czegoś w rodzaju pakietu (javowe skojarzenia wskazane) zawierającego kod, a więc klasy. Ze względu na to, że .NET Framework zawiera wbudowany kompilator, jest zupełnie możliwe, by nasz program udostępniał całe środowisko do pisania do niego pluginów, a następnie przerabiania ich na kod wykonywalny i podłączania ich do aplikacji. Nie twierdzę, że znam program, który rzeczywiście tak robi, ale przynajmniej w teorii jest to możliwe :)
Powszechniejsze wydaje mi się prostsze podejście, w którym assembly dostarcza się w postaci skompilowanej. Zazwyczaj jest to biblioteka .NET-owych, zapisana jako plik .dll, niemający przy tym zbyt wiele wspólnego z natywnymi czy COM-owymi DLL-ami. Wczytanie go jest bardzo proste, gdy wykorzystujemy do tego klasę System.Reflection.Assembly
:
Wskazana jest tutaj ostrożność np. w postaci zapewnienia, że każde assembly ładujemy tylko raz. Najlepiej jest wyznaczyć osobny podkatalog na wtyczki do naszego programu i ładować je jednokrotnie, zapewne podczas uruchamiania samej aplikacji.
Gdy mamy już gotowy obiekt klasy Assembly
(niezależnie od tego, czy skompilowaliśmy go sami czy wczytaliśmy go z gotowej binarki tak, jak powyżej), chcielibyśmy pewnie jakoś wykorzystać zawarty w nim kod. Są nim oczywiście jakieś klasy, które możemy pobrać metodą GetExportedTypes
. Zwróci nam ona metody tablicę obiektów Type
, czyli znanej zapewne metaklasy należącej do .NET-owego systemu refleksji. Z nimi zaś zrobić możemy… no, prawie wszystko :) Do interesujących w kontekście pluginów czynności należy przede wszystkim sprawdzenie, czy dana klasa implementuje jakiś ustalony przez nas interfejs “wtyczkowy”, a następnie utworzenie jej obiektu:
Takie beztroskie, dynamiczne tworzenie obiektów z binarnego kodu wczytanego już w trakcie działania programu jest jak najbardziej możliwe i, jak widać wyżej, całkiem proste – stosujemy do tego metodę CreateInstance
klasy o wdzięcznej nazwie Activator
. Wymogiem jest obecność w klasie wtyczki odpowiedniego konstruktora. W powyższym kodzie zakładamy na przykład najprostszy przypadek, iż dostępna jest jego wersja bezparametrowa.
To w sumie wszystko, jeśli chodzi o wczytywanie. To, w jaki sposób plugin może rozszerzać możliwości naszej aplikacji, zależy głównie od zawartości interfejsu, który nazwałem tutaj umownie IPlugin
. Projektując go, powinno się zadbać o jego elastyczność i prostotę, a także wziąć pod uwagę chociażby kwestie bezpieczeństwa.
W prawie wszystkich językach obiektowych istnieją tak zwane specyfikatory dostępu, przykładem których jest choćby public
czy private
. Pozwalają one ustalać, jak szeroko dostępne są poszczególne składniki klas w stosunku do reszty kodu. Modyfikatory te występują m.in. w trzech najpopularniejszych, kompilowalnych językach obiektowych: C++, C# i Javie.
Problem polega na tym, że w każdym z nich działają one inaczej. Mało tego: każdy z tych języków posiada inny zbiór tego rodzaju modyfikatorów. Dla kogoś, komu zdarza się pisać regularnie w przynajmniej dwóch spośród nich, może to być przyczyną pomyłek i nieporozumień.
Dlatego też przygotowałem prostą ściągawkę w postaci tabelki, którą prezentuję poniżej. Symbol języka występujący w komórce oznacza tutaj, że dany modyfikator (z wiersza) powoduje widoczność składnika klasy w określonym miejscu (z kolumny).
Specyfikator | Klasa | Podklasa | Pakiet | Reszta |
public |
C++ C# Java | C++ C# Java | C# Java | C++ C# Java |
protected |
C++ C# Java | C++ C# Java | Java | |
protected internal |
C# | C# | C# | |
internal |
C# Java | C# Java | ||
private |
C++ C# Java |
Parę uwag gwoli ścisłości:
public
, jako że w tym języku w ogóle nie istnieje pojęcie pakietu.internal
jest słowem specyficznym dla C#, a jego ekwiwalentem w Javie jest po prostu pominięcie modyfikatora w deklaracji (np. int x;
zamiast internal int x;
).protected internal
jest modyfikatorem występującym wyłącznie w C#/.NET.Dowiadując się, czym jest protokół TCP, można przy okazji usłyszeć lub przeczytać, że jest on niezawodny (reliable). Można wtedy pomyśleć, że to jakiś rodzaj białej magii – zwłaszcza, gdy pomyślimy sobie, jakie przeszkody mogą spotkać przesyłany kawałek danych podczas podróży tysiącami kilometrów kabli (a ostatnio i bez nich). Zatory w transmisji, źle skonfigurowane routery, nagła zmiana topologii sieci, i tak dalej… Mimo to niektóre programistyczne interfejsy próbują nam wmówić, że odczyt i zapis danych “w sieci” jest w gruncie rzeczy niemal identyczny np. z dostępem do dysku. To pewnie też skutek “myślenia magicznego” na temat pojęcia niezawodności w kontekście TCP.
A w praktyce nie kryje się za nim żadna magia. Niezawodność TCP ma swoje granice i obsługuje dokładnie tyle możliwych sytuacji i zdarzeń losowych, ile zostało przewidzianych – jawnie lub nie – w specyfikacji tego protokołu. (Zawartej w RFC 793, jeśli kogokolwiek ona interesuje ;]). Analogicznie jest na przykład z dostępem do dysków wymiennych: stare wersje Windows potrafiły radośnie krzyknąć sławetnym bluescreenem, gdy użytkownik ośmielił się wyciągnąć dysk z napędu w trakcie pracy aplikacji, która z niego korzystała. Była to bowiem sytuacja nieprzewidziana na odpowiednio wysokiej warstwie systemu.
Jakie więc niespodziewane sytuacje i problemy dotyczące TCP należy przewidywać, jeśli piszemy programy sieciowe? Jest ich przynajmniej kilka:
Send
u nadawcy może przekładać się na równie dowolną liczbę wywołań Receive
w celu odebrania wysłanych informacji. Stąd wynika konieczność rozróżniania porcji danych we własnym zakresie, o czym pisałem jakiś czas temu.Wreszcie, połączenie TCP można też zakończyć grzecznie (wymianą pakietów z flagami: FIN, FIN/ACK i ACK), co powinno dać się wykryć przez sieciowy interfejs programistyczny. W praktyce bywają z tym problemy, a jako takie powiadamianie o rozłączeniu działa tym lepiej, im niższy poziom API jest używany. W miarę pewnie wygląda to na warstwie POSIX-owych gniazdek (w Windows zaimplementowanych jako biblioteka WinSock) oraz w ich bezpośrednim opakowaniu na platformie .NET (System.Net.Sockets.Socket
) lub w Javie (java.net.Socket
). Wraz ze wzrostem poziomu abstrakcji (jak chociażby w specjalizacjach “sieciowych” strumieni I/O) sprawa wygląda już nieco gorzej…
Z niezawodnością TCP nie ma co więc przesadzać. W szczególności nie powinniśmy oczekiwać, że zapewni nam ona ochronę przed wszystkimi wyjątkowymi sytuacjami, jakie mogą wydarzyć się podczas komunikacji sieciowej. O przynajmniej kilku musimy pomyśleć samodzielnie.
Protokół TCP ma to do siebie, że możemy mu zaufać – zawsze mamy gwarancję, że dane wysłane trafią do odbiorcy (a jeśli nie trafią, to będziemy o tym wiedzieli). Dlatego możliwe jest traktowanie przesyłu danych tą drogą podobnie, jak chociażby wymiany danych między pamięcią operacyjną a plikiem na dysku. Z tego też powodu wiele języków programowania pozwala na opakowanie połączeń TCP/IP w strumienie o identycznym interfejsie jak te służące na przykład do manipulowania zawartością pliku.
W praktyce jednak nie da się pominąć zupełnie tego prostego faktu, iż odbierane dane pochodzą z sieci i wysyłane także tam trafiają. Dotyczy to na przykład takiej kwestii jak dzielenie informacji na małe porcje u nadawcy i ich interpretacja po stronie odbiorcy.
Jak bowiem wiadomo, dane przesyłane przez TCP/IP mogą być po drodze dzielone i składane, a zagwarantowana jest jedynie ich kolejność. Nie ma natomiast pewności, że kawałek danych wysłany jednym wywołaniem w rodzaju Send
zostanie odebrany także jednym wywołaniem Receive
. Granice między porcjami danych każdy protokół musi więc ustalać samodzielnie. Można to zrobić na kilka sposobów, jak chociażby:
\0
). Odbieranie danych polega wtedy na odczytywaniu kolejnych bajtów do bufora i interpretacji pakietu dopiero po otrzymaniu końcowego znacznika.<foo>...</foo>
– to koniec takiego elementu będzie jednocześnie wiadomością o końcu pakietu. Można to więc traktować jak nieco bardziej skomplikowany wariant znaczników końca.Jeśli tworzymy nowy protokół dla własnych aplikacji, to który wariant wybrać? Pierwszy wydaje się być dobry dla protokołów binarnych; tych jednak generalnie nie powinno się używać ze względu na liczne problemy z kodowaniem i pakowaniem danych. Druga opcja jest bardzo szeroko stosowana w wielu powszechnie używanych usługach sieciowych i wydaje się sprawdzać całkiem dobrze. Trzecia jest w gruncie rzeczy podobna, ale nieco bardziej złożona i może być kłopotliwa od strony kodu odbierającego dane.
Oprogramowanie open source nie grzeszy zazwyczaj jakością, lecz od każdej reguły istnieją przecież wyjątki. Ostatnio znalazłem właśnie taki wyjątek; należy on do tej kategorii programów, które wymagają pewnego przygotowania, jeśli chcemy z nich korzystać efektywnie. Lecz mimo tego, iż jest to dość specjalistyczne narzędzie, nie sposób przy jego pomocy zrobić krzywdy swojemu systemowi. Warto się więc mu przyjrzeć, bo wśród tego rodzaju programów jest ono prawdopodobnie jednym z najlepszych.
Mam na tu na myśli analizator pakietów sieciowych Wireshark. Przy jego pomocy możemy bez większych problemów podejrzeć, cóż takiego jest przesyłane wzdłuż biegnących od naszego komputera kabli (względnie fal radiowych). Takie programy są popularnie nazywane snifferami i mogą służyć do bardzo wielu pożytecznych celów oraz kilku innych, mniej chwalebnych ;-)
Wśród wyróżniających cech Wiresharka trzeba na pewno wymienić interfejs, który jest przejrzysty, a przy tym funkcjonalny – co nie zdarza się często nawet wśród nie-GPL-owych programów. Bez większych problemów możemy złapane pakiety przeglądać i filtrować według wielu różnych kryteriów. Mogą one obejmować także cechy dot. specyficznych protokołów sieciowych (możemy np. wyświetlić pakiety HTTP z żądaniami typu GET). A tych wspieranych przez program jest zresztą całkiem sporo. Pakiety należące do znanych protokołów są oczywiście automatycznie rozkodowywane i pokazywane w przyjaznej postaci z podziałem na warstwy OSI, pola w nagłówkach oraz zasadniczą treść.
To oczywiście nie wszystko; do innych bardzo użytecznych funkcji należy chociażby możliwość wyodrębnienia całego strumienia TCP (czyli np. “rozmowy” jakiejś aplikacji ze zdalnym serwerem). Podobnie przydatnych narzędzi jest zresztą więcej i ciężko byłoby je wszystkie tu wymienić.
Dodatkowo program ten jest wybitnie wszędobylski i posiada równoważne wersje dla prawie każdego sensownego systemu operacyjnego, z Windows i przeróżnymi Linuksami włącznie. Biorąc pod uwagę to, że działa sprawnie, szybko i intuicyjnie, trzeba przyznać, że to nieoceniony instrument dla programisty piszącego aplikacje sieciowe.
Pod odrobinę nieadekwatną nazwą RFC (Request For Comments, czyli prośba o komentarze) kryje się zbiór standardów czy raczej zaleceń dotyczących prawie każdego aspektu funkcjonowania Internetu. Opisy większości ważnych protokołów sieciowych – takich jak HTTP, FTP, POP3, SMTP, itd. – są zawarte właśnie w dokumentach RFC. Każdy taki dokument ma swój unikalny numer, który po publikacji nie ulega zmianie – podobnie zresztą jak sam dokument.
Brzmi to całkiem poważnie, prawda? W istocie, część dokumentów RFC ma kluczowe znaczenie dla działania globalnej sieci (jak choćby RFC2616, opisujący protokół HTTP). Wśród tysięcy już opublikowanych zdarzają się jednak i takie, które ze zwyczajowym technicznym przynudzaniem nie mają zbyt wiele wspólnego. Oto kilka takich przykładów:
matter-transport/sentient-life-form
. Jako że jest on przeznaczony do kodowania i przesyłania na odległości materii (nazwa wskazuje, że dotyczy to zwłaszcza organizmów żywych), będzie z pewnością bardzo użyteczny dla celów teleportacji :)Patrząc na tę listę, można stwierdzić, że wiele bardzo ciekawych pomysłów z niewiadomych przyczyn nie doczekało się realizacji. Nie wiem na przykład, dlaczego porzucono wykorzystywanie sprawdzonego sposobu komunikacji z wykorzystaniem ptaków na rzecz jakichś wydumanych i zupełnie nieprzemawiających do wyobraźni światłowodów ;-D