Network byte order, big endian と littel endian


普段何も意識せずに使っている 4バイト整数の int 型ですが、 例えば int x = 0x7f000001 がどのようにメモリに格納されているか ご存じでしょうか? この変数xのアドレスを無理矢理キャスト変換して このxがあるメモリ領域を char 型の配列で見てみましょう。

unsinged int x = 0x7f000001; unsigned char* ptr = (unsinged char*) &x; printf("%02x,%02x,%02x,%02x\n", ptr[0], ptr[1], ptr[2], ptr[3] );
あなたのマシンではどのような結果が得られたでしょう。以下のようになりましたか?
7f,00,00,01
それとも以下のようになりましたか?
01,00,00,7f
前者の結果が出た人は、それはあなたのマシンのCPUが big endian と呼ばれる メモリの扱い方をしているからです。たぶんあなたのマシンは HP, Sun, IBM, SGI のワークステーションなのでしょう。

後者の結果が出た人は、それはあなたのマシンのCPUが little endian と呼ばれる メモリの扱い方をしているからです。たぶんあなたのマシンは Intel社製のCPUを 積んだパソコンか、DECのワークステーションなのでしょう。

int型などの複数バイト長の変数のメモリ上での取扱がCPUによって異ることは 普段はまったく意識しないのですが、異る種類のマシン間でデータを共有する ネットワークプログラムにおいては意識しなくてはなりません。 結論を言うと、4バイト長のIPアドレスと、2バイト長のポート番号は big endian でその数値を記述しなくてはなりません。このような決まりを network byte order と呼びます。この決まりにはたとえ同じ種類の マシン間で通信する場合でも従わなくてはなりません。

例えばIPアドレス "127.0.0.1" を long型変数に設定するには big endian のマシンでは

unsigned long ip = 0x7f000001;
とするのですが、little endianのマシンでは
unsigned long ip = 0x0100007f;
とします。しかしマシンによって整数定数の表現が変わるのはプログラマに とって大変迷惑です。そこで次のようにします。
unsigned long ip = htonl(0x7f000001);
htonl関数は long型の値を host でのメモリ扱い方法を network の世界での方法 (network byte order = big endian) に、必要があれば、変換します。 なので、これでどのマシンでも自動的に big endian として 変数に値を代入できます。

同様に short型の値であるポート番号は htons関数で big endian に統一します。

unsigned short port = htons(80);

逆に network order を host order に戻す ntohl, ntohs 関数もあります。

printf("IP = %08x PORT=%u", ntohl(ip), ntohs(port) );


IPやポートの値だけでなく、ネットワーク通信するデータの中身にも この endian の問題があります。異なるendianのマシン間で 2 bytes 以上の整数型、浮動小数点型のデータを 通信する際には、どちらかのendianにデータを統一するように変換しなくては なりません。htonlなどの関数はマシンがbig endianなら何もしないので、 byte orderを強制的にひっくり返すには自分でそのような関数を作成 しなくてはなりません。種々の基本型の byte orderをひっくり返す 関数 bswap は以下のようになります。

inline void bswap( short& d ){ short s=d; char *ps((char*)&s), *pd((char*)&d); pd[0] = ps[1]; pd[1] = ps[0]; } inline void bswap( int& d ){ int s=d; char *ps((char*)&s), *pd((char*)&d); pd[0] = ps[3]; pd[1] = ps[2]; pd[2] = ps[1]; pd[3] = ps[0]; } inline void bswap( long& d ){ long s=d; char *ps((char*)&s), *pd((char*)&d); pd[0] = ps[3]; pd[1] = ps[2]; pd[2] = ps[1]; pd[3] = ps[0]; } inline void bswap( float& d ){ float s=d; char *ps((char*)&s), *pd((char*)&d); pd[0] = ps[3]; pd[1] = ps[2]; pd[2] = ps[1]; pd[3] = ps[0]; } inline void bswap( double& d ){ double s=d; char *ps((char*)&s), *pd((char*)&d); pd[0] = ps[7]; pd[1] = ps[6]; pd[2] = ps[5]; pd[3] = ps[4]; pd[4] = ps[3]; pd[5] = ps[2]; pd[6] = ps[1]; pd[7] = ps[0]; }
Templateでまとめたほうが良い気もしますが、ちゃんと最適化されるのか心配なので これらを使うことにします。

なお、マシンの byte order を簡単に調べるには以下のようにするのが良いでしょう。

int n=1; if( *(char*)&n ) printf("Little endian\n"); else printf("Big endian\n");


ネットワーク通信でデータの中身自身の問題には byte order の問題だけではなく、構造体型変数を通信する際の 構造体の構造自身の問題もあります。構造体のメンバ変数 がメモリ上でその構造体変数の先頭から何byte目に配置されているのか(offset)に 注意しなくてはなりません。構造体メンバのoffsetは以下のようにして確認できます。

#define StructOffset(Object,Property) (&((Object*)NULL)->Property) struct Test { char c; int n; double d; }; printf("%d %d %d\n", StructOffset(Test,c), StructOffset(Test,n), StructOffset(Test,d) );
Intel系CPUではこの例の結果は 0 4 8 となり、 4バイトごとにメンバが 並んでいることがわかります。しかし他の種類のCPUでは 0, 8, 16 と 8バイトごとにメンバが並ぶこともあります。 この場合には各メンバを union を使って太らせなくてはなりません。
struct Test { union{ char c; double dummy1; }; union{ int n; double dummy2; }; double d; };

目次

Copyright(C) by Naoki Watanabe. Jan 1st, 2001.
渡辺尚貴 naoki@cms.phys.s.u-tokyo.ac.jp