Serwisy partnerskie:
Close icon
Serwisy partnerskie

Filozofia sieci. Protokół UDP, część 1

Article Image
W cyklu dotyczącym „Filozofii sieci” został opisany protokół ARP. Jego zadaniem jest umożliwienie szeroko pojętej komunikacji między dwoma stacjami (jak pamiętamy, tworzy on powiązanie pomiędzy adresem IP a adresem MAC). Wśród wielu protokołów istniejących w obrębie sieci komputerowych są takie, które są przeznaczone do przesyłania danych oraz takie, które służą do celów organizacyjnych. Podstawowym „organizacyjnym” jest protokół ICMP natomiast do transmisji danych używa się protokołu UDP albo TCP. Obecnie opiszę protokół UDP, natomiast pozostałym wymienionym będą poświęcone kolejne części.

Protokół UDP (ang. User Datagram Protocol – protokół pakietów użytkownika) jest prostym, bezpołączeniowym protokołem przeznaczonym do przesyłania danych. W poprzednim zdaniu zostało użyte „tajemnicze” słowo: bezpołączeniowy. Oznacza to, że w tym wariancie transmisji nie występują żadne mechanizmy związane z kontrolą przepływu danych oraz nie występuje operacja dotycząca nawiązania i  zakończenia połączenia (jak jest to w protokole TCP). Ujmując rzecz najprościej, wysyłane za pośrednictwem UDP dane z jednej stacji „wyszły” ale nie ma żadnej informacji, czy „doszły”. W sieciach rozległych mogą zdarzyć się różne przypadki, gdzie przesyłany pakiet może zaginąć. W przypadku sieci lokalnych praktycznie to się nie zdarza: o ile  sieć jest technicznie sprawna, to pakiet UDP dotrze do adresata.

Rysunek 1.

Patrząc na strukturę ramki ethernetowej (pokazanej na rysunku 1), pole TYP (występujące tuż za adresami MAC nadawcy oraz odbiorcy) stanowi identyfikator protokołu. W przypadku gdy to pole zawiera liczbę 806 hex, to dane występujące dalej należy interpretować jako pakiet ARP (opisany dotychczas). W przypadku wartości 800 hex, dalsza część jest pakietem IP. Ma to oczywiście swoją strukturę. Tradycyjnie zaczyna się nagłówkiem, za którym występują „dane użytkowe”. Nagłówek IP jest pokazany na rysunku 2.

Rysunek 2.

Zawiera on następujące informacje:

  • VERS – wersja protokołu IP, dla sieci ethernetowych , które nas interesują, jest to IPV4 (pole ma wartość 4),
  • HLEN – wielkość nagłówka pakietu IP wyrażona w słowach 32-bitowych, w typowym przypadku, gdy nagłówek nie zawiera dodatkowych opcji (patrz niżej pole OPT), wielkość nagłówka wynosi 5 słów 32-bitowych,
  • TOS – (Type Of Service) typ obsługi – informacja o wymaganiach na jakość obsługi pakietu, zawiera jego poziom ważności, w moim przypadku w pakietach przychodzących te informacje nie są rozpatrywane, gdyż tym polem bardziej zainteresowane są routery, w pakietach wychodzących pole ma wartość 0,
  • TOTAL – długość całego pakietu IP (nagłówka i danych razem) wyrażona w bajtach,
  • IDENT – numer nadawany pakietom w celu poprawnego złożenia ich po fragmentacji, każdy fragment danego pakietu ma ten sam numer identyfikacyjny, ponieważ w moim rozwiązaniu, pakiety nie są defragmentowane w obsłudze sieci z racji małych zasobów pamięci operacyjnej RAM, każdy pakiet wychodzący ma kolejną wartość liczbową,
  • FLAGS – znaczniki bitowe, mogące oznaczać, że danego datagramu nie należy fragmentować lub że jest to ostatni fragment, w moim oprogramowaniu to pole nie jest rozpatrywane, gdyż nie realizuje defragmentacji danych, w pakietach wychodzących to pole jest wyzerowane,
  • FRAGOFFS – przesunięcie fragmentu w przypadku fragmentacji podaje położenie danego fragmentu względem początku pakietu przed fragmentacją, w przypadku braku fragmentacji równe zeru, moje oprogramowanie w pakietach przychodzących nie rozpatruje tego pola, w pakietach wychodzących jest wyzerowane,
  • TTL – (Time To Live) czas życia, licznik, który jest zmniejszany o 1 za każdym przejściem pakietu przez router, a gdy osiągnie zero jest usuwany, ma na celu zapobieganie niekończącemu się przesyłaniu datagramów, które uległy zapętleniu w sieciach rozległych, licznik powinien być tak ustawiony, aby przy prawidłowej pracy sieci nie zdążył osiągnąć zera przed dostarczeniem pakietu do celu, polem tym zainteresowane są głównie routery w sieciach rozległych, w moim oprogramowaniu dla pakietów wychodzących to pole jest ustawione na 64,
  • PROT – kod oznaczający protokół wyższej warstwy, którego dane zawarte są tuż za nagłówkiem pakietu IP, mogą być to protokoły TCP, UDP, ICMP i inne,
  • HCRC – suma kontrolna nagłówka IP, potwierdza nienaruszenie danych zawartych w omawianym tu nagłówku,
  • SRC – adres IP węzła nadającego dany pakiet IP,
  • DST – docelowy adres IP węzła, do którego skierowany jest dany pakiet IP,
  • OPT – opcje dodatkowe, pole o zmiennej długości, mogące zawierać opcje dotyczące sposobu wyznaczania trasy, bezpieczeństwa, znakowania czasu przybycia pakietu do routerów i temu podobne funkcje, tym polem zainteresowane są głównie routery w sieciach rozległych, w moim oprogramowaniu nie są rozpatrywane (jeżeli to pole występuje w pakiecie IP, cały jest ignorowany), obecność tego pole można rozpoznać po danych zapisanych w polu HLEN, zapisana tam wartość 5 oznacza brak opcji dodatkowych, gdyż wielkość nagłówka IP bez pola OPT (łącznie z polem FILL) wynosi 5 słów 32-bitowych,
  • FILL – wypełnienie nagłówka zerami by uzyskać jego wielkość jako wielokrotność słów 32-bitowych, ponieważ pole OPT ma zmienną długość, pole FILL „rozpycha” nagłówek, w sensie informacyjnym nie ma żadnego znaczenia.

