ネットワークゲームへのアプローチ 2  ソケット(サーバ)


スレッドの基本がわかったところで次はソケットです。ウィンドウズで使うのはWinsockというものですが、これはUNIXのソケットをウィンドウズ用に移植したものです。もともとがUNIXなので、使い方が特殊というか、くどいというか、とにかくつかいづらい代物です。私は理解するのに1ヶ月以上かかりました。ここではなるべくわかりやすく解説してみたいと思います。

まず、ソケットというのはデータの受け渡しをするためのもの、と考えてください。一言では説明できないので、まずはこういうものがあって、それはデータの受け渡しに使うものだ、と受け入れるわけです。説明するうちに徐々に理解できると思います。

まず、ソケットを動かしてみます。使用するのはコンソールプログラムです。

最初に作るのはサーバーです。・・・えー!!いきなりサーバーですか??  そうサーバーなんです。

サーバーというのは単に要求を受け取る方という意味です。御大層なプログラムをいきなり組むわけではありません。データのやり取りをするためには、常にデータを待ち構えている方と、自分の都合のよいときにデータを送ってなにかを要求するほうがあります。都合のよいときにデータを送ってサーバに何かの仕事を要求する方をクライアントといいます。

クライアントは常にソケットを開けておく(?)必要はありません。ですが、サーバーはいつでもデータを受け入れられるように常にソケットを開けておきます。(ソケットを開けるという言い方が普通なのかどうかわかりませんが・・・)開けておくだけではなく、常に監視していないといけません。ここが前項でやったスレッドを使う部分ですね。

このサーバとクライアントの関係は、HTTPサーバ(ホームページを送信する)とネットスケープなどのブラウザの関係と同じです。ゲームの場合、データは双方がやり取りするのでどちらもサーバであり、クライアントでもあります。便宜上、どちらかがサーバになる場合が普通ですが。

このプログラムの実験で厄介なのはサーバとクライアントがそろわないと動かせないし動いてもちゃんと動作しているかどうか、わからない点ですが・・・。でもご安心を。クライアントは後で作るとして、とりあえずはブラウザにクライアントの役割をしてもらえば良いのです。

ブラウザからのデータを自作のサーバで受けるというわけです。ブラウザはネットスケープでもIEでもかまいません。
まずサーバの動作確認をしてからクライアントを作ります。

--------------------------------------------------------------------------------

ここでソケットの説明をちょっとしましょう。

ソケットとはなにか?まずそれはオブジェクトです。ソケットはクラスとして定義されています。そのオブジェクトを作るとそれを利用してほかのサーバやクライアントと通信ができるわけです。プログラムをちょっとやっているとどういうようにつながるのか、不思議な気がします。このあたりは理解していなくても構わないので面倒な話が嫌いな人は飛ばしてください。

通常、プログラム側から見える範囲というのは限られています。プログラムのソースの中でさえ、見える場所(アクセスできる関数や、変数)は限定されています。アプリケーションでいえば読み込むビットマップファイルなどは同じディレクトリに存在するか、ほかのディレクトリにある場合は絶対パスを指定しないと見えません。

ソケットはどうでしょうか?

ソケットにはこの境界がありません。これはソケットが一つの空間を共有する、とも言えます。ソケットがあれば、それが同じコンピュータにあろうが、地球の裏側にあろうが(ネットワークにつながっていればの話ですが)同じように扱える、という意味です。

自分の部屋にコンピュータがあって、それがネットにつながっていればほかのコンピュータと交信が出来るわけですが、これとほぼ同じ事がプログラム上で行えるということも出来ます。ちょっと違うのはプログラムでは必要に応じてコンピュータ(ソケット)をいくらでも何度でも作れるということですね。

説明その2 ソケットとは、プログラムの中に作成する、トランシーバみたいなものです。
それを使うと、プログラムはどこかと交信することができるのですね。

--------------------------------------------------------------------------------

実際に組む最初のコードはコンソールで作ります。

コンソールプログラムの単純アプリケーションを選択してください。

プロジェクト>>設定>>リンクのオブジェクト/ライブラリモジュールに wsock32.lib を追加します。

