前回やったクライアント毎にプロセスを作成の続きで、今回は
クライアント毎にスレッドを作成
です。
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言語を続けようと思ってるんだな。
参考サイト
> gdbで同じことができるので、わりと簡単な方法なんでやっちゃいました。なお#defineのところはいまいちよく分かってません。
ソース内で使われてないので,ライブラリに与える環境変数的なものかな?と思いましたが,コンパイラに渡すマクロみたいですね.
例えば_XOPEN_SOURCEであれば
> (glibc 2.2 以降) 値が 600 以上の場合、 SUSv3 (UNIX 03; POSIX.1-2001 基本仕様 + XSI 拡張と同じ) 関連の定義と C99 での定義が追加で公開される。
※[1]より引用
おそらく普通にプログラムを書く上では不要かと思います.
書籍など読者がどのような環境でコンパイルしようとするか予測できないから明示的に示しているのではないでしょうか.
(完全に予想です(笑)
参考WEBサイト
[1] FEATURE_TEST_MACROS,http://linuxjm.sourceforge.jp/html/LDP_man-pages/man7/feature_test_macros.7.html
>かぼちゃさん
コメントありがとうございます。
>おそらく普通にプログラムを書く上では不要かと思います.
>書籍など読者がどのような環境でコンパイルしようとするか予測できないから明示的に
>示しているのではないでしょうか.
なるほどっすね。
記載いただいた参考サイトに
>公開される定義を制御することができる。
そんな感じなんだなで終わらせておきます(笑)
三十六計も大事ですよね。