こないだ作った簡易Webサーバーをマルチタスク化するよ。(2)

前回やったクライアント毎にプロセスを作成の続きで、今回は

クライアント毎にスレッドを作成

です。

forkと同じ感じですんなり終わるかなあと思ってたんですが、ところがどっこいだいぶ時間がかかりました。今回もEchoじゃなくてWebサーバーですが、いたるところで修正が必要で始めは全然動かずでした。

一番のポイントになったのはclose(SOCKETNAME)と、pthread_joinの2個でした。本書ではpthread_detachを使ってるんですが、pthread_joinの方が合うんじゃないかと思って変更しました。
あとはThreadArgsについての情報がウェブ上にも少なくて大変でした。

 

とりあえず、まずはプロセスを増やして処理をさせる場合と、スレッドで処理することの違いについて簡単にまとめますと

プロセスを生成する場合、生成するたびにメモリ、スタック、ソケットなどをコピーして、子プロセスが処理します。まるっとコピーする必要があるわけですな。
スレッドの場合は1個のプロセスで複数タスクが処理ができて、アドレス空間を共有するので、コストが安くなるという利点があるようだ。

ずいぶん前になってしまうけど、ApacheのMPMに凝ってたときにやりましたが、Workerは静的なサイトをたくさん処理させるのに向いているということにも一致するね。

スレッドの実装に関しては、本書を参考にしているので同じくPTreadを利用します。
ほかにもいろいろあるらしいですが、POSIXならだいたいのOSに入っているからという理由だそうです。

まずはソースからいきたいと思いますが、下の2個の関数はプロトタイプ宣言だけになってますので、TCP/IP Socket in C著者のMichael J. Donahoo氏のウェブサイトを参考にしてください。
参考サイトとして載せています。

19 int AcceptTCPConnection(int servSock);
 20 int CreateTCPServerSocket(unsigned short port);

こういうのが移植性が高いというのか、前回と同じくウェブサーバーの動作の部分になる、「HandleTCPclient改」と「GETHTML」についてはほとんどさわらずでmain関数の部分をいじくったところが多いです。

ソース TCPWebServer_type_Tread.c

#define _XOPEN_SOURCE 600
#define _POSIX_C_SOURCE 200112L
#undef _REENTRANT
#define _GNU_SOURCE

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/syscall.h>

void *ThreadMain(void *arg);
int AcceptTCPConnection(int servSock);
int CreateTCPServerSocket(unsigned short port);

pid_t gettid(void)
{
  return syscall(SYS_gettid);
}

struct ThreadArgs
{
  int clntSock;                      /* Socket descriptor for client */
};

typedef struct {
   char method[16];
   char path[128];
   char version[16];
} HEAD_DATA;

//#define DEBUG
#define MAXPENDING 5

void DieWithError(char *errorMessage)   //エラー処理関数
{
   perror(errorMessage);
   exit(1);
}

void CutCrLf( char *str )
{
   char *p;

   if     ((p = strchr(str, '\r')) != NULL) *p = '\0';
   else if((p = strchr(str, '\n')) != NULL) *p = '\0';
}

int CutChar( char *data, char *str, char c )
{
  unsigned int i;

  for(i=0; data[i] != c; i++)
    str[i] = data[i];

  str[i] = '\0';

  return i;
}

void GetHtml( FILE *sock_fp, HEAD_DATA *data )
{
   FILE *fp;
   char *buf;
   struct stat st;

   if(stat(data->path + 1, &st) < 0){
     fprintf(stderr, "404error\n");
     exit(1);
   }

  if((buf = (char *)calloc(st.st_size, sizeof(char))) == NULL){
    fprintf(stderr, "calloc\n");
    exit(1);
  }

   fprintf(sock_fp, "%s 200 OK\r\n", data->version);
   fprintf(sock_fp, "Server:sample\r\n");
   fprintf(sock_fp, "Content-Type:text/html\r\n");
   fprintf(sock_fp, "Content-Length:%d\r\n", (int)st.st_size);
   fprintf(sock_fp, "\r\n");

   if((fp = fopen(data->path + 1, "r")) == NULL){
     fprintf(stderr, "fopen\n");
     exit(1);
   }

  fread(buf, 1, st.st_size, fp);
  fwrite(buf, 1, st.st_size, sock_fp);

   free(buf);
   fclose(fp);

}

