複数クライアント接続対応って何?


前述のサーバは、一つのクライアントの接続を受けて、そのクライアントに対して仕事をして、終了する、というあまり応用用途をイメージできない動作をしていましたが、ここでは、次のような仕様を持つアプリケーションを作ります。

  • サーバはクライアントの接続を最低3つ許容する。
  • サーバはいつでもクライアントから接続を受け付ける。
  • サーバは一方のクライアントと接続中に他方のクライアントとも接続できる。
  • サーバはクライアントから「今何時。」というメッセージを受信したら、「HH:MMです。」というメッセージを必ず返す。
  • サーバはクライアントから「おーい、だれかいますか。」というメッセージを受信したら、送信してきたクライアント以外がいれば、そのクライアントに対して「おーい、だれかいますか。from ○」メッセージを送信する(○:サーバに接続してきた順に付与されるクライアント識別番号)。クライアントはその「おーい、だれかいますか。from ○」というメッセージを受信したら、サーバに「はーい、います。to ○」を送信する。そして、サーバはその「はーい、います。to ○」を受信したら、「はーい、△がいます。」(△:「はーい、います。to ○」を返してきたクライアントの識別番号)を「おーい、だれかいますか。」を送信してきたクライアントに返す。
  • クライアントから切断されるまでは接続を維持する。
  • クライアントは20秒周期で「今何時。」と「おーい、だれかいますか。」を交互に送信する。

この仕様は汎用的な言い方をすると次のようなことを実現しているといえます。

  • 接続、切断の自由
  • サーバが保持する情報の共有
  • サーバ経由の他クライアントとの通信

です。


では、サーバプログラムを見ていきましょう。

#include <stdio.h>
#include <time.h>
#include <winsock2.h>

#define PRINTF_WITH_TIME(s,...) do{print_time();printf(s, ## __VA_ARGS__);}while(0)
#define CLIENT_SOCK_MAX (3)

SOCKET commSock[CLIENT_SOCK_MAX];
 
void comm(int no);

void print_time(void)
{
  time_t r;
  struct tm t;
  r = time(NULL);
  localtime_s(&t, &r);
  printf("[%02d:%02d:%02d]", t.tm_hour, t.tm_min, t.tm_sec);
}

int main(int argc, char** argv)
{
  WSADATA wsaData;
  SOCKET listenSock;
  struct sockaddr_in addr;
  struct sockaddr_in client;
  int len;


  fd_set readfds;
  int i;

  for(i = 0; i < CLIENT_SOCK_MAX; i++){
    commSock[i] = -1;
  }


  PRINTF_WITH_TIME("サーバプログラムスタート\n");

  // Winsock2 DLL 初期化
  WSAStartup(MAKEWORD(2,0), &wsaData);

  PRINTF_WITH_TIME("接続要求受信用ソケットの作成\n");
  listenSock = socket(AF_INET, SOCK_STREAM, 0);

  addr.sin_family = AF_INET;
  addr.sin_port = htons(12345);           //任意のポート番号
  addr.sin_addr.S_un.S_addr = INADDR_ANY; // 全てのアドレス
  PRINTF_WITH_TIME("接続要求受信用ソケットの設定\n");
  bind(listenSock, (struct sockaddr *)&addr, sizeof(addr));

  PRINTF_WITH_TIME("クライアントからの接続待ち準備\n");
  listen(listenSock, 5);


  while(1){

    FD_ZERO(&readfds);

    FD_SET(listenSock, &readfds);

    for(i = 0; i < CLIENT_SOCK_MAX; i++){
      if(commSock[i] != -1){
        FD_SET(commSock[i], &readfds);
      }
    }


    select(0, &readfds, NULL, NULL, NULL);

    if(FD_ISSET(listenSock, &readfds)){
      int s;

      // クライアントからの接続要求受付
      len = sizeof(client);

      PRINTF_WITH_TIME("クライアントからの接続要求受付\n");
      s = accept(listenSock, (struct sockaddr *)&client, &len);

      for(i = 0; i < CLIENT_SOCK_MAX; i++){
        if(commSock[i] == -1){
          PRINTF_WITH_TIME("クライアント[%d]の接続を受付完了\n", i);
          commSock[i] = s;
          break;
        }
      }
      if(i == CLIENT_SOCK_MAX){
        //接続中クライアントがいっぱいなので
        //受け付けたがすぐ切断
        closesocket(s);
      }
    }

    for(i = 0; i < CLIENT_SOCK_MAX; i++){
      if(FD_ISSET(commSock[i], &readfds)){
        comm(i);
      }
    }
  }

  PRINTF_WITH_TIME("切断\n");
  for(i = 0; i < CLIENT_SOCK_MAX; i++){
    if(commSock[i] != -1){
      closesocket(commSock[i]);
    }
  }
  closesocket(listenSock);

  // Winsock2 DLL 終了
  WSACleanup();

  return 0;
}


