//Pytanie: "Omów problematykę little- oraz big-endian w technikach programowania sieciowego w protokołach TCP/UDP w IP"//


{{:programowanie_uslug_sieciowych:14.5.pdf|:wersja pdf}}
====== Endianess ======

===== Co to jest big-endian/little-endian =====
Endianness (na polskiej wikipedii znalazłem tłumaczenie //końcówkowość//) to pojęcie, które dotyczy sposobu umieszczenia wieloelementowych danych w jednowymiarowej przestrzeni. Jeśli mówimy o pamięci komputera, to często wykorzystuje się również pojęcie byte-order (//kolejność, porządek bajtów//). Wyróżniamy dwa typy uporządkowania bajtów: //big-endian// i //little-endian//.

Z nazwy byte-order łatwo wywnioskować, że pojęcia big-endian/little-endian tyczą się kolejności bajtów, a nie bitów, co jest częstą pomyłką.

Jak wiemy pamięć komputera składa się z komórek (bajtów) adresowanych liniowo. Jeśli w pamięci składujemy dane, które są interpretowane po jednym bajcie (np. tablice znakowe w C), to problem big/little-endian nas nie dotyczy. Tablica tekstowa:
<code>
+-----+-----+-----+-----+-----+-----+
| 100 | 101 | 102 | 103 | 104 | 105 |
+-----+-----+-----+-----+-----+-----+
|  t  |  e  |  k  |  s  |  t  | \0  |
+-----+-----+-----+-----+-----+-----+
</code>

Problem pojawia się w momencie, gdy w liniowo adresowanej pamięci zechcemy składać dane wielobajtowe (np. ''int'' lub ''long''). Załóżmy, że ''int'' zajmuje 2 bajty, a ''long'' 4 bajty. Rozważmy zmienną typu ''int''. Można ją zapisać w pamięci w dwojaki sposób: albo mniej znaczący bajt (bajt, nie bit!) będzie pod niższym adresem, a bardziej znaczący pod wyższym albo na odwrót. Pierwszy przypadek występuje w systemach little-endian (mniej znaczący pierwszy), a drugi w systemach big-endian (bardziej znaczący pierwszy). Poniższe ASCII-arty powinny wyjaśnić różnicę. Załóżmy, że w pamięci chcemy przechować liczbę 80 (''0x0050'') i 2 130 772 483 (''0x7f010203'' == 127.1.2.3):
<code>
0x0050      big-endian                        little-endian
            +------+------+                   +------+------+
            | 1000 | 1001 |                   | 1000 | 1001 |
            +------+------+                   +------+------+
            |  00  |  50  |                   |  50  |  00  |
            +------+------+                   +------+------+

0x7f010203  big-endian                        little-endian
            +------+------+------+------+     +------+------+------+------+
            | 2000 | 2001 | 2002 | 2003 |     | 2000 | 2001 | 2002 | 2003 |
            +------+------+------+------+     +------+------+------+------+
            |  7F  |  01  |  02  |  03  |     |  03  |  02  |  01  |  7F  |
            +------+------+------+------+     +------+------+------+------+
</code>

Architektury big-endian to: Motorola 68000, SPARC. Architektury little-endian to: Intel x86. Architektury mieszane (z możliwością konfiguracji trybu pracy) to: ARM, DEC Alpha, MIPS, PA-RISC, IA64.

Aby naocznie przekonać się o różnicy między big-endian i little-endian, można skorzystać z kodu źródłowego programu umieszczonego na końcu. Wystarczy go skompilować na marsie (AMD Athlon(TM) MP 2000+ -- little-endian) lub na zwykłym PCuju i uruchomić. Wyniki będą takie:
<code>
danadam@mars]$ ./a.out
u.c[0]=3
u.c[1]=2
u.c[2]=1
u.c[3]=0
</code>
Natomiast jeśli skompilujemy go i uruchomimy na studencie (PA-RISC 2.0 Rhapsody DC- 440 -- big-endian) to wyniki będą takie:
<code>
da57139@student]$ ./a.out 
u.c[0]=0
u.c[1]=1
u.c[2]=2
u.c[3]=3
</code>

