C言語でネットワークプログラミング --ソケット通信のワナ--

記事
IT・テクノロジー
ソケット通信の始め方
前回のブログでは、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ネットワークプログラミングバイブル」の本はそういう形で書かれていたし、他にあたっても、これで良いような感じでした。
が、実際には、これは通信エラーが頻発して、まともに動きません。これが、本ブログでの副題の「ソケット通信のワナ」なんです。これは、マルチスレッドサーバの場合で起こるワナです。

ということで、これ以降は、申し訳ございませんが、一応有料ページにさせていただきます。

この続きは購入すると読めるようになります。
残り:4,586文字
C言語でネットワークプログラミング ... 記事
IT・テクノロジー
500円
サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す