void comm(int no)
{
  char commBuf[256];
  int commLen;

  commLen = recv(commSock[no], commBuf, sizeof(commBuf), 0);
  if(commLen == 0 || commLen == -1){
    //切断が発生

    PRINTF_WITH_TIME("クライアント[%d]が切断\n", no);
    closesocket(commSock[no]);
    commSock[no] = -1;
    return;
  }

  commBuf[commLen] = '\0';
  PRINTF_WITH_TIME("[受信]\"%s\"\n", commBuf);

  if(strcmp(commBuf, "今何時。") == 0){
    time_t r;
    struct tm t;
    r = time(NULL);
    localtime_s(&t, &r);
    sprintf_s(commBuf, 256, "%02d:%02dです。", t.tm_hour, t.tm_min);

    PRINTF_WITH_TIME("[送信]クライアント[%d]へ\"%s\"\n", no, commBuf);
    send(commSock[no], commBuf, strlen(commBuf), 0);

  }else if(strcmp(commBuf, "おーい、だれかいますか。") == 0){
    int i;
    for(i = 0; i < CLIENT_SOCK_MAX; i++){
      if(i != no && commSock[i] != -1){
        sprintf_s(commBuf, 256, "おーい、だれかいますか。from %d", no);
        PRINTF_WITH_TIME("[送信]クライアント[%d]へ\"%s\"\n", i, commBuf);
        send(commSock[i], commBuf, strlen(commBuf), 0);
      }
    }
  }else if(strncmp(commBuf, "はーい、います。", strlen("はーい、います。")) == 0){
    int resp_no;
    sscanf_s(commBuf, "はーい、います。to %d", &resp_no);
    sprintf_s(commBuf, 256, "はーい、%dがいます。", no);
    PRINTF_WITH_TIME("[送信]クライアント[%d]へ\"%s\"\n", resp_no, commBuf);
    send(commSock[resp_no], commBuf, strlen(commBuf), 0);
  }

}

クライアントからの接続要求受付(listen)までの処理は、すでに説明したサーバプログラムと同様です。

クライアントからの接続要求や、クライアントからのメッセージはサーバに発生するイベントと考えることができます。そして、今回の仕様では、これらのイベントが、いつ、いかなるときに発生するか、サーバにはわかりません。

そのため、イベントが来るまで待機し、イベントが来たら処理する、という流れを繰り返しループ(while)の中で行います。

イベントは「あるソケットにデータが到着したので読めるよ!」、「あるソケットに接続要求が到着したから受付可能だよ!」といったものになります。
イベントが来るまでの待機に、selectを使っています。ソケットをfd_set型のデータ(readfds)に設定(FD_SET)し、さらにそのreadfdsをselectに設定することで、所望のイベントが到着するまで待機する、ということが実現できます。つまり、設定したソケットへの接続要求やデータの到着を、selectが監視してくれるようなイメージです。

イベントが発生するとselectが戻り、readfdsにイベント発生したソケットを判別するための情報が格納されます。

FD_ISSETマクロを使うことで、イベントが発生したソケットを知ることができるので、条件分岐させ、そのソケットに対する処理を行うことができます。


では、クライアントプログラムを見ていきましょう。

#include <stdio.h>
#include <time.h>
#include <winsock2.h>

#define PRINTF_WITH_TIME(s,...) do{print_time();printf(s, ## __VA_ARGS__);}while(0)
#define CYCLE_TIME_SEC (20)

SOCKET commSock;

int comm(void);

void print_time(void)
{
  time_t r;
  struct tm t;
  r = time(NULL);
  localtime_s(&t, &r);
  printf("[%02d:%02d:%02d]", t.tm_hour, t.tm_min, t.tm_sec);
}

