ソケット通信の始め方
前回のブログでは、C言語で凄いデータベースサーバを書くという話でした。
このデータベースサーバは、CGIとの間で、ソケット通信をします。したがって、CGIは、ソケット通信のクライアントで、データベースサーバは、ソケット通信のサーバになります。CGIは、WEBサーバのhttpdというデーモンプログラムからWEBへのリクエストが来るたびに立ち上げられ、ソケットのクライアントとして、データベースサーバに接続し、一方データベースサーバは、デーモンとして立ち上げておいて、CGIからのソケット通信のリクエストが来るのをlisten()で待っていて、そしてリクエストがきたら、新しいスレッドをpthread_create()で立ち上げて、そこに引渡しを行う、という話については、以下のブログで述べました。
このブログの中で、CGIは、ブラウザからのデータ(標準入力)をサーバに渡す際に、client_socket()という関数を使っていますが、これが、クライアント側から、サーバに対してソケット通信をするリクエストを出す関数です。この関数で、クライアント(CGI)は、サーバとの通信のためのファイルデスクリプタを取得し、以降は、read(), write()(あるいは、recv(), send())で、サーバとのメッセージのやり取りができます。ファイルデスクリプタは入力、出力両方とも行うことができます。
このあたりの基本的なことは、以下の教科書を参考にしました。
小俣光之・種田元樹著「Linux ネットワークプログラミングバイブル」秀和システム
ソケット通信のクライアント側のプログラミング
まず、クライアントプログラムからご紹介します。上記の client_socket()という関数です。
----ここから----
//まず、引数のPortStrは、ソケットのポート番号を表す文字列です。
//十進数の文字列になります。"1024"以降を仕様することになっています。
//IOMPtrErrorは、エラー報告するためのメモリー領域を
//管理するioManagerです。
//この関数の返り値は、ソケット接続で使うファイルデスクリプタです。
int32 client_socket (const char* PortStr, ioManager* IOMPtrError)
{
if (NULL ==PortStr) return -1;
char NBuff [NI_MAXHOST];
char SBuff [NI_MAXSERV];
addrinfo Hints;
addrinfo* Res0;
int32 SocketFD;
int32 ErrCode;
char ErrBuff [0x100];
memset (&Hints, 0, sizeof (Hints));
Hints.ai_family =AF_INET; Hints.ai_socktype =SOCK_STREAM;
//接続するサーバは、クライアントと同じマシンなので、
//接続相手は、"localhost"で、そこのPortStrで接続することを設定します。
if (0 != (ErrCode=getaddrinfo ("localhost", PortStr, &Hints, &Res0)))
{
//設定失敗の場合のエラー処理です。
sprintf (ErrBuff,"getaddrinfo:\t%s", gai_strerror (ErrCode));
if (NULL !=IOMPtrError) retError (ErrBuff,IOMPtrError); return -1;
}
//サーバの名前を確認します。
if (0 != (ErrCode=getnameinfo (Res0 ->ai_addr,Res0 ->ai_addrlen, NBuff, sizeof (NBuff),
SBuff, sizeof (SBuff), NI_NUMERICHOST |NI_NUMERICSERV)))
{
sprintf (ErrBuff,"getnameinfor():%s", gai_strerror(ErrCode));
if (NULL !=IOMPtrError) retError (ErrBuff,IOMPtrError); return -1;
}
//ここまでの部分で、接続する相手(サーバ)の色々設定をして、
//いよいよ、socket()で、接続時のファイルデスクリプタを取得します。
if (-1 ==(SocketFD =socket (Res0 ->ai_family, Res0 ->ai_socktype, Res0 ->ai_protocol)))
{
//ファイルデスクリプタ取得失敗時のエラー処理
close (SocketFD); freeaddrinfo (Res0);
if (NULL !=IOMPtrError) retError ("Socket Error", IOMPtrError); return -1;
}
//connect()で、接続を開始します。
if (-1 ==connect(SocketFD, Res0 ->ai_addr, Res0 ->ai_addrlen))
{
//接続失敗時のエラー処理
close (SocketFD); freeaddrinfo (Res0);
if (NULL !=IOMPtrError) retError ("Connect Error",IOMPtrError); return -1;
} freeaddrinfo (Res0); return SocketFD;
//最後に取得したファイルデスクリプタ(SocketFD)を返り値として返します。
}
----ここまで----
CGIプログラミングは、前回の「C言語でCGIを書く」で示したソースファイルに、このclient_socket()を組み込めば、終わりです。
CGI側からファイルデスクリプタでデータをサーバに送るときに気をつける点は、データを送るときに、送るサイズを明確にすることと、その際にエラーがあったときは、かならずファイルデスクリプタを、close()関数で閉じることです。閉じないでおくと、サーバ側はまだデータが送られてくるものとして、待ち状態になってしまい、サーバ側がハングすることになります。そのあたりのことは、「C言語でCGIを書く」の中のサンプルプログラムで書かれているので、御参照ください。
ソケット通信のサーバ側のプログラミング
さて、次は、サーバ側で、ソケットを作る部分です。サーバ側は、まず、ソケットを作り、その後、クライアントから通信が入るまでの待ち状態に入ります。ここでご紹介するのは、待ち状態になる前に行う、ソケットを作る部分で、server_socket()という関数です。
----ここから----
//この関数への引数は、ソケットのポートを表すPortという文字列です。
//上で書いたプログラムでは、PortStrになっていました。
//1024より大きいポートを表すための十進数の文字列です。
static int server_socket (const char* Port)
{
char NBuff [NI_MAXHOST]; char SBuff [NI_MAXSERV]; addrinfo Hints; addrinfo* Res0;
int Socket; int Opt; int ErrCode;socklen_t OptLen;
if (NULL ==memset (&Hints, 0, sizeof (Hints))) return -1;
Hints.ai_family =AF_UNSPEC; Hints .ai_socktype =SOCK_STREAM; Hints .ai_flags =AI_PASSIVE;
if (0 != (ErrCode=getaddrinfo (NULL, Port, &Hints, &Res0)))
{ syslog (LOG_USER|LOG_NOTICE, "getaddrinfo():\t%s.", gai_strerror (ErrCode)); return -1;}
if (0 != (ErrCode=getnameinfo (Res0 ->ai_addr,Res0 ->ai_addrlen,
NBuff, sizeof (NBuff), SBuff, sizeof (SBuff), NI_NUMERICHOST |NI_NUMERICSERV)))
{
syslog (LOG_USER|LOG_NOTICE, "getnameinfo():\t%s.", gai_strerror (ErrCode));
freeaddrinfo (Res0); return -1;
}
syslog (LOG_USER|LOG_NOTICE, "port =%s.", SBuff);
//ここまででいろいろ設定して、設定に問題があったら、エラーになります。
//で、いよいよサーバ側のソケットを作ります。
//クライアントでは、socket()で、ファイルデスクリプタが返って気ましたが、
//ここで帰って来るのは、ソケットのIDです。
if (-1 == (Socket =socket (Res0 ->ai_family, Res0 ->ai_socktype, Res0 ->ai_protocol)))
{ sysPerror("socket"); freeaddrinfo (Res0); return -1;}
Opt =1; OptLen =sizeof (Opt);
if (-1 ==setsockopt (Socket, SOL_SOCKET, SO_REUSEADDR, &Opt, OptLen))
{ sysPerror("socket"); freeaddrinfo (Res0); return -1;}
if (-1 ==bind (Socket, Res0 ->ai_addr, Res0 ->ai_addrlen))
{ sysPerror("bind"); close (Socket);freeaddrinfo (Res0); return -1;}
//ここまでで、ソケットを作り、つぎに、listen()で、作ったソケットに
//クライアントから通信のリクエストが来るのを待ちます。
if (-1 ==listen (Socket, SOMAXCONN))
{ sysPerror("listen"); close (Socket);freeaddrinfo (Res0); return -1;}
freeaddrinfo (Res0); return Socket;
}
----ここまで----
さて、server_socket()により、ソケットはクライアントから通信がくるのを待つ状態になります。それを待つのは、accept()という関数です。この関数は、クライアントから通信のリクエストが来ると、ファイルデスクリプタを返します。その後、ファイルデスクリプタでクライアントとの通信を read(), write()などの関数で送受信を行うようにします。
ただし、現在ご紹介しているデータベースサーバは、マルチスレッドサーバなので、accept()から返り値で返ってきたファイルデスクリプタを、新しく作ったスレッドに渡して、あとは、そのスレッドでクライアントとのソケット送受信をしてもらい、ふたたびaccept()でクライアントからの別の通信を待つ、という形です。つまり、
int Socket=server_client(Port);
で、ソケットを作ったあと、以下の無限ループで、雰囲気としては、以下のような感じになりそうです。
for(;;)
{
sockaddr From;//クライアントのソケットアドレス(IPアドレスなど)
socklen_t Len;//ソケットの長さ。
// acceptで、クライアントとの通信のファイルデスクプタを取得。
int32 SocketFD=accept(Socket, (sockaddr*)&From, &Len);
pthread_t ThreadId;
//SocketFDというファイルデスクリプタを、
//thread_functionというスレッド関数に渡す。
pthread_create(&ThreadId,NULL, thread_function,&SocketFD);
//スレッドができたら、また、ループに戻って
//次のクライアントからのリクエストを待つ。
}
実際のところ、上記で示した、「Linuxネットワークプログラミングバイブル」の本はそういう形で書かれていたし、他にあたっても、これで良いような感じでした。
が、実際には、これは通信エラーが頻発して、まともに動きません。これが、本ブログでの副題の「ソケット通信のワナ」なんです。これは、マルチスレッドサーバの場合で起こるワナです。
ということで、これ以降は、申し訳ございませんが、一応有料ページにさせていただきます。