読者です 読者をやめる 読者になる 読者になる

中年engineerの独り言 - crumbjp

LinuxとApacheの憂鬱

アパッチのソケットサーバ実装

少しずつアパッチの中の話をしていきます。

今回はネットワークサーバの基本となるソケット実装の話です。


アパッチのソケットサーバ実装は結構有名な方法だと思っていたのですが
人との会話中に、意外と知られていないのかもしれないと感じる事もあったので
書いてみようと思った次第です。

入門書などで出てくる一般的なソケットサーバの実装方法

 // ソケット作成
 listen_sock = socket(...);
 // アドレス:ポートを紐付け
 bind(listen_sock,...);
 // クライアント受付開始
 listen(listen_sock,...);
 // クライアント受付ループ
 for (;;){
   // クライアントの接続まで待機
   client_sock = accept(listen_sock,...);
   // 子プロセス作成
   if ( fork() == 0 ) {
     // 子プロセスの仕事
     // 子プロセスでは受付用ソケットは要らない
     close(listen_sock);
     // アプリケーションの仕事
     // client_sockの読み取りとか・・・
           :
     // 仕事が終わったら子プロセス終了
     exit();
   }else{ // fork()
     // 親プロセスの仕事
     // 親プロセスではクライアントソケットは要らない
     close(client_sock);
     // 次のクライアント接続を待つ為にループ
   }
 }
処理の流れ
  1. socket() : ソケット生成
  2. bind() : アドレス/ポート
  3. listen() : クライアント受付開始
  4. accept() : クライアント接続待ち
  5. fork() : 子プロセス生成
    1. -- 子プロセス --
    2. サーバ処理
    3. exit() : 子プロセス終了
  6. -- 親プロセス --
  7. accept()に戻る

この様な実装は良く見かけますし
実際、簡易なサーバアプリケーションならばこれで問題ありません。

一般的な実装の特徴と問題点

クライアント処理を行うプロセスを分離できるので、実装時に考慮するべき点が少ないので実装難度が低くなります。

不幸にも特定のクライアント処理中にプロセスが死んでしまった場合でも、他のクライアント処理には影響が無いので障害に強くなります。

例えば代表的なFTPサーバであるproftpdなどもこの実装です。
http://www.proftpd.org/
( 恐らくクライアント毎のsetuid,chrootがしたいが為の実装だと思いますが・・ )

しかしアパッチの様な不特定多数のクライアントを相手とする場合は
クライアントの接続毎に子プロセスを生成を行うこの方法は非効率です。


アパッチでは以下の様な実装方法で効率化を図っています。

アパッチ(prefork/worker)の実装方法

 // ソケット作成
 listen_sock = socket(...);
 // アドレス:ポートを紐付け
 bind(listen_sock,...);
 // クライアント受付開始
 listen(listen_sock,...);
 // ★ 子にそれぞれaccept()させる為にココでfork()
 if ( fork() == 0 ) {
   // 子プロセスの仕事
   // クライアント受付ループ
   for (;;){
     // クライアントの接続まで待機 <= ★この処理が子プロセス側
     client_sock = accept(listen_sock,...);
     // そのままアプリケーションの仕事
           :
     // 次のクライアント接続を待つ為にループ
   }
 }else{ // fork()
   // 親プロセスの仕事
   // 親プロセスの仕事は子プロセスを生成だけ
 }
処理の流れ
  1. socket() : ソケット生成
  2. bind() : アドレス/ポート
  3. listen() : クライアント受付開始
  4. fork() : 子プロセス生成
    1. -- 子プロセス --
    2. accept() : クライアント接続待ち
    3. サーバ処理
    4. 処理終了 : accept()に戻る
  5. -- 親プロセス --
  6. accept()に戻る


複数の子プロセスが独自に平行してaccept()しており
クライアント接続があった際にそのどれか一つのaccept()が復帰します。


そういえば私自身、アパッチのソースを読んで
初めてマルチプロセスで並列にaccept()が出来る事を知りました。
(しかしプラットフォーム依存の様です)


毎回fork()しないので効率的です。
また、クライアントプロセス数を調節することで簡単に処理能力を調整できます。


頑張って図解してみます。

図解


prefork と worker

尚、prefork と workerの違いですが
perforkは上記の方法そのままです。
workerでは、子プロセスがマルチスレッドになっていて以下のスレッドで処理されます。

listen_thread(単一スレッド)
accept()で得たclient_sockをキューに積む
worker_thread(複数スレッド)
上記のキューからclient_sockを取り出しWEBサーバとしての処理を行う。