ホームに戻る
 オンライン対戦ジャンケンゲームを作る

jyanken (2012/3/4 119kb/zip)

0、はじめに

オンラインで対戦できるジャンケンゲームを作ります。

サーバーはLinuxサーバーを使用。
サーバープログラムはC言語で作成。
クライアントはブラウザからFlashで実行する。
クライアントはActionScript3.0で作成。

TCP/IPでクラアントサーバ通信を考えます。
ストリームソケットを考えます。

また、ポート843で <policy-file-request/> に対応する
C言語のサーバーも書きました。

1、socket(サーバ、クライアント)

int srcSocket = socket(AF_INET, SOCK_STREAM, 0);

戻り値:エラーだと -1 を返す。

2、bind(サーバ)

指定したポートがすでに使用されているとエラーになる。

struct sockaddr_in srcAddr;

memset(&srcAddr, 0, sizeof(srcAddr));
srcAddr.sin_port = htons(port);
srcAddr.sin_family = AF_INET;
srcAddr.sin_addr.s_addr = htonl(INADDR_ANY);

bind(srcSocket, (struct sockaddr *)&srcAddr, sizeof(srcAddr));

戻り値:正常 0 エラー -1

3、listen(サーバ)

第2引数は接続の受け付けを5つまで許可。
この数値は環境によって意味が異なるので注意。
接続できるクライアントの数を5に制限するわけではない。

listen(srcSocket, 5);

戻り値:正常 0 エラー -1

4、accept(サーバ)

接続の受け付け。
受け付けるまでブロックする。

struct sockaddr_in dstAddr;
int dstAddrSize = sizeof(dstAddr);

int dstSocket = accept(srcSocket, (struct sockaddr *)&dstAddr, &dstAddrSize);

戻り値:エラーだと -1 を返す。

5、connect(クライアント)

クライアントがサーバーに接続。

struct sockaddr_in dstAddr;
int dstAddrSize = sizeof(dstAddr);

memset(&dstAddr, 0, sizeof(dstAddr));
dstAddr.sin_family = AF_INET;
dstAddr.sin_port = htons(port);
dstAddr.sin_addr.S_un.S_addr = inet_addr(ip);

int dstSocket = connect(srcSocket, (struct sockaddr *)&dstAddr, 

&dstAddrSize);

戻り値:正常 0 エラー -1

6、recv(サーバ、クライアント)

BUFFER_SIZE のサイズだけデータを受け取る。
buffer は書き込む位置のポインタ。
データを受け取れるまでブロック。
1回ですべてのデータを受け取れるわけではない。
1回で受け取れない場合は数回に分けて受け取る必要がある。
data_size は受け取ったデータサイズである。
data_size から計算して残りのデータサイズを計算できる。

#define BUFFER_SIZE 256

int data_size = recv(dstSocket, buffer, BUFFER_SIZE, 0);

戻り値:接続が切れると 0 エラーだと -1 を返す。

7、send(サーバ、クライアント)

1回ですべてのデータを送信するわけではない。
data_size は送ったデータサイズである。
送信バッファがいっぱいになるとブロックする。

int data_size = send(dstSocket, "ok", 2, 0);

戻り値:エラーだと -1 を返す。

8、close(サーバ、クライアント)

接続を切る。

close(srcSocket);

戻り値:正常 0 エラー -1

10、サーバーの流れ

以下のようになる。

1,socket
2,bind
3,listen
4,accept
5,recv
6,send

ジャンケンは2人対戦である。
よって、3人目の accept があった場合は強制的に close する。
また、対戦した2人は強制的に close はしない。
対戦する2人はスレッド内で recv と send を繰り返す。
クライアント側が切断した場合にサーバーは recv で 0 を受け取る。
recv で 0 を受け取ったら、スレッドのループを抜ける。
対戦しているクライアントが抜けた場合に、
新たなクライアントを accept できるようにする。

/*
*   ジャンケンゲーム用サーバープログラム
*   gcc serv.c -lpthread
*/

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define PORT 40002
#define BUFFER_SIZE 256

typedef struct{
  int *sock;
  int num;
  int *te;
  int *opposite_te;
}player;

static pthread_mutex_t mutex;

static void* thread(void* pParam);