===== Problem big-endian/little-endian w programowaniu sieciowym =====
O ile ograniczymy się do jednej maszyny, nie musimy się przejmować tym, jak dane są przechowywane w pamięci. Problem pojawia się dopiero wtedy, gdy będziemy chcieli wymieniać dane pomiędzy dwoma maszynami pracującymi w różnych trybach. Rozważmy co się stanie, gdy maszyna BIG pracująca w trybie big-endian prześle liczbę 80 (''0x0050'') do maszyny LITTLE pracującej w trybie little-endian. Maszyna BIG najpierw wysyła starszy bajt ''0x00'', a po nim młodszy bajt ''0x50''. Maszyna LITTLE odbiera pierwszy bajt ''0x00'' i traktuje go jako młodszy, a następnie drugi bajt ''0x50'' i traktuje go jako starszy. W końcowym rezultacie maszyna LITTLE interpretuje dane jako ''0x5000'', co daje liczbę 20 480.

Istnieją dwa sposoby rozwiązania tego problemu. Pierwszy sposób to ustalenie jednego trybu dla przesyłanych danych. Wtedy maszyny pracujące w przeciwnym trybie muszą dokonywać konwersji danych przed wysłaniem i po ich otrzymaniu. Drugi sposób to zezwolenie na przesyłanie danych w trybie w jakim pracuje dana maszyna z zastrzeżeniem, że do tych danych musi być dołączona informacja o tym jaki to jest tryb. Dzięki tej dodatkowej informacji odbiorca będzie wiedział czy musi wykonać konwersję do swojego trybu czy nie.

Twórcy protokołu IP wybrali pierwsze rozwiązanie. Dane numeryczne nagłówków tego protokołu (np. adres IP) muszą być wysyłane w trybie big-endian. W tym przypadku mówi się o sieciowej kolejności bajtów (ang. //network byte order//). To samo tyczy sie protokołów wyższej warstwy, czyli TCP i UDP. Numery portów muszą być przesyłane w trybie big-endian.

W najpopularniejszej implementacji protokołu TCP/IP, czyli w implementacji berkleyowskiej, isnieją cztery funkcje do konwersji liczb 16-bitowych i 32-bitowych pomiędzy sieciową kolejnością bajtów, a kolejnością bajtów maszyny (ang. //host byte order//):
<code c>
        #include <netinet/in.h>

        uint32_t htonl(uint32_t hostlong);
        uint16_t htons(uint16_t hostshort);
        uint32_t ntohl(uint32_t netlong);
        uint16_t ntohs(uint16_t netshort);
</code>

  * ''htonl'' (//host to network long//) -- host byte order -> network byte order, 32 bity
  * ''htons'' (//host to network short//) -- host byte order -> network byte order, 16 bitów
  * ''ntohl'' (//network to host long//) -- network byte order -> host byte order, 32 bity
  * ''ntohs'' (//network to host short//) -- network byte order -> host byte order, 16 bity

Powyższe funkcje należy użyć przygotowywując strukturę ''struct sockaddr_in'' dla funkcji ''bind()'' i ''connect()''. Pierwszy akapit tego rozdziału jest doskonałym przykładem dlaczego należy to zrobić.

Przypomnijmy, że w tym przykładzie maszyna BIG wysyłała dane do maszyny LITTLE. Z uwagi że //big-endian// jest równoważny z //network byte order//, rozważmy przykład odwrotny: maszyna LITTLE próbuje nawiązać połączenie TCP na port 80 maszyny BIG. Jak wiemy drugim polem w nagłówku TCP jest dwubajtowy port docelowy. Jeśli maszyna LITTLE wpisze tam liczbę 80 (''0x0050'') bez konwersji, to pierwszy bajt tego pola będzie miał wartość ''0x50'', a drugi ''0x00''. Taki nagłówek zostanie odebrany przez stos TCP/IP maszyny BIG. Stos TCP/IP zakłada, że dane zostały przysłane w trybie //big-endian//, więc z tych dwóch bajtów (''0x5000'') zbuduje liczbę 20 480. Następnie spróbuje przekazać pakiet do aplikacji nasłuchującej na porcie 20 480, co się oczywiście nie uda.

===== Dodatkowe informacje =====
  * http://en.wikipedia.org/wiki/Endianness -- o big-endian/little-endian
  * http://beej.us/guide/bgnet/output/htmlsingle/bgnet.html#structs -- big-endian/little-endian w programowaniu

===== Przykładowy program =====
<code c>
#include <stdio.h>

typedef union {
    char c[4];
    unsigned int i;
} U;

int main() {
    U u;

    u.i = 0x00010203;
    printf("u.c[0]=%d\n", u.c[0]);
    printf("u.c[1]=%d\n", u.c[1]);
    printf("u.c[2]=%d\n", u.c[2]);
    printf("u.c[3]=%d\n", u.c[3]);
}
</code>