アプリケーションデータのネットワークへの送信順

C言語でint型の整数値「1144201745」をメモリ上に格納すると、メモリの低位アドレスから高位アドレスに向かって4バイト分の領域が確保されます。ちなみに10進数の「1144201745」は16進数で表すと「0x44332211」です。メモリへの格納を考えると10進数で話をするより16進数で話をするほうがわかりやすいので「0x44332211」で話を進めます。

確保された4バイト分の領域ですがビットになおすと32ビット分です。このデータ値を構成するビット列のうち最上位にあるビットをMSB(Most Significant Bit)、最下位にあるビットをLSB(Least Significant Bit)と言います。なにが最上位でなにが最下位か(どちらが上でどちらが下か)というと、「0x44332211」を2進数に直したときに左側の桁(31ビット目の方)が最上位で、右側の桁(0ビットの方)が最下位になります。

MSB、LSBのことをバイトでの意味合いということで、(Most Significant Byte)、(Least Significant Byte)と書くときもあるようですが、この場合の捉え方は、MSBは最上位ビットが属するバイト、LSBは最下位ビットが属するバイト、と考えるべきと思います。

MSB、LSBをバイト単位で捉えるときの説明としてよく4バイトの領域をバイト単位で分割して左側を上位のバイト(MSB)、右側を下位のバイト(LSB)とするような説明を目にするのですが、そもそも4バイトで1つのデータを意味しており、それをバイト単位で「分割」してしまったらもとのデータの意味あいと変わってしまいます。それを上とか下とか言うのには違和感があって、MSB、LSBをバイト単位で捉えるときは、最上位ビット、最下位ビットそれぞれが属するバイトと考えるほうが納得がいきます。(これはあくまでも個人的な見解です。)

データは「バイト単位」にメモリに書き込まれるのですが、CPUの特性により格納方式が異なります。低位のアドレスから高位のアドレスに向かって、MSBからメモリに格納する方式をビッグエンディアン(big-endian)、LSBからメモリに格納する方式をリトルエンディアン(little-endian)と呼んでいます。「0x44332211」の場合、ビッグエンディアン、リトルエンディアンそれぞれの格納方法は以下のようになります。

メモリ上にビッグエンディアンで格納しようがリトルエンディアンで格納しようが、int型整数は4バイト単位でのアクセスとなるため、特に問題にはなりません。ただプログラムの実装にもよりますが、int型整数をchar型にキャストしてアクセスする場合は並び順が異なるためシステム環境(CPU特性)に左右されます。Aという機種でうまく動いていたものが、Bという機種でリコンパイルして動かしてみたら想定と違う、となることもありえます。

ネットワークへの送信についても同じで、ビッグエンディアンとリトルエンディアンとでは送信順が変わります。送信する際はメモリの低位アドレスのバイトから送信していきますが、ビッグエンディアンとリトルエンディアンではバイト単位で見ると並び順が異なりますので、送る側と受け取る側で並び順を決めておく必要があります。ネットワークへの送信にはビッグエンディアンを使うことになっていて、ネットワークバイトオーダーと呼ばれています。C言語ではネットワークバイトオーダーにあわせるためにlong型用にhtonl()、short型用にhtons()関数が用意されています。

ちなみにですが、big-endian、little-endianというのは「ガリバー旅行記」に出てきたものをマイクロプロセッサの世界で使っているみたいですね。

ネットワークへの送信はバイト単位で行われるのですが、バイトを構成するビットで考えた場合、一番最初に送信されるビットは最下位ビット(LSB)からとなります。

複数バイトのデータを送信する場合、低位アドレスの1バイト分がまず送信されるのですが、そのLSBが一番先にネットワークに流れます。1バイト(8ビット)が送られると次のバイトがネットワークに流れていきます。

イーサネットのMACアドレスは6バイト(48ビット)で構成されていて、前半の24ビットはOUI(Organizationally Unique Identifier)と呼ばれる機器ベンダー固有のコート値、後半24ビットがベンダーが自由に割り当てできる製品番号となっています。このMACアドレスですが、先頭1バイト目のLSBはI/Gビット(Individual/Groupビット)と呼ばれていて、これが「0」であればユニキャスト通信、「1」であればブロードキャストもしくはマルチキャスト通信を意味します。ネットワーク機器はI/Gビットを一番先に読み込んで、宛先の判断をしています。

なお、I/Gビットの左隣のビットはU/Lビット(Universal/Localビット)と呼ばれていて、これが「0」であれば世界で一意のグローバルアドレス、「1」であればローカルアドレスを意味しています。

PF_INETとAF_INETの微妙な違い

C言語についての話です。C言語でネットワークプログラムを作成するときにソケットを利用します。ソケットAPIを利用するときの書式は以下です。

int socket(int protocolFamily, int type, int protocol)

ソケットAPIは汎用的になるよう設計されたため、いくつかのプロトコルファミリーをサポートします。第一パラメータの protocolFamilyにはどのプロトコルファミリーを使用するのかを指定します。プロトコルファミリーには以下のようなものがあります。接頭辞のPFはプロトコルファミリーの略です。

