過去ログ 10
過去ログは下にいくほど新しい記事となっております。
※注意:このサイトにあるプログラムのソースコードや、その他の情報はすべて無保証です。やばいと思ったら実行しないでください。コンパイル、実行などによって生じた損害に対する責任は負いませんので、ご了承ください。
SDL_net 2
12-12-2009
せっかくなので続きを。
前回はクライアント側から入力したものをサーバー側に表示し、サーバーからクライアント側にデータを送信、クライアント側に表示ということをやりました。
今回はこの大きな流れをもとに、いろいろやってみます。
今回はちょっとゲームっぽくしてみました。数字が大きいか小さいかのアドバイスを受けながら正しい値を当てる数当てゲームです。scanf、if文とかの練習でよくやるプログラムです。↓こんなの
#include <stdio.h> #include <stdlib.h> int main(){ int i; int ans = rand() % 101; char c[10]; for(;;){ printf("入力してください (0 - 100)\n"); /* 注意!数字を入れないとバグるゾ! */ scanf("%d", &i); /* エンターのコードゲット */ scanf("%c", c); if(ans > i){ printf("もっとおおきい\n"); } else if(ans < i){ printf("もっとちいさい\n"); } else { printf("あたり\n"); break; } } return 0; }
これをクライアントとサーバーの通信でやる必要があるのかどうかは置いておいて・・・
今回のプログラムの流れは次のとおり。
- クライアント側で入力(0~100)
- クライアント側から送信
- サーバー側処理
- 答えの数より大きい場合、「s」を返す。
- 答えの数より小さい場合、「l」を返す。
- 答えの数と同じ場合、「c」を返す。
- クライアント側受信処理(結果表示)
クライアントプログラムです。
#include <SDL/SDL.h> #include <SDL/SDL_net.h> #define REMOTE_HOST_IP "192.168.0.3" #define REMOTE_HOST_PORT 5555 #define ZERO_MEMORY(x, y) memset(x, 0, y) int g_s_thread_alive = 0; int send_thread(void* pointer){ char data[1024]; int value; char buffer[10]; int result; g_s_thread_alive = 1; TCPsocket client_sock = (TCPsocket)pointer; while(1){ fgets(buffer, sizeof(buffer), stdin); ZERO_MEMORY(data, sizeof(data)); value = atoi(buffer); if(value < 0){ printf("too small\n"); } else if(value > 100){ printf("too large\n"); } data[0] = value; result = SDLNet_TCP_Send(client_sock, data, strlen(data)); if(result < strlen(data)){ break; } } g_s_thread_alive = 0; return 0; } int recv_thread(void* pointer){ char data[1024]; int result; TCPsocket client_sock; client_sock = (TCPsocket)pointer; while(1){ result = SDLNet_TCP_Recv(client_sock, data, sizeof(data) - 1); if(result <= 0){ break; } data[result] = 0x00; if(data[0] == 's'){ printf("smaller\n"); } else if(data[0] == 'l'){ printf("larger\n"); } else { printf("correct\n"); } } return 0; } int main(int argc, char* argv[]){ IPaddress client_ip; TCPsocket client_sock; SDL_Thread *s_thread; SDL_Thread *r_thread; SDL_Init(SDL_INIT_EVERYTHING); SDLNet_Init(); SDLNet_ResolveHost(&client_ip, REMOTE_HOST_IP, REMOTE_HOST_PORT); client_sock = SDLNet_TCP_Open(&client_ip); do { if(client_sock != NULL){ printf("Open\n"); } else { printf("%s\n", SDLNet_GetError()); break; } s_thread = SDL_CreateThread(send_thread, client_sock); r_thread = SDL_CreateThread(recv_thread, client_sock); SDL_WaitThread(r_thread, NULL); if(g_s_thread_alive == 1){ SDL_KillThread(s_thread); g_s_thread_alive = 0; } SDLNet_TCP_Close(client_sock); } while(0); SDLNet_Quit(); SDL_Quit(); return 0; }
続いてサーバープログラムです。
#include <SDL/SDL.h> #include <SDL/SDL_net.h> #define SERVER_HOST 5555 #define MAX_CONNECTION 2 #define ANSWER_NUMBER 54 #define ZERO_MEMORY(x, y) memset(x, 0, y) typedef struct { int connection_no; TCPsocket sock; } thread_arg; typedef struct { SDL_Thread *thread; thread_arg arg; } thread_set; int thread_func(void* pointer){ char data[1024]; char retData[1024]; int result; int value; thread_arg *arg; TCPsocket accept_sock; arg = (thread_arg*)pointer; accept_sock = arg->sock; while(1){ result = SDLNet_TCP_Recv(accept_sock, data, sizeof(data) - 1); if(result <= 0){ printf("Host%d: DISCONNECT\n",arg->connection_no); break; } data[result] = 0x00; value = data[0]; ZERO_MEMORY(retData, sizeof(retData)); if(value < ANSWER_NUMBER){ retData[0] = 'l'; /* LARGER */ } else if(value > ANSWER_NUMBER){ retData[0] = 's'; /* SMALLER */ } else { retData[0] = 'c'; /* CORRECT */ } SDLNet_TCP_Send(accept_sock, retData, strlen(retData)); } arg->connection_no = -1; SDLNet_TCP_Close(accept_sock); return 0; } int main(int argc, char* argv[]){ IPaddress server_ip; TCPsocket server_sock; int i; thread_set t_set[MAX_CONNECTION]; for(i = 0; i < MAX_CONNECTION; i++){ t_set[i].arg.connection_no = -1; } SDL_Init(SDL_INIT_EVERYTHING); SDLNet_Init(); SDLNet_ResolveHost(&server_ip, NULL, SERVER_HOST); server_sock = SDLNet_TCP_Open(&server_ip); do { if(server_sock != NULL){ printf("OPEN\n"); } else { printf("%s\n", SDLNet_GetError()); break; } while(1){ for(i = 0; i < MAX_CONNECTION; i++){ if(t_set[i].arg.connection_no == -1){ t_set[i].arg.sock = SDLNet_TCP_Accept(server_sock); if(t_set[i].arg.sock != NULL){ printf("ACCEPT Host %d\n", i); t_set[i].arg.connection_no = i; t_set[i].thread = SDL_CreateThread(thread_func, &t_set[i].arg); } else { SDL_Delay(100); i--; } } } SDL_Delay(100); } for(i = 0; i < MAX_CONNECTION; i++){ if(t_set[i].arg.connection_no != -1){ SDL_KillThread(t_set[i].thread); SDLNet_Close(t_set[i].arg.sock); t_set[i].arg.connection_no = -1; } } SDLNet_TCP_Close(server_sock); } while(0); SDLNet_Quit(); SDL_Quit(); return 0; }
答えの数は今回は決め打ちで54となっていますので、答えを知ってしまってからはゲームではなくなりますが、ランダム数にすればゲームとして成立します。
また、前回は、入力した値をそのまま文字列(テキスト)として送信していましたが今回は、バイナリ1バイトのデータとして送信しています。1バイトは0から255までの数字を表現できますので、0から100の値は十分に表現できます。こうすると、たとえば前回のプログラムで78というデータを送る場合、7と8で2文字、2バイト必要だったのに対し、今回のプログラムでは1バイトで送信できます。
あまりけちけちするな、と言われるかもしれませんが、複数のデータをまとめて送る場合、全部のデータをテキスト形式にすると、すごく大きなサイズを必要とすることもありますし、長さが変わるかもしれないテキスト形式で送るよりも、何バイト目から何バイト目までが何々というデータというのがきちんと決まっている方が、処理しやすいことも多いです。
通信制御
今回のプログラムの流れを図にしてみました。黒い線が通信のやりとり、青い線が時間の流れです。
ところで、前回のプログラムもそうですが、受信と送信を別スレッドにしているので、送信と受信は独立して動かすことができます。そのため、
みたいな動きも可能になります。
送信した後、受信して、大きいか小さいかの判定を表示させたあとに再入力、という流れにしないと不自然なので、受信したのちに入力可能にします。
方法はいろいろあります(mutex、受信フラグ、etc...)が、今回は単純に受信フラグにしましょう。
クライアント側
#include <SDL/SDL.h> #include <SDL/SDL_net.h> #define REMOTE_HOST_IP "192.168.0.3" #define REMOTE_HOST_PORT 5555 #define ZERO_MEMORY(x, y) memset(x, 0, y) int g_s_thread_alive = 0; int g_recv_flag = 0; int send_thread(void* pointer){ char data[1024]; int value; char buffer[10]; int result; g_s_thread_alive = 1; TCPsocket client_sock = (TCPsocket)pointer; while(1){ fgets(buffer, sizeof(buffer), stdin); ZERO_MEMORY(data, sizeof(data)); value = atoi(buffer); if(value < 0){ printf("too small\n"); } else if(value > 100){ printf("too large\n"); } data[0] = value; printf("send!\n"); result = SDLNet_TCP_Send(client_sock, data, strlen(data)); if(result < strlen(data)){ break; } g_recv_flag = 0; while(g_recv_flag == 0){ SDL_Delay(20); } } g_s_thread_alive = 0; return 0; } int recv_thread(void* pointer){ char data[1024]; int result; TCPsocket client_sock; client_sock = (TCPsocket)pointer; while(1){ result = SDLNet_TCP_Recv(client_sock, data, sizeof(data) - 1); if(result <= 0){ break; } data[result] = 0x00; if(data[0] == 's'){ printf("smaller\n"); } else if(data[0] == 'l'){ printf("larger\n"); } else { printf("correct\n"); } g_recv_flag = 1; } return 0; } int main(int argc, char* argv[]){ IPaddress client_ip; TCPsocket client_sock; SDL_Thread *s_thread; SDL_Thread *r_thread; SDL_Init(SDL_INIT_EVERYTHING); SDLNet_Init(); SDLNet_ResolveHost(&client_ip, REMOTE_HOST_IP, REMOTE_HOST_PORT); client_sock = SDLNet_TCP_Open(&client_ip); do { if(client_sock != NULL){ printf("Open\n"); } else { printf("%s\n", SDLNet_GetError()); break; } s_thread = SDL_CreateThread(send_thread, client_sock); r_thread = SDL_CreateThread(recv_thread, client_sock); SDL_WaitThread(r_thread, NULL); if(g_s_thread_alive == 1){ SDL_KillThread(s_thread); g_s_thread_alive = 0; } SDLNet_TCP_Close(client_sock); } while(0); SDLNet_Quit(); SDL_Quit(); return 0; }
サーバー側プログラムに1秒ウエイトを追加すると、その効果を確認できます。
コンソールで動かしている関係上、ウエイトしてもキー入力を許可してしまいますが、受信してから送信するという流れができていることが確認できます。while(g_recv_flag==0)の部分をコメント化すれば、受信関係なく送信できてしまうことが確認できます。
タイムアウト処理
さて、最後に通信のタイムアウト処理について考えます。
通信は通常は別のコンピュータとのやりとりをしますので、常にすぐ応答が返ってくるとは限りません。しかし、応答が返ってこないと、クライアント側でまったく処理が進まなかったりするので、一定時間待って応答がこなかったら、再送処理をする、エラー処理をするなどの対策をしなければなりません。
SDLNet_TCP_Recv()にはタイムアウト処理がないため、タイムアウトの時間カウント、その後の処理などの自由に行えるスレッドを作成する必要があります。
ついでに、コンソールインプット処理を送信スレッドから切り離します。
クライアントプログラムです。ずいぶん大きくなってしまった。
#include <SDL/SDL.h> #include <SDL/SDL_net.h> #define REMOTE_HOST_IP "192.168.0.3" #define REMOTE_HOST_PORT 5555 #define SEND_FLAG_EMPTY 0 #define SEND_FLAG_SET 1 #define SEND_FLAG_SENT 2 #define SEND_FLAG_RECV 3 #define ZERO_MEMORY(x, y) memset(x, 0, y) #define TIMEOUT 700 /* タイムアウト */ /* 送信データセット */ typedef struct { int flag; /* 送信状態のフラグ */ char send_data[1024]; /* 送信データ */ int len; /* 送信データの長さ */ unsigned int time; /* 時間カウント用 */ } t_send_data; int copy_send_data(t_send_data*); int set_send_data(char*, int); int set_send_flag(int); int set_send_time(unsigned int); int check_timeout(int); int g_s_thread_alive = 0; /* 送信スレッド生存フラグ */ int g_r_thread_alive = 0; /* 受信スレッド生存フラグ */ int g_comm_thread_alive = 0; /* 監視スレッド生存フラグ */ int g_finished_flag = 0; /* 終了フラグ(監視スレッド停止用) */ SDL_mutex *g_send_data_mutex; t_send_data g_send_data; /* 送信スレッド 引数はソケット */ int send_thread(void* pointer){ t_send_data data; int result; TCPsocket client_sock = (TCPsocket)pointer; data.flag = SEND_FLAG_EMPTY; while(1){ /* データがセットされるまで待機 */ while(data.flag != SEND_FLAG_SET){ SDL_Delay(20); copy_send_data(&data); } /* printf("send!\n"); */ /* 送信処理 */ result = SDLNet_TCP_Send(client_sock, data.send_data, data.len); if(result < data.len){ printf("send error\n"); break; } data.flag = SEND_FLAG_SENT; /* 送信したときの時間を記録 */ set_send_time(SDL_GetTicks()); /* 送信フラグを更新 */ set_send_flag(data.flag); } /* 生存フラグをOFFにしておく */ g_s_thread_alive = 0; return 0; } /* 受信スレッド 引数はソケット */ int recv_thread(void* pointer){ char data[1024]; int result; TCPsocket client_sock; client_sock = (TCPsocket)pointer; while(1){ /* 受信まで待機 */ result = SDLNet_TCP_Recv(client_sock, data, sizeof(data) - 1); if(result <= 0){ printf("disconnect\n"); break; } data[result] = 0x00; if(data[0] == 's'){ printf("smaller\n"); } else if(data[0] == 'l'){ printf("larger\n"); } else { printf("correct\n"); } /* 受信したら、フラグを更新する */ set_send_flag(SEND_FLAG_EMPTY); } /* スレッド生存フラグをOFFにする */ g_r_thread_alive = 0; return 0; } /* スレッド監視スレッド */ int comm_thread(void* pointer){ IPaddress client_ip; TCPsocket client_sock; SDL_Thread *s_thread; SDL_Thread *r_thread; g_comm_thread_alive = 1; /* 通信の準備はこのスレッド内で行う */ SDLNet_ResolveHost(&client_ip, REMOTE_HOST_IP, REMOTE_HOST_PORT); do { /* ソケットオープン */ client_sock = SDLNet_TCP_Open(&client_ip); if(client_sock != NULL){ printf("Open\n"); } else { printf("%s\n", SDLNet_GetError()); break; } /* ここに置くのはちょっと変かもしれないが・・・ */ g_s_thread_alive = 1; g_r_thread_alive = 1; /* 送受信スレッドの作成 */ s_thread = SDL_CreateThread(send_thread, client_sock); r_thread = SDL_CreateThread(recv_thread, client_sock); if(s_thread == NULL){ g_s_thread_alive = 0; } if(r_thread == NULL){ g_r_thread_alive = 0; } while(1){ /* スレッドが停止したら、もう片方のスレッドも停止させ、自身も終了に向かう */ if(g_r_thread_alive == 0){ if(g_s_thread_alive == 1){ SDL_KillThread(s_thread); } break; } if(g_s_thread_alive == 0){ if(g_r_thread_alive == 1){ SDL_KillThread(r_thread); } break; } /* タイムアウトチェック */ if(check_timeout(TIMEOUT) != 0){ /* タイムアウト とりあえず今回はスレッドを終了させる */ printf("timeout\n"); if(g_s_thread_alive == 1){ SDL_KillThread(s_thread); } if(g_r_thread_alive == 1){ SDL_KillThread(r_thread); } break; } /* メインスレッドからの停止依頼 */ if(g_finished_flag == 1){ if(g_s_thread_alive == 1){ SDL_KillThread(s_thread); } if(g_r_thread_alive == 1){ SDL_KillThread(r_thread); } break; } SDL_Delay(20); } /* ソケットクローズ */ SDLNet_TCP_Close(client_sock); } while(0); /* 生存フラグOFF */ g_comm_thread_alive = 0; /* 通信終了 --- mutex解放(現在メインスレッドもmutex * 管理の対象になっているので終了させるために念のため、 * ここで解放しておく。本来は不要) */ SDL_mutexV(g_send_data_mutex); return 0; } /* 送信データにセットする処理 */ int set_send_data(char* data, int len){ int ret_value = 0; SDL_mutexP(g_send_data_mutex); do { if(len < 1 || 1024 < len){ ret_value = -1; break; } /* フラグチェック */ if(g_send_data.flag == SEND_FLAG_EMPTY){ /* SEND_FLAG_EMPTYであればセットする */ memcpy(g_send_data.send_data, data, len); g_send_data.flag = SEND_FLAG_SET; g_send_data.len = len; } else { ret_value = g_send_data.flag; break; } ret_value = 0; } while(0); SDL_mutexV(g_send_data_mutex); return ret_value; } /* 送信フラグの初期化 */ int init_send_data(){ SDL_mutexP(g_send_data_mutex); g_send_data.flag = SEND_FLAG_EMPTY; SDL_mutexV(g_send_data_mutex); return 0; } /* 送信データの取得 */ int copy_send_data(t_send_data* dst){ SDL_mutexP(g_send_data_mutex); memcpy(dst, &g_send_data, sizeof(t_send_data)); SDL_mutexV(g_send_data_mutex); return 0; } /* 送信フラグのセット */ int set_send_flag(int flag){ SDL_mutexP(g_send_data_mutex); g_send_data.flag = flag; SDL_mutexV(g_send_data_mutex); return 0; } /* 送信時間のセット */ int set_send_time(unsigned int time){ SDL_mutexP(g_send_data_mutex); g_send_data.time = time; SDL_mutexV(g_send_data_mutex); return 0; } /* タイムアウトチェック */ int check_timeout(int timeout){ unsigned int time; unsigned int tick; int diff; int flag; SDL_mutexP(g_send_data_mutex); flag = g_send_data.flag; if(flag == SEND_FLAG_SENT){ time = g_send_data.time; } tick = SDL_GetTicks(); SDL_mutexV(g_send_data_mutex); /* フラグチェック */ if(flag != SEND_FLAG_SENT){ /* SEND_FLAG_SENT(送信済み)でなければ抜ける */ return 0; } /* タイムアウトの計算 */ diff = tick - time; if(diff < 0){ diff = time - 0xFFFFFFFF + tick + 1; } if(diff >= timeout){ return -1; } return 0; } /* メイン関数 */ int main(int argc, char* argv[]){ SDL_Thread *thread; char buffer[10]; char data[1024]; int ret_value; int value; SDL_Init(SDL_INIT_EVERYTHING); SDLNet_Init(); /* ミューテックスの作成 */ g_send_data_mutex = SDL_CreateMutex(); /* 送信フラグの初期化 */ init_send_data(); /* スレッド監視スレッドの作成 */ thread = SDL_CreateThread(comm_thread, NULL); do { if(thread == NULL){ break; } while(1) { fgets(buffer, sizeof(buffer), stdin); /* Eが入力されたら終了 */ if(buffer[0] == 'E'){ /* スレッド終了依頼 */ g_finished_flag = 1; /* スレッド終了まで待機 */ SDL_WaitThread(thread, NULL); } if(g_comm_thread_alive == 0){ /* 監視スレッドが終了していたら終了 */ break; } ZERO_MEMORY(data, sizeof(data)); value = atoi(buffer); if(value < 0){ printf("too small\n"); } else if(value > 100){ printf("too large\n"); } data[0] = value; /* 送信データにセット */ ret_value = set_send_data(data, 1); if(ret_value != 0){ printf("Couldn't send!\n"); } } } while(0); /* ミューテックス破棄 */ SDL_DestroyMutex(g_send_data_mutex); printf("program quit.\n"); SDLNet_Quit(); SDL_Quit(); return 0; }
スレッドを受信用、送信用に加えてそれら2つのスレッドを監視するスレッドを作成しました。送信、受信のスレッドが停止した場合はもう片方のスレッドを停止させる。またタイムアウトはTicktimeというものを監視して調べます。SDL_Ticks()でTicktimeを取得しています。SDL_Ticks()はSDLライブラリの起動、つまりプログラムの起動から経過した時間をミリ秒単位で返します。調べた結果、タイムアウトとして設定した時間(ミリ秒数)を経過したら、送信、受信、両方のスレッドを停止させ、自身も終了します。
スレッド監視用スレッドはメインスレッド(メイン関数のスレッド、今回はメイン関数自身)で監視しており、監視用スレッドが終了したら、プログラム終了処理に入ります。なお、今回のプログラムではメインスレッドでfgetsによるコンソール入力を受け付けており、監視スレッドが終了したら、即プログラムを終了させるということはできませんが、コンソール入力を分離する、またはコンソール入力ではなく、実際のSDLのグラフィカルな画面からの操作に切り替えれば、即終了させることも可能になります。
送信データはグローバル変数の構造体で管理しています。同時アクセスされるとまずいので、mutex制御を入れてあります。
構造体のflagがSEND_FLAG_EMPTYのときのみデータセットが可能になっていますので、送信前、送信後受信前に入力してもデータセットがされない作りになっています。
なお、コンソール入力で「E」を入力したら終了するようにしました。
※:あまり動きを確認していないのでバグがあるかもしれません。注意してください。
※メール、リンクは適当に。
Copyright (c) 2009 greencap