カテゴリー「node.js」

まとめをしてみます。

  1. 第1回
  2. 第2回
  3. 第3回
  4. 第4回
  5. 第5回
  6. 第6回
  7. 第7回

という感じにまとまりました。

結局java側だけしかかけなったけど、

javaでは排他制御をかける(synchronizedブロック)がほんとに重要。

 

nodeはシングルスレッドだから、

排他制御は基本的には不要。

 

その他、大事なトピックとしては、

  • クライアントに送信しようとしても送信できないケースがある(たぶん避けらない)
  • バックエンドでDBを使う場合は、基本的にはつなぎっぱなし(ただし、RDBとRedisではちょっと扱いが違うので注意)

などでしょうか。

 

最後にスケールアウトについて。

単純にスケールアウトする場合、pub/subを使うしか手がない。

pub/subができるのは、

  • redis
  • ActiveMQ
  • Java Message Service

などがあるが、pub/subがあるとないとでは、

実装がかなり異なる。

重要なのは、

「sub」の接続を先に確立して、

処理を行い、

対象者に「pub」して、

「sub」でメッセージ送信を行う

という流れである。

 

pub/subを行うと、

データ自体もメモリに持てなくなるので、

十分注意しながらやっていただければと思います。

 

以上で、websocketの話はほんとにおしまい。

 

投稿日時:2015年04月02日 18:36   カテゴリー:java, node.js, websocket   [コメントがあればどうぞ]

ラウンド編。

また、websocketの処理だけ書く。

package trmpj.ws;

import java.util.concurrent.ConcurrentHashMap;

import javax.websocket.CloseReason;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import trmpj.TPair;
import trmpj.TReq;
import trmpj.TSession;
import trmpj.TWs;
import trmpj.req.TReqEnd;
import trmpj.req.TReqGame;
import trmpj.req.TReqPrepared;
import trmpj.res.TResEnded;
import trmpj.res.TResError;
import trmpj.res.TResGame;
import trmpj.res.TResInvalid;
import trmpj.res.TResNone;
import trmpj.res.TResStart;
import trmpj.res.TResWait;

/**
 * 対戦用処理
 *
 */
@ServerEndpoint(value = "/ws/round/{pairId}")
public class TWsRound extends TWs {

    /**
     * pairId => TPairのマップ
     */
    private static ConcurrentHashMap<String, TPair> pairs = new ConcurrentHashMap<String, TPair>();

    /**
     * pairId => Threadのマップ
     */
    private static ConcurrentHashMap<String, Thread> threads = new ConcurrentHashMap<String, Thread>();

    /**
     * openハンドラ
     * @param pairId
     * @param session
     */
    @OnOpen
    public void onOpen(@PathParam("pairId") String pairId, Session session) {
        // セッションにアクセスIDを格納する
        String accessdId = generateAccessId();
        session.getUserProperties().put(KEY_ACCESS_ID, accessdId);

        // アクセスIDを、onMessage, onCloseで取得するときの関連を付ける
        // ※sessionsのキー側はsessionでもよいのですが、
        // runnableからの参照時に、セッションが破棄されていると困るので、
        // あえて文字列にしている
        TSession ts = new TSession(session);
        sessions.putIfAbsent(accessdId, ts);
    }