W nagłówku tym występuje pole identyfikujące protokół wyższej warstwy. W obrębie protokołu IP mogą być zakapsułkowane różne protokoły warstw wyższych, gdzie jednym z możliwych wariantów jest bieżąco opisywany protokół UDP (pole PROT ma wtedy wartość 17).

Oznacza to, że obszar położony za nagłówkiem IP jest pakietem UDP. Ten z kolei również składa się ze swojego nagłówka i danych użytkownika. Nagłówek UDP jest strukturą o ściśle określonej budowie (rysunek 3).

Rysunek 3.

Zawiera on:

  • port nadawcy jako informacja zapisana na 16 bitach,
  • port odbiorcy jako informacja zapisana na 16 bitach,
  • wielkość pakietu wyrażoną w oktetach jako liczba 16-bitowa,
  • suma kontrolna nagłówka UDP również jako liczba 16-bitowa.

W dalszej części pakietu UDP znajdują się dane użytkownika, czyli to, co jest przesyłane z jednej stacji do drugiej.

O ile dane dotyczące wielkości pakietu raczej powinny być intuicyjnie zrozumiałe, o tyle niewielkiego wyjaśnienia wymaga suma kontrolna. Jest to suma 16-bitowa obliczona dosyć pokrętnie. Jeżeli obszar danych użytkowych pakietu UDP (pole Dane na rysunku 2) potraktować jako ciąg liczb 16-bitowych, to suma kontrolna jest sumą tego ciągu oraz sumą pseudonagłówka. Kolejne nowe pojęcie, suma pseudonagłówka, to suma określonych pól pakietu IP oraz UDP (nie jest, jak można byłoby się spodziewać sumą samego nagłówka UDP). Na sumę pseudonagłówka ma wpływ adres IP nadawcy oraz odbiorcy, kod identyfikujący protokół UDP (czyli w rzeczywistości liczba 17) oraz wielkość transmitowanych danych. Jak łatwo zauważyć, adresy IP (nadawcy oraz odbiorcy) nie pochodzą z nagłówka UDP, więc określenie pseudonagłówek jest w pełni uzasadnione. Może się tak zdarzyć, że obliczona suma ma wartość zero. W takim przypadku jako sumę kontrolną wpisuje się liczbę FFFF hex (by ją odróżnić od liczby zero, zero oznacza, że nie jest liczona suma kontrolna).

Większego wyjaśnienia wymagają pojęcia określone jako port nadawcy oraz port odbiorcy. Należy je rozumieć jako pewnego rodzaju rozszerzenie adresowe.  Pamiętając, że w nagłówku pakietu IP znajdują się już adresy IP nadawcy oraz odbiorcy, numery portów są dodatkową informacją o charakterze adresowym. Można sobie to wyobrazić w następujący sposób. Jeżeli adres IP będzie analogią do adresu pocztowego w sensie nazwa ulicy oraz numer bloku, to numer portu można interpretować jako numer mieszkania w obrębie określonego bloku. Przesłanie danych jako pakietu UDP można wyobrazić sobie jako transfer z jednego mieszkania (w określonym bloku) do innego mieszkania (w innym bloku). W ogólnym przypadku nie jest zabronione przesłanie danych do samego siebie (gdzie adres IP nadawcy i odbiorcy jest taki sam, co można rozumieć jako przeprowadzkę do innego mieszkania w tym samym bloku). W przypadku „dorosłych” komputerów taki chwyt często jest stosowany (jak choćby stwarza możliwość przesłania wiadomości od jednego programu do innego programu w obrębie jednego komputera). Jest nawet wydzielony z puli adresowej IP jeden specjalny adres: 127.0.0.1, który występując w roli docelowego adresu IP, oznacza przesłanie danych do samego siebie.

