C MAGAZINE Linux programming Tips
第11回 ソケットプログラミング道
■はじめに
ソケットは、最も利用されているプロセス間通信です。それもほとんど全ての OS間で利用できるプロセス間通信です。ちょっと思い出してください。 あなたのマシンから何のOSで動いているのか分からないWebサーバーへアクセスできますよね。これは、 あなたのマシンのWebブラウザというクライアントとWebサーバーとのプロセス間通信なのです。
このLinux Programming Tipsでも、以前にもソケットが含まれるプログラムが登場しましたが、ソケット自身のプログラミングの説明は行っていませんでした。今回は、 このソケットに的を絞りLinuxにおけるソケットプログラミングの基本を説明したいと思います。
■ソケットについて
ソケットは、プロセス間通信と言いましたが、正確には、 プロセス間通信を行うための通信のエンドポイントになります。2つエンドポイント間の通信経路が正しく確立するとプロセスは通信可能状態になります。 つまりプロセス間でデータの送信や受信が可能な状態になります。
ソケットプログラミングにおいて重要な概念が2つあります。ドメインとタイプです。
| |
ソケットのドメイン
ソケットは、接続するドメインにより以下の2種類になります。
- INETドメインソケット : ネットワーク上でマシンを越えてのプロセス間通信
- UNIXドメインソケット : 同じマシン上で動いているプロセスが通信を行うためのソケット。
プログラム中では、ドメインは、ソケットを生成する関数 socket()にプロトコルファミリーとして与えます。以下の表に、主なプロトコルファミリーを挙げます。
| 定義名 |
意味 |
| PF_UNIX |
UNIXドメイン : ローカル通信 |
| PF_INET |
IPv4 インターネットプロトコル |
| PF_INET6 |
IPv6 インターネットプロトコル |
| PF_IPX |
IPX - Novell プロトコル |
| PF_APPLETALK |
アップルトーク |
ソケットのタイプ
ソケットには、通信方式によっていくつかのタイプが存在します。OSI参照モデル対応づけると、ソケットは、 セッション層にあたります。Linux的(ほとんどの OSがそうですが)には、ソケット以降の処理は、カーネルによる制御になります。ちなみに、 SSL(Secure Socket Layer)と呼ばれるものは、このソケットのレイヤーの上に暗号通信、認証通信、データ完全性の機能を追加したものになります。

