ホーム < ゲームつくろー! < IKD備忘録

サーバ
ログイン!


 前章で待機状態になったクライアントにサーバからお知らせをするCometが出来ました。でも、前章のサンプルはサーバ自身がusleep関数で待って返すだけなので、返すきっかけはサーバが握っていました。一方、例えば将棋など対戦相手がいるゲームの場合、先手が指し手を考えている間後手はその手が打たれるまで待機しています。そして先手が指したのをきっかけとして後手に番手が回ってきます。つまり、後手へ反応を返すきっかけを与えるのはサーバではなくて先手のクライアントという事になります。

 これが何を意味するのか?サーバは先手と後手の両方の存在を知っている必要があり、また一方が呼んだPHPのプログラムに対して他方が作用させる機構が必要です。これを実現するにはどうしたら良いか、調べてみる事にします。



@ 「私はAです!」を言うには?

 クライアントAさんとBさん、それぞれが独立に呼んだPHPプログラムは、当然ですが独立に動きます。今AさんとBさんをゲームで対戦させようと思ったら、サーバはどういう事をしなければならないか考えてみます。

 まず、少なくともAさんとBさんのどちらかがサーバにアクセスして「ゲームしたいんですけど…」と尋ねます。するとサーバは「了解っす。じゃぁ対戦相手が決まるまで待っててね」と返答を返します:

 「参加する」というのはどういう事か?サーバにAさんの名前(ID)を覚えてもらい、ゲームをする権利を貰う事です。つまり「プレイヤー作成」です。

 プレイヤー作成ではプレイヤーの名前(ID)とパスワードを入力してもらうのが普通です。サーバ側はその名前とパスワードの対を
覚えます。そう、ここでMySQLなどのデータベースがお目見えとなるわけです。登録ができたら、Aに対して「登録できたよ〜」とお知らせし、その時に「身分証明書」を渡します。なぜ身分証明書が必要なのか?

 AさんにOKと返すと、サーバとAさんとの関係はそこでいったん切れてしまいます。そこでAさんはサーバへ再度アクセスし、今度は「対戦相手が決まったら教えて下さい」と要求します。この時、サーバは再度接続してきた人がAさんであるかどうかをその身分証明書で判断します。なので、サーバは「Aさんの名前(ID)」「Aさんのパスワード」そして「Aさんの身分証明書」と、Aさんについて