int main(){
  int result;
  int te[2];
  player p[2];

  unsigned short port = PORT;
  int srcSocket;
  int dstSocket[2];

  struct sockaddr_in srcAddr;
  struct sockaddr_in dstAddr;
  int dstAddrSize = sizeof(dstAddr);

  pthread_t tid[2];

  te[0] = 0;
  te[1] = 0;

  dstSocket[0] = -1;
  dstSocket[1] = -1;

  memset(&srcAddr, 0, sizeof(srcAddr));
  srcAddr.sin_port = htons(port);
  srcAddr.sin_family = AF_INET;
  srcAddr.sin_addr.s_addr = htonl(INADDR_ANY);

  /* socket */
  srcSocket = socket(AF_INET, SOCK_STREAM, 0);

  if(srcSocket == -1){
    printf("socket error\n");
    return -1;
  }

  /* bind */
  result = bind(srcSocket, (struct sockaddr *) &srcAddr, sizeof(srcAddr));

  if(result == -1){
    printf("bind error\n");
    return -1;
  }

  /* listen */
  result = listen(srcSocket, 5);

  if(result == -1){
    printf("listen error\n");
    return -1;
  }

  pthread_mutex_init(&mutex, NULL);

  while(1){
    int i;
    int dstSock;

    /* accept */
    dstSock = accept(srcSocket, (struct sockaddr *)&dstAddr, &dstAddrSize);

    if(dstSock == -1){
      continue;
    }

    for(i = 0; i < 3; i++){
      if(i == 2){
        printf("login is denied\n");
        close(dstSock);
        break;
      }
      else if(dstSocket[i] == -1){
        printf("player%d:login\n", (i + 1));
        dstSocket[i] = dstSock;
        p[i].sock = &dstSocket[i];
        p[i].num = (i + 1);
        p[i].te = &te[i];
        p[i].opposite_te = &te[(i + 1) % 2];
        pthread_create(&tid[i], NULL, thread, &p[i]);
        break;
      }
    }
  }

  pthread_mutex_destroy(&mutex);
}

/* thread */
void* thread(void* pParam)
{
  int numrcv;
  char buffer[BUFFER_SIZE + 10];
  player *p = (player *)pParam;
  int *dstSocket = p->sock;
  int num = p->num;
  int *te = p->te;
  int *opposite_te = p->opposite_te;
  while(1){
    numrcv = recv(*dstSocket, buffer, BUFFER_SIZE, 0);
    if(numrcv == 0 || numrcv == -1) {
      goto close;
    }
    buffer[numrcv] = '\0';
    printf("received%d: %s\n", num, buffer);

    if(buffer[0] == 'g'){
      *te = 1;
    }
    else if(buffer[0] == 'c'){
      *te = 2;
    }
    else if(buffer[0] == 'p'){
      *te = 3;
    }
    else{
      continue;
    }

    while(1){
      numrcv = recv(*dstSocket, buffer, BUFFER_SIZE, 0);
      if(numrcv == 0 || numrcv == -1) {
        goto close;
      }
      buffer[numrcv] = '\0';
      printf("received%d: %s\n", num, buffer);

      if(*opposite_te != 0){
        char buf[10];

        pthread_mutex_lock(&mutex);

        if(*opposite_te == 1){
          buf[0] = 'g';
          buf[1] = '\0';
          send(*dstSocket, buf, 2, 0);
        }
        else if(*opposite_te == 2){
          buf[0] = 'c';
          buf[1] = '\0';
          send(*dstSocket, buf, 2, 0);
        }
        else if(*opposite_te == 3){
          buf[0] = 'p';
          buf[1] = '\0';
          send(*dstSocket, buf, 2, 0);
        }

        printf("send%d: %s\n", num, buf);

        *opposite_te = 0;

        pthread_mutex_unlock(&mutex);

        break;
      }
    }
  }

close:
  *te = 0;
  *dstSocket = -1;
  printf("player%d:close\n", num);
  return;
}

11、クライアントの流れ

以下のようになる。

1,socket
2,connect
3,send
4,イベントにて受信

クライアントはActionScript3.0で書く。
SocketではなくXMLSocketを使用。
AS3.0では send はメソッドで呼ぶ。
データの受信はイベントで受け取れる。
切断は close を呼ぶか画面を閉じると切断になる。