Do przesyłania danych za pośrednic­twem protokołu UDP w komputerach wykorzystuje się mechanizm gniazd (ang. socket). Jest to pewna struktura danych przechowywana w pamięci operacyjnej mikrokontrolera LPC2378 (jak i każdego komputera). By wysłać lub odebrać dane UDP, należy w programie utworzyć takie gniazdo. Wiąże ono przede wszystkim adres IP z numerem portu. Tworząc gniazdo, system operacyjny (jak również LPC2378) nada tworzonemu gniazdu numer portu z pewnej puli dostępnych numerów. W oprogramowaniu mikrokontrolera są dwie stałe determinujące przedział liczbowy, z którego generowane są te numery (o nazwach AutoIncrementUDPPortNoLoBound i AutoIncrementUDPPortNoUpBound: patrz plik udpservice.h). Często jednak jest wymagane, by numer portu był ściśle określony. Można go zmienić, wywołując funkcję BindUDPSocket (tak się nazywa funkcja w oprogramowaniu mikrokontrolera). Po takim zabiegu jest utworzona para adresowa: adres IP (znany w oprogramowaniu jako adres stacji) oraz numer portu („wylosowany” z puli w operacji tworzenia gniazda z ewentualną zmianą na ściśle określony poprzez wywołanie funkcji  BindUDPSocket). W komunikacji sieciowej występuje jeszcze pojęcie serwera i klienta. Ma to swoje odbicie w oprogramowaniu. Klient to ta strona, która tworzy gniazdo (z ewentualną operacją zmiany numeru portu), transmituje dane i po wykorzystaniu może gniazdo zamknąć (czyli rozwiązać strukturę kojarzącą adres IP i numer portu). Serwer to ta strona, która tworzy gniazdo, nadaje mu ściśle określony numer i cierpliwie nasłuchuje. Takie gniazdo musi zostać oznaczone jako nasłuchowe (wywołanie funkcji SetUDPListen). Tu warto zauważyć, że ta struktura egzystuje w programie cały czas. Reasumując, serwer tworzy gniazdo, nadaje mu ściśle określony numer (cała reszta sieci lokalnej jako klienci muszą znać parametry serwera jako adres IP i numer portu, by móc coś przesłać do serwera). Serwer jako odbiorca pakietu UDP zna dokładnie dane nadawcy (pochodzą one z odebranych danych: adres IP z nagłówka IP oraz numer portu nadawcy z nagłówka UDP). Pozwoli to na normalną komunikację pomiędzy dwoma stacjami.

By poznać dokładniej tematykę, proponuję lekturę odpowiedniego programu prezentacyjnego. Program ten, po zainicjowaniu obsługi sieci (wystarczająco dokładnie opisanym w poprzednich częściach) zawiera nowe dowiązanie do instancji obsługującej protokół UDP (wywołanie funkcji StandardAddUDPInstance (Instance -> IPLayer) ;, jak pokazuje listing 1). Uwaga – wszystkie listingi wymienione w artykule dostępne są też w Elportalu wśród materiałów dodatkowych do tego numeru EdW.

Listing 1.

static void StartNet ( PtrUDPExampleInstanceRecT Instance )
{
  EtherSpeedMode EthMode ;
  /*--------------------------------------------------------------*/
  EthMode = Mode_AutoNeg ;
  Instance -> EMACLayer = InitPHYLayerInstance ( MachineMACAddr , EthMode ) ;
  Instance -> IPLayer = InitIPLayerInstance ( Instance -> EMACLayer ,
                                              ( ULONG ) MachineIPAddress ,
                                              ( ULONG ) MachineIPSubnet ) ;
  Instance -> IPLayer -> MyDefaultGWIPAddress = ( ULONG ) MachineGWIPAddress ;
  Instance -> UDPLayer = StandardAddUDPInstance ( Instance -> IPLayer ) ;
} /* StartNet */

