そこで今回の記事では、ソケットを使ったTCP/IPプログラミングを解説します。ソケットは、インターネットを根幹から支える技術でありながら、非常にシンプルで汎用性も高いという特徴を持っています。
ソケットの基礎知識
特徴と用途
ソケットを使えば、2台のコンピュータをOS、CPUに関係なく接続し、データをやりとりさせることができます。しかも、この1対1の接続を、ひとつのコンピュータが同時に65536個作ることができるのです。クライアント・サーバモデルに適した仕様と言えるでしょう。ただし、接続は必ず1対1であることに注意が必要です。同じデータを複数のコンピュータに送りたい場合でも、各接続毎に一回ずつ発信する必要があります。例外はありますが、マルチキャスト、ブロードキャストは基本的にできません。
この様な特徴を活かし、ソケットは、WWWサーバやFTPサーバ、メールやニュース、ネットワークファイルシステムや様々な対戦ゲームなどに、用いられています。
IPアドレスとコンピュータの名前
Netscapeなどを使っていると、www.komaba.ecc.u-tokyo.ac.jpという文字列をよく見ると思います。これは、駒場情報教育棟のwwwという名前のコンピュータを意味しています。このように、ネットワークに繋がっているコンピュータには、名前が付けられています。
一方で、ネットワーク上のコンピュータを一意に識別するためには、 IP(Internet Protocol)アドレスというものが用いられています。現在のインターネットの規約であるIP Ver.4では、IPアドレスは32ビットの値です。テキストで表す場合は、123.45.67.89の様に、1バイトごとにピリオドで区切って書くことになっています。同じIPアドレスを持つコンピュータが、ネットワーク上に複数存在することは、絶対に許されません。世界中でこの約束は守られる必要があります。そこでIPアドレスの割り当ては、日本ではJPNIC (Japan Network Information Center)という組織が管理しています。
このような事情のため、ソケットを使うプログラムは、コンピュータの名前を、 IPアドレスに変換する作業が必要です。無駄に見えるこの仕組みには、ちゃんと理由があります。1つのIPアドレスを持つコンピュータに、複数の名前を持たせることができるのです。
ポート番号
IPアドレスだけを使ってコンピュータ同士の接続を管理してしまうと、同時にひとつの接続しか張れないことになります。ソケットでは、
16ビット値のポート番号という概念を導入して、複数の同時接続を可能にしています。例えば、ある接続はコンピュータAの2345番ポートとコンピュータBの3456番ポート、別の接続はコンピュータAの4567番ポートとコンピュータCの5678番ポート、という具合です。
1023番以下のポートは特別なポートとして予約されています。 HTTP(Hyper-Text Transfer Protocol)は80番、 SMTP(Simple Mail Transfer Protocol)は25番、 FTP(File Transfer Protocol)は20番と21番、 TELNETは23番、などです。1024番以降は自由に使えます。例えばitalkサーバは12345番のボートを使用しています。
クライアントプログラムが、サーバプログラムと接続を確立するまでの流れを、簡単に説明しておきましょう。例としてWWWブラウザを考えます。ブラウザはまず、ブラウザが動いているコンピュータの空いているポートと、ホームページを管理しているコンピュータの 80番ポートとの、接続要求を出します。HTTPサーバは常に80番ポートを監視していて、接続要求が来ると、要求元と空いているポート(ここが肝心) を使って接続します。HTTPサーバはこの接続を使ってホームページを送信すると同時に、再び暇になった80番ポートの監視を続けます。
TCPとUDP
ソケットにはTCP(Transfer Control Protocol)とUDP
(User Datagram Protocol)の2種類があります。
TCPはストリーム型の接続で、送ったデータが送った順番に届きます。ネットワークを通過中にデータが壊れたり無くなったりした場合は、再送信をシステムが自動的に行います。大抵のプログラムは TCPを使います。
UDPはデータグラム型の接続で、パケット単位にデータを送ります。送信順に届くとは限らず、パケットが抜け落ちる可能性すらあります。最新のデータを維持すればよいようなプログラムが、UDPを用います。
ネットワークバイトオーダ
ソケットはマルチプラットフォームであるため、データのエンディアンを統一する必要があります。ネットワークで使われるバイトオーダは、ビッグエンディアンです。
エンディアンとは何か、一応説明しておきましょう。十進数の12345を十六進数で表記すると3039になります。これをメモリに格納するときは、 30と39に分けるのですが、どちらのバイトを低いアドレスに入れるか問題になります。39を先に入れる方式をリトルエンディアン、 30を先に入れる方式をビッグエンディアンと言います。どちらの方が優れているとは一概に言えず、CPUの種類によりばらばらに採用されています。 Intelのx86系CPUはリトルエンディアンで、情報教育棟にあるSunの Sparcはビッグエンディアンです。
ソケットの使い方
UNIX系OSではsys/types.hとsys/socket.h、netinet/in.h、arpa/inet.h、netdb.hを
includeして下さい。
Windowsではwinsock.hをincludeし、wsock32.libをリンクして下さい。
サーバがクライアントとTCP接続する手順
1. ソケットを得る
まず、接続要求を受け付けるためのソケットを確保します。 socket()を使います。
int socket(int af, int type, int protocol);
af | 必ずAF_INET |
type | SOCK_STREAMと書く。(UDPではSOCK_DGRAM) |
protocol | 0に固定。 |
戻り値がUNIXではソケットのファイルディスクリプタ、 Windowsではソケットハンドラです。ソケットの確保に失敗した場合、値が-1になります。
2. ポート番号を指定する
監視するポート番号をbind()で指定します。
int bind(int s, const struct sockaddr *name, int namelen);
s | 接続要求を受け付けるソケット |
name | sockaddr_in構造体のポインタをsockaddr*にキャストして渡す。 |
namelen | sizeof(sockaddr_in)を指定。 |
sockaddr_in構造体は以下のような形をしています。
struct sockaddr_in{ short sin_family; // AF_INETを代入 u_short sin_port; // ポート番号(ネットワークバイトオーダで!) u_long sin_addr.s_addr; // INADDR_ANYを代入 }
コメントに従って値を代入し、bind()の引数として下さい。関数htons()で、16ビット値をネットワークバイトオーダに変換できます。 htons(12345)の様に使えば、CPUのバイトオーダに依存しないソースが書けます。
bind()は失敗すると-1を返します。すでに他のプログラムなどが、指定のポートを使用している場合に起きます。これを利用して、同種のサーバが複数起動されるのを防ぐことができます。
3. ソケットを接続要求の監視モードにする
listen()を使います。
int listen(int s, int backlog);
s | 接続要求を受け付けるソケット |
backlog | 接続要求キューのサイズ |
一度に複数の接続要求が来ると、それらはキューにためられます。指定できるキューの大きさの最大値はSOMAXCONNです。この値は5という小さな値になっている場合がほとんどなので、サーバを設計するときは留意して下さい。また、次のaccept()によって接続が確立されれば、要求はキューから外されるので、backlogはクライアントの最大数ではありません。誤解しないで下さい。
4. accept()で接続要求を受理する。
accept()で、接続要求して来たクライアントとの接続を確立します。接続要求のない状態でこの関数を実行すると、要求が来るまで、そのプログラムはaccept()の内部でsleep状態になります。処理は戻って来ません。これをブロッキングと言います。
接続要求が来ているか調べる方法は、後のselect()のところで解説します。
int accept(int s, const struct sockaddr *addr, int *addrlen);
s | 接続要求を受け付けたソケット |
addr | sockaddr_in構造体のポインタをsockaddr*にキャストして渡す。 |
addrlen | *addrlenはsizeof(sockaddr_in)で初期化。 |
返値がクライアントと接続されたソケットです。このソケットは自動的に新しく確保されたもので、sとは違います。返値のソケットでクライアントとの通信を行い、sは引き続き接続要求の監視に当たります。
失敗した場合は値が-1になりますが、接続要求がないと言う意味ではありません。成功した場合、addrにはクライアントの情報が格納され、addrlenにはaddrの実際に有効なサイズが代入されます。
クライアントがサーバとTCP接続する手順
1. ソケットを得る
サーバ側と全く同じです。ソケットの接続を始める前に、コンピュータの名前からIPアドレスを調べる作業が、大抵のプログラムには必要なはずです。その方法は後述します。
2. サーバに接続する。
connect()を使います。
int connect(int s, const struct sockaddr *name, int namelen);
s | 接続するソケット |
name | sockaddr_in構造体のポインタをsockaddr*にキャストして渡す。 |
namelen | sizeof(sockaddr_in)を指定。 |
sockaddr_in構造体は以下のような初期化して下さい。
struct sockaddr_in{ short sin_family; // AF_INETを代入 u_short sin_port; // ポート番号(ネットワークバイトオーダで!) u_long sin_addr.s_addr; // 接続相手のIPアドレス }
ポート番号をネットワークバイトオーダへ変換するには、htons()を使って下さい。 IPアドレスは、最初からネットワークバイトオーダで与えられるはずなので、変換の必要はありません。もし必要な場合は、htonl()を使います。
返値は、接続に成功すれば0、失敗の場合は-1です。
TCP接続後のデータの受送信
接続完了後は、データを受送信する方法に、サーバとクライアントの区別はありません。
データの送信
send()を使います。
int send(int s, const char *buf, int len, int flags);
s | 接続してあるソケット |
buf | 送信すべきデータ |
len | 送信するデータのサイズ |
flags | 通常は0を指定 |
返値は実際に送信されたデータのバイト数です。lenより小さい場合があります。送信するデータは一度システムのバッファに蓄えられます。このバッファにはサイズに限界があり、満杯になると lenより小さい値が戻ってくるのです。
もし、システムのバッファがすでに満杯だった場合は、少しでも空きができるまで、send()内でブロックされてしまいます。
これらに対処するため、プログラムのデータ送信部分はやや複雑になるでしょう。システムのバッファに空きがあるかどうか調べる方法は、後のselect()のところで解説します。
送信がネットワークのエラーなどで失敗した場合、-1が返ってきます。 UNIX系OSでは、-1が与えられる前にシグナルSIGPIPEが発生します。
send()から処理が戻ってきても、クライアントプログラムがデータを受信したとは限りません。クライアント側のコンピュータの受信バッファに、データが保存されただけで、次に説明するrecv()は、まだ実行されていないかもしれません。
データの受信
recv()を使います。
int recv(int s, char *buf, int len, int flags);
s | 接続してあるソケット |
buf | 受信したデータの格納先 |
len | bufに格納可能なサイズ |
flags | 通常は0を指定 |
返値は受信したデータのバイト数です。データを1バイトも受信できない場合、 recv()内でプログラムはブロックされます。受信可能なデータがあるか調べる方法は、後のselect()のところで解説します。
接続が相手側によって切断された場合は、返値が0になります。
TCP接続の切断
UNIX系の場合はclose()、Windows系の場合はclosesocket()を使います。引数はソケットただ一つです。
接続相手は、recv()の返値が0になることによって、切断を検知できます。
UDPで通信する手順
UDP接続では、クライアントとサーバの区別はありません。また、接続の確立・切断の概念もありません。
1. ソケットを確保する
socket()の第2引数にSOCK_DGRAMを指定して下さい。他はTCPと同じです。
2. ポート番号の指定
bind()を使います。方法はTCPのサーバと全く同じです。
3. データの送信
sendto()を使います。
int sendto(int s, const char *buf, int len, int flags, const struct sockaddr *to, int tolen);
s | bind()してあるソケット |
buf | 送信すべきデータ |
len | 送信するデータのサイズ |
flags | 通常は0を指定 |
to | 送信先を表すsockaddr_in構造体 |
tolen | sizeof(sockaddr_in)を指定 |
返値は送信に成功したバイト数です。例えばWindowsでは、64KBまでのデータを一度に送信できるようです。失敗の場合は-1になります。
4. データの受信
recvfrom()を使います。
int recvfrom(int s, char *buf, int len, int flags, struct sockaddr *from, int *fromlen);
s | bind()してあるソケット |
buf | 受信したデータの格納先 |
len | 格納できるデータのサイズ |
flags | 通常は0を指定 |
to | 送信元を表すsockaddr_in構造体 |
tolen | *tolenはsizeof(sockaddr_in)で初期化。 |
返値は受信したバイト数です。失敗の場合は-1になります。
名前からIPアドレスを得る方法
ユーザは、接続先をコンピュータのIPアドレスではなく、コンピュータの名前で指定したいでしょう。それを解決するのが
gethostbyname()です。
struct hostent* gethostbyname(const char *name);
name | コンピュータの名前。 |
nameは"www.ecc.komaba.u-tokyo.ac.jp"などの文字列です。返値のhostent構造体は次のような形をしています。
struct hostent { char *h_name; char ** h_aliases; short h_addrtype; short h_length; char ** h_addr_list; };
h_addr_list[0]が目的のIPアドレスですが、マクロによりh_addrで参照できます。バイトオーダを守るためにchar*型で与えられますが、実体はもちろんunsigned longです。
IPアドレスの調査に失敗した場合、NULLが返ってきます。
gethostbyname()の実行は、状況により長い時間がかかります。 1分以上かかる可能性すらありますので、プログラムによっては対策が必要でしょう。
"123.45.67.89"の様な、IPアドレスのテキスト表記をunsigned longに変換したい場合は、inet_addr()を使って下さい。
わかりにくいところなので、サンプルを書いておきます。 char*型の変数host_nameに調べたいコンピュータの名前が入っているとします。
struct hostent *host; unsigned long IPaddr = inet_addr(host_name); if(IPaddr == -1){ host = gethostbyname(host_name); if(host) IPaddr = *((unsigned long *)(host->h_addr)); }
IPaddrにネットワークバイトオーダに従ったIPアドレスが入っています。
IPアドレスから名前を得る方法
接続してきたクライアントの名前を、サーバ側で知りたいことがあるでしょう。まず、ソケットの接続先のIPアドレスを得るためにgetpeername()を実行します。
int getpeername(int s, struct sockaddr *name, int *namelen);
s | 接続されているソケット |
name | 接続相手の情報を入れるsockaddr_in構造体のポインタ(要キャスト) |
namelen | sizeof(sockaddr_in)で初期化すること。 |
返値は成功すれば0、失敗すれば-1です。成功した場合、name->sin_addrが目的のIPアドレス (32ビット値、ネットワークバイトオーダ)になります。
IPアドレスが得られたら、次にgethostbyaddr()を使います。
struct hostent *gethostbyaddr(const char *addr, int len, int type);
addr | IPアドレス(ネットワークバイトオーダ)へのポインタ |
len | IPアドレスの大きさ。4に固定。 |
type | IPアドレスの種類。AF_INETに固定。 |
成功した場合、hostent構造体へのポインタが得られます。 hostentの要素h_nameが接続相手のコンピュータの名前になります。失敗した場合、返値はNULLです。
コンピュータに名前が付いていないことは、十分あり得ます。 IPアドレスがないことは、あり得ません。そこでinet_ntoa()を使い、IPアドレスをドット付きテキスト表記に変換し、名前の代わりにするとよいでしょう。
char *inet_ntoa(struct in_addr in);
in | IPアドレス |
戻り値は"123.45.67.89"の様な、IPアドレスのドット付きテキスト表記です。 ソケットsの接続先の名前を調べる、プログラムのサンプルを書いておきます。参考にして下さい。
char *name = NULL; struct sockaddr_in addr; int len = sizeof(addr); if( !getpeername(s, (sockaddr*)&addr, &len) ){ struct hostent *host; host = gethostbyaddr((char*)&addr.sin_addr, 4, AF_INET); if(host) name = host->h_name; else name = inet_ntoa(addr.sin_addr); }
nameに目的の名前が入っています。 name == NULLの場合、エラーです。
select()でブロッキングを回避する
send()やrecv()などでブロッキングが発生することは、プログラムの種類によっては重大な問題となります。ネットワークでのデータの受送信がない間は、別の処理をしていたいプログラムがあるでしょう。サーバプログラムの様に、複数のクライアントと同時に通信をしたい場合もあるでしょう。プログラムをマルチプロセス、マルチスレッド化する対策法もありますが、select()を使う方がもっと簡単です。
select()は、ソケットの集合を引数に渡すと、受信可能なソケットの集合、送信可能なソケットの集合、エラーが発生しているソケットの集合を返す機能を持っています。『ソケットの集合』の操作の仕方を理解することが、 select()の使い方を知る上で最も重要な点となります。まず、そこから説明を始めましょう。
『ソケットの集合』はfd_setという型にtypedefされています。 fd_setを宣言したら、ただちにFD_ZERO()を使って初期化して下さい。
FD_ZERO(fd_set *fds)
fds | 空集合にしたいfd_setへのポインタ |
FD_ZERO()の実体はマクロです。返値はありません。 fd_setの元にソケットを加えるにはFD_SET()を使います。
FD_SET(int s, fd_set *fds)
s | 追加したいソケット |
fds | sを追加するfd_setへのポインタ |
FD_SET()もマクロです。返値はありません。
fd_setの元を取り除きたい場合は、FD_CLR()を使います。
FD_CLR(int s, fd_set *fds)
s | 除外したいソケット |
fds | sを除外するfd_setへのポインタ |
FD_ZERO()もマクロで、返値はありません。
あるソケットがfd_setの元であるか調べるには、FD_ISSET()を使います。
FD_ISSET(int s, fd_set *fds)
s | 元であるか調べたいソケット |
fds | sを元に持つか調べたいfd_setへのポインタ |
返値は、sが元である場合0以外の値、元でない場合は0です。
『ソケットの集合』の操作方法はこれでわかりました。次はselect()の説明です。
int select(int nfds, fd_set *rfds, fd_set *wrds, fd_set *efds, struct timeval *timeout);
nfds | rfds、wfds、efdsの元のうちで、最も値の大きいソケット+1 |
rfds | 受信可能か調べるソケットの集合 |
wfds | 送信可能か調べるソケットの集合 |
efds | 例外が発生していないか調べるソケットの集合 |
timeout | select()がタイムアウトする時間の指定 |
nfdsがselect()を使う上でやっかいなところです。引数の3つのfd_setの元の最大値+1を与える必要があります。つまり、プログラムはFD_SET()を実行するたびに、加えたソケットが最大値かどうか判定し保存しなければなりません。 nfdsに正しい値を与えなければ、select()は誤動作します。最大値に+1することを、くれぐれも忘れないで下さい。
rfds、wfds、efdsの説明は、だいたい上の通りです。 TCP接続が相手側によって切られしまったソケットは、rfdsによって与えられます。 efdsの扱う例外とは、接続やネットワーク上のエラーという意味ではありません。大抵のプログラムは、例外を用いる必要がないので、説明は省略します。例外の詳細については、send()の引数flagsをマニュアル等で調べて下さい。
これら3つに同じポインタを与えることはできません。select()は、実際に受信可能、送信可能、エラーが発生しているソケットを、これら3つの集合を直接書き換えることによって、知らせてくるからです。 TCP接続において、listen()してあるソケットがaccept()可能か調べたい場合は、そのソケットをrfdsに追加して下さい。
select()は与えられたソケットの集合を監視し、どれかのソケットが受信可能、送信可能、エラー発生になるまで処理をブロックします。しかし、timeoutによってブロッキングの最大待機時間を指定できます。時間0を指定すれば、まったくブロックしませんし、NULLを与えれば、永久に待機し続けます。
timeval構造体は次のような形で、UNIX系OSではsys/time.hにて定義されています。
struct timeval { long tv_sec; //秒 long tv_usec; //マイクロ秒 }
select()の返値は、3つの集合に含まれるソケットの総和、タイムアウトしたときは0、select()の実行そのものにエラーが発生した場合は-1です。
select()の使い方はわかりにくいので、サンプルを書いておきます。
while(1){ struct timeval timeout = {0, 0}; fd_set rfds, wrds, efds; FD_ZERO(&rfds); FD_ZERO(&wfds); FD_ZERO(&efds); FD_SET(sock, &rfds); FD_SET(sock, &wfds); FD_SET(sock, &efds); int n = select(sock +1, &rfds, &wfds, &efds, &timeout); if(n > 0){ if(FD_ISSET(sock, &rfds)) ネットワークからデータ読み込み if(FD_ISSET(sock, &wfds)) ネットワークにデータ書き込み if(FD_ISSET(sock, &efds)) ネットワークに例外発生 } else その他の処理を実行 }
select()が複数のソケットを監視できることを利用すれば、チャットサーバ程度は1プロセスのプログラムで作ることができます。
UNIX系OSの場合、select()で監視できるのはソケットだけに限りません。キー入力やパイプもfd_setに加えることができます。
UNIX系OSでソケットを使う場合の注意
接続が切れてしまったソケットに対して、送受信などのアクセスを行うと、関数の返値がエラーになるだけでは済まず、SIGPIPEが発生します。接続を切る前に合図を互い送るような設計をしても、相手プログラムの異常終了や、経由ネットワークの異常切断には対処できません。SIGPIPEのデフォルト動作は、プログラムの強制終了です。堅牢なプログラムを作るためには、SIGPIPEをsignal()でトラップし、recv()が無事に-1を戻せるようにする必要があります。
またUNIXでは、ソケットはファイルディスクリプタを使うので、サーバプログラムを作る場合はlimitにも注意する必要があります。
サーバプログラムがOSに強制終了されられると、しばらくの時間はサーバを再起動しても、bind()に失敗するようになります。これではサーバをデバッグするとき不便ですので、以下の様なコードを追加して下さい。 server_sockはbind()前のソケットです。
#ifdef _DEBUG_ int temp = 1; if(setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &temp, sizeof(temp))) fprintf(stderr, "setsockopt() failed"); #endif
Windows系OSでソケットを使う場合の注意
Windowsではソケットを使用する前に、ソケットシステム(Winsock)を初期化する必要があります。次の様なコードを実行して下さい。
WSAData wsad; WSAStartup(MAKEWORD(1,1), &wsad);
またWindowsでは、ソケットの拡張APIにWSAAsyncSelect()があります。ソケットがアクセス可能になると、ウインドウに対してメッセージが送られてくるようにできます。Windowsではこちらの方が便利ですので、自分で調べてみて下さい。
Windowsでは、ひとつのアプリケーションが使うことのできるソケットの数は、デフォルトで64個です。マクロFD_SETSIZEに定義することにより、この制限は変えられます。
終わりに
インターネットの普及に伴い、ソケットをサポートしていないOSは、ほとんど存在しなくなりました。ソケットは、ファイルシステム並に普遍性のある概念になったと言っても、過言ではないでしょう。
この記事を機会にソケットを勉強して、ネットワークを使った愉快なプログラムをぜひ作って下さい。
TCP接続が相手側によって切られば場合の説明と、select()による例外検知、UNIX系OSでの
SIGPIPEの扱い方、以上三点について加筆修正しました。