コンピューター:C言語講座:TCP/IPプログラム(その2:サーバ)
概要
前回の講座でTCP/IPクライアントを作成し、ftp,smtpなどのサーバに接続してみました。しかし、もともとftp,mailなどそれらのサーバ用のクライアントは存在しているわけで、とくに自分で作っても前回の程度では何も便利にはなりません。そこで、今回はサーバを作成し、実用的ではないとしてもサーバ・クライアント両方を自作し、その仕組みの理解を深めたいと思います。前回同様に詳細なTCP/IPのプロトコルなどは専門書を参照していただくとして、ここではプログラミングの方法を説明します。
サーバ
サーバと一言で言ってもさまざまな形態があり、一つのクライアントしか管理できないようなシンプルな物からマルチクライアントで並列処理を行なうサーバまで幅広くあります。せっかくですから、サーバらしさを味わう為にここではマルチクライアント管理可能なサーバを考えてみたいと思います。
マルチクライアントのサーバの実現方法もまた、非常にさまざまな方法があります。例えばサーバプロセス自体は一つで、多数のクライアントとのやりとりを順番に全て管理する方法があります。この方法は自分自身で多重処理することになります。メリットとしてはクライアント同士の連携もサーバが管理してあげられることで、私自身、CADシステムを構築する際にGUIモジュール・簡易言語モジュール・CAD専用モジュール・データベースモジュールなどモジュール化したクライアントを連携させる時にその管理サーバとしてそのような形態のサーバを作成したことがあります。デメリットとしては多重処理を効率良く処理するプログラミングがかなり大変なのと、サーバを一つ立ち上げるごとにTCP/IPのポートを一つ使ってしまうこと、サーバがデットロックするような事態になるとそこにぶら下がっているクライアント全てが使用不可能になることでしょうか。
通信自体はサーバとクライアントが1対1で行なえれば良いと言う場合、例えばデータベースサーバのようにクライアントとサーバの1対1の通信のような場合ですが、この場合はもうすこし簡単に考えられます。サーバはクライアントからの接続要求を受け付けると同時にforkで分身を作成してしまい、分身とクライアントが1対1で通信させます。本体はまた次の受付を管理します。こうすれば分身は担当のクライアント1つに対してやりとりが出来れば良いので簡単になりますし、通信でデットロックするような事態になってもその1ペアがダメになるだけで他には影響がでません。また、TCP/IPのポートも本体が1つ使えれば良いので複数は必要ありません。
今回は後者の方式でサンプルを考えてみます。分身を作るforkについては「fork,exec,pipeについて」も参照してください。
サンプル
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/stat.h>
#include <sys/param.h>
#include <sys/time.h>
#include <sgtty.h>
#define DB(x) x
int Soc=NULL;
int Acc=NULL;
void MainLoop();
void CloseSocket();
void CloseChild();
void main(argc,argv)
int argc;
char *argv[];
{
InitSocket();
InitSignal();
MainLoop();
}
int InitSocket()
{
char hostname[MAXHOSTNAMELEN];
struct hostent *myhost;
struct servent *se;
struct sockaddr_in me;
int opt;
DB(fprintf(stderr,"InitSocket\n"));
if(gethostname(hostname,MAXHOSTNAMELEN)== -1){
perror("gethostname");
exit(-1);
}
DB(fprintf(stderr,"hostname=%s\n",hostname));
if((se=getservbyname("TestServer","tcp"))==NULL){
perror("getservbyname");
exit(-1);
}
DB(fprintf(stderr,"getservbyname:OK\n"));
if((myhost=gethostbyname(hostname))==NULL){
perror("gethostbyname");
exit(-1);
}
DB(fprintf(stderr,"gethostbyname:OK\n"));
if((Soc=socket(AF_INET,SOCK_STREAM,0))<0){
perror("socket");
exit(-1);
}
DB(fprintf(stderr,"socket:OK\n"));
opt=1;
if(setsockopt(Soc,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int))!=0){
perror("setsockopt");
exit(-1);
}
DB(fprintf(stderr,"setsockopt:OK\n"));
memset((char *)&me,0,sizeof(me));
me.sin_family=AF_INET;
me.sin_port=se->s_port;
if(bind(Soc,&me,sizeof(me))== -1){
perror("bind");
exit(-1);
}
DB(fprintf(stderr,"bind:OK\n"));
return(0);
}
void CloseSocket()
{
DB(fprintf(stderr,"CloseSocket\n"));
if(Soc!=NULL){
close(Soc);
DB(fprintf(stderr,"close(Soc)\n"));
Soc=NULL;
}
if(Acc!=NULL){
close(Acc);
DB(fprintf(stderr,"close(Acc)\n"));
Acc=NULL;
}
DB(fprintf(stderr,"CloseSocket:exit\n"));
exit(0);
}
void CloseChild()
{
int pid;
DB(fprintf(stderr,"CloseChild\n"));
pid=wait(0);
DB(fprintf(stderr,"wait:pid=%d:end\n",pid));
signal(SIGCLD,CloseChild);
}
int InitSignal()
{
DB(fprintf(stderr,"InitSignal\n"));
signal(SIGINT,CloseSocket);
signal(SIGTERM,CloseSocket);
signal(SIGCLD,CloseChild);
return(0);
}
void MainLoop()
{
struct sockaddr_in from;
int len,width,pid;
fd_set readOk,Mask;
struct timeval timeout;
char client[64];
DB(fprintf(stderr,"MainLoop\n"));
width=0;
FD_ZERO(&Mask);
FD_SET(Soc,&Mask);
FD_SET(0,&Mask);
width=Soc+1;
listen(Soc,SOMAXCONN);
DB(fprintf(stderr,"losten:OK\n"));
while(1){
readOk=Mask;
timeout.tv_sec=0;
timeout.tv_usec=100000;
switch(select(width,(fd_set *)&readOk,NULL,NULL,&timeout)){
case -1:
perror("select");
break;
case 0:
break;
default:
if(FD_ISSET(Soc,&readOk)){
len=sizeof(from);
Acc=accept(Soc,&from,&len);
if(Acc<0){
if(errno==EINTR){
continue;
}
perror("accept");
continue;
}
sprintf(client,"%d.%d.%d.%d",
from.sin_addr.S_un.S_un_b.s_b1,
from.sin_addr.S_un.S_un_b.s_b2,
from.sin_addr.S_un.S_un_b.s_b3,
from.sin_addr.S_un.S_un_b.s_b4);
DB(fprintf(stderr,"accept:%s\n",client));
if((pid=fork())==0){
close(Soc);
Soc=NULL;
StartChild(client);
DB(fprintf(stderr,"MainLoop:exit\n"));
exit(0);
}
DB(fprintf(stderr,"pid=%d\n",pid));
setpgrp(pid,getpid());
close(Acc);
Acc=NULL;
}
if(FD_ISSET(0,&readOk)){
KeyIn();
}
break;
}
}
}
int KeyIn()
int flag;
{
char buf[512];
gets(buf);
DoKeyInCommand(buf);
return(0);
}
int DoKeyInCommand(str)
char *str;
{
DB(fprintf(stderr,"(%s)\n",str));
if(strcmp(str,"end")==0){
killpg(getpid(),SIGTERM);
CloseSocket();
}
return(0);
}
int StartChild(client)
char *client;
{
char buf[8192],name[80],passwd[80];
int ret,level;
DB(fprintf(stderr,"StartChild\n"));
sprintf(buf,"Server Ready.\n");
send(Acc,buf,strlen(buf),0);
while(1){
ret=recv(Acc,buf,sizeof(buf),0);
if(ret== -1){
perror("recv");
break;
}
buf[ret]='\0';
if(DoCommand(buf)== -1){
/* end */
break;
}
}
DB(fprintf(stderr,"StartChild:end\n"));
return(0);
}
int DoCommand(com)
char *com;
{
int ret;
char buf[512];
DB(fprintf(stderr,"[%s]\n",com));
if(strncmp(com,"end",strlen("end"))==0){
ret= -1;
}
else{
sprintf(buf,"受け取ったメッセージ:[%s]\n",com);
send(Acc,buf,strlen(buf),0);
ret=0;
}
return(ret);
}
これまでで最も長いサンプルになってしまったかも知れませんが、先頭から順に説明します。グローバル変数を2つ使っていますが、Socが受け付け用ソケット、Accがクライアントからの接続を受け付けたソケットで、実際にクライアントとのやりとりに使うのはAccの方で、Socは受け付け専用です。
mainでははじめにInitSocket()でソケットの準備をし、シグナルをキャッチする為にInitSignal()で準備し、MainLoop()でクライアントの受け付けループとなります。
InitSocket()がサーバとしてのソケットの準備を行なう関数です。クライアント同様にホスト名・ポート番号が必要で、gethostbyname(),getservbyname()などで準備しておきます。今回はポートはTestServerという名前にしましたので、/etc/servicesにTestServer
9999/tcpという感じに指定しておきます。socket()でソケットを用意し、そのソケットに対してsetsockopt()でオプションを指定していますが、SO_REUSERADDRを指定しておかないと、このサーバを何らかの形で終了した後に再起動すると、1分〜5分くらいポート番号がロックされます。クライアントの場合はソケットを準備したらconnect()でつなぐのですが、サーバの場合はbind()でソケットに名前を割り当てます。これでTestServerというポートが受け付け準備完了となります。
CloseSocket()関数はサーバを停止する場合に呼ばれますが、ソケットを閉じる処理を行ないます。
CloseChild()関数は子プロセスが終了した時に呼ばれ、wait()で終了を受け取ります。
InitSignal()は上記2つの関数をシグナルをキャッチした際に呼び出すようにセットしています。
MainLoop()ではクライアントの接続受付を行ないます。また、サーバ自身で標準入力を得て、サーバの管理者がサーバを停止したりも出来るようにしてみました。クライアント同様にソケットと標準入力の2つを受け付ける為にselect()を使用します。はじめにlisten()でソケットへの接続受付を準備し、無限ループに入ります。select()でソケットに待ちデータがあるとなった時にはaccept()で接続要求の最初の1津を取り出し、接続先用にソケットを生成し、ファイルディスクリプタを割り当てます。クライアントのネットワークアドレスもわかると便利なので、fromという変数にかえってきた値を192.1.1.96という感じに表示しています。
accept()で返答されたファイルディスクリプタAccに対して読み書きすることでクライアントと通信できますが、このサーバはマルチクライアント対応とするので、ここで通信のループに入ってしまうと次の接続要求が処理できません。通信ループを多重化して更に接続要求まで含めて処理すればマルチクライアントとなりますが、今回はここでサーバの分身をfork()で生成し、子サーバはStartChild()関数で通信ループを行なわせ、親サーバは更に受付ループを回るようにしました。したがって子サーバ側はStartChild()から抜けるとexit()で終了するようにしてあります。子サーバも親サーバが終了などした際に一緒に終了などしたほうが良いと思い、setpgrp()でプロセスグループを指定しています。これにより親サーバが受け取ったシグナルは子サーバにも送られます。さらに、親サーバ側はAccは使用しないのでclose()してNULLにしておきます。
また、標準入力からデータが来た場合はKeyIn()という関数で処理します。KeyIn()は単純にgets()で読み込み、DoKeyInCommand()に渡していますが、ここではendという内容だった時にkillpg()でプロセスグループに対して終了シグナルを送り、CloseSocket()を呼び出して終了処理をしています。
親サーバではここまでで、StartChild()以下は子サーバによってのみ処理されます。また、子サーバはStartChild()以下しか処理しません。
StartChild()ではとりあえずつながったということをsend()でクライアントに送り、その後、無限ループとなります。ループではrecv()で受信し、DoCommand()でその内容を処理させますが、サンプルではendの場合に終了、それ以外は受け取った内容をクライアントに送り返しています。
まとめ
いかがでしたでしょうか?少々ホームページとしては長すぎる感じですが、サーバの感じは掴んでいただけましたでしょうか?今回はソケットとプロセス管理が一緒にでて来てしまったので少し難しかったかも知れませんが、fork()の威力が良くわかったのでは無いでしょうか?今回のサンプルと同様のことをfork()で分身を作らずに実現するのは結構大変です。是非チャレンジしてみていただきたいところですが、おそらく今回の方式と同レベルの処理速度はでないと思います。ここがマルチタスクOSの強みで、自分でマルチ処理を記述しなくても複数のプロセスで同時処理が出来てしまいます。もっとも前述したように、クライアント同士も連携させたり、サーバ自身がクライアントを起動したりするようなタイプの場合は全て自分で管理したほうが良い場合もあります。
いずれにしても、TCP/IPを用いた通信が出来れば今回のサンプルのようにマルチクライアントのサーバがネットワーク上に構築できます。これに機能を追加して行けばデータベースサーバなども作成できます。
今回も前回同様に通信のデッドロックに関しては対応していません。したがってクライアントから4098バイトを超えるデータを送るとクライアントが賢くない場合はデットロックします。このサンプルはもともとはそれに対応して入出力バッファーを備えたプログラムだったのですが、あまりに長くなってしまうので、敢えて単純に直して作りました。デッドロック対応に関してはそのテーマだけで別に取り上げる予定です。
それにしても、こういったプログラムを作成していて感じることはUNIXのシステムコールとC言語の言語使用の使いやすさで、開発者が望むことが容易に実現できる方法が確実に用意されていることを痛感します。もっともなんでも出来すぎて初めての場合にはとっつきにくい感じもありますが。このようなシンプルで組み合わせやすい環境は是非Windows系のOSにも取り入れて欲しいところです。私の印象としてはWindows系はMS-DOS時代の環境をひきずっている為か、通信などは取ってつけたような実装に感じます。それでも時代はWindowsに流れているようですので、開発者も大変です。