Ten program będzie dosyć specyficznie przetwarzał pakiety UDP, będzie je jedynie odbierał bez jakiejkolwiek reakcji na przychodzące dane, gdyż w programie nie zostało utworzone żadne gniazdo, więc każdy pakiet UDP odebrany jako IP (należy pamiętać, że pakiety UDP są zakapsułkowane w obrębie pakietów IP) nie znajdzie swego miejsca docelowego (numeru portu), ale technicznie zostanie odebrany przez zespół EMAC w mikrokontrolerze. Środowisko prezentacyjne przedstawia rysunek 4. System z mikrokontrolerem LPC2378 o IP=192.168.0.55, MAC=08-14-16-22-28-2D jest przyłączony do lokalnej sieci domowej. W tej sieci znajduje się mój komputer o IP=192.168.0.68, MAC=00-18-7D-0A-6E-25. Całość jest spięta przez switch pełniący jednocześnie funkcję routera do sieci wychodzącej na zewnątrz. Spełnia on dodatkowo funkcję bramy domyślnej. Dla przypomnienia, brama domyślna w sieci lokalnej jest stacją, która obsługuje ruch sieciowy
wychodzący poza sieć lokalną. Z tego powodu ma swój adres (u mnie jest to IP=192.168.0.254). Do systemu z mikrokontrolerem (poprzez kanał komunikacyjny UART0) przyłączony jest komputer jako port szeregowy (we współczesnych komputerach może zachodzić konieczność zastosowania odpowiedniej przejściówki RS232 ↔ USB). Rysunek 4 pokazuje koncepcję środowiska badawczego, czyli użycie dwóch komputerów, chociaż w rzeczywistości może być zastosowany jeden. W jednym komputerze jest uruchomiony program o nazwie UDPTESTER, w drugim komputerze jest uruchomiony wcześniej już używany program terminalu (oba do pobrania jako materiały dodatkowe umieszczone w Elportalu). Zadaniem programu UDPTESTER jest wysyłanie oraz odbieranie pakietów UDP.

Rysunek 4.

Po zaprogramowaniu pamięci FLASH mikrokontrolera oraz przyłączeniu go do domowej sieci, korzystając z wiersza poleceń, można sprawdzić, czy moduł z procesorem jest widoczny w sieci. Do takich operacji stosowane jest polecenie PING, które wysyła do drugiej stacji sygnał i oczekuje na zwrotne potwierdzenie. Służy ono do badania „drożności” technicznego połączenia. O PING było wspomniane w części poświęconej ARP, a więcej będzie w części poświęconej ICMP. Moduł z mikrokontrolerem LPC2378 odpowiada na polecenia diagnostyczne drożności technicznego połączenia (rysunek 5). Parametrem komendy PING jest adres IP stacji, do której badana jest drożność połączenia. 

Rysunek 5.

W odpowiednich polach programu UDPTESTER (rysunek 6) należy wpisać docelowy adres IP (adres IP systemu z mikrokontrolerem, czyli IP=192.168.0.55) oraz numer portu jako 20001 (chwilowo mało istotne, jaką ma wartość, ale musi być podany). W polu Dane należy wpisać cokolwiek i kliknąć na przycisk Wyślij. To spowoduje, że program wyśle wpisane dane jako pakiet UDP do wskazanego miejsca docelowego.

Rysunek 6.

Uruchomienie programu w mikrokontrolerze spowoduje wysłanie podstawowych informacji dotyczących konfiguracji adresowej na ekran terminalu (programu uruchomionego w drugim komputerze), rysunek 7. Mamy tu podany adres IP i adres MAC systemu z mikrokontrolerem.

Rysunek 7.

Wysłanie pakietu UDP (z komputera nadzorowanego przez Windows) będzie wymagało „poznania” adresu MAC miejsca docelowego, toteż system Windows w pierwszej kolejności wyśle zapytanie ARP (opisane w poprzedniej części „Filozofii sieci”). Zapytanie ARP, pochodzące z komputera z uruchomionym program UDPTESTER (podany adres MAC=00-18-7D-0A-6E-25, rysunek 4), tradycyjnie ma adres docelowy MAC w postaci FF-FF-FF-FF-FF-FF, wartość, która oznacza: do wszystkich. Po uzyskaniu odpowiedzi na zapytanie ARP, system Windows przesłał pakiet UDP (w rzeczywistości jest to pakiet IP z zakapsułowanym pakietem UDP, rysunek 1). Program mikrokontrolera rozpoznał odebrane dane jako pakiet IP wysłany ze stacji o adresie IP=SRC=192.168.0.68 adresowany do stacji o adresie IP=DST=192.168.0.55. Spośród różnych pól nagłówka IP odebrany pakiet zawiera pole PROT=11 hex=17 dec. Ta wartość tego pola informuje, że pakiet IP niesie w sobie pakiet UDP (rysunek 1). Jego nagłówek zawiera cztery pola (rysunek 3). Port nadawcy to SRCPORT=1000, port odbiorcy to DSTPORT=20001, co łatwo skojarzyć z informacjami występującymi w odpowiednich polach programu UDPTESTER (rysunek 6). Występująca wielkość pakietu to LGTH=19. Jak łatwo policzyć, przesyłane dane („Jakies dane”) zawierają 11 oktetów. Sam nagłówek UDP zawiera 8 oktetów (4 pola po 2 oktety, rysunek 3), co daje łącznie 19 oktetów. Suma kontrolna pakietu UDP to 29E2 (w zapisie szesnastkowym). Oczywiście odebrane dane zostaną zignorowane, gdyż w mikrokontrolerze nie zostało utworzone nasłuchowe gniazdko UDP. Drugi program prezentujący protokół UDP nie ma tej „wady”, ma utworzone gniazdo nasłuchowe dla portu o numerze 20001. Utworzenie właściwego gniazda pokazuje listing 2.

