現在の多くのネットワークプログラムは、サーバーとクライアントで通信をやり取りします。実際、ネットワークにはサーバーやクライアントと呼ばれるコンピュータだけではなく、データを配送するハブやルーターと呼ばれるネットワーク機器が接続されています。
- サーバー: ネットワークに接続されたコンピュータに対して、サービスを提供するソフトウェアまたはハードウェアのこと
- クライアント: ネットワークに接続され、サーバーからサービスを受ける側のソフトウェアまたはハードウェアのこと
通常ソフトウェアがネットワーク上で通信を行う場合は、クライアントとサーバー間でのデータのやり取りにTCPやUDPのいずれかのプロトコルが用いられる。
- TCPは長いデータを分割してやり取りするため、分割したものを受け取った側で元に戻す仕組みがあります。また、データを運ぶ途中でエラーが起こったとしても、自動的にそれを修復したり再送したりする機能を持ちます。
- UDPはエラーの回復機能を持たない簡便なプロトコル。そのため、通信の処理を行うプロセッサの負担が軽く、ネットワークを流れるデータ量が少ないため、動作が速くなる。
UDPを用いた場合におけるクライアントとサーバー間のデータのやり取りは、次の4つの動作によって実現化される。
- 名前解決
- ソケット・オブジェクトの作成
- データの送信
- レスポンスデータの受信
データの送信相手のサーバー名は、プログラムの引数などで与えられますが、実際に通信をするためにはIPアドレスが必要。
なぜならば、ネットワーク中に存在するルーターやスイッチなどのネットワーク機器はサーバー名を理解できず、IPアドレスに従ってパケットを運ぶように作られているから。
- つまり、IPアドレスを用いた方がネットワーク機器にとって中継処理時の負荷が軽くて済む。
- しかし、人にとってはIPアドレスよりサーバー名の方が覚えやすいため、プログラムの引数にサーバー名を指定することも許容している。
そこで、通信プログラムはサーバー名からIPアドレスを検索する。これを名前解決をいう。
--名前解決の仕組み--
- ファイルに登録する方式
-
ファイルに登録する方式では、名前とアドレスのセットをファイルに保存する方式。
- TCP/IP系のネットワークでは*hostsファイル*を使用する。サーバー名とIPアドレスのセットが記録されている。
- メリット: hostsファイルは単純なテキストファイルなので、簡単に変更を加えることができる
- デメリット: ネットワークに接続するすべてのサーバーの情報を記述するわけにはいかないので、接続台数が多く頻繁に追加・変更が起こる場合にはhostsファイルによる名前解決には向いていない。
- 全員に問い合わせる方式
-
LANではブロードキャストに送信することで、全コンピュータに宛てて問い合わせできます。対象となるホストが、アドレス情報を含んだ応答を返してくるのを期待する方式。
- ブロードキャストを用いた名前解決を実現するには、まず名前登録という作業が必要になる。
- 作業①: 名前を付ける場合に、すでにネットワーク上に同じ名前があっては困るので、自分の名前をブロードキャストでネットワーク内の全員に通知する。同じ名前を持つコンピュータは、名前の重複を返してくれるようにしておきます。一方、重複していなければ、何も返さないようにします。
- 作業②:名前を付けたいコンピュータが全員に通知して、しばらく待機しても重複を知らせる返信がなければ、その名前は使用可能と判断して、自分の名前とする。これが名前登録になる。
- メリット: 各PCがhostsファイルに類似するものを保持しなくてもよい
- デメリット: 大規模なネットワークでは全員に問い合わせる方式は用いられません。なぜならば、ブロードキャストルーターを超えることができないから。そのため、この方式はインターネットでは使用しません。
- サーバーに問い合わせる方式
-
サーバーに名前とアドレスの情報をあらかじめ登録しておき、そのサーバーに問い合わせすることで指定した名前に対応するアドレスを教えてもらう方式。
TCP/IP系のネットワークではDNS(Domain Name System)が使用されます。DNSサーバーに情報を登録しておき、そこに問い合わせを行います。
名前解決の使い方はアプリケーション側ではなくOS側が担当する。
UNIXにおける名前解決の優先順位は、OSやバージョンによって異なるが、一般には次の優先順位が多い傾向にある。
- hostsファイルで名前解決する
- DNSサーバーに問い合わせる
- NISサーバーに問い合わせる
※Linuxであれば、`/etc/host.conf`ファイルで優先順位を変更できる。
OSは通信状態を管理するためのテーブルが持っている。そのテーブルには次のような項目がある。
通信しているプログラム名 |
通信元(クライアント)のポート番号 |
通信先(サーバ)のIPアドレス |
通信先(サーバ)のポート番号 |
プロトコルの種類(TCP/UDP) |
通信状態 |
hogehoge |
hogehoge |
hogehoge |
hogehoge |
hogehoge |
hogehoge |
- この表の1レコードが1つのソケットに対応している。ソケットは端末内に置けるエンドポイントであり、電気器具でソケットにつなぐイメージとなる。
- ただし、
通信状態
はTCPのみで使用する。また、UDPでソケットを生成したときは、通信先のIPアドレス
やポート番号
は指定されない。つまり、空のソケットを作成すると同時に、OSが送信元のポート番号を自動的に割り当て、更新してくれる。
- ソケットが存在しないと、ネットワークからパケットが届いたときに、OSはそれをどのアプリケーションに渡したらよいのかが判断できない。つまり、このようなことがないように、通信前に必ずソケットを作る決まりになっている。
- ソケットはパケットの受信時だけでなく、送信時やエラー検出時にも使用される。
送信用に送信バッファを確保して、そこに送信データを格納する。
送信動作を実際に行うのはOSの役目であり、プログラム側はOSに送信依頼を出すだけ。その際、送信先の情報としてIPアドレスとポート番号を指定する。IPアドレスは名前解決ですでに判明しており、ポート番号はサービスごとに決めっている。
- 送信内容のチェック
-
- OSは送信依頼を受けたならば、依頼内容に問題がないことを確認する。
- ソケットが存在するか、送信データを格納するバッファの長さが適切かどうかを調べる。
- 経路探索
-
問題がなければ、ルーティングテーブルを参照して、通信相手のIPアドレスから、そのパケットを送信する相手を割り出す。なぜならば、「通信相手=パケットを送信する相手」とは限らないから。
- MACアドレスの特定
-
パケットを渡す相手のIPアドレスが決まったら、IPアドレスに対応するMACアドレスを割り出す。
その際、ARPキャッシュでIPアドレスを検索する。該当するIPアドレスが見つかったらならば、対応するMACアドレスを採用する。もし見つからなければ、ARPプロトコルを用いる。ARPとは、IPアドレスからMACアドレスを特定するためのプロトコル。
ARP問い合わせを送信する。それに対してARP応答があれば、格納されているMACアドレスを採用する。さらに、そのMACアドレスをARPキャッシュに登録する。
- パケットの作成
-
これで、送信先のIPアドレスとMACアドレス、送信データが揃った。
送信先のIPアドレスとMACアドレスから、Ethernetヘッダを作成する。その後ろに送信データを配置する。
UDPでは1パケットが最大64Kバイトのデータと決められている。送信データがこれより大きい場合、分割する。このとき、ヘッダ部分にデータの通し番号を追加する。これをフラグメンテーションと呼ぶ。
受信側はこの通し番号により、元の状態に組み立てることができるわけです。これをリアセンブリングと呼ぶ。
- パケットの送信
-
作成したパケットはLANドライバに渡す。
LANドライバ: NICを制御するためのソフトウェア。
- LANドライバは受け取ったパケットをNIC内部にあるメモリにコピーする。
- NIC上にあるLANコントローラチップに送信コマンドを送る
- LANコントローラがLANの状態を確認する。送信可能であれば、パケットを先頭から順番に電気や光のアナログ信号に変換して、ケーブルを通じて送り出す。
以上がクライアントのパケットの送信動作
受信側(サーバー側)がレスポンスデータを受信したとする。
- ケーブルを通じて電気や光の信号を受信する
- NICのLANコントローラチップは信号を受けったならば、信号の先頭から順番にデジタルデータに変換する
- ヘッダに記載された宛先MACアドレスを調べる
- 宛先MACアドレスが自分のそれを一致する場合: 自分宛のパケットと判断し、それ以降のデジタルデータに変換する作業をそのまま続行する
- 自分宛のパケットではない場合: 受信動作とデジタルデータの変換処理を中止し、残りの信号を無視する
- ヘッダに通し番号があれば、リアセンブリングで元のデータに復元する
- 元に戻すためのパケットがすべて揃っていない場合は、受信エラーとなる
- デジタルデータに変換したならばNIC内のメモリに格納する
- メモリにパケットを格納し終えたら、CPUに通知する。CPUはこのとき別の仕事をしている可能性が高く、LANコントローラチップの動作を常に監視しているわけではない。そこでCPUに割り込みを行い、パケットの到着を知らせる
- パケットの通知を受けたOSは、パケットのヘッダの正当性を確認する
-
ヘッダのフォーマット、宛先IPアドレスに問題がないことを確認する。もし問題がなければ、ヘッダにある宛先ポート番号をソケットの表で検索する。
- ソケットの表にポート番号が見つからない場合: 宛先が不明として認識され、発信元にそれを知らせる。
- ソケットの表にポート番号が見つかった場合: そこにプログラム名が記載されているので、そのプログラムに受信したデータを渡す
- プログラム側はデータを送信してから、その応答が返ってくるまで待機する。応答が返ってくれば、次の行に制御を移ります。
しかし、いつまで経っても応答が返ってこない場合もある。それでは困る場合は、ソケットの待ち時間を設定しておくのが一般的。
- 切断動作
- プログラムを終了する場合などのように、もう通信をする必要がなくなったら、ソケットディスクリプタを閉じる。これにより、ソケットの表からソケットの行が削除される。
UDPは1つのパケットで64Kバイトまでのデータを運ぶことができ、大きなデータであれば分割して送信できる。しかし、分割した結果の1つでも送信先に届かない場合は、すべてを送り直す必要がある。
一方、TCPでは分割したデータの一部が送信先い届かない場合は、その一部だけを送り直すことができる。よって、巨大なデータの場合、すなわち分割数が多い場合は、UDPは非効率であるため、一般にTCPが採用される。
TCPを用いたクライアントとサーバー間のデータのやり取りは、次の4つの動作によって実現化される
- 名前解決
- ソケット・オブジェクトの作成
- データの送信
- レスポンスデータの受信
この処理はUDPクライアントにおける名前解決とまったく同じ動作をする。
ソケットのテーブルには次のような項目があることをUDPで述べた
通信しているプログラム名 |
通信元(クライアント)のポート番号 |
通信先(サーバ)のIPアドレス |
通信先(サーバ)のポート番号 |
プロトコルの種類(TCP/UDP) |
通信状態 |
hogehoge |
hogehoge |
hogehoge |
hogehoge |
hogehoge |
hogehoge |
TCPではパケットが相手に届いたことを確認する。確認した結果、届いていなければ送信し直す。つまり、送ったパケットを一時的に保存しておかなければならない。しかし、コンピュータは同時に様々な通信を行っており、それらをすべて保存しておくならば、工夫して管理しておかないと仕組みが複雑になる。そこで、TCPではソケットごとに通信状態
も保持する
UDP |
TCP |
UDPでは接続という概念はなく、パケットを送信するだけ。つまり、UDPではソケットを生成するときに、接続相手を指定しない。 |
TCPで通信先のIPアドレスとポート番号を指定して、ソケットを生成する。IPアドレスは名前解決ですでに判明しており、ポート番号はサービスごとに決まっている。通信後、パケットの送受信の状態によって、ソケットの通信状態を更新していく |
送信用に送信バッファを確保して、そこに送信データを格納する。
送信動作を実際に行うのはOSの役目であり、プログラム側はOSに送信依頼を出すだけです。
詳細は「ハッカーの学校」 P.52を参照
データは分割して送信され、相手がどこまで受信したかにより再送するものが変わってくる。
そこで、シーケンス番号とACK番号を用いて、受信側がどこまで受信したかを送信側が把握する。
- シーケンス番号: 送信するデータの連番
- ACK番号: 受信したデータの連番
- パケットを送信するときに、ヘッダ部にシーケンス番号を含めておく。これにより受信側は「何バイト目以降のデータを送っている」ことを知る。
- 受信側は「受け取ったシーケンス番号+受信データのバイト数」をACK番号として、応答を返す。
これにより、送信側は「受信側が何バイト目までデータを受信している」ということを知る
受信側から来たACK番号が、送信したデータよりも小さければ、その番号以降のデータは相手に届いていないと判断し、再配送する。
- 切断動作
-
プログラムを終了する場合などのように、もう通信する必要がなくなったら、切断動作を行うことを受信側に通知する。
このタイミングで、受信側はまだデータ送信の途中の可能性もあります。すでに送信動作に入ったものは送信しますが、それ以外のものについては停止します。
サーバー: クライアントに対してサービスを提供する側
- 接続動作のパケットの到着を待機する
-
一般的にはクライアントからのリクエストに対して、適切な処理をしてレスポンスを返す。つまり、TCPであれば、クライアントは接続する側(接続動作を行う側)、サーバーは接続される側になる。
クライアント(接続する側)は、ソケットを生成したならば、接続動作のパケットを送信する。一方、サーバー(接続される側)では、ソケットを生成したならば、接続動作のパケットが到着するのを待たなければならない。
接続動作が終われば、その後はデータの送受信になり、クライアントとサーバーは同様の制御を行う。
- 複数のリクエストに対する処理
-
サーバーでは複数のクライアントを相手にする。
最初に接続動作をしたクライアントの通信が終わるまで、次の接続動作をしたクライアントを待たせてしまうわけにはいかない。
そこで、マルチスレッドにより、複数のクライアントのリクエストを並行処理する。一般的には、クライアントから接続動作が来たら、そのクライアントの処理を担当するスレッドを新たに生成する。OSの管理情報に対応するソケットを渡す。これで、生成したスレッドがクライアントと通信できるようになる。