// Main.as
package {
  import flash.display.Sprite;
  import flash.events.*;
  import flash.text.TextFormat;
  import flash.text.TextField;
  import flash.net.XMLSocket;
  import flash.utils.*;
  import flash.system.Security;
  import flash.errors.IOError;

[SWF(width="300", height="200", backgroundColor="0xFFFFFF")]

  public class Main extends Sprite {
    private var socket:XMLSocket;
    private var timer:Timer;
    private var te1:Te1;
    private var te2:Te2;
    private var rect1:Rect1;
    private var text_field:TextField;
    private var ite1:int;
    private var ite2:int;
    private var state:int;
    private var list:Array = [[0, 1, 2],[2, 0, 1],[1, 2, 0]];

    public function Main():void {
      state = 0;

      text_field = new TextField();
      var format:TextFormat = new TextFormat();
      format.size = 20;
      text_field.defaultTextFormat = format;

      text_field.x = 175;
      text_field.y = 25;
      text_field.width = 100;
      text_field.height = 50;

      addChild(text_field);

      te1 = new Te1();
      te1.x = 50;
      te1.y = 100;
      addChild(te1);

      te2 = new Te2();
      te2.x = 50;
      te2.y = 0;
      addChild(te2);

      rect1 = new Rect1(this, 175, 125, 100, 50);

      rect1.setButtonText("log in");

      addChild(rect1);

      Security.loadPolicyFile("xmlsocket://127.0.0.1:843");

      socket = new XMLSocket();

      socket.addEventListener(Event.CLOSE, closeHandler);
      socket.addEventListener(Event.CONNECT, connectHandler);
      socket.addEventListener(DataEvent.DATA, dataHandler);
      socket.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
      socket.addEventListener(ProgressEvent.PROGRESS, progressHandler);
      socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);

      timer = new Timer(3000, 0);
      timer.addEventListener(TimerEvent.TIMER, onTick);
      timer.start();  
    }

    public function push():void {
      var sendText:String;

      if(state == 0){
        socket.connect("127.0.0.1", 40002);
      }

      else if(state == 1) {
        state = 2;  

        text_field.text = "waiting...";

        te2.set_te2(3);

        wait(20);

        if(te1.get_te1() == 0) {
          ite1 = 0;
          sendText = "g";
        }
        else if(te1.get_te1() == 1) {
          ite1 = 1;
          sendText = "c";
        } 
        else if (te1.get_te1() == 2) {
          ite1 = 2;
          sendText = "p";
        }

        try{
          socket.send(sendText);
        }
        catch(e:IOError) {
          text_field.text = "error!";
        }
      }
    }

    public function wait(count:uint ):void{
      var start:uint = getTimer();
      while(getTimer() - start < count){}
    }

    private function closeHandler(event:Event):void {
      trace("closeHandler: " + event);
    }

    private function connectHandler(event:Event):void {
      text_field.text = "log in ok!";
      state = 1;
      rect1.setButtonText("pong");
      trace("connectHandler: " + event);
    }

    private function dataHandler(event:DataEvent):void {
      trace("dataHandler: " + event);

      if(state == 2) {
        var o:oto = new oto();
        o.play();
        wait(1000);
        var str:String = event.data;

        if(str.charAt(0) == 'g') {
          ite2 = 0;
          te2.set_te2(0);
        }
        else if(str.charAt(0) == 'c') {
          ite2 = 1;
          te2.set_te2(1);
        }
        else if(str.charAt(0) == 'p') {
          ite2 = 2;
          te2.set_te2(2);
        }
        else{
          te2.set_te2(3);
        }

        if(list[ite1][ite2] == 0) {
          text_field.text = " draw.";  
        }
        else if (list[ite1][ite2] == 1) {
          text_field.text = " you win!";
        }
        else if (list[ite1][ite2] == 2) {
          text_field.text = " you lose.";
        }

        state = 1;
      }
    }

    private function ioErrorHandler(event:IOErrorEvent):void {
      text_field.text = "log in error.";

      trace("ioErrorHandler: " + event);
    }

    private function progressHandler(event:ProgressEvent):void {
      trace("progressHandler loaded:" + event.bytesLoaded + " total: " + event.bytesTotal);
    }

    private function securityErrorHandler(event:SecurityErrorEvent):void {
      trace("securityErrorHandler: " + event);
    }

    private function onTick(evt:TimerEvent):void {
      var sendText:String;

      if(state != 0) {
        sendText = "t";
        socket.send(sendText);
      }
    }
  }
}

