O (nie)zawodności TCPDowiadują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.
Statyczne składowe i takież konstruktoryWiadomo, że konstruktor to specjalna metoda, która jest wywoływana przy tworzeniu nowego obiektu i ma za zadanie go zainicjalizować. Okazuje się, że w innej wersji "konstruktor" (albo coś, co go przypomina) może służyć do inicjalizacji nie obiektu, lecz samej klasy. A co takiego ma klasa, że należy czasem zadbać o jej prawidłową inicjalizację? Ano chociażby składowe statyczne - wspólne dla wszystkich obiektów.
W C# możemy sobie zdefiniować statyczny konstruktor, który może się tym zająć - jak również (niemal) czymkolwiek innym. Taki konstruktor, podobnie jak zwykły ma postać metody o tej samej co klasa nazwie, lecz jest opatrzony modyfikatorem static. Zostaje on wywołany przed stworzeniem pierwszego obiektu klasy lub próbą dostępu do jakiejkolwiek składowej statycznej. Całkiem praktyczne, prawda?
Coś podobnego ma również Java, a zwie się to oryginalnie 'statycznym inicjalizatorem'. W odróżnieniu od wariantu z C#, inicjalizator ten jest wywoływany wcześniej - już w momencie załadowania klasy przez maszynę wirtualną. Właściwie jednak nie ma to wielkiego znaczenia, bo efekt jest analogiczny: jeśli ów inicjalizator ma zrobić coś przed pierwszym użyciem klasy, na pewno zostanie to zrobione. Jego kod pisze się w definicji klasy, w bloku otoczonym nawiasami klamrowymi i opatrzonym - a jakże - słówkiem static.
A jak pod tym względem wypada C++? No cóż, jak zwykle blado :) Kruczkiem standardu jest fakt, że wszelkie składowe statyczne klas mają niezdefiniowaną kolejność inicjalizacji, jeśli są przypisane do odrębnych modułów (plików .cpp). Jeśli więc takie składowe zależą od siebie, to wynikiem mogą być tylko kłopoty.
Poradzić na to można dwojako - zależnie od tego, którą zaletę statycznych konstruktorów chcemy wykorzystać. Jeżeli na przykład chcemy, aby jakaś zmienna statyczna lub globalna była zainicjowana niezależnie od tego, gdzie i kiedy chcemy ją użyć, najlepiej uczynić ją lokalną statyczną zmienną w funkcji:
Jeśli z kolei musimy mieć pewność, że przed stworzeniem obiektu naszej klasy zostaną wykonane jakieś dodatkowe czynności, to możemy to częściowo osiągnąć za pomocą statycznej flagi:
Niezależnie jednak od tego, co jest nam akurat potrzebne, nie powinniśmy nigdy zapominać o rozważeniu zależności między składowymi statycznymi klas i/lub zmiennymi globalnymi w naszym kodzie, jeśli chcemy uniknąć niezdefiniowanego zachowania. Bo wtedy po prostu wszystko się zdarzyć może :]
Triki z PowerShellem #3 – PowerGUI
W Windows większość czynności administracyjnych można wykonać przy pomocy interfejsu graficznego. Wraz z wprowadzeniem PowerShella częściowo się to zmieniło, bo - jak to już kilka razy pokazywałem - stanowi on bardzo ciekawą tekstową alternatywę. Istnieje jednak narzędzie, które zamyka to kółeczko i powraca do interfejsu graficznego - to PowerGUI.
Pomysł wydawać się może dziwny, bo przecież GUI nigdy nie osiągnie tego, co jest największą zaletą dobrej konsolki i powłoki tekstowej - ogromnej elastyczności. PowerGUI radzi sobie jednak całkiem nieźle, oferując spore możliwości dostosowywania tego, jakie informacje chcemy przeglądać i jakie dodatkowe akcje możemy na nich wykonać. Obie te rzeczy koduje się po prostu za pomocą poleceń PowerShella; mamy tu elementy zwane script nodes, które wyświetlane są w formie drzewka i po wybraniu prezentują rezultaty jakiejś komendy, np. listę wszystkich uruchomionych procesów (Get-Process). Dodatkowo możemy przypisać im też akcje wykonywane na zwróconych elementach lub odwołania do innych kolekcji obiektów (np. usług zależnych od danej).
Ogólnie pomysł wydaje się całkiem zgrabny i ma potencjał oszczędzania czasu - zwłaszcza że klepanie dość rozwlekłych komend PowerShella może być raczej męczące.
Według mnie znacznie przydatniejszą częścią PowerGUI jest dołączony edytor skryptów. To nic innego, jak (niemal) pełnowartościowe IDE do skryptów .ps1. Oprócz kolorowania składni oferuje ono debugowanie przy pomocy pracy krokowej i breakpointów oraz podglądanie wartości zmiennych - czyli prawie wszystko, co programiście do szczęścia potrzebne :)
I właśnie ten edytor jest, jak sądzę, najważniejszych powodem, dla którego powinniśmy rzucić okiem na PowerGUI, jeśli przynajmniej od czasu do czasu używamy PowerShella. Nie od rzeczy jest też argument, że cały ten pakiet licencjonowany jest jako freeware, więc możemy go używać bez ograniczeń do dowolnych celów. Zanim więc druga wersja PowerShella (która ma oferować między innymi środowisko graficzne) wyjdzie w świat, jest to całkiem rozsądna propozycja.
Kopiowanie do katalogu wyjściowego w VSKiedy program korzysta z zewnętrznych plików (np. obrazków, dźwięków, itp.), które docelowo mają się znaleźć razem z nim w tym samym katalogu, w Visual Studio pojawia się pewien problem. Otóż nie bardzo wiadomo, gdzie dokładnie pliki te należy umieścić, by aplikacja miała do nich dostęp. Zazwyczaj najwygodniej byłoby wrzucić je do katalogu głównego z projektem i/lub całym solution. Tyle że wersje skompilowane trafiają zwykle do katalogów w rodzaju Debug czy Release, czyli folderów pod- lub równorzędnych. Skutek jest często taki, że aplikacja nie potrafi znaleźć potrzebnych plików, gdyż nie znajdują się one we wspomnianych katalogach ze skompilowanymi wersjami programu.
Można je oczywiście ręcznie tam skopiować, ale to niezbyt dobre rozwiązanie - zwłaszcza, jeśli ręcznie musielibyśmy te pliki aktualizować przy każdej zmianie (i to w dwóch miejscach!). Alternatywą jest ustawienie w opcjach projektu odpowiedniego katalogu roboczego (working directory), dzięki czemu moglibyśmy otwierać nasze niezbędne pliki posługując się ścieżkami względnymi. W finalnej wersji programu należałoby jednak pozbyć się tej zależności od katalogu roboczego. Jej pozostawanie sprawiłoby bowiem, że nasz program nie odszukałby potrzebnych plików, jeśli tylko zostałby uruchomiony z innym katalogiem roboczym (np. z poziomu wiersza poleceń lub przy pomocy odpowiednio przygotowanego skrótu). Byłby to ewidentny błąd.
Dlatego też lepiej, aby wszystkie pliki "towarzyszące" otwierać przy pomocy ich pełnych ścieżek, złożonych przy pomocy ścieżki do katalogu z plikiem EXE. Ją zaś można pobrać na wiele sposobów, w zależności od języka i platformy. W Windows API można na przykład użyć funkcji GetModuleFileName (do uzyskania pełnej ścieżki do pliku wykonywalnego) oraz PathRemoveFileSpec (do wyrzucenia z niej nazwy pliku). W .NET będzie to pole System.Windows.Forms.Application.ExecutablePath w połączeniu np. z metodą System.IO.Path.GetDirectoryName.
Wówczas jednak dochodzimy do punktu wyjścia, jako że podczas uruchamiania w debuggerze program będzie szukał swoich plików w katalogach Debug/Release. Można temu w prosty sposób zaradzić. Wystarczy mianowicie:
Domyślną wartością dla tej właściwości jest Do not copy. Wybranie wariantu Copy always sprawi, że zawsze po kompilacji Visual Studio skopiuje wskazany plik do katalogu z plikami wykonywalnymi (np. Debug). Możemy też wybrać Copy if newer, przez co kopiowanie zostanie wykonane tylko wtedy, jeśli od ostatniego zbudowania projektu nasz plik uległ zmianie. Ten wariant jest zwykle najrozsądniejszy, bo oszczędza niepotrzebnego kopiowania tych samych wersji plików.
Struktury danych nieobecne w .NETProgramując z użyciem platformy .NET można niekiedy mieć wrażenie, że jest w niej niemal wszystko. Oczywiście takie przekonanie jest nietrwałe: wystarczy bowiem znaleźć coś, co być w niej mogłoby (a zwłaszcza coś, co występuje w innych, podobnych bibliotekach), a czego jednak nie ma. Nietrudno zauważyć, że takich rzeczy jest mimo wszystko większość - bo w sumie nie może być przecież inaczej :)
Niektóre braki są aczkolwiek bardziej dotkliwe niż inne. Należą do nich chociażby luki w systemie kolekcji obiektów, czyli - mówiąc ogólniej - wszelkiego rodzaju struktur danych. W .NET brakuje bowiem takich rzeczy jak:

