おはようございます。かっちゃんです。
実家に帰ってるのですが、誘惑が多くてなかなかプログラムが進みません・・・。
さて、今回はネットワークプログラムについての話でもしようと思います。
知ってる人は知っているであろう、ポーリングという技術について・・・。
[0回]
ネットワークプログラミングでは、関数処理が実行されるまで待機し続ける関数があります。
read()、recv()、accsep() などですね。
普通にゲームに組み込んでしまうと、そこで一旦処理が止まってしまいます。
なんとかする必要があります。
最も一般的な解決策としてスレッドを用いるということだと思います。
これで、ゲームの処理ネットワークの処理を並列で行うことで、ゲームの処理は止めずに済みます。
普通はこれでOKです。
しかし、MMOのサーバーなどになると話が変わります。
スレッドでは問題が出てきます。
厳密にはスレッドは、CPUに処理させる内容を高速で切り換えているだけで、
並列処理と言っても、あくまで疑似的なものです。
そして、その高速な切り替えにも、わずかですが時間はかかります。
つまりCPUへの負荷があるわけです。
数千単位のユーザーが参加するであろうMMOサーバーで、
1クライアントにつき1スレッドを割り当ててしまうと、
切り替えだけでCPUへの負荷がものすごいことになります。
そこでポーリングの出番。
recv()などの問題点、それは関数の処理が実行されるまで待機することです。
じゃぁ実行出来る状況の時だけ呼べば、待機時間は発生しないじゃん。
イベント駆動ノンブロッキングというやつです。
その状況をrecv()とかの関数を呼ぶ前に調べてしまおう ってのがポーリングです。
Winソケットで例を。
必要な関数を用意
//fd_setオブジェクト(対象のソケットを保存するリストのようなもの)
fd_set fds_recv;
fd_set fds_send;
fd_set fds_error;
//対象ソケットの中で一番大きな値
int fds_max;
//調べる時の待ち時間
timeval fds_time;
//クライアントとの通信用ソケット(実際のサーバー等では、これがたくさんある)
SOCKET socket;
まず、ポーリングを行うソケットを登録します。
そうすると、そのソケットの状況を監視している状態になります。
//対象に登録します。
FD_SET(socket, &fds_recv);
FD_SET(socket, &fds_send);
FD_SET(socket, &fds_error);
//追加したソケットの中で一番大きなものを保存しておく
fds_max = socket;
//待ち時間
fds_time.tv_sec = 0; //秒
fds_time.tv_usec = 0; //マイクロ秒
次に、登録して監視しているソケットの現在の状況を取得します。
そこで使うのが今回の主役、select()関数さんです!
select()は、渡されたfd_setオブジェクトに登録されたソケットから、
それぞれの状況のものだけ選択して登録しなおします。
この時、オブジェクトが書き換えられてしまうので、一時変数を作ってコピーして使用しています。
//一時オブジェクト
fd_set fds_recv_temp;
fd_set fds_send_temp;
fd_set fds_error_temp;
//一時オブジェクトにコピー
memcpy(fds_recv_temp, fds_recv, sizeof(fd_set));
memcpy(fds_send_temp, fds_send, sizeof(fd_set));
memcpy(fds_error_temp, fds_error, sizeof(fd_set));
//各監視しているオブジェクトの状況を取得
select(fds_max+1, fds_recv_temp, fds_send_temp, fds_error_temp, fds_time);
これで、各一時オブジェクトの中にそれぞれの状況のソケットが入ります。
第2引数→読み込み(受信)可能なソケット(recvやacceptなどが可能)
第3引数→書き込み(送信)可能なソケット(sendなどが可能)
第4引数→エラーが起こったソケット
あとはそのオブジェクトの中に、指定のソケットがあるかどうか調べることで、状況が分かります。
それぞれの関数を呼ぶ前に調べてやればOKです。
char data[1024];
//ソケットが受信可能オブジェクトの中にあるかどうか
if (FD_ISSET(socket, fds_recv_temp))
{
recv(socket, data, 1024, 0); //あれば受信
}
//ソケットが送信可能オブジェクトの中にあるかどうか
if (FD_ISSET(socket, fds_send_temp))
{
send(m_socket, data, 1024, 0); //あれば送信
}
//ソケットがエラーが起こったオブジェクトの中にあるかどうか
if (FD_ISSET(socket, fds_send_temp))
{
//あれば良い感じのエラー処理
}
終了前には登録解除して上げます。
FD_CLR(socket, fds_recv);
FD_CLR(socket, fds_send);
FD_CLR(socket, fds_error);
以上です。
◆まとめ
①FD_SET() でソケット登録
②select() で状況取得
③FD_ISSET() で状況と比較
④FD_CLR() で登録解除
これで問題となっていた関数では一切止まることはなくなり、スレッドも使わずに済みます。
スレッド使わないので、設計も実装もデバッグもしやすい!
ゲームプログラムで一般的な、タスクシステムとの相性も良いです。
ということで、需要あるかどうかはわかりませんが
ポーリングを使ったイベント駆動ノンブロッキング実装による
シングルスレッドネットワークプログラミングでした。
ではまたお会いしましょう!
PR
COMMENT