// Te1.as
package
{
  import flash.display.Sprite;
  import flash.events.Event;
  import flash.events.MouseEvent;
  import flash.display.Graphics;

  public class Te1 extends Sprite {
    private var now_te:int;
    private var bitmap:Array;
    [Embed(source = 'gu.png')]
    private var Image0:Class;
    [Embed(source = 'choki.png')]
    private var Image1:Class;
    [Embed(source = 'pa.png')]
    private var Image2:Class;

    public function Te1() {
      now_te = 0;

      bitmap = new Array();
      bitmap[0] = new Image0();
      bitmap[1] = new Image1();
      bitmap[2] = new Image2();

      bitmap[now_te % 3].visible = true;
      bitmap[(now_te + 1) % 3].visible = false;
      bitmap[(now_te + 2) % 3].visible = false;

      addChild(bitmap[0]);
      addChild(bitmap[1]);
      addChild(bitmap[2]);

      this.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
    }

    public function get_te1():int{
      return now_te % 3;
    }

    private function onMouseDown(event:MouseEvent):void {
      now_te++;
      bitmap[now_te % 3].visible = true;
      bitmap[(now_te + 1) % 3].visible = false;
      bitmap[(now_te + 2) % 3].visible = false;
    }
  }
}

// Te2.as
package
{
  import flash.display.Sprite;
  import flash.events.Event;
  import flash.events.MouseEvent;
  import flash.display.Graphics;

  public class Te2 extends Sprite {
    private var now_te:int;
    private var bitmap:Array;  
    [Embed(source = 'gu.png')]
    private var Image0:Class;
    [Embed(source = 'choki.png')]
    private var Image1:Class;
    [Embed(source = 'pa.png')]
    private var Image2:Class;
    [Embed(source = 'hatena.png')]
    private var Image3:Class;

    public function Te2() {
      var i:int;

      now_te = 3;

      bitmap = new Array();
      bitmap[0] = new Image0();
      bitmap[1] = new Image1();
      bitmap[2] = new Image2();
      bitmap[3] = new Image3();

      for (i = 0; i < 3; i++) {
        bitmap[i].rotation = 180.0;
        bitmap[i].x += 100.0;
        bitmap[i].y += 100.0;
      }

      addChild(bitmap[0]);
      addChild(bitmap[1]);
      addChild(bitmap[2]);
      addChild(bitmap[3]);
    }

    public function set_te2(te:int):void {
      bitmap[te % 4].visible = true;
      bitmap[(te + 1) % 4].visible = false;
      bitmap[(te + 2) % 4].visible = false;
      bitmap[(te + 3) % 4].visible = false;
    }
  }
}

// Rect1.as
package 
{
  import flash.display.Sprite;
  import flash.events.Event;
  import flash.events.MouseEvent;
  import flash.display.Graphics;
  import flash.text.TextField;
  import flash.text.TextFormat;
    
  public class Rect1 extends Sprite {
    private var px:int;
    private var py:int;
    private var pw:int;
    private var ph:int;
    private var g:Graphics;
    private var text_field:TextField;
    private var main:Main;

    public function Rect1(main:Main, px:int = 0, py:int = 0, pw:int = 0, ph:int = 0) {
      this.main = main;

      this.px = px;
      this.py = py;
      this.pw = pw;
      this.ph = ph;

      g = this.graphics;

      this.draw();

      text_field = new TextField();
      var format:TextFormat = new TextFormat();
      format.size = 30;
      text_field.defaultTextFormat = format;

      text_field.selectable = false; 

      addChild(text_field);

      this.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
      this.addEventListener(MouseEvent.MOUSE_OUT, onMouseOut);
      this.addEventListener(MouseEvent.MOUSE_UP, onMouseUp); 
    }

    public function setX(px:int):void { this.px = px;}
    public function setY(py:int):void { this.py = py;}
    public function getX():int { return this.px;}
    public function getY():int { return this.py;}

    public function setButtonText(str:String):void {
      text_field.text = str;
      text_field.x = this.px + (pw - text_field.textWidth) / 2;
      text_field.y = this.py + (ph - text_field.textHeight) / 2;
      text_field.width = 100;
      text_field.height = 50;
    }

    public function draw(color:int = 0xFFFFFF):void {
      g.lineStyle(2, 0x000000, 1.0);
      g.beginFill(color, 1.0);
      g.drawRoundRect(this.px, this.py, this.pw, this.ph, 20, 20);
      g.endFill();
    }      

    private function onMouseDown(event:MouseEvent):void {
      this.draw(0x000000);
      text_field.textColor = 0xFFFFFF;
    }

    private function onMouseOut(event:MouseEvent):void {
      this.draw();
      text_field.textColor = 0x000000;
    }

    private function onMouseUp(event:MouseEvent):void {
      this.draw();
      text_field.textColor = 0x000000;

      main.push();
    }
  }
}