    /**
     * messageハンドラ
     * @param pairId
     * @param session
     * @param message
     */
    @OnMessage
    public void onMessage(@PathParam("pairId") String pairId, Session session, String message) {
        // アクセスIDを取得
        String accessdId = (String) session.getUserProperties().get(KEY_ACCESS_ID);
        TSession ts = null;

        try {
            // アクセスIDをもとに、openハンドラで登録したTSessionを呼び出す
            if (!sessions.containsKey(accessdId)) {
                throw new Exception();
            }
            ts = sessions.get(accessdId);

            // リクエストを解析
            TReq req = ts.receiveMessage(message, TReq.class);
            if (req == null) {
                throw new Exception();
            }

            // ペアIDに応じたペア情報をマップから取り出す
            if (!pairs.containsKey(pairId)) {
                pairs.putIfAbsent(pairId, new TPair(pairId));
            }
            TPair pair = pairs.get(pairId);

            // ペア情報に排他制御をかける
            // ※重要
            synchronized (pair) {
                if (req.command.equals(TReq.COMMAND_PREPARED)) {
                    // preparedコマンド
                    TReqPrepared reqPrepared = ts.receiveMessage(message, TReqPrepared.class);
                    if (reqPrepared == null) {
                        throw new Exception();
                    }

                    // 既にペアが揃った状態ならここで終了
                    if (pair.isPrepared()) {
                        throw new Exception();
                    }

                    // 自分の情報をセッションに追加
                    ts.setUser(reqPrepared.user);

                    // ペア情報に自分の情報を追加
                    pair.addTSession(ts);

                    // 同一ユーザIDはエラー
                    if (pair.isDuplicated()) {
                        throw new Exception();
                    }

                    // 2人そろった状態なら、2人にstartを返す
                    if (pair.isPrepared()) {
                        // スレッドを止める
                        threads.remove(pairId);

                        TResStart resStart = new TResStart();
                        resStart.users.addAll(pair.getUsers());
                        pair.sendMessage(resStart);
                        return;
                    }

                    // 揃っていない状態なので、自分にwaitを返す
                    TResWait resWait = new TResWait();
                    ts.sendMessage(resWait);

                    // 別スレッドで、相手がくるまで数秒待機する
                    if (!threads.containsKey(pairId)) {
                        threads.putIfAbsent(pairId, new Thread(new TRunnable(pairId)));
                        threads.get(pair).start();
                    }
                    return;

                } else if (req.command.equals(TReq.COMMAND_GAME)) {
                    // gameコマンド
                    TReqGame reqGame = ts.receiveMessage(message, TReqGame.class);
                    if (reqGame == null) {
                        throw new Exception();
                    }

                    // 同一ユーザIDはエラー
                    if (pair.isDuplicated()) {
                        throw new Exception();
                    }

                    // 揃った状態でない、または終了状態なら何もしない
                    if (!pair.isPrepared() || pair.isEnded()) {
                        throw new Exception();
                    }

                    // 2人に返す
                    TResGame resGame = new TResGame();
                    resGame.useId = ts.getUser().userId;
                    resGame.data = reqGame.data;
                    pair.sendMessage(resGame);
                    return;

                } else if (req.command.equals(TReq.COMMAND_END)) {
                    // endコマンド
                    TReqEnd reqEnd = ts.receiveMessage(message, TReqEnd.class);
                    if (reqEnd == null) {
                        throw new Exception();
                    }

                    // 同一ユーザIDはエラー
                    if (pair.isDuplicated()) {
                        throw new Exception();
                    }

                    // ペアを終了状態にする
                    pair.setEnded(true);

                    // 自分に返す
                    TResEnded resEnded = new TResEnded();
                    ts.sendMessage(resEnded);
                    return;

                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        // エラー処理
        if (ts != null) {
            TResError resError = new TResError();
            ts.sendMessage(resError);
        }
    }

    /**
     * closeハンドラ
     * @param pairId
     * @param session
     * @param reason
     */
    @OnClose
    public void onClose(@PathParam("pairId") String pairId, Session session, CloseReason reason) {
        // アクセスIDを取得
        String accessdId = (String) session.getUserProperties().get(KEY_ACCESS_ID);
        TSession ts = null;

        try {
            // アクセスIDをもとに、openハンドラで登録したTSessionを呼び出す
            if (!sessions.containsKey(accessdId)) {
                throw new Exception();
            }
            ts = sessions.get(accessdId);

            // ペアIDに応じたペア情報をマップから取り出す
            if (!pairs.containsKey(pairId)) {
                throw new Exception();
            }
            TPair pair = pairs.get(pairId);

            // ペア情報に排他制御をかける
            // ※重要
            synchronized (pair) {
                // 自分の情報削除
                pair.removeTSession(ts);

                // 空になっていたら削除
                if (pair.isEmpty()) {
                    pairs.remove(pairId);
                    return;
                }

                // game中で、endでない場合は、残っている方にnoneを送信
                if (pair.isPrepared() && !pair.isEnded()) {
                    TResNone resNone = new TResNone();
                    pair.sendMessage(resNone);
                    return;
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        // セッション情報も破棄する
        sessions.remove(accessdId);
    }

    /**
     * 2人が揃うまで待つ別スレッド
     * @author furuta
     *
     */
    public class TRunnable implements Runnable {

        /**
         * ペアID
         */
        private String pairId;

        /**
         * コンストラクタ
         * @param pairId
         */
        public TRunnable(String pairId) {
            this.pairId = pairId;
        }

        /**
         * 処理
         */
        public void run() {
            try {
                // 10秒まつ
                Thread.sleep(10000);

                // ペアを取得
                if (!pairs.containsKey(pairId)) {
                    throw new Exception();
                }
                TPair pair = pairs.get(pairId);

                synchronized (pair) {
                    if (!pair.isPrepared()) {
                        // 揃っていない状態であれば、残っている人にinvalidを返す
                        TResInvalid resInvalid = new TResInvalid();
                        pair.sendMessage(resInvalid);
                    }
                }

            } catch (Exception e) {
                e.printStackTrace();
            }

            // スレッドを削除
            threads.remove(pairId);
        }
    }
}

ポイントはsynchronizedでしっかり排他制御をかけること。
これをやらないとだめだぜ。。

ソースコードはちゃんとテストしてないけど、
処理の流れはわかるはず。。

とりあえず、作成したコードをzipで固めておきます。

ダウンロード

 

これで、websocketは終了。

nodeは書く気力なし。。

投稿日時:2015年04月02日 18:23   カテゴリー:java, node.js, websocket   [コメントがあればどうぞ]

やっとかく。

とりあえず、websocketのメイン処理をさらす。

 

マッチング

package trmpj.ws;

import java.util.LinkedList;
import java.util.concurrent.ConcurrentHashMap;

import javax.websocket.CloseReason;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import trmpj.TReq;
import trmpj.TSession;
import trmpj.TWs;
import trmpj.req.TReqEntry;
import trmpj.res.TResEntried;
import trmpj.res.TResError;
import trmpj.res.TResPrepare;

/**
 * マッチング用処理
 *
 */
@ServerEndpoint(value = "/ws/matching/{type}")
public class TWsMatching extends TWs {

    /**
     * type => Set<TSession>のマップ
     */
    private static ConcurrentHashMap<Integer, LinkedList<TSession>> entries = new ConcurrentHashMap<Integer, LinkedList<TSession>>();

    /**
     * openハンドラ
     * @param type
     * @param session
     */
    @OnOpen
    public void onOpen(@PathParam("type") int type, Session session) {
        // セッションにアクセスIDを格納する
        String accessdId = generateAccessId();
        session.getUserProperties().put(KEY_ACCESS_ID, accessdId);

        // アクセスIDを、onMessage, onCloseで取得するときの関連を付ける
        // ※sessionsのキー側はsessionでもよいのですが、
        // runnableからの参照時に、セッションが破棄されていると困るので、
        // あえて文字列にしている
        TSession ts = new TSession(session);
        sessions.putIfAbsent(accessdId, ts);
    }

    /**
     * messageハンドラ
     * @param type
     * @param session
     * @param message
     */
    @OnMessage
    public void onMessage(@PathParam("type") int type, Session session, String message) {
        // アクセスIDを取得
        String accessdId = (String) session.getUserProperties().get(KEY_ACCESS_ID);
        TSession ts = null;

        try {
            // アクセスIDをもとに、openハンドラで登録したTSessionを呼び出す
            if (!sessions.containsKey(accessdId)) {
                throw new Exception();
            }
            ts = sessions.get(accessdId);

            // リクエストを解析
            TReq req = ts.receiveMessage(message, TReq.class);
            if (req == null) {
                throw new Exception();
            }

            // タイプに応じたエントリー情報をマップから取り出す
            if (!entries.containsKey(type)) {
                entries.putIfAbsent(type, new LinkedList<TSession>());
            }
            LinkedList<TSession> es = entries.get(type);

            // タイプごとのエントリー情報に排他制御をかける
            // ※重要
            synchronized (es) {
                if (req.command.equals(TReq.COMMAND_ENTRY)) {
                    // entryコマンド
                    TReqEntry reqEntry = ts.receiveMessage(message, TReqEntry.class);
                    if (reqEntry == null) {
                        throw new Exception();
                    }

                    // 自分の情報をセッションに追加
                    ts.setUser(reqEntry.user);

                    try {
                        // 先にエントリーしている人の情報をとる
                        TSession tsPrev = es.pop();

                        // 情報をとった結果、同じユーザIDの場合は、エラーとする
                        if (tsPrev.getUser().userId.equals(ts.getUser().userId)) {
                            throw new Exception();
                        }

                        // エントリーしている人の情報と、自分の情報を、マッチング成立として両者に返す
                        TResPrepare resPrepare = new TResPrepare();
                        resPrepare.type = type;
                        resPrepare.seed = (int) (Math.random() * 10000) + 1;
                        resPrepare.users.add(ts.getUser());
                        resPrepare.users.add(tsPrev.getUser());

                        ts.sendMessage(resPrepare);
                        tsPrev.sendMessage(resPrepare);
                        return;

                    } catch (Exception e) {
                        // ここはpopで例外が発生するが、無視する
                    }

                    // 自分を加える
                    es.add(ts);

                    // 自分にエントリー完了を送信する
                    TResEntried resEntried = new TResEntried();
                    ts.sendMessage(resEntried);
                    return;

                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        // エラー処理
        if (ts != null) {
            TResError resError = new TResError();
            ts.sendMessage(resError);
        }
    }

    /**
     * closeハンドラ
     * @param type
     * @param session
     * @param reason
     */
    @OnClose
    public void onClose(@PathParam("type") int type, Session session, CloseReason reason) {
        // アクセスIDを取得
        String accessdId = (String) session.getUserProperties().get(KEY_ACCESS_ID);
        TSession ts = null;

        try {
            // アクセスIDをもとに、openハンドラで登録したTSessionを呼び出す
            if (!sessions.containsKey(accessdId)) {
                throw new Exception();
            }
            ts = sessions.get(accessdId);

            // 自分が既に登録されていたら、削除する
            if (!entries.containsKey(type)) {
                throw new Exception();
            }
            LinkedList<TSession> es = entries.get(type);
            es.remove(ts);

        } catch (Exception e) {
            e.printStackTrace();
        }

        // セッション情報も破棄する
        sessions.remove(accessdId);
    }
}

最大のポイントは、
onMessageにおけるsynchronizedブロックである。

これでちゃんと排他制御をかけないと、

エラーになるぜ。。

 

ながいので、ラウンド編は次回に。

 

投稿日時:2015年04月02日 18:11   カテゴリー:java, node.js, websocket   [コメントがあればどうぞ]

ずいぶん間が空いたけど、

javaでwebsocketやるときのサーバ側クラス設計ができました。

↓のような感じです。

websocket_uml

(クラス図あってるかな??)

 

ポイントとしては、

  • websocketのSessionインスタンスを内包するクラスを作る
  • ユーザ情報はセッション内で管理する
  • ペア(自分と相手)の情報を管理するクラスを作成しておく
  • 実際のwebsocket処理では、スレッドセーフなCollectionを使ってインスタンスを管理する

という感じになります。

 

スレッドセーフなCollectionでないと、インスタンスの取り違えが起きますので、ご注意を。

ThreadLocalを使うという手もありますが、

websocketの場合、

  • openハンドラの呼び出しは1回
  • messageハンドラの呼び出しはn回
  • closeハンドラの呼び出しは1回

なので、open時に必要なインスタンスを作成して、closeで破棄するとよいです。

(※messageで作成するときもありますが)

 

実は、websocketではこの考えは結構重要で、

たとえば、DB接続する際、

messageハンドラで、

connect -> disconnect

を繰り返すと、高負荷に耐えられないです。

ここで、

openハンドラでconnectし、closeハンドラでdisconnectする。

そして、messageハンドラで操作する、

という形だと接続のコストが抑えられるので、

結構有効です。

(※もちろん、アプリケーションの特性によりますが)

 

次は、javaのソースコードを公開する予定。

node.js側は気力があったら作成します。。

 

投稿日時:2015年02月19日 15:45   カテゴリー:java, node.js, websocket   [コメントがあればどうぞ]

websocketにおいて、

node.jsでpm2を使う際の問題点をまとめてみる。

 

バージョンは、

<node.js>

v0.10.xx

<pm2>

v0.11.xx

v0.12.xx

 

上記の場合、pm2のバージョンに関わらず、

  • windowsでは使えない
  • clusterモードで再起動(restart)を行うと、ポート開放されない(→killする必要あり)
  • GodDaemonを起動したユーザ以外では操作不可能(pm2 listでもGodDaemonが起動するので要注意)
  • 再起動の抑制オプションがない

という感じです。

 

clusterモードの最悪な点として、

1.アプリが落ちる⇒2.再起動⇒3.ポート開放されずにアクセス不可能

となる点かなと。

これが4プロセス起動していたら、

1プロセスのダウンが全プロセスに影響すること。

つまり、

pm2 start app.js -i 4

なんてして、4プロセス起動しても、

1プロセスダウンすれば、もうおしまい。

1つのプロセスの再起動が、他プロセスにも影響して、アクセス不可能になる。

再起動の抑止オプションがあれば、まだいけそうな気もするが。。。

回避するには、

node自体をv.11.xx(現時点でunstable)にすればいけるようだが、未検証なので、何とも言えない。。。

 

じゃあ、forkモードで起動すればいいじゃんとなりますが、

forkの場合は、プロセスとポートが1対1なので、

4プロセス起動するには、4ポート必要になりますので、柔軟性は低くなる。

ただ、forkモードでは、再起動時にポート開放されますので、

node.jsの前に置くフロントサーバをコントロールできれば問題はないかなと。

 

まだまだ、node.jsは発展途上なわけなので、

このあたりのリスクを十分に踏まえる必要がありますね。

 

余談ですが、websocketは再起動が命取りになるので、

foreverでポート分散して、再起度を抑止するようにしておくほうが

現在取りうる手段としてはベターかなと感じています。

 

投稿日時:2015年01月13日 12:47   カテゴリー:node.js, websocket   [コメントがあればどうぞ]

前回ゲーム仕様を決めたので、

今回は詳細は処理フローを作成します。

以下のような感じを想定しています。

websocket_seq

上記イメージの解説です。

  1. マッチングサーバと接続します。
  2. エントリーに必要な情報をおくります。サーバ側が1人目の受付の場合、「受付完了」という情報を返します。もし、ここで、2人目なら、「成立完了」という情報を2人に返します。
  3. 「成立完了」を受け取ったクライアントは、マッチングサーバから抜けて、対戦サーバ側と接続します。
  4. 対戦サーバと接続したら、「準備完了」を送ります。ここで、2人がそろって初めて「開始」を返します。
  5. ゲーム中は、片側(相手)から送られた情報を、2人に送信します。
  6. 無事ゲームが終わったら(終了判断はクライアントに依存)、1人1人が別々に「終了」を送信し、「終了」を受け取ったら各人で、対戦サーバから抜けます。
  7. もし、ゲームが始まっている状態で、片側(相手)が切断したら、残っている方に「相手が切断した」という情報を送ります。今回の仕様では、相手の勝利となり、相手側は「終了」を送信します。

ポイントは4、6、7です。

4のとき、相手が来ないケースもあるので、制限時間以内に相手が来なかったらどのような情報を返すかをサーバ側で対応する必要があります。

6はなぜ2人に送信しないかというと、下手に2人に送信してしまうと、終了時のクライアントの表示を片側が変更可能となってしまうからです。

7は片方の切断時に、残っている方へ何かしらの通知を行う対応です。

 

pub/subを使わない前提である以上、

上記の操作をすべてメモリ上の変数にて制御をかけます。

(※当然サービスのレベルでは、何かしらのデータベースへ書き込む等がありますが、ここでは考慮しません。)

 

次は、サーバ側のクラス設計を書きます。

 

投稿日時:2014年12月01日 12:43   カテゴリー:java, node.js, websocket   [コメントがあればどうぞ]

今のところ、3回にわたりwebsocketの比較を書いてきましたが、

これからは、

  • 処理フローの設計
  • プログラミング

となります。

 

とその前に、一服。

 

個人的な見解としては、

node.jsよりjavaの方がwebsocketは組みやすいかなと思っています。

理由は、

  • 過去資産が豊富なので、選択肢が多い
  • 型制約があるので、全体的に堅牢になる
  • eclipseなどのIDEが優秀

といったところでしょうか。

node.jsはjavascriptなので、

なんでもできてしまう分(たとえば、ユーザ定義オブジェクトになんでも突っ込めるとか)、

コードがわかりにくくなりがちです。(※規約で気を付けないとね)

また、IOが基本的に非同期で実行されるとかが、

やっぱりやりにくいなと感じてます。

ただ、java側はAPサーバを動かすコスト(メモリ消費)がやや高いので、

省エネなのはnode.jsかなとも思います。

 

ちなみに、javaでwebsocketをやる場合、

jetty8とかは独自実装になっており、

jetty9でJSR356に対応してます。

※jetty9の場合、jettyの独自パッケージの中で、JSR356に読み替えているようです。mavenのpomファイルも後で作成します。

JSR356に対応しておけば、tomcatに移植しても動くので、独自実装はもう使わないほうがよかです。

なお、あのplayframework2.3のwebsocketはjetty8でした。(playは機能面はすごいけど、ちょっと重いよね。。)

 

最終的には、どちらもクセはあります。

取り巻く環境、開発者の経験等を考慮して、

選択するとよいと思います。

(go言語なんかでもやっている人はいるのかな?goはこれから勉強します。)

 

投稿日時:2014年11月26日 13:04   カテゴリー:java, node.js, websocket   [コメントがあればどうぞ]

前回の続き。

前回の大切な前提として、

pub/subはなし(つまり、メモリ内で処理を完結可能)

というのがありました。

 

実際に対戦ゲームの作成を通して、処理フローを整理しましょう。

別に面白いものを作る必要はないので、

簡単なゲームとして、以下のような仕様とします。

  1. 2人対戦の神経衰弱
  2. ゲームの種別として、4×4、4×8、4×12の3種類(使用枚数が16枚、32名、48枚)
  3. ユーザはマッチング時に、ゲーム種別を選択可能
  4. マッチングが成立したらゲーム開始
  5. ゲーム中に離脱した場合、その時点で残っている人の勝利
  6. 離脱者なしにゲームが終わった場合、自分の取得枚数が多ければ勝利で、少なければ敗北

 

pub/subはないので、

スケールアウトの手段が現状皆無です。

そこで、URLを絡めて、効果的にスケールアウトができるようにしておきましょう。

URLはこんな感じです。

【マッチング】 /ws/matching/${type}

${type}とは、上記の4×4などをコード化したもの

【対戦】 /ws/round/${pairId}

${pairId}とは対戦者同士を結びつける一意のキーで、形式は0-9a-zの繰り返し

 

こんな感じでURLを設計しておくことで、

websocketのリバースプロキシ可能なサーバ(nginxやapache2.4など)で、

URLに応じた振り分けが可能となります。

この設計方式だと、マッチングが成立したら、

一旦マッチングサーバとの接続を閉じて、

新たに対戦サーバと接続する必要があります。

 

※nginxの設定例

vim /etc/nginx/websocket_params

proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Document-URI $document_uri;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";

vim /etc/nginx/conf.d/game.conf


# マッチング4x4
upstream ws_matching_4x4 {
    server localhost:8081;
    server localhost:8082 backup;
}
# マッチング4x8
upstream ws_matching_4x8 {
    server localhost:8083;
    server localhost:8084 backup;
}
# マッチング4x12
upstream ws_matching_4x12 {
    server localhost:8085;
    server localhost:8086 backup;
}

# 対戦(ペアIDの先頭がcからnまでの12個)
upstream ws_round_0b {
    server localhost:8181;
    server localhost:8182 backup;
}
# 対戦(ペアIDの先頭がcからnまでの12個)
upstream ws_round_cn {
    server localhost:8183;
    server localhost:8184 backup;
}
# 対戦(ペアIDの先頭がoからzまでの12個)
upstream ws_round_oz {
    server localhost:8185;
    server localhost:8186 backup;
}

server {
    listen 80;
    server_name game.example.com;

    charset utf-8;
    root /var/www/game/public;

    # エラーページ
    error_page 404 /404.html;
    error_page 500 502 503 504 /50x.html;

    # websocket_paramsのインクルード
    include /etc/nginx/websocket_params;

    # マッチング4x4
    location ~ ^/ws/matching/4/$ {
        proxy_pass http://ws_matching_4x4;
    }
    # マッチング4x8
    location ~ ^/ws/matching/8/$ {
        proxy_pass http://ws_matching_4x8;
    }
    # マッチング4x12
    location ~ ^/ws/matching/12/$ {
        proxy_pass http://ws_matching_4x12;
    }

    # 対戦(ペアIDの先頭がcからnまでの12個)
    location ~ ^/ws/round/[0-9a-b][0-9a-z]+?/$ {
        proxy_pass http://ws_round_0b;
    }
    # 対戦(ペアIDの先頭がcからnまでの12個)
    location ~ ^/ws/round/[c-n][0-9a-z]+?/$ {
        proxy_pass http://ws_round_cn;
    }
    # 対戦(ペアIDの先頭がoからzまでの12個)
    location ~ ^/ws/round/[o-z][0-9a-z]+?/$ {
        proxy_pass http://ws_round_oz;
    }
}

※全部localhostにしてポートを変更していますが、細分化すれば、上記設定で12台に切り分けられます。

※もちろん、もっと細かく分けることも可能です。

 

という具合で考えると、

ちょっと大げさに以下のようなイメージでインフラが考えられます。

websocket_struct

 

 

このようにフロントのサーバの機能を利用することで、

pub/subを使わずにwebsocketを通じたメッセージのやり取りができます。

狙いとしては

同一属性者を同じブロセスに閉じ込めることで、メモリ内の操作で処理を完結させる

ということになります。

 

投稿日時:2014年11月25日 18:17   カテゴリー:java, node.js, websocket   [コメントがあればどうぞ]

さて、本腰入れて。

今回の比較に当たり、大きな前提として、

pub/subはなし(つまり、メモリ内で処理を完結可能)

というのをあげておきます。

というのも、pub/subがあるときと、ないときでは、

結構実装が異なってしまうためです。

(※うまく抽象化できればいいのですが、node.js側は上手に継承を使うのが難しいので)

 

この前提のもと、

javaとnode.jsの大きな違いは、

java:マルチスレッド

node.js:シングルスレッド

ということであります。

 

この違いは、プログラムを書く上で、

排他制御」が必要か、不要かということにつながります。

 

たとえば、対戦ゲームを作るとした場合、

ゲームには「マッチング」と「対戦中」で処理が大きく2つに分かれます。

マッチングは諸条件あるものの、基本的には来た順番にさばく手法がとられると思います。

このとき、javaでは必ず排他制御をかける必要があります。

それに対し、node.jsでは、シングルスレッドなので、排他制御をかける必要はありません。

 

ここが両者の最大の違いとなります。

 

※あくまでメモリ内で完結する場合です。(つまり1プロセス)

複数台を使う場合は、node.jsでも排他制御が必要となる場合があります。

投稿日時:2014年11月25日 14:23   カテゴリー:java, node.js, websocket   [コメントがあればどうぞ]

websocketというと、チマタではnode.jsが流行っていますね。

 

しかし、javaもJSR356という規約の下、

各サーブレットコンテナで対応が完了しており、

個人的にはnode.jsと対をなす位までの能力があるかと思います。

 

javaでもnode.jsでもwebsocketを構築した筆者が、

数回にわたりお互いの特徴と実装の注意点などを書いていきます。

 

なお、バージョンは

【java】

jetty9.X(JSR356に対応した形)

【node.js】

v10.3X

となります。

 

 

投稿日時:2014年11月25日 13:37   カテゴリー:java, node.js, websocket   [コメントがあればどうぞ]