S-JIS[2006-05-07/2007-06-16]

ソケット通信

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ソケット

サーバータイプとクライアントタイプの両方のアプリケーションを作らないといけないなら、サーバータイプから作るべきだろう。
(クライアントタイプだけ先に作っても動かせないから。まぁサーバータイプだけ動かしても、待ってるだけであまり意味無いけど(苦笑))
でも仕組みはクライアントタイプの方が簡単。

TCPを使う場合は、通信の最初にコネクションの確立を行う必要がある。
サーバーでlistenaccept、クライアントで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すれば切断処理も行ってくれるのであまり気にしなくていいと思う。


TCPソケット(クライアントタイプ)

仕組みはサーバータイプより単純。

まずライブラリを用いてソケットを生成する。(ソケットの識別子あるいはインスタンスが返される。)
このソケットに対し相手先のIPアドレス・ポート(UNIXドメインの場合はソケット名)を指定してconnectしてサーバーに接続する。
あとはソケットを使って送受信するだけ。送信も受信も必要に応じて何回でもできる。(個別に電文が送信されても、受信側では区切りが分からないので注意)
最後に使い終わったソケットをクローズする。


TCPソケット(サーバータイプ)

仕組みはクライアントタイプより若干複雑。

  1. 複雑さの1番目は、待ち受け。
    まずはクライアントタイプと同じくライブラリを用いてソケットを生成する(ソケットの識別子あるいはインスタンスが返される。このソケットをリスニングソケットと呼ぶ)。

    そしてそのソケットに受信待ちのポート(UNIXドメインの場合はソケット名)を指定(bind)する。
    (待つ側なので、IPアドレスは特に指定しない(IPアドレスは自分のマシンしか有り得ないので)。もっとも、複数のIPアドレスを持ってる場合は「特定のIPアドレスだけ受け付ける」という意味で指定することも可能。)

    次にリスニング(listen:接続待ち)を開始するが、実際に受付待ちをするのはacceptと呼ばれる処理。
    acceptを呼び出すと、クライアントから接続される(か、指定した時間でタイムアウトする)まで制御が戻ってこない。
    クライアントから接続された場合、acceptはそのクライアントとの通信用に新しいソケット(これを接続済みソケットと呼ぶ)を返す。実際の送受信は、この接続済ソケットを使って行う。リスニングソケットを使って再度acceptを呼び出してやると、別の接続待ちに入る。
    つまり、listenはリスニングソケットに対して最初の1回しか行わないが、acceptは何度も行う。
     

  2. 複雑さの2番目が、accept接続済ソケットの処理の関係。
    接続済ソケットはコネクションが確立したソケットで、送受信処理を行う。acceptは別の接続を待つ。
    すなわち、これらは並行(マルチスレッド)で動かすことが可能。(非効率でいいか、あるいは1回処理したらアプリケーションを終了するなら、別にマルチスレッドにする必要は無い)
     
  3. ソケットのクローズは、接続済ソケットリスニングソケットのそれぞれに必要。
    1回の接続を終えるときは接続済ソケットをクローズする。リスニングソケットをクローズすると待ち受けが終了する。
    接続済ソケット少なくともクローズで暗黙に切断して終わるけど、リスニングソケット切断したことないな…どうなるんだろう?)

接続済ソケットマルチスレッドで扱う場合、一番簡単なのは接続1回につきスレッドを1つ新しく作って送受信を処理することだろう。
でもスレッドの新規作成にはそれなりにコストがかかるし、信頼性が高い必要のあるアプリケーションならスレッド作成失敗時の処理も考えなければならない。
スレッド数の上限を決めて初期処理でまとめてスレッドを用意したとすると、そのスレッドに接続済ソケットをどうやって渡すか考えなければならないし。acceptをどうやって処理するかを考えたりするのはけっこう大変。
この辺りは、会社ならまさに「技術の継承」で伝えていくテーマかと思う。


TCPソケット関連ちょいネタ

バックログ
listenには、バックログ数というパラメータが指定できる。
これは、サーバー側でacceptが呼び出されるまで ソケットライブラリの中で接続要求を保持する個数(listenキューの長さ)。定義はOSによって違うかも…
listenしていてもacceptしないでいると、クライアントがconnectしに行ったとき、 サーバー側のバックログ数までは「接続待ち」状態になる(そのままならconnectはいずれタイムアウトする)が、バックログ数を超えるとconnectは(待たずに)接続失敗になる。
 
ブロッキング [2006-05-16]
ソケット関数の説明を見ていると、「ブロッキング・非ブロッキング」とか「ブロックする/しない」という表現が出てくる。
「ブロックする」とは、その関数を呼び出すと、何か起きるまで制御が戻ってこないことを指す。
逆に「ブロックしない」場合は、結果がどうあれ すぐに関数呼び出しから戻ってくる。
ブロッキングするかどうかは、fcntl等のオプション設定で切り替えたり、ノンブロッキングのメソッドが用意されていたりする。
 
電文送受信待ち
電文が到着するまで(あるいは送信できるようになるまで)待つ為の関数がある。
何も起きずに指定時間が経過するとタイムアウトしてくれるので、非常に便利。
他に何も処理せずただ待つのであれば、わざわざ「マルチスレッドにして、あるスレッドは受信待ちし、別のスレッドでタイムアウトを監視する」なんて愚の骨頂な構成を採る必要は無い

電文の終了検知
ソケットライブラリは、受信した電文をバッファリングしている。
送信した側が何回かに分けてsendしたとしても一括してrecvすることが出来るし、逆に一回しかsendしていなくても分割してrecvすることも出来る。(ただし、分割してsendしたものを一括してrecvしようとしても、全部が到着していない場合は最初のいくつかが取れるだけであることは注意)
したがってアプリケーション側で何らかの終端を決めてやらない限り、一電文がどこまでなのかは把握できない。
終端は、以下のようなものが考えられる。
切断の種類
shutdownには、書込・読込・両方というオプションがある。
書込をshutdownすると、以降のsendはエラーになる。
読込をshutdownすると、以降のrecvはエラーになる。
書込シャットダウンはTCPレベルにも影響があるが、読込シャットダウンはTCPレベルには影響がない。読込シャットダウンには何の意味があるんだろう…?

ハーフコネクション状態
片方からの送信(書込)がシャットダウンされた状態をハーフコネクション状態という。
この状態でも、他方からの送信は問題なく行える。
つまり、「もうこれ以上送信しないよ!」というのは書込シャットダウンによって伝えられるが、「もうこれ以上受信しないよ!」というのを伝える方法はTCPレベルでは存在しない。

UDPソケット

UDPは一方的に電文を送信するだけなので、TCPのような接続(listen・accept・connect)といった処理は無い。
受信側はrecvfromで待っていて、送信側はsendtoで送るだけ。

ただし、(UNIXの)recvfromは受信待ちポート(UNIXドメインの場合はファイル名)を指定するように見えるが、そうではない。TCPのサーバータイプと同じように、ソケットに対してbindしておく必要がある。
(UNIXの)sendtoは、そのまま送信先を指定する。

UDPでは相手に届いたかのチェックは行わないので、電文が消失する可能性もあるし到着順序が入れ替わる可能性もある。
とは言うものの、UNIXドメインの場合は同一マシン内だしまず消失することは無いだろう。順序の方は、Solarisで2つの送信プロセスから1つの受信プロセスへ送ったときに入れ替わってたことがあったから、ちょっと怪しい…。


技術メモへ戻る
メールの送信先:ひしだま