TCP/IPやUDP/IPを用いた通信についての自分なりのまとめ。
TCPやUDP通信は、アプリケーションレベルではソケットと呼ばれる識別子(あるいはクラス)を用いた関数群(あるいはクラス群)を使って扱うことが多い。
TCPやUDPの約束事はソケットライブラリ(プロトコルスタック?OS?)が自動的にやってくれるので、細かいことはとりあえず気にしなくてよい。
プロトコル | 概要 | 備考 |
---|---|---|
TCP/IP | 信頼性が高い。 | 電文が相手に届いたかどうかチェックし、届いていなければ再送する。 到着した電文は、送信された順に並べ替えられる(到着順序が保証されている)。 そして、それらに失敗した場合はエラーになることが保証されている。 これらはTCPのソケットライブラリが内部で行ってくれる。 |
UDP/IP | 基礎的な通信。 | 電文は投げっぱなしで、相手に届いたかどうかのチェック等はしない。 (エラーの検知はしない。変な受信データもただ破棄されるのみ) その分単純で高速。 |
ソケットにはINETドメインソケットとUNIXドメインソケットがあるが、UNIXドメインソケットは名前の通りUNIXでしか使えない。
ドメインの違いは、ソケットの使用法から見ればソケットの作成方法の違いだけ。
一旦ソケットを作成してしまえば、後はTCP,UDPの作法に従って扱う。
ドメイン | 概要 | 備考 |
---|---|---|
INETドメイン | インターネットのIPアドレスとポートを使用。 | |
UNIXドメイン | UNIXのファイル名を使用。 | UNIXのファイルシステムを介した入出力と思われる。 Solarisでは、UNIXドメインよりINETドメインの方が通信速度が速いらしい。 |
通信の手順の概要は、クライアントタイプとサーバータイプで若干異なる。
違うのは、通信のきっかけ。サーバータイプはクライアントからの通信を待つので常駐型になるのに対し、クライアントは必要なときだけサーバーへつなぎに行く。
TCPでは通信開始に先立ってコネクションを確立し、お互いに好きなときに送受信できる。とは言っても、アプリケーションの都合によってどんな電文をどんな順序で送受信するかは決
まるだろう。
UDPにはコネクションといったものは無く(コネクションレス)、一方的に送信するだけ。UDPを使って送受信しようと思ったら、お互いがサーバーのようにソケットを用意する必要がある。
TCP/IP | UDP/IP | 備考 | |||||||
---|---|---|---|---|---|---|---|---|---|
クライアント | サーバー | クライアント | サーバー | ||||||
1 | 初期化 | socket | 初期化 | socket | 初期化 | socket | 初期化 | socket | ソケットを生成する。 |
2 | 待ち受けポートを指定 | bind | 待ち受けポートを指定 | bind | UNIXドメインの場合はファイル名(ソケット名)を指定する。 | ||||
3 | 待ち受け開始 | listen | |||||||
4 | 接続待ち | accept | →受信待ち | recvfrom | |||||
5 | 接続 | connect | |||||||
6 | 送信→ | send | 送信→ | sendto | |||||
7 | →受信 | recv | |||||||
8 | ←送信 | send | |||||||
9 | 受信← | recv | |||||||
10 | 切断 | shutdown | 切断 | shutdown | たいてい省略する。closeでやってくれるし。 | ||||
11 | 終了 | close | 終了 | close | 終了 | close | 終了 | close | ソケットを破棄する。 |
サーバータイプとクライアントタイプの両方のアプリケーションを作らないといけないなら、サーバータイプから作るべきだろう。
(クライアントタイプだけ先に作っても動かせないから。まぁサーバータイプだけ動かしても、待ってるだけであまり意味無いけど(苦笑))
でも仕組みはクライアントタイプの方が簡単。
TCPを使う場合は、通信の最初にコネクションの確立を行う必要がある。
サーバーでlisten・accept、クライアントでconnectが成功すればコネクションが確立したことになる。
どのポート番号を使うかについては、サーバー側はアプリケーションの作成者が決める必要がある。[/2007-06-16]
クライアント側のポート番号は、ソケットライブラリがそのマシンで使っていない番号を自動的に割り振ってくれるので、気にしなくてよい。
IANAの基準では、1〜1023は「よく知られたポート(well known port)」、1024〜49151(bfffh)は「予約済みポート(registered port)」。IANAがポート番号を管理しているので、そこへ申請すれば自分のポートも世界的に有名になれる(笑)
で、1024〜5000をクライアントのポート(エフェメラルポート(短命なポート))として使うOSも多いらしい。FreeBSDでは49152(c000h)〜65535が使われたりするそうだが。
自分のローカル環境でサーバーを試したいなら、65535以下でそれに近い番号を使っておけばいいと思われる。
(参考:Winsock
Programmer's FAQ)
コネクションが確立すると、お互いに送受信ができるようになる。
これはつまり、どちらが先に送信するかはアプリケーションが決めるということ。
普通の考えならクライアントから要求を送信してサーバーで受信して処理し、応答を返す(サーバーから送信してクライアントが受信する)ことになると思うが、接続直後にサーバーからクライアントへ初期情報を送信するような作りでも別に構わない。
通信を終了する際には、コネクションの切断を行う。
これはshutdownを使って明示的に行うこともできるが、ソケットをcloseすれば切断処理も行ってくれるのであまり気にしなくていいと思う。
仕組みはサーバータイプより単純。
まずライブラリを用いてソケットを生成する。(ソケットの識別子あるいはインスタンスが返される。)
このソケットに対し相手先のIPアドレス・ポート(UNIXドメインの場合はソケット名)を指定してconnectしてサーバーに接続する。
あとはソケットを使って送受信するだけ。送信も受信も必要に応じて何回でもできる。(個別に電文が送信されても、受信側では区切りが分からないので注意)
最後に使い終わったソケットをクローズする。
仕組みはクライアントタイプより若干複雑。
そしてそのソケットに受信待ちのポート(UNIXドメインの場合はソケット名)を指定(bind)する。
(待つ側なので、IPアドレスは特に指定しない(IPアドレスは自分のマシンしか有り得ないので)。もっとも、複数のIPアドレスを持ってる場合は「特定のIPアドレスだけ受け付ける」という意味で指定することも可能。)
次にリスニング(listen:接続待ち)を開始するが、実際に受付待ちをするのはacceptと呼ばれる処理。
acceptを呼び出すと、クライアントから接続される(か、指定した時間でタイムアウトする)まで制御が戻ってこない。
クライアントから接続された場合、acceptはそのクライアントとの通信用に新しいソケット(これを接続済みソケットと呼ぶ)を返す。実際の送受信は、この接続済ソケットを使って行う。リスニングソケットを使って再度acceptを呼び出してやると、別の接続待ちに入る。
つまり、listenはリスニングソケットに対して最初の1回しか行わないが、acceptは何度も行う。
接続済ソケットをマルチスレッドで扱う場合、一番簡単なのは接続1回につきスレッドを1つ新しく作って送受信を処理することだろう。
でもスレッドの新規作成にはそれなりにコストがかかるし、信頼性が高い必要のあるアプリケーションならスレッド作成失敗時の処理も考えなければならない。
スレッド数の上限を決めて初期処理でまとめてスレッドを用意したとすると、そのスレッドに接続済ソケットをどうやって渡すか考えなければならないし。acceptをどうやって処理するかを考えたりするのはけっこう大変。
この辺りは、会社ならまさに「技術の継承」で伝えていくテーマかと思う。
UDPは一方的に電文を送信するだけなので、TCPのような接続(listen・accept・connect)といった処理は無い。
受信側はrecvfromで待っていて、送信側はsendtoで送るだけ。
ただし、(UNIXの)recvfromは受信待ちポート(UNIXドメインの場合はファイル名)を指定するように見えるが、そうではない。TCPのサーバータイプと同じように、ソケットに対してbindしておく必要がある。
(UNIXの)sendtoは、そのまま送信先を指定する。
UDPでは相手に届いたかのチェックは行わないので、電文が消失する可能性もあるし到着順序が入れ替わる可能性もある。
とは言うものの、UNIXドメインの場合は同一マシン内だしまず消失することは無いだろう。順序の方は、Solarisで2つの送信プロセスから1つの受信プロセスへ送ったときに入れ替わってたことがあったから、ちょっと怪しい…。