通信データ最適化って何?


前章で取り上げたプログラムを以下のように変更し、通信データを最適化しても、変更前と同じ結果を出力することができます。

共通ヘッダ(common.h)

#ifndef __COMMON_H__
#define __COMMON_H__

//デバッグプリント
#define MESSAGE_STRING_WHATS_TIME_REQ "今何時。"
#define MESSAGE_STRING_WHATS_TIME_RES "%02d:%02dです。"
#define MESSAGE_STRING_ANYONE_THEARE_REQ_CS "おーい、だれかいますか。"
#define MESSAGE_STRING_ANYONE_THEARE_REQ_SC "おーい、だれかいますか。from %d"
#define MESSAGE_STRING_ANYONE_THEARE_RES_CS "はーい、います。to %d"
#define MESSAGE_STRING_ANYONE_THEARE_RES_SC "はーい、%dがいます。"

//メッセージタイプ
typedef enum _MESSAGE_TYPE
{
  MESSAGE_TYPE_WHATS_TIME_REQ,    // 今何時。
  MESSAGE_TYPE_WHATS_TIME_RES,    // XX:XXです。
  MESSAGE_TYPE_ANYONE_THEARE_REQ, // おーい、だれかいますか。
                                  // or
                                  // おーい、だれかいますか。from X 
  MESSAGE_TYPE_ANYONE_THEARE_RES, // はーい、います。to X
                                  // or
                                  // はーい、Xがいます。
} MESSAGE_TYPE;



//各種メッセージデータ構造

typedef struct _HOUR_MIN
{
  unsigned char hour;
  unsigned char min;
} HOUR_MIN;

typedef struct _WHATS_TIME_RES
{
  MESSAGE_TYPE message_type;
  HOUR_MIN hhmm;
} WHATS_TIME_RES;

typedef struct _ANYONE_THEARE_REQ
{
  MESSAGE_TYPE message_type;
  int from;
} ANYONE_THEARE_REQ;

typedef struct _ANYONE_THEARE_RES
{
  MESSAGE_TYPE message_type;
  int client_no;
} ANYONE_THEARE_RES;

#endif //__COMMON_H__

サーバ