3つの事を覚えておく必要があるわけです。これはそのままMySQLの「Playerテーブル」の項目になるという事ですね。

 とりあえず、ここまで作りましょうか(^-^;



A プレイヤー作成(アカウント作成)

 じゃぁまずクライアントから作ります。ブラウザが立ち上がったら、プレイヤーの名前とパスワードを入れる簡素なフォームを作ります。送信ボタンを押したらサーバにそれらを送信します。ちなみに、今はセキュリティーの事は考えません(※参照に注意下さい)

 ブラウザ上に入力フォームを置くには<input>タグを使います。こんな感じです:

プレイヤー作成画面
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Loginテスト</title>
    </head>
    <body>
        <table>
            <tr>
                <td><p>ID</td>
                <td>:<input type="text"></td>
            </tr>
            <tr>
                <td>Password</td>
                <td>:<input type="password"></td>
            </tr>
        <table>
        <input type="button" style="width:80px;height:20px;" value="送信" onclick=requestPlayerCreate()>
    </body>
</html>

フォームにプレイヤーIDとパスワードを入れたら、送信ボタンを押します。するとonclickに設定してあるrequestPlayerCreate関数が呼ばれる…という予定のコードです。画面はこういう感じです:

こういう所はHTML楽です(^-^)。

 さて、送信ボタンを押した時に呼ばれる手筈となっているrequestPlayerCreate関数はJavaScriptで記述します。この関数がする事はサーバに対して「作成して下さい!」と嘆願する事です。そこでサーバ側で呼ばれる関数もrequestPlayerCreate関数としておきましょう。ちょっとここで新要素。今までは呼び出したPHPファイル≒関数のような感じになっていました。これでももちろん動作しますが、このままだと関数が増えるとPHPファイル数もとんでもない事になります。それよりは一つのPHPファイルに複数の関連する関数を入れた方が効率的です。では、PHPの中に含まれる複数の関数からクライアントで指定された関数をコールするにはどうするか?

 クライアントからは関数名をパラメータとして渡します(引数もついでに)。サーバ側にそれが入ってきた時、その関数名を次のようにいきなり関数として呼び出せてしまいます!

PHPは関数名を関数として呼べる!
<?php

    if ( array_key_exists( 'function', $_GET ) == false )
        return;

    $func = $_GET['function'];
    $func();

    function requestPlayerCreate() {
        print( "呼べた!" );
    }

?>

太文字に注目です。クエリストリングスの引数の中にfunctionというキーがあった場合、それを文字列として$funcに格納しています。そして、それをまるで関数であるかのように呼び出すと、あっさりと関数コールできてしまいます。もし$funcの中に「requestPlayerCreate」という文字列があったら、定義しているその関数が呼ばれ、クライアント側には「呼べた!」という文字列が返ります。実際にやってみましょう:

login.htmlのスクリプト部分
<script>
function requestPlayerCreate() {
    var xmlHttpRequest = new XMLHttpRequest();
    xmlHttpRequest.open( "GET", "php/login.php?function=requestPlayerCreate", "true" );
    xmlHttpRequest.onreadystatechange = function( e ) {
        // とりあえず空で
    }
    xmlHttpRequest.send( null );
}
</script>

このサーバへの要求部分は前回説明した所なので細かい事は省きます。openでlogin.phpを呼び出し、functionパラメータにrequestPlayerCreate関数呼び出しをお願いしている所が勘所です。先のプレイヤー作成画面にこのスクリプトを追加して実行すると:

呼べました(^-^)。クライアント側でPHPファイル内で定義されている関数名をパラメータで渡すと約束しておけば、サーバ側ではそのファイル内にある指定関数を好きなように呼べるようになるわけです。ただ、一つ問題があります。それは「引数」です。



A 引数はJSON文字列で渡す!

 先の方法で呼び出した関数には引数は渡していませんでしたが、普通関数には引数があり、その数や種類は関数によって異なります。となると先程のような自動化が引数の違いがあるためうまくいかなくなってしまいます。これを回避するには「引数を同じにする」事を徹底しなければなりません。そこで、次のような約束をします:

・ クライアントのクエリストリングスは、function=[関数名]&args=[引数文字列]と、関数名と文字列な引数一つだけにする。引数が無くてもこれは徹底する。
・ 文字列は何らかの書式に従って複数の引数を含められるようにする。
・ サーバは文字列をパースして引数を引き出す。

 こうすれば引数がいくつあってもPHP側の関数に同じように渡す事ができます。これを実現するのに必要になるのが「複数引数のデコードとエンコード」ですが、嬉しい事にPHPには「json_decode関数」という強力な関数が用意されています(PHP5.2.0以降)。この関数はJSON文字列と呼ばれるハッシュテーブルを表現する文字列から実際にハッシュテーブルを返してくれます。例えば、

json_decode関数
<?php

    $json = '{"a":1,"b":2,"c":3,"d":4,"e":5}';

    $hash = json_decode( $json, true );

?>

$jsonがJSON文字列です。これをjson_decode関数に投げると、

array(5) {
    ["a"] => int(1)
    ["b"] => int(2)
    ["c"] => int(3)
    ["d"] => int(4)
    ["e"] => int(5)
}

という連想配列にデコードしてくれます。便利(^-^)。後はクライアント側であるJavaScriptでJSON文字列を作ればいいんです。「え〜めんどくさい…」と思うかもしれませんが大丈夫、なんとJavaScript側にもJSONを扱う関数が用意されています。と言いますか、JSONというのは「JavaScript Object Notation」の略で、元々JavaScriptの機能です。PHPがそれに対応したという事ですね。

 では、JavaScript側でプレイヤーIDとパスワード、そして呼び出すrequestPlayerCreate関数名をJSON文字列化してサーバに渡してみます:

JavaScript側でJSON文字列を作ってサーバへ
<script>
    function requestPlayerCreate() {
       // フォームの入力項目を格納
        var playerId = document.getElementById("playerId").value;
        var password = document.getElementById("password").value;

        // JSON化
        var args = {
           "playerid" : playerId,
           "password" : password,
       };
       var arg_json = JSON.stringify( args );


       // サーバへ送信
       var xmlHttpRequest = new XMLHttpRequest();
       xmlHttpRequest.open( "GET", "php/login.php?function=requestPlayerCreate&args=" + arg_json, "true" );
        xmlHttpRequest.onreadystatechange = function( e ) {
        }
        xmlHttpRequest.send( null );
    }
</script>

太文字の所に注目です。フォームに入力されているプレイヤーIDとパスワード、そして呼び出し関数名を上のようにハッシュテーブル化します。これをJSON.stringify関数に投げると、JSON文字列を返してくれます。まぁ〜〜便利!後はそれをサーバ側に送信するだけです。

 サーバ側は渡されたJSON文字列な引数群をハッシュ引数化して関数に渡します:

サーバ側でJSON文字列からハッシュ引数を作成
<?php
function requestPlayerCreate( $args ) {
print( "PlayerID: ".$args['playerid']." Password: ".$args['password'] );
}

    if ( array_key_exists( 'function', $_GET ) == false || array_key_exists( 'args', $_GET ) == false) {
        printf( "何か無いっす" );
        return;
    }

    // JSON文字列 → ハッシュ引数
    $args = json_decode( $_GET['args'], true );

    $func = $_GET['function'];
    $func( $args );
?>

$_GET['args']にJSON文字列な引数が入っているはずなので、json_decode関数を使ってハッシュ引数に戻します。後は関数を呼び出してそのハッシュ引数を渡すだけです。これで、どのような引数でも同じ形式でPHPの関数に渡せるようになりました!!わ〜い(^-^)/。ちなみに、上のコードで次のようにちゃんとプレイヤーIDとパスワードがサーバに渡されています:

さてと、ではせっかく渡されたプレイヤーIDとパスワード、そして本来プレイヤーに返すべき「身分証明書」を作り、それをMySQLに格納してみましょうか。



B MySQLにプレイヤー情報を格納

 プレイヤー作成画面で入力してもらったプレイヤーIDとパスワードをMySQLに格納します。そのためにまずはMySQLにデータベースを作成しましょう。今回は「submarine」という名前で作ります:

mysql > create database submarine;

続けてplayerというテーブルを作成します。テーブルの項目は「name(文字列)」「pass(文字列)」とします:

mysql > use submarine;
mysql > create table player ( name varchar(64), pass varchar(32) );

もう一つテーブルを作ります。それは身分証明書とプレイヤー名が項目にあるテーブルで「tokenテーブル」という名前にします。さ、出ました「トークン(token)」。先ほどから「身分証明書」と言っていたのはトークンと呼ばれている固有IDです:

mysql > create table session ( session varchar(64), name varchar(32) );

プレイヤーを作成したら、その名前、パスワードをplayerテーブルに登録します。この時同時にトークンを発行し、名前と共にtokenテーブルに登録します。そしてプレイヤー(クライアント)にそのトークンを渡します。以後ゲーム中にクライアントがサーバにアクセスする際には必ず貰ったトークンを一緒に渡します。サーバは渡されたトークンをtokenテーブルから検索し、あった場合はそれに紐付いたプレイヤーIDを取りだします。これでアクセスした人がその人であると判別するわけです。

 「何でそんなめんどくさい事を?登録されているplayerIDがあるのだから、それをサーバに直接渡せばいいんじゃないの?」と思うかもしれませんが、これは危険なんです。ユーザが自由に付けられるplayerIDは通常短文で非常に類推しやすい物でもあります。もしplayerIDだけでクライアントを識別していたら、サーバに登録してある全playerIDの中で一つでも該当してしまえば、それで「なり済まし」が出来てしまいます。一度なり済ましてしまえば、もうサーバがそのplayerIDを抹消しない限りやり放題です。そして普通サーバはその違法アクセスに気付けません(本人かどうか判断できない)ので、勝手にID抹消なんて出来ないんです。なのでplayerIDでの直接識別は絶対にNG!一方、サーバが発行するトークンは通常完全にランダムで無意味な長い文字列になっています。類推は不可能なので総当たりするしかないのですが、例えばアルファベット大文字小文字といくつかの記号を1文字として1ケタを60進数にしてしまうと、「a8Gk<Z」と6桁くらいの文字列でも60の6乗、つまり約467億通りの表現ができます。トークンとして32桁くらいの文字列にすると、それはもう天文学的なパターン数です(10の56乗くらいのようです(^-^;)。なので、playerIDの代わりにこのトークンでクライアントを識別すると、ランダムアタックによるなり済ましを防止する事が出来るわけです。

 という事で、トークンを使います。
 トークンは無意味な文字列として発行します。トークン発行器は乱数を使って無意味文字列を作ります。例えばこんな感じです:

// トークン発行
function generateToken() {
    $token = "";
    $str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    for ( $i = 0; $i < 32; $i++ )
        $token = $token.substr( $str, mt_rand(1, 62), 1 );
    return $token;
}

サーバ側でプレイヤーID、パスワードそしてトークンをMySQLに登録するコードはこうなります:

プレイヤーID、パスワード、トークンをテーブルに登録
<?php
    // トークン発行
    function generateToken() {
        $token = "";
        $str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        for ( $i = 0; $i < 32; $i++ )
            $token = $token.substr( $str, mt_rand(1, 62), 1 );
        return $token;
    }

    function requestPlayerCreate( $args ) {
        // MySQLにプレイヤーID、パスワード、トークンを登録
        $link = mysql_connect( "localhost", "root", [パスワード] );
        if ( !$link )
            die("MySQLの接続に失敗しました");

        $error = mysql_select_db( 'submarine', $link );
        if ( !$error ) {
            mysql_close( $link );
            die( "データベースへの接続に失敗しました:".mysql_error() );
        }

        mysql_query( "SET NAMES utf8" );

        $token = generateToken();  // トークンを発行
        $playerid = $args['playerid'];
        $pass = $args['password'];
        $error = mysql_query( "insert into player ( name, pass ) values ('".$playerid."','".$pass."')", $link );
        if ( !$error )
            die( "登録失敗!:".mysql_error());
        $error2 = mysql_query( "insert into token ( token, name ) values ('".$token."','".$playerid."')", $link );
        if ( $error2 ) {
            //playerテーブルの登録を削除する
            mysql_query( "delete from player where name='".$playerid."'", $link );
            die( "登録失敗!:".mysql_error());
        }

        mysql_close( $link );

        print( "PlayerID: ".$args['playerid']." Password: ".$args['password']." Token: ".$token );
    }

    if ( array_key_exists( 'function', $_GET ) == false || array_key_exists( 'args', $_GET ) == false)
        die( "何か無いっす" );

    // JSON文字列 → ハッシュ引数
    $args = json_decode( $_GET['args'], true );

    $func = $_GET['function'];
    $func( $args );
?>

 最初にJSON文字列として渡ってきた引数を展開、function名(今回はrequestPlayerCreate関数)に引数$argsを渡しています。この関数の中ではMySQLのsubmarineデータベースにある2つのテーブル(player, token)に引数にあるplayerid、password、tokenをそれぞれ登録しています。MySQLの手続きが中々に面倒なのですが、ここはいずれ統一的に扱えるようにリファクタリングされるのかなと思います。

 これで、

という所がようやく形になりました。ただ、重ねて申しますが、セキュリティーの事はまだ度外視しています!とりえあず、次のプロセスに進みましょう。



C Bさん「Aさんと対戦したいのですが」(マッチング)

 今の段階でAさんはサーバに登録されました。同じように別のブラウザからBさんも登録できます。AさんBさん共にサーバからトークンも渡されているので、以後の通信はトークンを見てクライアントを判断できます。ただ、AさんもBさんもお互いの存在はまだ知りません。次にやる事は、対戦相手を見つける作業、すなわち「マッチング」です。

 登録が終わった直後のBさんの視点で考えてみます。登録後Bさんは「対戦相手を知りたいのですが…」という要求(getMatchingList)をサーバに投げます。もちろんこの時にはトークンも忘れずに、です。サーバは現在サーバに登録されて対戦可能な相手をピックアップし、そのリストをBさんに返します。ここまでをやってみましょう。

 クライアント側は登録完了後にマッチング画面に移行するとしましょう。プレイヤー生成画面からマッチング画面へは、せっかくですからJavaScriptを使ってHTML画面のリロードする事無しに遷移するようにします。その為に、ちょっとHTMLのタグの構造を整理する事にしましょう。今プレイヤー生成画面にあったエディットボックスやボタンは、<body>の直下に置かれていました。これだと削除作業が面倒になるので、<body>の子に<div id=scene_root>タグを付ける事にします。シーンの切り替えはこのタグごとごっそりと削除して、次のシーンで再度このタグをbodyに追加する事で実現しましょう。

 シーン切り替えタイミングはプレイヤー作成画面で送信ボタンが押されて、その返答が返ってきた時です。それは@のクライアント側のJavaScript内の「とりあえず空で」というコメント部分に該当します。ここで「changeScene関数」というのを呼び出して、その引数に次のシーン(マッチング画面)を作るファクトリー関数を呼び出すようにすればシーン遷移が実現できます。ざっくり作るとこんな感じになりました:

マッチング画面への遷移
<!DOCTYPE html>
<html>
    <head>
    <meta charset="utf-8">
    <title>Loginテスト</title>

    <script>
        function createMatching( newSceneRoot, args ) {
            var tmp = document.createElement('div');
            tmp.innerText = "マッチング画面";
            newSceneRoot.appendChild( tmp );
        }

        function changeScene( nextSceneFunc, args ) {
            // 既存のシーンを破棄する
            var sceneRoot = document.getElementById("scene_root");
            document.body.removeChild(sceneRoot);

            // 次のシーンを作成するためにscene_rootタグを再びbodyに付ける
            var newSceneRoot = document.createElement('div');
            newSceneRoot.id = "scene_root";

            document.body.appendChild( newSceneRoot );

            // 次のシーン作成へ
            nextSceneFunc( newSceneRoot, args );
        }

        function requestPlayerCreate() {
            // フォームの入力項目を格納
            var playerId = document.getElementById("playerId").value;
            var password = document.getElementById("password").value;

            // JSON化
            var args = {
                "playerid" : playerId,
                "password" : password,
            };
            var arg_json = JSON.stringify( args );

            // サーバへ送信
            var xmlHttpRequest = new XMLHttpRequest();
            xmlHttpRequest.open( "GET", "php/login02.php?function=requestPlayerCreate&args=" + arg_json, "true" );
            xmlHttpRequest.onreadystatechange = function() {
                if ( xmlHttpRequest.readyState == 4 )
                    // マッチング画面へ
                    changeScene( createMatching, null );
            }
            xmlHttpRequest.send( null );
        }
    </script>

</head>

<body>
    <div id=scene_root>
        <table>
            <tr>
                <td><p>ID</td>
                <td>:<input type="text" id="playerId"></td>
            </tr>
            <tr>
                <td>Password</td>
                <td>:<input type="password" id="password"></td>
            </tr>
        <table>
        <input type="button" style="width:80px;height:20px;" value="送信" onclick=requestPlayerCreate()>
    </div>
</body>
</html>

 プレイヤー作成画面で「送信」ボタンを押すと、サーバで登録作業が行われ、クライアントに反応が返ります。それは上のコードのrequestPlayerCreate関数にあるxmlHttlRequest.onreadystatechangeメソッドで、その中のchangeScene()が最終的にコールされます。この関数はシーン切り替え作業をしてくれる関数です。中では<div id=scene_root>というシーンを管理する親タグを一度削除し、空の同じ親タグを<body>下に付けて、引数に渡された生成関数を呼ぶ、という事をしています。今生成関数として「createMatching」という関数をchangeScene関数の第1引数に渡しているので、プレイヤー作成画面のお掃除が終わったらこの関数が呼ばれます。

 これでマッチング画面の作り込みに注意を絞る事ができます(^-^)。

 さて、そのマッチング画面ではサーバに対して「対戦相手のリストを下さい(getMatchingList)」という要求を出します。プレイヤー作成画面同様に要求後に待ちを入れて、返されてくる文字列からプレイヤー情報としてとりあえずplayerIDを列挙して、テーブルに並べる事にしましょう。で、その文字列ですがもちろんJSON文字列でやってきます。先程はクライアントでJSON文字列を作りましたが、今度はサーバからやってきたJSON文字列をJavaScriptで使える形に整える訳です。もちろんパースする必要は無くて「JSON.parse」という関数を使います。この関数の引数にJSON文字列を入れると、JavaScriptのハッシュテーブルにしてくれます。も〜のすごく便利です(^-^)。

 受け取る部分まで上のcreateMaching関数を作りこんでしまいましょう:

createMatching関数
var matchingList = null;

function createMatching( newSceneRoot, args ) {
    var machingRoot = document.createElement('div');
    machingRoot.innerText = "マッチング画面";

    newSceneRoot.appendChild( machingRoot );

    // 対戦相手リストを要求
    var args = {
        "token" : curToken,
    };
    var arg_json = JSON.stringify( args );

    var xmlHttpRequest = new XMLHttpRequest();
    xmlHttpRequest.open( "GET", "php/matching.php?function=getMatchingList&args=" + arg_json, "true" );
    xmlHttpRequest.onreadystatechange = function() {
        if ( xmlHttpRequest.readyState == 4 ) {

            // 対戦相手をハッシュテーブルで取得
            var hash = JSON.parse( xmlHttpRequest.responseText );
            matchingList = hash;

            // 対戦相手テーブルを作成
            var listSize = (!hash.size ? 0 : hash.size);
            var table = document.createElement('table');
            for ( var i = 0; i < listSize; i++ ) {
                var tr = document.createElement('tr');
                var td = document.createElement('td');

                td.innerHTML = "<a href='javascript:void(0)' onclick=selectMatching(" + i + ")>"+ hash[i] + "</a>";

                tr.appendChild( td );
                table.appendChild( tr );
            }
            machingRoot.appendChild( table );
        }
    }
    xmlHttpRequest.send( null );
}

 サーバに対戦相手リストを要求する為、まずJSON文字列な引数を作っています。今回は単に「リストくれ」というだけなので、サーバに渡すのは身分証明書であるトークンだけです。サーバから応答が来たら、戻り値としてJSON文字列なリストが来ているはずなので、それをハッシュ化してJavaScript側で扱えるようにします。対戦相手の選択はHTMLの<a>タグの機能を流用です。ただ今はどこか別のページに飛ぶという事ではなくて「クリックした」というイベントだけが欲しいので、リンク先は「javascript:void(0)」というのにしています。これは「クリックしても何もしないでね」という無効化に使うようです。代わりにonclickイベントでJavaScript側の関数である「selectMatching関数」を呼ぶようにしています。関数の引数にはインデックスを渡しています。これ、playerIDで本来は良いのですが、HTML側の都合で「半角スペースが入った文字列を渡そうとすると落ちる」というのがありまして、やむなくインデックスのみ渡しています。このため、グローバルな「matchingList」という変数を一つ設けています。

 これでマッチング画面のひな型が出来ましたので、サーバ側の実装に移りましょう。

 サーバ側にはトークンと一緒に「getMatchingList」という関数呼び出し要求が来ます。やる事は、

・ ログイン(プレイヤー生成)している人であるかトークンでチェック
・ MySQLからプレイヤーIDをplayerテーブルから取得し配列化
・ 新しいトークンを作成してリストと一緒にクライアントに返す

です。

 トークンはMySQLのtokenテーブルに収めていたのでした。テーブルの中に指定のトークンが含まれているかはselect〜where構文を使います。引数で渡されるトークンを$tokenとすると、

$list = mysql_query( "select name from token where token='".$token."'", $link );

if ( !$list )
    die( "クエリが無効です:".mysql_error());

$item = mysql_fetch_array( $list );

if ( !$item )
    die( "トークンが無効です:".$args['token']." :".mysql_error());

$playerid = $item['name']; // クライアント判明

こういう感じになります。え〜と、

select [項目名1],[項目名2],... from [テーブル名] where [条件文]

という構文です。selectの後に付ける項目名は、返して欲しいリストの項目になります。例えばテーブルに100個くらい項目があったとしても、その中から特定の項目だけ選んだリストを作れるわけです。条件文には色々な書き方ができるようですが、上では「token項目が$tokenに等しいものだけ抽出」と条件を絞り込んでいます。関数が返すのは抽出されたリスト(配列)です。クエリがエラーだった場合はfalseになります。

 得たリストはmysql_fetch_array関数で1レコードずつ取り出す事ができます。クエリに成功したものの、該当するトークンが一つも無かった場合は$itemがfalseになるので、そこで弾いています。無効判定を通り抜ければ、トークンの持ち主のplayerIDが判明するわけです。

 登録プレイヤーリストを作成するのも上とほとんど同じ方法でできます:

// プレイヤーリスト作成
$list = mysql_query( "select name from player", $link );
if ( !$list )
    die( "リスト取得失敗!:".mysql_error());

$outArgs = array(
    "size" => 0
);
$size = 0;

while( $item = mysql_fetch_array($list) ) {
    if ( $item['name'] != $playerid ) {
        $outArgs[$size] = $item['name'];
        $size++;
    }
}

$outArgs['size'] = $size;

select文で全プレイヤーのリストを得ます。$outArgsがクライアントに返す引数で、連想配列になるため[インデックス, 名前]というセットにしています。ただし、対戦相手のリストなので自分自身は除外しています。

 リストアップした後、クライアントにそれを戻せば良いのですが、その前に呼び出したプレイヤーのトークンを更新しておきます。tokenテーブルのトークンを差し替えるわけです。これにはMySQLのupdate構文を用います:

// トークン更新
$newToken = generateToken();
mysql_query( "update token set token='".$newToken."' where name='".$playerid."'", $link );

update構文は、

update [テーブル名] set [項目名1]=[値1],[項目名2]=[値2],... where [条件文]

で、指定のテーブルの特定の(条件文に該当する)項目を変更します。上のupdate文は、tokenテーブルの中で$playeridに該当するレコードのtoken項目のトークンを変更します。

 という事で、getMachingList関数はこんな感じとなりました:

<?php
    // トークン発行
    function generateToken() {
        $token = "";
        $str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        for ( $i = 0; $i < 32; $i++ )
            $token = $token.substr( $str, mt_rand(1, 62), 1 );
        return $token;
    }

    function getMatchingList( $args ) {
        // MySQLオープン
        $link = mysql_connect( "localhost", "root", [パスワード] );
        if ( !$link )
            die("MySQLの接続に失敗しました");

        $error = mysql_select_db( 'submarine', $link );
        if ( !$error ) {
            mysql_close( $link );
           die( "データベースへの接続に失敗しました:".mysql_error() );
        }

        mysql_query( "SET NAMES utf8" );

        // トークンチェック
        if ( array_key_exists( 'token', $args ) == false )
        die( "トークンがありません" );

        $list = mysql_query( "select name from token where token='".$args['token']."'", $link );
        if ( !$list )
            die( "クエリが無効です:".mysql_error());
        $item = mysql_fetch_array( $list );
        if ( !$item )
            die( "トークンが無効です:".$args['token']." :".mysql_error());

        $playerid = $item['name']; // クライアント判明


        // プレイヤーリスト作成
        $list = mysql_query( "select name from player", $link );
        if ( !$list )
            die( "リスト取得失敗!:".mysql_error());

        $outArgs = array(
            "size" => 0
        );
        $size = 0;
        while( $item = mysql_fetch_array($list) ) {
            if ( $item['name'] != $playerid ) {
                $outArgs[$size] = $item['name'];
                $size++;
            }
        }
        $outArgs['size'] = $size;

        // トークン更新
        $outArgs['token'] = generateToken();
        mysql_query( "update token set token='".$outArgs['token']."' where name='".$playerid."'", $link );

        mysql_close( $link );

        // JSON文字列でクライアントへ
        print( json_encode( $outArgs ) );
    }

    if ( array_key_exists( 'function', $_GET ) == false || array_key_exists( 'args', $_GET ) == false) {
        die( "何か無いっす" );
    }

    // JSON文字列 → ハッシュ引数
    $args = json_decode( $_GET['args'], true );

    $func = $_GET['function'];
    $func( $args );
?>

ここでサーバ側のコードをわざわざ挙げたのは、ここに冗長な所がたっくさんあるからです。後でリファクタリングですね、これは(^-^;。

 で、ここまでを実行すると、次のような(味気ないですが)対戦相手リストを表示できます:

後はリストの誰かをクリックして対戦相手を決定する事になります。実際のマッチングは状況にあった人をもっと絞るのですが、今はこれで十分(^-^)。



D 「対戦開始!」の通知は?

 対戦相手を選択したら、クライアント側に定義されているJavaScriptのselectMatching関数がインデックスを引数として呼ばれる手筈になっています。この段階で「自分のトークン」「相手のplayerID」が分かっているので、両者を結び付ける事ができます。出来るのですが、大切な事をすっかり忘れていました。上のマッチング画面で選んだ人に通知する手段を作っていませんでした。

 現段階で、クライアント側の待ち受けはありません。そのため、サーバからクライアントに通知する手段がありません。そこで、プレイヤーを作成したらクライアント側に待ち受け専用の窓口を設ける事にしましょう。

 作るタイミングはサーバからトークンを貰った後です(トークンが無いとサーバとの通信ができないため)。そこでサーバ側にあるmessageConnection.phpのstartConnection関数を呼び出す事にします。この関数でクライアントにサーバからのメッセージを送るルートを作る事にします。

 まずはサーバ側。messageConnection.phpのstartConnection関数の中では、クライアントへの応答を一時的に止めます。前章ではusleep関数を用いて本当に一時的にプログラムをせき止めていましたが、今回はサーバ側が何らかのきっかけを捉えて適切に応答を返す必要があります。…て、おや?ちょっと待てよ…。例えば2人のクライアントがサーバに対して「独立」にstartConnection関数でメッセージを受ける窓口を確立したとして、サーバはどうやってその動いている最中のプログラムにきっかけを与えるのでしょうか?

ん〜、これはちょっと長くなりそうな予感…。しゃーない、いったんこの章は閉じる事にします(^-^;