Listing  2.

int main ( void )
{
  PtrUDPSocketDescriptionRecT LockUDPSocket ;
  USHORT BindStatus ;
  /*------------------------------------------------------------*/
  SysInit ( ) ;
  InitSysTime ( ) ;
  HardwareInit ( )  ;
  SoftwareInit ( ) ;
  UART0HardwInit( FOSC , N81Mode , 9600 ) ;
  HelloMessage ( ) ;
  StartNet ( & UDPDemoInstance ) ;
  LockUDPSocket =  CreateUDPSocket ( UDPDemoInstance . UDPLayer ) ;
  BindStatus = BindUDPSocket ( LockUDPSocket , UDPPortNumber ) ;
  if ( BindStatus != UDP_BindPortOK )
  {
    Halt ( Error ) ;
  } /* if */ ;
  SetUDPListen ( LockUDPSocket , UDPStreamService ) ;
  for ( ; ; )
  {
    SysTimerPool ( ) ;
    UDPDemoInstance . IPLayer -> IPLayerPoolService ( UDPDemoInstance . IPLayer ) ;
  } /* loop */ ;
} /* main */

Po wykonaniu niezbędnych czynności inicjujących, program w mikrokontrolerze LPC2378 tworzy gniazdko UDP (wywołanie funkcji CreateUDPSocket, gdzie w parametrach podaje się dowiązanie do instancji związanej z obsługą protokołu UDP). W kolejnym kroku zostaje zmieniony numer portu (utworzenie gniazdka przydzieli jakiś numer portu z ustalonej puli numerów). Realizuje to wywołanie funkcji BindUDPSocket (listing 2), gdzie w parametrach podaje się wskaźnik do utworzonego gniazda oraz oczekiwany numer portu (jako stała UDPPortNumber o wartości 20001).

Kolejne wywołanie funkcji SetUDPListen określa, że dane gniazdo (podane w wywołaniu funkcji) jest nasłuchowe. Dodatkowo następuje powiązanie gniazdka z funkcją obsługi. W wyniku odebrania pakietu UDP adresowanego do wybranego numeru portu, jego przetwarzanie sprowadza się do wywołania skojarzonej funkcji (zgłoszonej w wywołaniu funkcji SetUDPListen, listing 2). Do wywołania funkcji przewidzianej do „konsumpcji” odebranych danych dochodzi w module UDPService. 

W mojej obsłudze protokołu UDP funkcja skojarzona z przetwarzaniem danych odbieranych przez określone gniazdko dostaje od obsługi protokołu UDP w parametrach następujące dane (przykładowa obsługa jest pokazana na listingu 3):

  • dowiązanie do gniazdka, poprzez które zostały odebrane dane,
  • adres IP stacji nadawczej,
  • numer portu nadawcy,
  • adres IP stacji docelowej (w rzeczywistości nasz adres IP),
  • numer portu odbiorcy (numer portu, przez które zostały odebrane dane),
  • dowiązanie do bufora odbiorczego,
  • liczba oktetów odebranych danych.
Listing  3.

static void UDPStreamService ( PtrUDPSocketDescriptionRecT UDPSocket ,
                               ULONG                       SenderIPAddress ,
                               USHORT                      SenderPortNo ,
                               ULONG                       DestIPAddress ,
                               USHORT                      DestPortNo ,
                               void *                      UDPDataBuffer ,
                               USHORT                      UDPDataSize )
{
  UCHAR * ChPtr ;
  USHORT Loop ;
  UCHAR Number [ 20 ] ;
  /*--------------------------------------------------------------*/
  UART0SendString ( SrcInfo01 ) ;
  IPToString ( Number , SenderIPAddress ) ;
  UART0SendString ( Number ) ;
  UART0SendString ( SrcInfo02 ) ;
  WordToStr ( Number , 5 , ' ' , SenderPortNo ) ;
  UART0SendString ( BegOfString ( Number ) ) ;
  UART0SendString ( SrcInfo03 ) ;
  IPToString ( Number , DestIPAddress ) ;
  UART0SendString ( Number ) ;
  UART0SendString ( SrcInfo02 ) ;
  WordToStr ( Number , 5 , ' ' , DestPortNo ) ;
  UART0SendString ( BegOfString ( Number ) ) ;
  UART0SendString ( SrcInfo04 ) ;
  ChPtr = ( UCHAR * ) UDPDataBuffer ;
  if ( UDPDataSize == 0 )
  {
    UART0SendString ( NoUDPData ) ;
  } /* if ... */
  else
  {
    for ( Loop = 0 ; Loop < UDPDataSize ; Loop ++ )
    {
      UART0Send ( * ChPtr ) ;
      ChPtr ++ ;
    } /* for */ ;
  } /* if ... else */ ;
  UART0Send ( '\r' ) ;
  UART0Send ( '\n' ) ;
} /* UDPStreamService */