// oto.as
package
{
  import flash.media.Sound;

  public class oto {
    private var sound:Array;  
    [Embed(source = 'jyanken.mp3')]
    private var Sound0:Class;

    public function oto() {
      var i:int;
      sound = new Array();
      sound[0] = new Sound0();
    }

    public function play():void {
      sound[0].play();
    }
  }
}

12、ソケットポリシーファイル

FlashをサーバーにTCPで接続する場合、
どのドメインのFlashでどのポートへの接続かを確認します。
2012年3月の時点でFlashは次のようなチェックを行います。

まず、843ポートに <policy-file-request/> を送る。
以下のような返答があれば
mniwa.web.fc2.com でダウンロードされたFlashを
12345 のポートにアクセスさせることを許可します。
(ワイルドカード * も有効です。)

<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
    <allow-access-from domain="mniwa.web.fc2.com" to-ports="12345"/>
</cross-domain-policy>

もし3秒たっても返答が無い場合は、
Flashから 12345 のポートに <policy-file-request/> を送って、
上の返答があれば通信を許可します。

この仕組みを満たさない場合は通信が行えません。
以下はC言語のサーバープログラムのサンプルです。
ポート843を使用するので root で起動してください。

/*
*   Flash からの <policy-file-request/> に対して
*   port 843 で返答するサーバープログラム
*   許可するドメインとポート番号は XML文を書き換えてください
*
*   gcc pserv.c
*/

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define PORT 843
#define BUFFER_SIZE 256

int main(){
  int result;
  unsigned short port = PORT;
  int srcSocket;
  int dstSocket;

  struct sockaddr_in srcAddr;
  struct sockaddr_in dstAddr;
  int dstAddrSize = sizeof(dstAddr);

  memset(&srcAddr, 0, sizeof(srcAddr));
  srcAddr.sin_port = htons(port);
  srcAddr.sin_family = AF_INET;
  srcAddr.sin_addr.s_addr = htonl(INADDR_ANY);

  /* socket */
  srcSocket = socket(AF_INET, SOCK_STREAM, 0);

  if(srcSocket == -1){
    printf("socket error\n");
    return -1;
  }

  /* bind */
  result = bind(srcSocket, (struct sockaddr *)&srcAddr, sizeof(srcAddr));

  if(result == -1){
    printf("bind error\n");
    return -1;
  }

  /* listen */
  result = listen(srcSocket, 1);

  if(result == -1){
    printf("listen error\n");
    return -1;
  }

  while(1){
    int numrcv;
    char buffer[BUFFER_SIZE + 10];

    /* accept */
    dstSocket = accept(srcSocket, (struct sockaddr *)&dstAddr, &dstAddrSize);

    if(dstSocket == -1){
      printf("accept error\n");
      continue;
    }

    /* recv */
    numrcv = recv(dstSocket, buffer, BUFFER_SIZE, 0);

    if(buffer[0] == '<' && buffer[1] == 'p'){
      /* XML文 */
      char *str_xml = "<?xml version=\"1.0\"?>\n<!DOCTYPE cross-domain-policy SYSTEM \"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd\">\n<cross-domain-policy>\n    <allow-access-from domain=\"127.0.0.1\" to-ports=\"40002\"/>\n</cross-domain-policy>";

      /* send */
      send(dstSocket, str_xml, strlen(str_xml), 0);
      printf("send str_xml\n");
    }
    else{
      printf("error recieve:%s\n", buffer);
    }

    close(dstSocket);
  }
}

13、Linuxコマンドの覚え書き

cd   ディレクトリ移動
rm   ファイルを削除
mkdir ディレクトリを作成
rm -r ディレクトリごと削除
ls   ファイル一覧を見る
cp   ファイルコピー
ps   起動プロセス確認
&    最後につけるとプロセスをバックグラウンドで起動
kill -HUP PID プロセスを停止
cdmod 755 file アクセス権を設定(r = 4, w = 2, x = 1)
find / -name httpd
yum -y install httpd
service httpd restart
chkconfig --level 35 httpd on

viコマンド

hjkl で上下左右移動
x    で文字削除
a    で追加入力モード
i    で挿入入力モード
/    前方検索
?    後方検索
Esc  で入力モードから戻る
:q!  で終了
:wq  で書き込み終了

iptables の設定

iptables -L
iptables -P INPUT DROP
iptables -A INPUT -p tcp --dport 80 -j ACCEPT

inserted by FC2 system