上の図で、ソケットの下の層が一つでないことに気がつかれたかもしれません。この図では、ソケットからはTCP、UDPにアクセスできるようになっています。 これは通信方式のレイヤーになります。TCP(Transmission Control Protocol)は、誤り訂正や再送の機能を持ち、 セッションを確立してからでないとデータの送受信ができないプロトコルです。一般的に、多くのアプリケーションは、 TCPを利用します。一方、UDP(User Datagram Protocol)は、誤り訂正や再送の機能はなく、 セッションを確立しないでデータを送り出すプロトコルです。そのため UDPの方が軽いため高速性を求められる動画転送などに利用されています。実際には、 TCP,UDP以外にもいくつか通信方式が存在します。
ドメインと同様に、プログラム中では、ソケットを生成する関数 socket()にタイプ(通信方式)を指定することになります。以下の表に、主なタイプを挙げます。
| 定義名(タイプ) |
動作 |
| SOCK_STREAM |
TCPの通信方式をとる |
| SOCK_DGRAM |
UDPの通信方式をとる |
| SOCK_RAW |
生のネットワークプロトコルへのアクセスを提供する |
ほとんどのプログラムでは、SOCK_STREAM, SOCK_DGRAMで実装されています。コマンド pingなどの実装では、SOCK_RAWが使用されています。
|
■ソケット通信の手順
ソケットプログラミングにおいて一番厄介で面倒な部分は、通信を確立するまでの手順です。一旦、正しく接続ができてしまうと、(ディスクリプターに対して )、 例えば 関数read()やwrite()でデータの受信、送信ができてしまいます。一般的に、通信確立後は、以下のようにいたって簡単な処理になります。
通信確立後の処理
[サーバー側]
// サーバー側のおまじない
// ポート番号Nでサーバーになる準備
// クライアントへのディスクリプター sを用意
...
write(s, buf, sizeof(buf));
...
close(s);
[クライアント側]
// クライアント側のおまじない
// IPアドレス xxxのポート番号Nのサーバーへ
// 接続する準備
// サーバーへのディスクリプター sを用意
...
read(s, buf, sizeof(buf));
...
close(s); |
では接続手順について説明します。一般によく利用されるINETドメイン,TCPでの、サーバー、クライアントにおける通信手順をチャート化してみました(下図)。ここでは、 5つの関数 socket(), bind(), listen(), accept(), connect()が登場します。それぞれに大まかに何を行っているかの説明をつけておきました。
この手順を実装したものがリスト1,2になります。コンパイルおよび実行方法は、以下のようになります。実行してみれば分かりますが、 この例ではいたって簡単なことしかしておりません。クライアントが接続すると、サーバーは文字列 "I'm a server.\n"を送り、クライアントはそれを受取るというものです。
コンパイル
$ cc -Wall -o simple_client simple_client.c
$ cc -Wall -o simple_server simple_server.c
サーバの実行
$ ./simple_server
クライアントの実行(別のターミナルで、できれば別のマシン上で)
$ ./simple_client 192.168.0.2 <- サーバーのIPアドレス
I'm a server. |
リスト1: サーバー実装例 - simple_server.c
#include <stdio.h>
#include <unistd.h> /* read(),write(),fork(),close(),... */
#include <sys/socket.h> /* socket(), bind(), listen(), accept()... */
#include <netinet/in.h> /* struct sockaddr_in,... */
#define PORT (23456)
int main(int argc,char *argv[])
{
int s1,s2,len;
struct sockaddr_in saddr,caddr;
char buf[BUFSIZ];
if( (s1=socket(AF_INET,SOCK_STREAM,0)) < 0 ) {
perror("socket");
exit(1);
}
memset((char*)&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(PORT);
if( bind(s1, (struct sockaddr*)&saddr, sizeof(saddr))<0) {
perror("bind"); exit(1);
}
if (listen(s1, 1) < 0) {
perror("listen"); exit(1);
}
len = sizeof(caddr);
if ((s2 = accept(s1, (struct sockaddr *)&caddr, &len)) < 0) {
perror("accept"); exit(1);
}
close(s1);
strcpy(buf, "I'm a server.\n");
write( s2, buf, sizeof(buf));
close(s2);
return 0;
} |
リスト2: クライアント実装例 - simple_client.c
#include <stdio.h>
#include <string.h>
#include <unistd.h> /* read(),write(),fork(),close(),... */
#include <sys/socket.h> /* socket(), bind(), listen(), accept()... */
#include <netinet/in.h> /* struct sockaddr_in,... */
#include <netdb.h> /* gethostbyname(),.... */
#define PORT (23456)
int main(int argc, char* argv[])
{
int s;
struct sockaddr_in addr;
struct hostent* hp;
char buf[BUFSIZ];
if( argc<2 ) {
printf("%s SERVER\n",argv[0]);
exit(1);
}
if( (s=socket(AF_INET, SOCK_STREAM, 0)) <0 ) {
perror("socket"); exit(1);
}
memset((char*)&addr, 0, sizeof(addr));
if( (hp=gethostbyname(argv[1]))==NULL) {
perror("gethostbyname"); exit(1);
}
bcopy(hp->h_addr, &addr.sin_addr, hp->h_length);
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
if (connect(s, (struct sockaddr *)&addr, sizeof(addr)) < 0){
perror("connect"); exit(1);
}
read(s, buf, sizeof(buf));
printf("%s", buf);
close(s);
return 0;
} |
リスト1、2の中で分かりにくそうなところを部分的に説明します。
まずサーバー側の実装について説明します。INETドメインのソケットで、関数 bind()を呼び出すには、 構造体 sockaddr_in (IPv4インターネットアドレス)を利用します。ちなみにUNIXドメインの場合は、 構造体 sockaddr_unを利用します。以下は、linuxにおける構造体 sockaddr_inの定義は以下のようになっています。
構造体 sockaddr_inのメンバー
struct sockaddr_in {
sa_family_t sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
.....
}; |
変数saddrを0 埋めるのは、bind()の誤動作を防ぐためです。変数 saddrの sin_portは、ネットワークバイトオーダーのため、 x関数 htons()を使ってポート番号を変換しています。sin_addr.s_addrに INADDR_ANYを指定していますが、これはどこからの接続も受け入れることを意味します。
関数bind()の前処理
memset((char*)&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(PORT);
if( bind(s1, (struct sockaddr*)&saddr, sizeof(saddr))<0) {
perror("bind"); exit(1);
} |
次にクライアント側の実装について説明します。関数 connect()の前処理で、サーバーのアドレス、ポート番号を設定するための手続きが必要になります。 関数 gethostbyname()は、ホスト名、 IPv4 アドレス、 IPv6 アドレスを内部処理で必要な情報に変換します。ここで得られた値を、 構造体sockaddr_inのメンバーにコピーすることでサーバーアドレスを設定します。
関数connect()の前処理
memset((char*)&addr, 0, sizeof(addr));
if( (hp=gethostbyname(argv[1]))==NULL) {
perror("gethostbyname"); exit(1);
}
bcopy(hp->h_addr, &addr.sin_addr, hp->h_length);
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
if (connect(s, (struct sockaddr *)&addr, sizeof(addr)) < 0){
perror("connect"); exit(1);
} |
■もう少し複雑な例
次にもう少し複雑な例を紹介します。簡単なネットワーク型コピープログラムの例として、クライアントが、 サーバーから特定のファイルを取得できるようなプログラムを記述してみます。さらに複数のクライアントがサーバーに接続できるようにしてみます。 このようなサーバーを不用意にインターネット上のアクセスできるようにしておくとセキュリティ的によくないのでアクセス可能なクライアントを限定できる機能を付けておきましょう。
最初にコマンドラインを考えてみます。クライアントプログラムの引数には、サーバー名と取得したいファイルのパスが必要になります。また前述のプログラムは、 ポート番号は固定でしたが、指定できるようにしてみます。クライアントが変更できるなら、 サーバーも変更できるようにしないといけません。ということで実行例を想像すると以下のようになると思います。
サーバの実行 (この例では、サーバーのIPは、192.168.0.2)
$ ./copy_server 23456 &
クライアントの実行(別のターミナルで、できれば別のマシン上で)
$ ./copy_client 192.168.0.2 23456 /etc/hosts 23456 > getfile |
クライアント側は、以下のような処理になります。接続手順に関しては、前述のクライアントと同じで大丈夫です。
クライアント側の処理
[クライアント側]
// クライアント側の接続手順
write(s, filename, sizeof(filename));
...
サーバーから送られてきたバッファーを読み込み、stdoutへ出力
... |
クライアント実装例は、以下のようになります。関数 client_socket_procedure()は、 クライアントに必要な手続きをまとめたものです (common.h, common.c を参照してください)。
リスト3: クライアント実装例 - copy_client.c
#include "common.h"
static int copy_client(char* server, char* fn , int port)
{
int s;
char buf[BUFSIZ];
int rc;
s = client_socket_procedure(server,port);
strcpy(buf, fn);
write(s ,buf , strlen(buf));
while( (rc=read(s,buf,BUFSIZ)) >0 ) {
printf("%s",buf);
}
close(s);
return 0;
}
int main(int argc,char *argv[])
{
int port = PORT_NO;
if( argc < 3 ) {
printf("Usage: %s SERVER FILENAME [PORT]\n",argv[0]);
return 1;
}
if( argc < 4) {
port = PORT_NO;
} else {
port = atoi(argv[3]);
}
printf("port no. = %d\n", port);
return copy_client(argv[1],argv[2],port);
} |
サーバー側の手順は、リスト1のサーバーの実装例+複数のクライアントへの対応+アクセス元の検証の処理が必要になります。
複数のクライアントに対応する方法として、いくつかの実装法がありますが、今回は、関数 fork()を利用して クライアント要求の処理を子プロセスとしておこなうことにします。
サーバー側の処理
[サーバー側]
// サーバー側の接続手順
...
for(;;) {
if(( s2=accept(s1, (struct sockaddr*)&addr, &len))<0) {
....
}
// s2からアクセス元を検証する
if( (pid=fork()) <0 ) {
.....
} else if(pid==0) {
// 子プロセスとしてクライアントからのリクエストを処理する
read(s, filename, sizeof(filename));
// filenameのファイルを開く
// ファイルの内容を ディスクリプターs に 出力
...
}
close(s2);
} |
アクセス元の検証は、以前にProgramming Tipsに登場した tcp wrappers ライブラリを利用することにします。実装は、 以下のようになります。関数 access()から得た ディスクリプターからアクセス元を検証するようにします。
そのためアクセスコントロールリストは、/etc/hosts.allow(あるいは /etc/hosts.deny)になります。/etc/hosts.allowの設定例は、 (ローカル)ネットワーク 192.168.0.0/24の全てのマシンから接続を許す場合は以下のようになります。
ALL : 127.0.0.1
sshd : ALL
copy_server: 192.168.0.0/255.255.255.0
ALL : ALL : deny |
リスト4: アクセス元の検証 - acl.c
#include "common.h"
int allow_severity = LOG_INFO;
int deny_severity = LOG_WARNING;
int is_access_granted(char* daemon, int s)
{
struct request_info req;
request_init(&req, RQ_DAEMON, daemon, RQ_FILE, s, NULL);
fromhost(&req);
return hosts_access(&req);
return 0;
} |
これで必要な要素は出そろいました。以上をまとめて、サーバーの実装すると以下のようになります。
リスト5: サーバーの実装例 - copy_server.c
#include "common.h"
static int copy_server(int port)
{
int s1, s2;
struct sockaddr_in addr;
int len = sizeof(addr);
int rc;
char buf[BUFSIZ];
s1 = server_socket_procedure(port);
for(;;) {
int pid;
if(( s2=accept(s1, (struct sockaddr*)&addr, &len))<0) {
perror("accept");
return 1;
}
if( ! is_access_granted("copy_server", s2) ) {
printf("Access from %s : denied\n", inet_ntoa(addr.sin_addr));
close(s2);
continue;
}
if( (pid=fork()) <0 ) {
perror("fork");
return 1;
} else if(pid==0) {
int fd;
int cnt;
close(s1);
rc = read(s2, buf, BUFSIZ);
printf("Request from %s: '%s'\n", inet_ntoa(addr.sin_addr), buf);
fd = open(buf, O_RDONLY);
if( fd>0 ) {
char linebuf[BUFSIZ];
while( (cnt=read(fd, linebuf, BUFSIZ))>0 ) {
write(s2, linebuf, cnt);
}
} else {
strcpy( buf,"No such file or directory\n");
write(s2,buf,sizeof(buf));
}
exit(0);
}
close(s2);
}
return 0;
}
int main(int argc,char *argv[])
{
int port = PORT_NO;
if( argc<2 ) {
port = PORT_NO;
} else {
port = atoi(argv[1]);
}
printf("port no. = %d\n", port);
return copy_server(port);
} |
■おわり
今回は、ソケットを使ったプログラミング際に最低限必要なプログラム要素について説明しました。 プログラム例をいろいろ変更してオリジナルの楽しいサーバーを作って見ましょう。
ソケットに関する話題は、まだまだ多くあり、例えば、ソケットのより細かな制御やセキュアな通信経路を確立方法、 あるいは最近流行りのIPv6におけるソケット通信の方法などまだまだ興味深いところがたくさんあります。今後、 これらに関するプログラミングを取り上げていきたいと思います。ただ基本は大事なので今回の範囲はしっかり把握するようにしましょう。
■プログラムへのリンク
simple_server.c
simple_client.c
copy_server.c
copy_client.c
acl.c
common.c
common.h