Obsługa ta sprowadza się do wyświetlenia w okienku programu terminalu informacji typu: kto (adres IP i numer portu UDP), do kogo (adres IP i numer portu UDP) i co (treść odebranych danych). Wysłanie danych, jak jest pokazane na rysunku 6, generuje reakcję mikrokontrolera przedstawioną na rysunku 8. „Konsumpcja” odebranych danych jest jedynie przykładowa. Oprogramowanie sieciowe wykorzystane w mikrokontrolerze oferuje wszelkie możliwe informacje, jakie mogą zostać wykorzystane. Tu warto zauważyć, że informacje docierające do funkcji przetwarzającej odebrane dane są już w formie rozpakowanej, gotowej do użycia i nie wymagają już żadnych dodatkowych operacji. Co zostanie użyte, zależy jedynie od autora programu, przykładowo dane mogą zostać wykorzystane w dalszym przetwarzaniu, jeżeli numer portu nadawcy jest parzysty. Oczywiście można się zastanawiać nad sensem takich czynności, bo przytoczona koncepcja jest jedynie przykładem. System z mikrokontrolerem może być częścią składową większej całości i protokół UDP, jako najprostsza możliwa komunikacja, może zostać użyty do sterowania. Za pośrednictwem sieci można przesyłać przykładowo jakieś polecenia. Innym wariantem może być system pomiarowy, gdzie protokół UDP może zostać użyty do gromadzenia tych danych, by później odtworzyć historię zdarzeń. Co z tego można uzyskać, zależy jedynie od inwencji twórcy.

Rysunek 8.

Trzeci program prezentacyjny pokazuje sposób nadawania pakietów UDP do określonego odbiorcy. W programie odbiorca jest ustalony na sztywno (jest to mój komputer IP=192.168.0.68, port=1000). Program w stosunku do poprzedniej wersji został rozbudowany o możliwość wczytania tekstu z kanału szeregowego (dane wysłane z programu terminalu). Wczytywane znaki są gromadzone w buforze i po napotkaniu znaku kontrolnego (dowolnego) zawartość zgromadzonego bufora zostaje wysłana jako pakiet UDP do ściśle określonego miejsca docelowego. W funkcji main w nieskończonej pętli dodana jest obsługa danych przychodzących z kanału szeregowego jako wywołanie funkcji SerialPool ( );. Postać tej funkcji jest pokazana na listingu 4.

Listing 4.

#define DestinIPAddress         0xC0A80044
#define DestinPortNumber        1000

static void SendCurrentBuffer ( void )
{
  /*--------------*/
  if ( NetBufferIndex )
  {
    UDPSendData ( UDPDemoInstance . UDPSocket , DestinIPAddress ,
                  DestinPortNumber , NetBuffer , NetBufferIndex ) ;
  } /* if */ ;
  NetBufferIndex = 0 ;
} /* SendCurrentBuffer */

void SerialPool ( void )
{
  UCHAR Ch ;
  /*--------------*/
  while ( UART0DataPresent ( ) )
  {
    Ch = UART0GetData ( ) ;
    if ( Ch < 0x20 )
    {
      SendCurrentBuffer ( ) ;
    } /* if ... */ 
    else
    {
      NetBuffer [ NetBufferIndex ] = Ch ;
      NetBufferIndex ++ ;
      if ( NetBufferIndex >= NetBufferSize )
        SendCurrentBuffer ( ) ;
    } /* if ... else */ ;
  } /* while */ ;
} /* SerialPool */

Wysłaniem zajmuje się funkcja SendCurrentBuffer, w której zastosowana jest funkcja pochodząca z pakietu obsługi UDP (funkcja UDPSendData). W jej parametrach wywołania występuje gniazdko, które zostało utworzone do nasłuchu, adres IP miejsca docelowego (jako stała, jeżeli jesteś zainteresowany, Czytelniku, eksperymentami z tym programem i Twój komputer ma inny adres IP, to konieczna jest zmiana wartości tej stałej i przekompilowanie programu), numer docelowego portu UDP, bufor z danymi zgromadzonymi z kanału szeregowego oraz wielkość tego bufora.

Rysunek 9.

Wysłanie danych pokazuje rysunek 9. Skompletowanie danych, które zaowocowało wysłaniem pakietu UDP, spowodowało w pierwszej kolejności wysłanie zapytania ARP do docelowego komputera (moduł z mikrokontrolerem nie znał adresu MAC miejsca docelowego). Zapytany komputer odesłał informację (odpowiedź na ARP), która pozwoliła powiązać adres IP miejsca docelowego z adresem MAC. Tabela powiązań nawet została wyświetlona na ekranie. Uzyskane wszystkie szczegóły adresowe pozwoliły wysłać pakiet UDP. Sama procedura utworzenia i wysłania pakietu jest pokazana na listingu 5.

Listing 5.