最初の実験は以下のとおり
 

 #include "stdafx.h"
 
 #include <windows.h>
 #include <winsock.h>
 #include <process.h>
 #include <iostream.h>
 #include <stdio.h>
 
 void SockTread(void *sock);
 
 int main()
 {
 
 //----- サーバのコード
 WSADATA Data;
 int result = WSAStartup (MAKEWORD(1,1), &Data);
 // m_List1.AddString(" int result = WSAStartup (MAKEWORD(1,1), &Data);");
 cout << "WSAStartup (MAKEWORD(1,1), &Data);" << endl;
 
 if (WSAStartup(MAKEWORD(1,1), &Data)!=0)
 {
 // m_List1.AddString("Server : Winsock DLL ロードに失敗しました");
  cout << "Server : Winsock DLL load error" << endl;
  return 0;
 }
 //----- WinsockDLLの初期化に成功した -----------------------
 // m_List1.AddString("Server : Winsock DLL ロードしました");
 cout << "Server : Winsock DLL load success" << endl;
 
 //☆☆☆☆☆☆☆☆☆☆☆☆
 
 //-----WinsockDLLの後処理-----------------------------------
 result = WSACleanup();
 
 if (result == 0)
 // m_List1.AddString("WSAStartup clennup 良好に終了しました");
  cout << "WSAStartup clennup test end" << endl;
 else
 // m_List1.AddString("WSAStartup cleanup error");
  cout << "WSAStartup cleanup 失敗しました" << endl;
 
  return 0;
 }
 

まずはDLLをロードします。Winsockを使うにはWsock.DLLというライブラリをロードする(使えるようにセッティングする)必要があります。それをやってくれるのがWSAStartup()という関数です。うまく行くと0を返します。

ロード出来たら次に星印部分でソケットを構築し、接続し、データを送り出す、という作業を行います。まずはDLLをロードしたのを確認してDLLを開放します。これをやっていくれるのがWSACleanup()という関数です。

それぞれメッセージが出るようになっているので実行して見てください。



こんな風になればちゃんと動いています。

--------------------------------------------------------------------------------

DLLがちゃんとロードできたら次はいよいよソケットの制作です。ちょっとややこしいのですが、一つづつ見ていけば理解できます。このコードは上にあるコートの☆ジルシ部分に入れます。
 

 //----- ソケットを作る
 SOCKADDR_IN svAddress, clAddress;// アドレスとポートを指定するためのデータ構造
 SOCKET svSocket, clSocket;

 memset(&svAddress, // svAddress を初期化する
 0 , // 設定する文字
 sizeof(SOCKADDR_IN)); // 文字数

 svAddress.sin_family = AF_INET; // 定型いつも同じ
 svAddress.sin_port = htons(1225);// ポートを指定する
 svAddress.sin_addr.s_addr=htonl(INADDR_ANY); // ここにアドレスがくるはずだが・・・・・
 // svAddress.sin_addr=htonl("128.0.0.1");//INADDR_ANY); // ここにアドレスがくるはずだが・・・・・

 svSocket = socket(AF_INET, // 定型いつも同じ
 SOCK_STREAM, // tcp/ipの場合はこれ
 0); // 最後は0にする
 
 if (svSocket == INVALID_SOCKET)
 {
  // m_List1.AddString("Server Socket make error");
  cout << "Server Socket make error" << endl;
  return 0;
 }
 //----- ソケットを作ことができた
 // m_List1.AddString("Server ソケットを作りました");
 cout << "Server ソケットを作りました" << endl;
 
 //----- ソケットのアドレスを指定する
 result = bind( svSocket,
 (const struct sockaddr *) &svAddress,
 sizeof(svAddress));
 if (result == SOCKET_ERROR)
 {
 // m_List1.AddString("Server Socket アドレス指定が失敗しました");
  cout << "Server Socket アドレス指定が失敗しました" << endl;
  return 0;
 }
 //----- ソケットのアドレスを指定することができた
 // m_List1.AddString("Server Socket アドレス指定が成功しました");
 cout << "Server Socket アドレス指定が成功しました" << endl;
 //☆☆☆☆☆☆☆☆☆☆☆☆
 
 //----- ソケットをクロースする
 closesocket(svSocket);
 // m_List1.AddString("Server ソケットをクローズしました");
 cout << "Server ソケットをクローズしました" << endl;
 

☆ジルシ部分にはソケットを開けたあとにデータを受信するプログラムが入ります。

でもここではソケットがちゃんと出来ているか確認します。



こんな風になればOKです。

アドレスとポートの番号を指定しています。クライアントは接続するときにこのアドレスとポート番号を使います。アドレスはINADDR_ANYが指定されていますが、こうすると128。0。0。1というアドレスになります。なぜかはまだ解明されていません。好きな番号に出来るようになったらこの項は書き直す予定です。

127・0・0・1は知っている人は知っている、プロキシサーバのアドレスです。なんと勝手にプロキシになってしまっています。ここまでくると、どうやってブラウザからアクセスするかわかった人がいるかな?

ブラウザの接続のプロパティをPROXYを使う、にして、ポート番号を1225にすれば良いのです。サーバが出来たら早速やってみましょう。ポート番号は好きに決めてかまいませんが、0ー1024まではシステムで決まっているのでそれ以上の番号を使うのが余計なトラブルを避ける知恵ですね。

ソケットは使いおわったらちゃんと閉じなければいけません。

--------------------------------------------------------------------------------

次はいっきにスレッドまでいってみましょう。

