サーバ側のプログラムは、
多数のクライアントに対応するため、
マルチクライアント化に
する必要があります。
マルチクライアントに対応していないと、
1クライアントが
サーバと通信している間は、
他のクライアントは通信できません。
正確にはlisten が完了した段階で、
3ウェイハンドシェークは完了できますが、
サーバとの次の通信では進めません。
したがって実用的なサーバであれば、
多くのクライアントとの通信に
耐えられなければいけません。
マルチクライアント化をするには
いくつか方法がありますが、
今回はマルチスレッドを採用します。
マルチスレッドを実装するために
pthreadライブラリを使用します。
今回行うマルチスレッドの
イメージはこんな感じです。
ちなみに並行処理できる
最大クライアント数は3です。

スレッド化する場所は
acceptの箇所です。
ちなみに、
もしacceptまで1つ流れで行い、
実際のサーバ処理(send-recv)を
マルチスレッド化した場合は、
acceptは常に確認している状態なので、
クライアントが次々と
無数につながってしまいます。
厳密にはlistenでキューに入った
クライアント数だけ並行処理出来ます。
今回はクライアント数を3にして、
4クライアント目で
サーバはaccept出来ないこと
確認してみます。
逆を言えば、3クライアントまでは
accept出来るとを実感してみます。
pthreadを使用しているので、
基礎的な使い方が分からない方は、
こちらも併せて読むと理解しやすいです。
【C】マルチスレッド&ミューテックスの使い方
https://wireless-network.net/multi-thread-mutex/
コード
それでは今回のソースはこちらです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
#include<stdlib.h> #include<stdio.h> #include<string.h> #include<sys/socket.h> #include<sys/types.h> #include<arpa/inet.h> #include<netinet/in.h> #include<netdb.h> #include<unistd.h> #include<pthread.h> #define NUM_THREAD 3 //並行処理可能な最大クライアント数 #define PORT 3030 //適当にポート指定 pthread_mutex_t mutex; //mutex int busy = -1; //mutex確保中のスレッドIDを入れる変数 int server_socket(void); void *accept_loop(void *arg); void handle_conn(int soc); //IPv4サーバソケットを作成 int server_socket(void){ int soc, opt=1; struct sockaddr_in addr; if((soc = socket(AF_INET, SOCK_STREAM, 0))<0){ perror("[-]socket()"); exit(1); } if(setsockopt(soc, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))==-1){ perror("[-]setsockopt()"); close(soc); exit(1); } memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(soc, (struct sockaddr*)&addr, sizeof(addr))==-1){ perror("[-]bind()"); close(soc); exit(1); } if(listen(soc, SOMAXCONN)==-1){ //SOMAXCONNはシステムが接続可能な最大数 perror("[-]listen()"); close(soc); exit(1); } return soc; } //マルチスレッド化する関数。acceptして次の処理関数をコール void *accept_thread(void *arg){ int soc, acc; struct sockaddr_in addr; socklen_t len; soc = *(int *)arg; pthread_detach(pthread_self()); //終了時にリソース解放する for(;;){ printf("[+]<%d> Start to lock\n", (int)pthread_self()); pthread_mutex_lock(&mutex); //mutex確保してaccept待ち busy = (int)pthread_self(); //私がbusy状態だと宣言。自分のスレッドIDを入れる printf("[+]<%d> Got lock\n", (int)pthread_self()); len = (socklen_t)sizeof(addr); if((acc=accept(soc, (struct sockaddr *)&addr, &len))==-1){ perror("[-]accept()"); printf("[-]<%d> Release lock\n", (int)pthread_self()); busy = -1; pthread_mutex_unlock(&mutex); }else{ printf("[+]Accept: %s (%d)\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port)); //accept成功したらクライアントのIPとポートを出力 printf("[+]<%d> Got lock\n", (int)pthread_self()); busy = -1; //mutexの役目終わるのでmutex開いたよ、という pthread_mutex_unlock(&mutex); //実際にmutexアンロック handle_conn(acc); //サーバ側で行う実際の処理。今回は文字を出力する。 close(acc); } } return (void *)0; } //処理関数(クライアントが入力した文字をサーバ側は標準出力) void handle_conn(int acc){ int len; char *buf; buf = (char *)malloc(1024); for(;;){ if((len=recv(acc, buf, 1, 0))==-1){ //1文字ずつ受信する perror("[-]recv()"); break; }else{ if(*buf=='\n'){ //エンター(改行)の場合はbreak putchar(*buf); break; } putchar(*buf); //文字を出力 buf += 1; //次の文字へ } } free(buf); } int main(void){ int soc; pthread_t thread_id; soc=server_socket(); //サーバソケット作成 pthread_mutex_init(&mutex, NULL); //mutexの初期化 for(int i=0; i<NUM_THREAD; i++){ if(pthread_create(&thread_id, NULL, accept_thread, (void*)&soc)!=0){ //スレッド作成 perror("[-]pthead_create()"); }else{ printf("[+]pthread_created: thread_id = %d\n", (int)thread_id); } } printf("[+]Ready for accept...\n"); //デバッグ用 for(;;){ sleep(10); printf("[+]<%d> State: lock: %d\n", getpid(), (int)busy); } close(soc); pthread_mutex_destroy(&mutex); //mutexを破壊 return 0; } |
コードの詳細と実行
用意する関数は4つです。
- main
- server_socket
- accept_thread
- handle_conn
main関数で
サーバソケットを作成し、
accept_threadの関数の
スレッドを複数作成します。
他の各関数については、
ソース中のコメントアウトで
説明してある処理を行います。
なお全てのスレッド(accept_thread)が
acceptで待っていると、
kernelでエラーを起こす場合があるらしいので、
mutexをロックしたスレッドのみが、
acceptで待つようになっています。
実際に動かしてみると
以下の感じになります。

実行して数十秒たったあとの
画面キャプチャです。
緑枠はスレッド作成が作成できたことを
意味し、スレッドIDが表示さています。
下4行のState lockを見ると、
今は2121824000のスレッドが
mutexをロックして、
acceptのところで待機しています。
実際にtelnetで接続してみましょう。
※上の画像とスレッドIDが違うのは、
気にしないでください。
プログラムを再実行させたためです。

3クライアントから接続しました。
緑枠で囲った箇所が
接続成功を意味しています。
次にこの画像を見て下さい。
まずは緑枠からです。

画像の右半分が、
3クライアント目の画面(Cygwin)です。
左半分がサーバ側の画面です。
クライアント側でaaaaaaaaaと、
入力してエンターを押します。
すると、サーバ側で入力された文字が
出力されて、接続が閉じます。
次に赤枠を見て下さい。
実は4クライアント目が接続待ち
していたため、3クライアント目が
コネクションを閉じた瞬間に
4クライアント目がつながりました。
赤枠の3行目はデバッグですが、
現在mutexをロックしている
スレッドIDを表示させています。
-1を表示しているので、
どのスレッドもmutexを
確保していないことが分かります。
つまりどのスレッドも、
handle_conn関数の処理中なので、
acceptで待っていないということです。
すぐには難しいと思うので、
実際に動かしてみると
理解しやすいです。
最後まで読んでいただきありがとうございました。
もっと詳しく知りたい方は、
下記の図書を参考にすると良いです。
ネットワークプログラミング全般から
マルチクライアント化についても
大変詳しく説明が載っています。
参考資料:
linux ネットワークプログラミングバイブル
著 : 小俣光之, 種田元樹