トップ 戻る猫でもわかるネットワークプログラミング

デスクトップ送信実験

 今回は、WinSockである程度まとまったデータをネットワークで送る際のパフォーマンスや処理方式を検討するために、現在のディスプレイの画像を送受信してみます。最近の画面解像度はXGA(1024*768)あたりが最低線で、これをフルカラー(1ピクセル3バイト)で送るとなるとデータ量は3MB程度にはなるでしょう。このくらいのデータ量なら、データの送受信にも、ある程度の時間がかかるはずです。そこで、送受信のパラメータをいろいろ変えてみてデータ転送の効率がどうなるのか、試してみることにしました。

デスクトップ画像の生成と送信

 今回は、画像を送信する側と受信する側2つのプログラムを作成します。まず最初に送信側のプログラムを作ってみましょう。送信プログラムは、単に画像を生成・送信するだけなのでコマンドラインで動くプログラムにしてみました。

 画像データを送信するには、最初に送信する画像データを作らないといけませんね。今回は現在のディスプレイを画像として送信するので、ディスプレイのデバイスコンテキストを取得し、それを24ビットDIBSectionに描画して24ビットDIBピクセル列を取得することにしました。

  /* ディスプレイのデバイスコンテキスト作成 */
  hdc = CreateDC("DISPLAY", NULL, NULL, NULL);

  /* 画面サイズ取得 */
  iWidth = GetDeviceCaps(hdc, HORZRES);
  iHeight = GetDeviceCaps(hdc, VERTRES);

  BITMAPINFO biInfo;

  /* DIBSection 用BITMAPINFO構造体設定 */
  biInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
  biInfo.bmiHeader.biWidth = iWidth;
  biInfo.bmiHeader.biHeight = iHeight;
  biInfo.bmiHeader.biPlanes = 1;
  biInfo.bmiHeader.biBitCount = 24;
  biInfo.bmiHeader.biCompression = BI_RGB;
  biInfo.bmiHeader.biSizeImage = 0;
  biInfo.bmiHeader.biXPelsPerMeter = 0;
  biInfo.bmiHeader.biYPelsPerMeter = 0;
  biInfo.bmiHeader.biClrUsed = 0;
  biInfo.bmiHeader.biClrImportant = 0;

  /* BITMAPINFOと画面のDCからDIBSectionを作成 */
  hBMP = CreateDIBSection(hdc, &biInfo, DIB_RGB_COLORS,
           (LPVOID *)&lpPixels, NULL, 0);

  /* メモリDCを作成 */
  hdcMem = CreateCompatibleDC(hdc);

  /* メモリDCにDIBSectionを選択 */
  hOld = (HBITMAP)SelectObject(hdcMem, hBMP);

  /* 画面全体をDIBSectionのビットマップに描画 */
  BitBlt(hdcMem, 0, 0, iWidth, iHeight, hdc, 0, 0, SRCCOPY);

 これでDIBSectionのピクセル列lpPixelsに画面の画像データが得られました。

 画像データができたら、これを送信します。今回は、通信のプロトコルを以下のように定めました。

  1. データ受信側(サーバー)は、ポート7000番で送信側(クライアント)からの接続を待つ。
  2. クライアントは、サーバーに接続したらまず画像サイズを縦横各4バイト、計8バイトで送る。
  3. サーバーは取得した画像サイズを元に受信準備を行い、1バイトの応答を返す。
  4. クライアントはサーバーの応答を待って、実際の画像データを送信する。
  5. 送信が終わったら、ソケットを閉じる。

 このプロトコルでは、途中のエラーは一切考慮しません。送信側で行う処理は、受信側に接続して8バイトの画像サイズを送り、受信側からの応答を待つ。応答があったら、あとは画像のピクセル列をそのまま送信して接続を切る、という流れになります。

 送信するデータ総量は、先頭の画像サイズデータ8バイトと1ピクセル3バイトの画像データなので、

  /* 送信データサイズ計算 */
  iBufferSize = 8 + iWidth * iHeight * 3;

 となります(通常画面の横幅は4の倍数ピクセルなので、今回は4バイト境界は考慮しません)。バッファサイズが求まったら、実際にバッファを確保しデータを書き込んでおきましょう。

  /* 送信データ用バッファ確保 */
  lpBuf = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, iBufferSize);

  /* バッファ先頭に画像サイズ書き込み */
  ((int *)lpBuf)[0] = iWidth;
  ((int *)lpBuf)[1] = iHeight;

  /* バッファに画面の画像データをコピー */
  CopyMemory(lpBuf+8, lpPixels, iWidth * iHeight * 3);

 これで送信するデータができたので、実際にサーバーと通信を開始します。まず、通信ソケットの作成は、サーバーのアドレス(lpszAddr)とポート(wPort)を元に以下のようにします。

  WSAStartup(MAKEWORD(1,1),&wdData);

  dwAddr = inet_addr(lpszAddr);

  sConnect = socket(PF_INET,SOCK_STREAM,0);

  ZeroMemory(&saConnect, sizeof(sockaddr_in));
  ZeroMemory(&saLocal, sizeof(sockaddr_in));

  saLocal.sin_family = AF_INET;
  saLocal.sin_addr.s_addr = INADDR_ANY;
  saLocal.sin_port = 0;

  bind(sConnect, (LPSOCKADDR)&saLocal, sizeof(saLocal));

  saConnect.sin_family = AF_INET;
  saConnect.sin_addr.s_addr = dwAddr;
  saConnect.sin_port = htons(wPort);

  /* サーバーに接続 */
  connect(sConnect, (sockaddr *)(&saConnect), sizeof(saConnect);

 通信に使うソケットができたら、最初にバッファの先頭8バイト(画像サイズ)を送信し、サーバーから来るはずの1バイトの応答を待ちます。

  /* 画像サイズを送信 */
  send(sConnect, (char *)lpBuf, 8, 0);

  /* 送信済みサイズ設定 */
  iSend = 8;

  // サーバーからの応答待ち
  while(recv(sConnect,&data,1,0) != 1);

 サーバーから応答があったら、あとはバッファの内容をすべて送信するだけです。送信時には、send()の引数に送信サイズを指定するので、今回はその数値(iLineSize)に最大iSendSizeを指定するようにしました。
 送信ループでは、送信量がバッファサイズに達するまで、send()を繰り返します。

  /* データ送信 */
  while (iSend != iBufferSize) {

      /* データ送信サイズを計算 */
      if (iLineSize > iBufferSize - iSend) {
          iLineSize = iBufferSize - iSend;
      } else {
          iLineSize = iSendSize;
      }

      /* データ送信 */
      iLineSize = send(sConnect, (char *)(lpBuf + iSend),
                  iLineSize, 0);

      iSend += iLineSize;

  }

  closesocket(sConnect);

画像受信処理

 送信処理はできたので、次は受信側です。受信側は、受信した画像を表示するので画像表示用のウインドウを持つWindowsアプリにしました。最初に受信用のスレッドを作成し、そのスレッドでクライアントからの通信を待ち、受信処理を行います。

 通信スレッド関数threadfunc()では、最初にポート7000番に待機用のソケットを作成して接続されるのを待ちます。

  sWait = socket(PF_INET, SOCK_STREAM, 0);

  ZeroMemory(&saLocal, sizeof(saLocal));

  /* ポート7000番に接続待機用ソケット作成 */
  saLocal.sin_family = AF_INET;
  saLocal.sin_addr.s_addr = INADDR_ANY;
  saLocal.sin_port = htons(wPort);

  bind(sWait, (LPSOCKADDR)&saLocal, sizeof(saLocal));

  listen(sWait, 2);

  iLen = sizeof(saConnect);

  /* sConnectに接続受け入れ */
  sConnect = accept(sWait, (LPSOCKADDR)(&saConnect), &iLen);

 ソケットsConnectで接続を確立したら、最初に画像サイズの8バイトを受信して画像データ用バッファを確保します。

  iRecv = 0;

  // 最初に8バイト受信
  while (iRecv < 8) {

      iRecvSize = recv(sConnect, &data, 1, 0);

      if (iRecvSize > 0) {

          szBuf[iRecv] = data;
          iRecv += iRecvSize;

      }

  }

  // 受信データから画像サイズを設定
  iWidth = *(int *)szBuf;
  iHeight = *((int *)(szBuf + 4));

  // 画像データサイズを計算
  iSize = iWidth * iHeight * 3;

  // 画像データ用バッファ確保
  lpPixels = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, iSize);

 画像データを受信するバッファを確保したら、クライアントに応答し、実際に画像データを受信。受信時のrecv()でも受信サイズを指定するので、そのサイズを最大iGetSizeバイトになるよう調整します。

  iRecv = 0;

  /* 画像データ受信 */
  while(iRecv < iSize) {

      /* 受信単位のサイズ計算 */
      if (iGetSize > iSize - iRecv) {
          iRecvSize = iSize - iRecv;
      } else {
          iRecvSize = iGetSize;
      }

      /* データ受信 */
      iRecvSize = recv(sConnect, szBuf, iRecvSize, 0);

      if (iRecvSize > 0) {

          CopyMemory(lpPixels + iRecv, szBuf, iRecvSize);

          iRecv += iRecvSize;

      } else {
          break;
      }

  }

 データを受信したら、ソケットを閉じて受信した画像データ用の24ビットDIBヘッダを作成します。そして、画像表示フラグをセットし、画面に画像を表示。

  shutdown(sConnect, 2);
  closesocket(sConnect);

  closesocket(sWait);

  /* DIB用BITMAPINFO構造体設定 */
  biInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
  biInfo.bmiHeader.biWidth = iWidth;
  biInfo.bmiHeader.biHeight = iHeight;
  biInfo.bmiHeader.biPlanes = 1;
  biInfo.bmiHeader.biBitCount = 24;
  biInfo.bmiHeader.biCompression = BI_RGB;
  biInfo.bmiHeader.biSizeImage = 0;
  biInfo.bmiHeader.biXPelsPerMeter = 0;
  biInfo.bmiHeader.biYPelsPerMeter = 0;
  biInfo.bmiHeader.biClrUsed = 0;
  biInfo.bmiHeader.biClrImportant = 0;

  /* 画像受信フラグセット */
  bPixels = true;

  /* ウインドウ再描画 */
  InvalidateRgn(hwMain, NULL, FALSE);
  UpdateWindow(hwMain);

 これで、クライアントでのデータ作成・送信とサーバーでのデータ受信・画像表示という一連の流れができました。