void HandleTCPClient(int clntSocket)
{
   FILE *sock_fp;
   char *data[64], buf[2048];
   HEAD_DATA headData;

   if((sock_fp = fdopen(clntSocket, "r+")) == NULL){
     fprintf(stderr, "fdpoen\n");
     exit(1);
   }

   unsigned int len;
   unsigned int i, k;

   for(i = 0; i < 64; i++){

     fgets(buf, sizeof(buf), sock_fp),  (void)CutCrLf(buf);

     if((len = strlen(buf)) == 0) break;

     if((data[i] = (char *)calloc(len + 1, sizeof(char))) == NULL){
       fprintf(stderr, "calloc\n");
       exit(1);
     }
     strcpy(data[i], buf);
   }
   k = i;

#ifdef DEBUG
     printf("%s\n", data[0]);
#endif

   len =  CutChar( data[0]      , headData.method,     ' '  ), len++;
   len += CutChar( data[0] + len, headData.path,       ' '  ), len++;
   len += CutChar( data[0] + len, headData.version,    '\0' ), len++;

#ifdef DEBUG
     printf("%s %s %s\n", headData.method, headData.path, headData.version);
#endif

   if(strstr("headData.version" , "1.0") != NULL || strstr("headData.version" , "1.1") != NULL){
     fprintf(stderr, "request error\n");
     exit(1);
   }

   GetHtml(sock_fp, &headData);
     fflush(sock_fp);
   for(i=0; i < k; i++){
     printf("%s", data[i]);
     free(data[i]);
   }
}

int main(int argc, char *argv[])
{
   int servSock;           //サーバのソケットディスクリプタ
   int clntSock;           //クライアントのソケットディスクリプタ
   struct sockaddr_in echoClntAddr;        //クライアントアドレス
   pthread_t threadID;
   struct ThreadArgs *threadArgs;
   unsigned short echoServPort;            //サーバポート
   unsigned int clntLen;                   //クライアントのアドレス構造体の長さ

   if(argc != 2)   //引数の数をチェック
   {
     fprintf(stderr, "Usage: %s <Server Port>\n", argv[0]);
     exit(1);
   }

   echoServPort = atoi(argv[1]);   //ローカルポート
     servSock = CreateTCPServerSocket(echoServPort);

   for(;;) //繰り返し実行
   {
       //入出力パラメータのサイズをせっと
     clntLen = sizeof(echoClntAddr);

         clntSock = AcceptTCPConnection(servSock);

       if ((threadArgs = (struct ThreadArgs *) malloc(sizeof(struct ThreadArgs))) == NULL)
          DieWithError("malloc() failed");
         threadArgs -> clntSock = clntSock;

       fprintf(stderr, "1:tid = %d\n", gettid());

       /* Create client thread */
     if (pthread_create(&threadID, NULL, ThreadMain, (void *) threadArgs) != 0)
       DieWithError("pthread_create() failed");
         pthread_join(threadID, NULL);
         close(clntSock);
      printf("with thread %ld\n", (long int) threadID);

   }
   //この部分には到達しない
}

void *ThreadMain(void *threadArgs)
{
  int clntSock;                   /* Socket descriptor for client connection */

   fprintf(stderr, "2:tid=%d\n", gettid());

   clntSock = ((struct ThreadArgs *) threadArgs) -> clntSock;
   free(threadArgs);

   HandleTCPClient(clntSock);

   return(NULL);
}

あとはデバッグをしていた時に処理が変なところで止まったりしていたので、Debug用にprintfをいろんなところに入れて停止のタイミングを確認しました。

#include <stdio.h>      /* for printf() */
#include <sys/socket.h> /* for accept() */
#include <arpa/inet.h>  /* for sockaddr_in and inet_ntoa() */

void DieWithError(char *errorMessage);  /* Error handling function */