上のコードの☆ジルシ部分に以下のコートを入れてください。
 

 //----- ソケットを接続待ちにする
 result = listen(svSocket, 1);
 if (result == SOCKET_ERROR)
 {
  // m_List1.AddString("listenが失敗しました");
  cout << "listenが失敗しました" << endl;
  closesocket(svSocket);
  result = WSACleanup();
  return 0;
 }
 //----- ソケットのアドレスを指定することができた
 // m_List1.AddString("listenが成功しました");
 cout << "listenが成功しました" << endl;

 
 //----- 接続待ちか?
 int limit = 0;
 
 while(1)
 {
 int adlen = sizeof(clAddress);
 
 clSocket=accept // 待機状態にする
 (svSocket,
 (struct sockaddr *) &clAddress,
 &adlen);
 
 if (result == INVALID_SOCKET)
 {
 // m_List1.AddString("待機状態になれませんでした");
  cout << "待機状態になれませんでした" << endl;

  break;
 }
 //----- ソケットのアドレスを指定することができた 
 // m_List1.AddString("待機状態になりした");
 cout << "待機状態になりした";
 

 //----- 接続用のスレッドを作成する
 DWORD thread;
 HANDLE threadHandle =
 CreateThread(0,0,
 (LPTHREAD_START_ROUTINE) SockTread,
 (void*)clSocket, 0, &thread);
 
 if (threadHandle == NULL)
 {
  cout << "Server : thread creation error" << endl;
  closesocket(clSocket);
 }
 limit ++;
 if( limit > 2 )break;
 }
 
これもおきて破りの無限ループになっています。実際に動かすと、Accept部分でハング状態になります。始めこれの原因がわからなかったのですが、データを受信するまで帰ってきません。これはちょっと悩みましたね。
 
でもこれは仕様なので、これでシステムに悪影響があるわけではないので慌てる必要はありません。
 
ともあれ、データが来たらスレッドを起動してまた待機に入ります。そのままだと永遠に終わらないので2回で終わるようにしてあります。
 
次がサーバがデータを待ち構えるために動かすスレッドです。
 
 #define SD_BOTH 0x02
 
 //----- サーバ用スレッド
 void SockTread(void *sock)
 {
 double x;
 
 char *y = "これを送ります1234567890";
 
 int result;
 
 cout << "スレッドを開始します" << endl;
 // ソケットを渡される
 SOCKET clSock=(SOCKET)sock;
 while(1)
 {
  //----- 受信する
  result = recv(
  clSock,
  (char*)&x,
  sizeof(double),
  0);
 //----- 0を受信したらスレッドを終了する
 if(result==0)
 {
  // m_List1.AddString("Server : clSock : closed;");
  cout << "Server : clSock : closed;" << endl;
  break;
 }
 //----- 受信エラーならスレッドを終了する
 else if(result == SOCKET_ERROR)
 {
  // m_List1.AddString("Server : clSock : recv error");
  cout << "Server : clSock : recv error" << endl;
  break;
 }
 else
 //----- うまく受信できた場合は
 {
  //y = x*x; //----- 受信した数を倍にして
  //----- 送り返す
  result = send(
  clSock,
  (char*)&y,
  sizeof(y),
  0);
 //----- 送信に失敗したらスレッドを終了
 if (result==sizeof(double))
 {
 // m_List1.AddString("Server : clSock : send error");
  cout << "Server : clSock : send error" << endl;
  break;
 }
  // m_List1.AddString("Server : recv : ");
  //----- 送った値を表示する
  cout << "Server : recv : " << x << y << endl;
 }
 }
  //----- データ転送を禁止する
  cout << "データ転送を中止しました";
  result = shutdown(clSock, SD_BOTH);
 //----- データ転送を禁止できたら
 if (result == 0)
  //----- ソケットをクローズして終わり
  result = closesocket(clSock);
  cout << "スレッドを終了しました" << endl;
 }
 

0を受信するとスレッドが終了するようになっています。

そえぞれ動作ごとにメッセージが出るようになっているので、動かして見てください。まずはハングのような状態になります。

次にブラウザを立ち上げて接続を変え、プロキシのアドレスを127・0・0・1、ポート番号を1225にします。

アドレスに何かを入れてリターンキーを押すとハング状態のコンソールが動きます。データを受信しているわけですね。

サーバは適当な文字列を返しますが、ブラウザとの間に何も約束が無い(普通ならブラウザが出すのはHTTPのアドレスで、帰ってくるのがHTTPのデータ(ホームページ))のでお互いに話が通じません。

画面にでるのは



みたいな画面です。でもちゃんとデータのやり取りができるのはわかりました。3回ほど送るとサーバは回数をみてソケットを切断します。ブラウザには「データ受信中にエラーが発生しました」というメッセージがでます。ブラウザは意味の無いデータを受け取った挙げ句に切断されてしまうんですね。