送受信実験

 では、実際にサーバー・クライアントプログラムを使って画像の送受信実験を行ってみましょう。今回、送受信の効率に影響しそうなのはsend()/recv()で指定する送受信のデータ単位サイズ(iSendSize/iGetSize)です。そこで、この2つの大きさをいろいろ変えて画像を送受信してみることにしました。

 まず、ローカルマシン(Windows XP/1152*864ピクセル)で試してみたところ、経過時間の値が正の値にならずクライアント側の測定ができないことが多いです(^^;。大体、測定できる時はクライアント側が600-800Mbps、サーバー側が60Mbps程度になるようですね(といっても、測定できないこともあるのでこの数値の精度にも問題がある気がしますが)。

 次に100Base接続のセカンドマシン(Windows Me/1024*768ピクセル)にクライアントを入れて、メインマシンのサーバーと通信し、ネットワーク経由(LAN)の通信時間を測定してみました。その結果が以下の表です。表は、横軸がクライアント側のiSendSize、縦軸がサーバー側のiGetSizeです。測定データは、上がクライアント、下がサーバー側の送受信処理時間(ビットレート)という形になっています。プログラムは、Borland C++ 5.5.1でビルドした物を使用しました。

51212832
512271(69.6Mbps)
318(59.3Mbps)
294(64.2Mbps)
320(59.0Mbps)
275(68.6Mbps)
294(64.2Mbps)
128290(65.1Mbps)
308(61.3Mbps)
279(67.7Mbps)
305(61.9Mbps)
291(64.9Mbps)
318(59.3Mbps)

 なんか、あまり差がないですね。このあたりの値は、あまり極端な設定をしなければ大差がないのでしょうか。

使用法

 まず、クライアントプログラムの最初の方にあるアドレス設定

  LPCTSTR lpszAddr = "127.0.0.1";

 を環境に合わせて書き換えてください。そして、クライアントとサーバー両方のプログラムをビルドしたら、まずサーバーを起動します。続いてクライアントを起動すると、クライアントプログラムが動いているマシンのデスクトップ画像がサーバープログラムに送られ表示されます。結果を確認したら、両方のプログラムを終了しましょう(一度送信したら、必ずサーバーも終了してください)。

クライアントプログラム
サーバープログラム

トップ 戻る