int main(int argc, char** argv)
{
  WSADATA wsaData;
  struct sockaddr_in server;
  fd_set readfds;
  struct timeval tv;
  bool isWhatsTime = true;
  DWORD dwStart;

  PRINTF_WITH_TIME("クライアントプログラムスタート\n");

  // Winsock2 DLL 初期化
  WSAStartup(MAKEWORD(2,0), &wsaData);

  PRINTF_WITH_TIME("ソケットの作成\n");
  commSock = socket(AF_INET, SOCK_STREAM, 0);

  // 接続先データの準備
  server.sin_family = AF_INET;
  server.sin_port = htons(12345);
  server.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

  PRINTF_WITH_TIME("接続\n");
  connect(commSock, (struct sockaddr *)&server, sizeof(server));

  // CYCLE_TIME_SEC秒でselectタイムアウトするようにします
  tv.tv_sec = CYCLE_TIME_SEC;
  tv.tv_usec = 0;
  dwStart=timeGetTime();



  while(1){
    int ret;
    DWORD remain;


    FD_ZERO(&readfds);

    FD_SET(commSock, &readfds);

    ret = select(0, &readfds, NULL, NULL, &tv);
    remain = CYCLE_TIME_SEC*1000 - (timeGetTime()-dwStart);
    if(ret == 0 || remain <= 0){
      //タイムアウトした場合

      if(isWhatsTime){
        
        PRINTF_WITH_TIME("[送信]\"%s\"\n", "今何時。");
        send(commSock, "今何時。", strlen("今何時。"), 0);
      }else{
        PRINTF_WITH_TIME("[送信]\"%s\"\n", "おーい、だれかいますか。");
        send(commSock, "おーい、だれかいますか。", strlen("おーい、だれかいますか。"), 0);
      }
      isWhatsTime = (!isWhatsTime);
      tv.tv_sec = CYCLE_TIME_SEC;
      tv.tv_usec = 0;
      dwStart=timeGetTime();
      continue;
    }else{
      //残り時間設定
      tv.tv_sec = remain/1000;
      tv.tv_usec = (remain-tv.tv_sec*1000)*1000;
    }


    if(FD_ISSET(commSock, &readfds)){
      ret = comm();
      if(ret < 0){
        break;
      }
    }
  }

  PRINTF_WITH_TIME("切断\n");
  if(commSock != -1){
    closesocket(commSock);
  }

  // Winsock2 DLL 終了
  WSACleanup();

  return 0;
}


int comm(void)
{
  char commBuf[256];
  int commLen;

  commLen = recv(commSock, commBuf, sizeof(commBuf), 0);
  if(commLen == 0 || commLen == -1){
    //切断が発生

    PRINTF_WITH_TIME("サーバが切断\n");
    closesocket(commSock);
    commSock = -1;
    return -1;
  }

  commBuf[commLen] = '\0';
  PRINTF_WITH_TIME("[受信]\"%s\"\n", commBuf);

  if(strncmp(commBuf, "おーい、だれかいますか。", strlen("おーい、だれかいますか。")) == 0){
    int resp_no;
    sscanf_s(commBuf, "おーい、だれかいますか。from %d", &resp_no);
    sprintf_s(commBuf, 256, "はーい、います。to %d", resp_no);
    PRINTF_WITH_TIME("[送信]\"%s\"\n", commBuf);
    send(commSock, commBuf, strlen(commBuf), 0);
  }
  
  return 0;
}

サーバへ接続するまでの処理は、すでに説明したクライアントプログラムと同様です。
クライアントでは大きく二つのイベントが発生します。一つはサーバからのメッセージ到着、もう一つは20秒周期処理を実現するための「20秒経過しました」イベントです。サーバ同様、イベントが来るまで待機し、イベントが来たら処理する、という流れを繰り返しループ(while)の中で行います。
サーバと比較するとselectの使い方が若干異なっています。それは第5引数にタイムアウト時間を設定していることです。ソケットに対するイベントが発生せず、第5引数に設定した時間が経過した場合、selectは0を返します。この機能を利用し、20秒周期を実現しています。

timeGetTime()という関数を呼び出していますが、使用するにはwinmm.libというライブラリが必要です。前述のWinsock2ライブラリ(ws2_32.lib)と同じように追加してください。


では、実際に動作させてみましょう。

  1. まず、サーバを動作させます。
  2. クライアントを3つ動作させます。5秒ほど間隔を空けて、順に動作させると結果がわかりやすいです。Visual C++上で同じプログラムを3つ動作させることはできないので、直接実行ファイルを3つダブルクリックして起動させます。実行ファイルはプロジェクトフォルダの配下にあります。(下記、hogeというユーザでデフォルトのフォルダにmclientプロジェクトを作成した場合を例にしています。)
  3. 以下のような結果になるはずです。

    サーバ

    クライアント(0)

    クライアント(1)

    クライアント(2)


どうでしょうか。サーバが持つ情報の全クライアントでの共有、クライアント間のサーバ経由の通信ができていることがわかると思います。
また、クライアントを一つ終了させて(右上の×で終了)、さらに新しく起動させてみたりしてみてください。クライアントの接続、切断が自由にいつでもできることがわかると思います。


今回のプログラムは非常に小規模なものですので、それほど実感はないかもしれませんが、送受信しているデータがあまりイケてません。もっとシンプルにかつ保守しやすいように、「プロトコルって何?」で説明したような典型的な通信データへ最適化することができます。>>通信データ最適化って何?


comment

メールアドレスが公開されることはありません。

© 2024 BLuE AND PuRE