Contains o wiadomym przeznaczeniu, jednak tylko kontenery słownikowe sprawdzają unikalność umieszczanych w nich obiektów. Te zaś zasadniczo służą do czegoś innego: mapowania jednych obiektów na inne. Brakuje więc zwykłej, prostej klasy Set z operacjami dodawania, usuwania i wyszukiwania elementów - odpowiednika std::set z STL.SortedDictionary, które jednak nie są zasadniczo do tego przeznaczone. Kolejka priorytetowa powinna mieć przecież niemal identyczny interfejs do zwykłej kolejki FIFO (czyli głównie metody Push i Pop), bo różnica polega wyłącznie na porządku zwracanych elementów, określonym przez ich porównania.Tę listę można by oczywiście ciągnąć bardzo długo, ale zdecydowałem się ograniczyć do tych czterech rzeczy z dwóch powodów. Po pierwsze, przynajmniej raz zdarzyło się, że potrzebowałem którejś z wymienionych tutaj struktur w .NET i byłem zmuszony poradzić sobie we własnym zakresie. Po drugie, bez problemu można podać przykłady innych bibliotek, języków czy środowisk, gdzie jedna z nich, większość lub nawet wszystkie są obecne. Najlepszym przykładem jest C++ (z STL) uzupełniony o bibliotekę Boost, który pod względem struktur danych prezentuje się w tej postaci naprawdę potężnie.
Nie widzę więc specjalnych usprawiedliwień dla faktu, że w .NET lista standardowych struktur prezentuje się nadzwyczaj skromnie. Może więc zamiast dodawać w nowych wersjach platformy kolejne wątpliwej jakości "usprawnienia" - jak wyrażenia lambda wbudowane w C# czy zabawki typu LINQ - programiści Microsoftu zajęliby się raczej zaniedbanymi dotąd podstawami?...
Tej funkcji brakuje ‘n’W dzisiejszych niebezpiecznych czasach niemal każdy program może stać się przedmiotem ataku, za pomocą którego można - jak to się zwykło mówić w żargonie speców od bezpieczeństwa - "wykonać dowolny kod" (execute arbitrary code). Chyba najbardziej znanym exploitem tego typu jest przepełnienie bufora (buffer overflow), które w najprostszym wypadku może dotyczyć na przykład takiej sytuacji jak poniższa:
Gdy bowiem bufor rezyduje na stosie, a jakaś funkcja przypadkowo "wyjedzie" poza jego zakres, możliwe jest nadpisanie innej części stosu. Jedną z nich może być odkładany przy wywołaniu każdej funkcji adres powrotu. Po zakończeniu działania kodu funkcji, adres ten jest zdejmowany ze stosu i program skacze pod uzyskane w ten sposób miejsce w pamięci. Dzięki temu wykonanie programu może wrócić do miejsca tuż za wywołaniem funkcji i kontynuować działanie. Jeśli jednak ów adres zostanie nadpisany przez exploit, możemy w rezultacie skoczyć pod zupełnie inny adres i wykonać dowolny - potencjalnie szkodliwy - kod.
Dlatego też nie należy nigdy ślepo zakładać, że przekazywane nam tablice będą zawsze wystarczająco duże na pomieszczenie rezultatów funkcji. Powinniśmy na to zwrócić baczną uwagę zwłaszcza wtedy, gdy mamy zapisać w buforze dane pochodzące z zewnątrz - z pliku, sieci, od użytkownika, itd. Pisząc funkcje operujące na buforach będących tablicami w C, powinniśmy więc zawsze dodawać do nich jeszcze jeden parametr: rozmiar przekazywanego bufora. Oczywiście nalezy potem dbać, aby go nie przekroczyć.
A co z już istniejącymi funkcjami C, jak sprintf, strcpy, gets?... Wszystkie aktualne dokumentacje do kompilatorów, w których funkcje te występują (MSDN, linuksowy man, itp.), solidnie przestrzegają przed potencjalnym przepełnieniem bufora, które może być skutkiem ich używania. Zwykle też podawane są bezpieczne alternatywy, do których przekazuje się rozmiar bufora: w Visual C++ są one oznaczone końcówką _s (np. sprintf_s), a w GNU GCC dodatkową literką n w nazwie (np. snprintf). To ich właśnie należy używać, jeśli występuje potencjalna możliwość przepełnienia bufora.
Wyjątkiem jest gets, której to... nie powinniśmy w ogóle używać :) Co ciekawe, tylko MSDN wspomina o jej bezpiecznym odpowiedniku (gets_s). Pod GCC funkcji gets po prostu permanentnie brakuje n ;-]
Wyjątki mają warstwyKiedy w kodzie zdarza się coś niedobrego, na co nie mamy natychmiastowego rozwiązania, zwykle rzucamy wyjątek. To nam odwija stos, wychodząc po kolei z głęboko zagnieżdżonych funkcji - aż w końcu natrafimy na handler, który potrafi rzeczony błąd obsłużyć. Odwiecznym problemem wyjątków jest to, gdzie należy tak naprawdę je łapać; zwykle bardzo łatwo jest umieścić kod ich obsługi na zbyt wysokim poziomie, tłumacząc się, że przecież "niżej" nic na ten błąd nie można było poradzić.
Faktycznie zaradzić wyjątkowi często nie można, ale prawie zawsze można w tym pomóc już na wczesnym etapie jego obsługi. Błąd gdzieś "w środku" powoduje bowiem niepowodzenie wywołania wszystkich funkcji po drodze, a każda z takich porażek jest specyficzna dla czynności, która dana funkcja wykonuje. Jeśli przykładowo chcemy odczytać jakiś parametr konfiguracyjny programu, który wymaga załadowania z pliku, to może się okazać, że tego pliku nie da się otworzyć. Wówczas odczytywanie konfiguracji nie powiedzie się - co samo w sobie jest błędem, ale także powoduje, że niemożliwe jest uzyskanie wartości żądanego parametru - kolejny błąd. Widać więc, że błędy-wyjątki mogą być dla siebie kolejno przyczyną i skutkiem.
Taka szczegółowa "historia błędów" jest przydatna, bo pozwala oddzielić informacje ważniejsze od mniej ważnych. Nasza aplikacja może na przykład w ogóle nie wiedzieć, że jej konfiguracja jest zapisywana w pliku (a nie np. Rejestrze) i dlatego wyjątek 'Nie znaleziono pliku konfiguracyjnego' powinien być przed nią ukryty. Trzeba go opakować w błąd wyższego rzędu.
Nazywa się to wewnętrznymi wyjątkami (inner exceptions) i jako wbudowany mechanizm występuje chociażby w .NET i Javie. Idea jest prosta: kiedy złapiemy wyjątek "z dołu", którego nie możemy do końca obsłużyć, zapakowujemy go w nowy obiekt, sygnalizujący niepowodzenie na "naszym" poziomie kodu. Ten nowy wyjątek wyrzucamy dalej; może on potem podlegać bardzo podobnemu procesowi.
I tak w przykładzie z ładowaniem konfiguracji, próba odczytania nieistniejącego pliku skończy się w .NET wyjątkiem IOException, który zostanie pewnie natychmiast opakowany w FileNotFoundException. Funkcja odczytująca dane konfiguracyjne z pliku najpewniej sama wsadzi ten wyjątek w nowy, np. ArgumentException, co mówi wywołującemu, że przekazany argument - tu: nazwa pliku z konfiguracją - jest nieprawidłowy. W końcu, ponieważ nie będzie można odczytać żądanej wartości, funkcja która miała ją zwrócić może nas ostatecznie uraczyć wyjątkiem InvalidOperationException, zawierającym w środku wszystkie wymienione wcześniej błędy. Których było zresztą całkiem sporo :)
Gdy w końcu złapiemy cały ten wielokrotnie zapakowany wyjątek w innym miejscu, możemy dostać się do całego łańcucha przyczynowo-skutkowego, który powstał podczas odwijania stosu. W tym celu korzysta się z właściwości InnerException w .NET lub metody getCause w Javie - na przykład tak:
Zapewne jednak rzadko będziemy sięgali aż do samego dna - zwykle tylko w celach logowania. Dzięki odpowiedniemu opakowaniu możemy bowiem zająć się od razu sytuacją wyjątkową "najwyższego rzędu" - adekwatną do czynności, którą chcieliśmy wykonać (a nie tą pochodzącą z głębokich czeluści kodu niższego poziomu, od której wszystko się zaczęło).