#include <stdio.h>
#include <time.h>
#include <winsock2.h>
#include "common.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)
{
  MESSAGE_TYPE message_type;
  int commLen;

  //メッセージタイプ読み込み
  commLen = recv(commSock[no], (char*)&message_type, sizeof(message_type), 0);
  if(commLen == 0 || commLen == -1){
    //切断が発生
    goto disconnected;
  }

  switch(message_type){
  case MESSAGE_TYPE_WHATS_TIME_REQ:
    {
      time_t r;
      struct tm t;
      WHATS_TIME_RES res;

      PRINTF_WITH_TIME("[受信]\"" MESSAGE_STRING_WHATS_TIME_REQ "\"\n");

      r = time(NULL);
      localtime_s(&t, &r);

      res.message_type = MESSAGE_TYPE_WHATS_TIME_RES;
      res.hhmm.hour = t.tm_hour;
      res.hhmm.min = t.tm_min;
      PRINTF_WITH_TIME("[送信]クライアント[%d]へ\"" MESSAGE_STRING_WHATS_TIME_RES "\"\n", no, t.tm_hour, t.tm_min);
      send(commSock[no], (const char*)&res, sizeof(res), 0);
    }
    break;
  case MESSAGE_TYPE_ANYONE_THEARE_REQ:
    // おーい、だれかいますか。
    {
      int i;
      ANYONE_THEARE_REQ req;

      PRINTF_WITH_TIME("[受信]\"" MESSAGE_STRING_ANYONE_THEARE_REQ_CS "\"\n");

      req.message_type = MESSAGE_TYPE_ANYONE_THEARE_REQ;
      req.from = no;

      for(i = 0; i < CLIENT_SOCK_MAX; i++){
        if(i != no && commSock[i] != -1){
          PRINTF_WITH_TIME("[送信]クライアント[%d]へ\"" MESSAGE_STRING_ANYONE_THEARE_REQ_SC "\"\n", i, no);
          send(commSock[i], (const char*)&req, sizeof(req), 0);
        }
      }
    }
    break;
  case MESSAGE_TYPE_ANYONE_THEARE_RES:
    //はーい、います。to X
    {
      int client_no;
      ANYONE_THEARE_RES res;

      //client_noの部分を読み込み
      commLen = recv(commSock[no], (char*)&client_no, sizeof(client_no), 0);
      if(commLen == 0 || commLen == -1){
        //切断が発生
        goto disconnected;
      }

      PRINTF_WITH_TIME("[受信]\"" MESSAGE_STRING_ANYONE_THEARE_RES_CS "\"\n", client_no);

      res.message_type = MESSAGE_TYPE_ANYONE_THEARE_RES;
      res.client_no = no;
      PRINTF_WITH_TIME("[送信]クライアント[%d]へ\"" MESSAGE_STRING_ANYONE_THEARE_RES_SC "\"\n", client_no, no);
      send(commSock[client_no], (const char*)&res, sizeof(res), 0);
    }
    break;
  default:
    break;
  }

  return;

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

クライアント

#include <stdio.h>
#include <time.h>
#include <winsock2.h>
#include "common.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){
      //タイムアウトした場合
      MESSAGE_TYPE message_type;

      if(isWhatsTime){
        PRINTF_WITH_TIME("[送信]\"" MESSAGE_STRING_WHATS_TIME_REQ "\"\n");
        message_type = MESSAGE_TYPE_WHATS_TIME_REQ;
      }else{
        PRINTF_WITH_TIME("[送信]\"" MESSAGE_STRING_ANYONE_THEARE_REQ_CS "\"\n");
        message_type = MESSAGE_TYPE_ANYONE_THEARE_REQ;
      }
      send(commSock, (const char*)&message_type, sizeof(message_type), 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)
{
  MESSAGE_TYPE message_type;
  int commLen;

  //メッセージタイプ読み込み
  commLen = recv(commSock, (char*)&message_type, sizeof(message_type), 0);
  if(commLen == 0 || commLen == -1){
    //切断が発生
    goto disconnected;
  }

  switch(message_type){
  case MESSAGE_TYPE_WHATS_TIME_RES:
    {
      HOUR_MIN hhmm;

      //hhmmの部分を読み込み
      commLen = recv(commSock, (char*)&hhmm, sizeof(hhmm), 0);
      if(commLen == 0 || commLen == -1){
        //切断が発生
        goto disconnected;
      }

      PRINTF_WITH_TIME("[受信]\"" MESSAGE_STRING_WHATS_TIME_RES "\"\n", hhmm.hour, hhmm.min);
    }
    break;
  case MESSAGE_TYPE_ANYONE_THEARE_REQ:
    {
      ANYONE_THEARE_RES res;

      res.message_type = MESSAGE_TYPE_ANYONE_THEARE_RES;

      //client_noの部分を読み込み
      commLen = recv(commSock, (char*)&res.client_no, sizeof(res.client_no), 0);
      if(commLen == 0 || commLen == -1){
        //切断が発生
        goto disconnected;
      }

      PRINTF_WITH_TIME("[受信]\"" MESSAGE_STRING_ANYONE_THEARE_REQ_SC "\"\n", res.client_no);

      PRINTF_WITH_TIME("[送信]\"" MESSAGE_STRING_ANYONE_THEARE_RES_CS "\"\n", res.client_no);
      send(commSock, (const char*)&res, sizeof(res), 0);
    }
    break;
  case MESSAGE_TYPE_ANYONE_THEARE_RES:
    {
      int client_no;

      //client_noの部分を読み込み
      commLen = recv(commSock, (char*)&client_no, sizeof(client_no), 0);
      if(commLen == 0 || commLen == -1){
        //切断が発生
        goto disconnected;
      }

      PRINTF_WITH_TIME("[受信]\"" MESSAGE_STRING_ANYONE_THEARE_RES_SC "\"\n", client_no);
    }
    break;
  default:
    break;
  }
  
  return 0;

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

やり取りされるメッセージの内容は固定的に決まっており、たかだか数個です。そのため、メッセージを文字列としてそのまま送るのではなくメッセージを識別できる情報(MESSAGE_TYPE)をやりとりするだけでメッセージの交換は可能です。
例えば、「今何時。」というメッセージは、8バイト必要でしたが、これを4バイト(MESSAGE_TYPE型のみ)にして送信しています。
また、「今何時。」に対する返答や、「はーい、△がいます。」といったメッセージのように、変動する情報に関しては、MESSAGE_TYPEの後に付加するような形で送れば、受信側でメッセージを再構成することができます。


今回の変更は、送受信するデータのサイズを小さくすることに重きを置いて変更しました。しかしながら、メッセージによって処理が異なり(一回受信するだけのメッセージや二回受信が必要なメッセージがあったり、、、)、分かりやすさはいまいちです。例えば、送受信されるメッセージの最大サイズがたかだか8バイト程度なので、どのメッセージタイプのデータ構造も持つ共用体を一つ作成し、全てのメッセージをこの共用体データで送信することで、共用体をまるごと受信し、あとは共用体内のメッセージタイプ、詳細データの順に解析する、というシンプルな仕組みにすることもできます。

また、今回は計測はしていませんし、この程度の小さいプログラムでは大して差はないのかもしれませんが、処理速度も通信データ構造次第で変化してくるものです。


往々にして、処理速度・メモリ使用量・わかりやすさが、トレードオフの関係にあり、プログラム開発では何に重きを置いてプログラムを作るのか分析し開発することが必要です。分析には多種多様な経験が必要となってきます。今回の講座が、その多種多様な経験を積む上で、有用な基礎となり、貢献できることを祈って、本講座を締めたいとおもいます。

ありがとうございました。

コメントを残す

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

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>