PF_UNIX, PF_LOCAL・・・Unix上でのローカル通信
PF_INET・・・IPv4での通信
PF_INET6・・・IPv6での通信
PF_IPX・・・IPXでの通信
PF_APPLETALK・・・AppleTalkでの通信
PF_PACKET・・・データリンク層での通信

今から20〜30年ぐらい前はOSI参照モデルのネットワーク層としてIPのほかに、ノベル社のネットワークOSで使用されたIPX、AppleのMacintoshで使用されたAppleTalkなどがありました。PF_IPX や PF_APPLETALK はそれらのプロトコルを使用する際にソケットパラメータに指定しました。インターネットが普及してTCP/IPがネットワーク/トランスポート層のデファクトスタンダートになり、IPXやAppleTalkを見かけることは今はなくなってしまいました。

第二パラメータの type ですが、ここにはソケットの種類を指定します。

SOCK_STREAM・・・ストリームソケット
SOCK_DGRAM・・・データグラムソケット

ストリームソケットは信頼性があり双方向に接続されたバイトストリームでの通信の提供、データグラムソケットはベストエフォート型の固定最大長メッセージの通信を提供します。

第三パラメータの protocol ですが、エンドツーエンドで使用される固有のプロトコルを指定します。以下のものがあります。

IPPROTO_TCP・・・TCPでの通信
IPPROTO_UDP・・・UDPでの通信

第三パラメータにゼロを指定した場合はOSが自動的に適切なプロトコル(通常はストリームソケットの場合はIPPROTO_TCP、データグラムソケットの場合はIPPROTO_UDP)を設定してくれます。ただ、もともとのソケットの設計思想ではストリームソケット(もしくは、データグラムソケット)を実現するプロトコルは複数あってもよくて、そのなかのどれを使うかを第三パラメータに設定するという考えです。例えば、TCP以外で双方向で信頼性を保証する何か新しいプロトコルが登場した場合は、第二パラメータにSOCK_STREAM、第三パラメータに新しいプロトコルの設定、となります。

実際は、TCP、UDP以外に新しいプロトコルは出てきておらず、またインターネットのルーティングはIPで行われているためTCP/IP、UDP/IPとなり、ソケットのパラメータの組み合わせはほぼ決まっています。

PF_INET+SOCK_STREAM+IPPROTO_TCP・・TCP/IPv4
PF_INET+SOCK_DGRAM+IPPROTO_UDP・・UDP/IPv4

ここまでプロトコルの話をしたのですが、通信を行うためには相手のネットワークアドレスが必要です。C言語ではアドレスをsockaddr構造体に格納して利用します。sockaddr構造体は以下のようになっています。

struct sockaddr
{
 unsigned short sa_family;
 char sa_data[14];
}

sa_family にはアドレスファミリーの指定、sa_data には実際のアドレス情報の格納をします。プロトコルによってアドレッシングの方法は異なるので、sa_dataの使われ方もプロトコルごとに差異がでます。そのためプロトコルごとに使いやすくなるよう、それぞれのプロトコルに特化した構造体が用意されています。PF_UNIXの場合は sockaddr_un構造体、PF_INETの場合は sockaddr_in構造体、PF_INET6の場合は sockaddr_in6構造体です。以下にIPv4で利用されるsockaddr_in構造体を示します。

struct sockaddr_in
{
 unsigned short sin_family;
 unsigned short sin_port;
 struct in_addr sin_addr;
 char sin_zero[8];
}

sin_family にはアドレスファミリー(IPv4の場合は AF_INET)を指定し、sin_port と sin_addr にはそれぞれポート番号とIPアドレスを格納します。sin_zero[8]は未使用です。sockaddr_in構造体は汎用的なsockaddr構造体をIPv4用に再定義(sa_data[14] を sin_port、sin_addr、sin_zero[8] に分割)したものとなります。なお、AF_INETの接頭辞であるAFはアドレスファミリーの略です。

ここまでで、PF_INETはプロトコルファミリーを表し、AF_INETはアドレスファミリーを表すことを書いてみました。ここで重要なことは「プロトコルファミリーとアドレスファミリーをわけて考えている」ということです。あるプロトコルファミリーがあって、そのプロトコルファミリーのアドレス指定方法は1つでも、その後拡張されて複数になったとしても構わないということです。IPv4の場合は、XXX.XXX.XXX.XXXの32ビットで表す形式1種類(AF_INET)しかないのですが、それが別のアドレッシング方法が出来たとしても(その場合、そのアドレッシング方法に対して、AF_INET_2などとして)ソケットAPIは対応できるということです。

現状はIPv4に対して複数のアドレス指定方式はないので、PF_INET と AF_INET は同じ値で定義されています。ですので、プロトコルファミリーにAF_INETを指定しても問題なくプログラムは動くのですけれど。

2020/2/20追記
IPv4とIPv6は互換性がありません。なのでプロトコルファミリーは別々となっています。もしIPv6がIPv4と互換性があり、IPプロトコルのアドレス指定方法が32ビットだけでなく128ビットの指定方法でも機能したのであれば、PF_INETとAF_INET6の組み合わせも出来たんだろうと思います(そういうことだと思う)。