USHORT UDPSendData ( PtrUDPSocketDescriptionRecT Socket ,
                     ULONG                       DestIPAddress ,
                     USHORT                      DestPortNo ,
                     void *                      DataBufferPtr ,
                     USHORT                      DataSize )
{
  PtrUDPLayerInstanceRecT UDPLayerInstance ;
  PtrIPLayerInstanceRecT IPLayerInstance ;
  EMACInstanceRecT * InterfInstance ;
  void * AllUDPDataBuffer ;
  ULONG TargetAddress ;
  PtrOSFrameRecT OutFrame ;
  USHORT CheckSum ;
  USHORT BCMode ;
  USHORT Loop ;
  USHORT CheckSumOffset ;
  /*-----------------------------------*/
  if ( Socket )
  {
    if ( ! DataSize )
      return ( UDP_SendFailed ) ;
    UDPLayerInstance = Socket -> UDPLayerInstanceLink ;
    if ( DataSize > MaxUDPSegmentSize )
      return ( UDP_SendSegmentToBig ) ;
    IPLayerInstance = UDPLayerInstance -> IPLayerInstance ;
    InterfInstance = IPLayerInstance -> InterfaceInstance ;
    BCMode = FALSE ;
    if ( DestIPAddress == 0xFFFFFFFF )
    {
      BCMode = TRUE ;
      TargetAddress = DestIPAddress ;
    } /* if ... */
    else
    {
      if ( ( DestIPAddress & IPLayerInstance -> MyLocalNetMask ) == ( IPLayerInstance -> MyLocalIPAddress & IPLayerInstance -> MyLocalNetMask ) )
      {
        TargetAddress = DestIPAddress & ( ~ IPLayerInstance -> MyLocalNetMask ) ;
        if ( TargetAddress == ( ~ IPLayerInstance -> MyLocalNetMask ) )
          BCMode = TRUE ;
        TargetAddress = DestIPAddress ;
      } /* if ... */
      else
      {
        TargetAddress = IPLayerInstance-> MyDefaultGWIPAddress ;
        if ( ! TargetAddress )
        {
          return ( UDP_SendRouteFailed ) ;
        } /* if */ ;
      } /* if ... else */ ;
    } /* if ... else */ ;
    OutFrame = InterfInstance -> InterfaceAllocFrameMemory ( sizeof ( OSFrameRecT ) ) ;
    if ( OutFrame )
    {
      StartEthFrame ( OutFrame , NIL , InterfInstance -> InterfaceMACAddress , FRAME_IP ) ;
      CreateIPHeader ( OutFrame , Prot_UDP , IPLayerInstance -> MyLocalIPAddress , DestIPAddress ) ;
      PutUSHORTSwapField ( OutFrame , IPTotalLengthOffset , IPHeaderSize + UDPHeaderSize + DataSize ) ;
      CheckSum = CalculateChecksum ( & OutFrame -> Data [ IPVersOffset ] , IPHeaderSize ) ;
      PutUSHORTNoSwapField ( OutFrame , IPHeaderChecksumOffset , CheckSum ) ;
      AllUDPDataBuffer = ( void * ) ( OutFrame -> Data + OutFrame -> Length ) ;
      AddUSHORTSwapField ( OutFrame , Socket -> ListenPortNo ) ;
      AddUSHORTSwapField ( OutFrame , DestPortNo ) ;
      AddUSHORTSwapField ( OutFrame , DataSize + UDPHeaderSize ) ;
      CheckSumOffset = OutFrame -> Length ;
      AddUSHORTSwapField ( OutFrame , 0 ) ;   /* checksum */
      AddByteArrayField ( OutFrame , DataBufferPtr , DataSize ) ;
      CheckSum = CalculateUDPChecksum ( IPLayerInstance -> MyLocalIPAddress , DestIPAddress ,
                                        AllUDPDataBuffer , 
                                        OutFrame -> Length - EtherPackedHeaderRecTSize - IPHeaderSize ) ;
      if ( ! CheckSum )
        CheckSum = 0xFFFF ;
      PutUSHORTSwapField ( OutFrame , CheckSumOffset , CheckSum ) ;
      if ( BCMode )
      {
        for ( Loop = 0 ; Loop < EthMACAddressSize ; Loop ++ )
          OutFrame -> Data [ Loop ] = 0xFF ;
        InterfInstance -> InterfaceSendFrame ( OutFrame ) ;
      } /* if ... */
      else
      {
        IPLayerInstance -> SendIPPackedService ( IPLayerInstance , OutFrame , TargetAddress ) ;
      } /* if ... else */ ;
      InterfInstance -> InterfaceDeallocFrameMemory ( OutFrame ) ;
      return ( UDP_SendOK ) ;
    } /* if */ ;
  } /* if */ ;
  return ( UDP_SendFailed ) ;
} /* UDPSendData */

Wewnątrz funkcji wysyłającej weryfikowany jest adres IP miejsca docelowego. Są rozpatrywane trzy możliwości:

  • adres IP odpowiadający wariantowi IP=255.255.255.255, znaczenie i rola tego wariantu będzie opisana w kolejnej części,
  • adres IP należący do sieci lokalnej, czyli pakiet jest wysłany do miejsca identyfikowanego przez podany adres IP,
  • adres IP nie należy do sieci lokalnej, czyli pakiet jest wysłany do sieci rozległej, reprezentowanej przez bramę domyślną.