int AcceptTCPConnection(int servSock)
{
  int clntSock;                    /* Socket descriptor for client */
  struct sockaddr_in echoClntAddr; /* Client address */
  unsigned int clntLen;            /* Length of client address data structure */

  printf("うに\n");
  /* Set the size of the in-out parameter */
  clntLen = sizeof(echoClntAddr);

  printf("いくら\n");
  /* Wait for a client to connect */
  if ((clntSock = accept(servSock, (struct sockaddr *) &echoClntAddr,
    &clntLen)) < 0)
   DieWithError("accept() failed");

  printf("なまこ\n");
  /* clntSock is connected to a client! */

  printf("Handling client %s\n", inet_ntoa(echoClntAddr.sin_addr));

  return clntSock;
 }

gdbで同じことができるので、わりと簡単な方法なんでやっちゃいました。なお#defineのところはいまいちよく分かってません。

サーバー側で動かして、ウェブからアクセスした時の動作は下のような感じです。
1回アクセスした後は、「いくら」の表示で止まります。
acceptで待ってるってことですね。

[root@testserver_centos ~]# ./TCPWebServer_type_Thread 8000
うに
いくら
なまこ
Handling client 192.168.24.53
1:tid = 14387
2:tid=14430
GET /index.html HTTP/1.1Host: 192.168.24.61:8000
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:28.0) Gecko/20100101 Firefox/28.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflateCookie: _ga=GA1.4.2104063878.1399427134Connection: keep-aliveCache-Control: max-age=0
with thread 139711973316352
うに
いくら
なまこ
Handling client 192.168.24.53
1:tid = 14387
2:tid=14461
GET /index.html HTTP/1.1Host: 192.168.24.61:8000User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:28.0) Gecko/20100101 Firefox/28.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflateCookie: _ga=GA1.4.2104063878.1399427134Connection: keep-aliveCache-Control: max-age=0
with thread 139711973316352
うに
いくら
なまこ
Handling client 192.168.24.53
1:tid = 14387
2:tid=14492
GET /index.html HTTP/1.1Host: 192.168.24.61:8000User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:28.0) Gecko/20100101 Firefox/28.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflateCookie: _ga=GA1.4.2104063878.1399427134Connection: keep-aliveCache-Control: max-age=0
with thread 139711973316352
うに
いくら
なまこ
Handling client 192.168.24.53
1:tid = 14387
2:tid=14493
GET /index.html HTTP/1.1Host: 192.168.24.61:8000User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:28.0) Gecko/20100101 Firefox/28.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflateCookie: _ga=GA1.4.2104063878.1399427134Connection: keep-aliveCache-Control: max-age=0
with thread 139711973316352
うに
いくら
^C

socnetについて思い返してみます。

socket・ソケットを生成 →
bind・ソケットのバインド →
listen・接続準備 →
accept・接続待機

という事でしたので、これと一致しますね。

 

スレッドなので何回リロードしても、プロセスの数は増えない。
ブラウザから同時に3つアクセスしてみたら、スレッドIDが増えるだけでプロセスは1個の状態が続きました。

実験成功っす。

 

懸念点は、動作するプログラムはできたけれど、Pthreadについての勉強が足りていないことや、Defineで定義しているものはとりあえず書いただけ(笑)なとこ。

スレッドでこんなに時間がかかるとは予想していなかったので、ちょっと驚きでした。
こんどApacheのMPMを例にして、勉強してみたいと思います。

次で最後になります制限つきマルチタスクですね。

 

この4か月間、TCP/IP ソケットプログラミングC言語編をやるためにC言語の勉強をしてきた訳ですが、最初は呪文のように見えたソケットプログラミングも、サンプルコードを使って、自分で構成したりまでは、できるようになりました。

Perlが、これまた厄介でC言語と日本語と英語くらい違っててw
そろそろPerlの方をしっかりやりたいと思っているんだけど、もうちょっとだけ続くのじゃってやつで、もうちょいC言語を続けようと思ってるんだな。

 

参考サイト

Michael J. Donahooさんのページ

pthread プログラミング要項