W stwierdzeniu, czy adres należy do sieci lokalnej, bierze udział maska podsieci. Jeżeli część własnego adresu stacji ziloczynowana logicznie (funkcja AND) z adresem IP stacji docelowej (również ziloczynowanym) jest identyczna, to miejsce docelowe należy do sieci lokalnej. Najlepiej wyjaśnić to na rzeczywistym przykładzie: adres stacji LPC2378 to IP=192.168.0.55, wysyłane dane są do stacji IP=192.168.0.68 oraz maska podsieci ma wartość 255.255.255.0. Zapisując poszczególne liczby binarnie, mamy:192 dec=11000000 bin, 168 dec=10101000 bin, 55 dec=00110111 bin, 68 dec=01000100 bin oraz 255 dec=1111111 bin. Jeżeli uzyskany wynik końcowy jest identyczny, to oba adresy należą do tej samej sieci lokalnej, rysunek 10.

Rysunek 10.

Po przydzieleniu pamięci na tworzony pakiet do wysłania następuje wypełnienie jego pól. W pierwszej kolejności tworzony jest nagłówek ramki ETH (wywołanie funkcji StartEthFrame). Tuż za nim umiejscowiony jest nagłówek pakietu IP (wywołanie CreateIPHeader). W dalszej kolejności obliczona jest suma kontrolna IP i umieszczona w odpowiednim miejscu. Tu warto zauważyć, że suma kontrolna IP dotyczy jedynie nagłówka IP, bez obszaru, który zostanie zakapsułkowany jako dane pakietu IP. Kolejne dołączone dane to numer własnego portu UDP, numer portu docelowego, wielkość przesyłanego obszaru jako suma wielkości nagłówka UDP (8 oktetów, rysunek 3) i obszaru danych użytkownika oraz suma kontrolna (początkowo dodane jako 0, które później zostanie zmienione na właściwą wartość). Za tak utworzonym nagłówkiem w obszar pakietu zostaje wkopiowany obszar danych użytkownika (wywołanie AddByteArrayField). 

Mając zgromadzone w jednym miejscu wszystkie dane, zostaje obliczona suma kontrolna (wywołanie funkcji CalculateUDPChecksum). Tak spreparowany pakiet zostaje przekazany do instancji zajmującej się wysyłaniem. Dalsze losy danych są uzależnione od aktualnej sytuacji. Jeżeli instancja wysyłająca zna powiązanie adresu IP z adresem MAC, to pakiet zostaje wytransmitowany. W przeciwnym wypadku zostaje zakolejkowany do późniejszego wysłania, gdyż należy zdobyć adres MAC miejsca docelowego.

Jednocześnie w drugim komputerze uruchomiony program UDPTESTER odebrał wysłany pakiet i jego treść wyświetlił na ekranie (rysunek 11). Tu warto zwrócić uwagę na fakt, że program ma jedynie wprowadzone dane określone jako „mój IP” oraz „mój port”. Te dane pozwalają utworzyć w programie windowsowym gniazdko z nadanym numerem portu, które zostało zgłoszone jako gniazdko do nasłuchu.

Rysunek 11.

Program może odebrać pakiety UDP adresowane zgodnie z informacjami występującymi w odpowiednich polach programu UDPTESTER. Nie jest możliwe wysyłanie danych (pola identyfikujące adresata nie są wprowadzone). W okienku z odebraną treścią pokazane są parametry nadawcy: adres IP=192.168.0.55 oraz port=20001. Można łatwo ustalić w kodzie źródłowym programu mikrokontrolera, że te parametry identyfikują gniazdko zgłoszone do nasłuchu. Z kolei w parametrach funkcji wysyłającej podany jest adres IP komputera oraz numer portu obsługiwany przez program UDPTESTER (o numerze 1000, listing 4).

W następnej odsłonie „Filozofii sieci” zajmę się innymi przykładami prezentującymi możliwości protokołu UDP.

Do pobrania
Download icon Filozofia sieci. Protokół UDP
Firma:
Tematyka materiału: Protokół UDP, User Datagram Protocol
AUTOR
Źródło
Elektronika Praktyczna grudzień 2020
Udostępnij
Zobacz wszystkie quizy
Quiz weekendowy
Edukacja
1/10 Jak działa rezystor LDR?
Oceń najnowsze wydanie EdW
Wypełnij ankietę i odbierz prezent
W tym numerze znajdziesz źródłową wersję artykułu publikowanego obok
Elektronika Praktyczna
grudzień 2020
Elektronika Praktyczna
Przejrzyj i kup
UK Logo
Elektronika dla Wszystkich
Zapisując się na nasz newsletter możesz otrzymać GRATIS
najnowsze e-wydanie magazynu "Elektronika dla Wszystkich"