cocos2d-xでsocket.ioを使う Android版

cocos2d-xでsocket.ioを使う iOS版 - おかひろの雑記 ではCocos2d-xでsocket.ioを使うためのiOSの設定をしましたが、今度はAndroidの方を設定します。

Androidではsocket.io-java-client(https://github.com/Gottox/socket.io-java-client)を使いますが、そのまま使うと、切断時にCPU使用率が下がらなくなるバグがあるようなので、少し手を加えます。


今回使用したバージョン

  • Cocos2d-x 3.9
  • socket.io-java-client 最新のもの
  • Java-WebSocket 1.3.0

動作するサンプルはこちら。 https://github.com/okahiro/Cocos2dxSocketIO
(Cocos2d-xのフルプロジェクトなので容量大きいです。socket.ioサーバーを用意する必要があります。)


ライブラリをセットアップ
まずはsocket.io-java-clientとJava-WebSocketをそれぞれダウンロードしてzipを解凍します。

まず、Java-WebSocketの方を解凍したフォルダにターミナルで移動し、antコマンドを実行します。

$ ant

するとdistフォルダにjava_websocket.jarが作成されるので、これをWebSocket.jarという名前に変更し、
socket.io-java-clientを解凍したフォルダのlibsのWebSocket.jarを上書きします。
その後、socket.io-java-clientのフォルダにターミナルで移動し、ant jarコマンドを実行します。

$ ant jar

これによりjarフォルダと中にsocketio.jarが作成されます。
このsocketio.jarをCocos2d-xのプロジェクトに組み込みます。(AndroidStudioを使う場合は、proj.android-studio/app/libsにコピー)


実装
NativeCodeLauncher.hとSocketManagerクラスはcocos2d-xでsocket.ioを使う iOS版 - おかひろの雑記と同じです。

NativeCodeLauncher.cpp

#include "NativeCodeLauncher.h"
#include "NativeCodeLauncherJni.h"

namespace Cocos2dExt
{
	// Socket.IO関連処理
	void NativeCodeLauncher::connectToSocketIO(char const *host,int port)
	{
		connectToSocketIOJNI(host,port);
	}
	void NativeCodeLauncher::emitToSocketIO(char const *event,char const *message)
	{
		emitToSocketIOJNI(event,message);
	}
	void NativeCodeLauncher::disconnectFromSocketIO()
	{
		disconnectFromSocketIOJNI();
	}
}

NativeCodeLauncherJni.h

#include <jni.h>
#include <string>

extern "C"
{
	// Socket.IO関連処理
	extern void connectToSocketIOJNI(char const *host,int port);
	extern void emitToSocketIOJNI(char const *event,char const *message);
	extern void disconnectFromSocketIOJNI();
}

NativeCodeLauncherJni.cpp

#include "NativeCodeLauncherJni.h"
#include <android/log.h>
#include "platform/android/jni/JniHelper.h"
#include "SocketManager.hpp"

#define  LOG_TAG    "NativeCodeLauncher"
#define  LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define CLASS_NAME "org/cocos2dx/cpp/AppActivity"

typedef struct JniMethodInfoEx_
{
    JNIEnv *    env;
    jclass      classID;
    jmethodID   methodID;
} JniMethodInfoEx;

extern "C"
{
	// get env and cache it
	static JNIEnv* getJNIEnv(void)
	{
		JNIEnv *env = 0;

		// get jni environment
		if (cocos2d::JniHelper::getJavaVM()->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK)
		{
			LOGD("Failed to get the environment using GetEnv()");
		}

		if (cocos2d::JniHelper::getJavaVM()->AttachCurrentThread(&env, 0) < 0)
		{
			LOGD("Failed to get the environment using AttachCurrentThread()");
		}

		return env;
	}

	// get class and make it a global reference, release it at endJni().
	static jclass getClassID(JNIEnv *pEnv)
	{
		jclass ret = pEnv->FindClass(CLASS_NAME);
		if (! ret)
		{
			LOGD("Failed to find class of %s", CLASS_NAME);
		}

		return ret;
	}

	static bool getStaticMethodInfo(JniMethodInfoEx &methodinfo, const char *methodName, const char *paramCode)
    {
		jmethodID methodID = 0;
		JNIEnv *pEnv = 0;
		bool bRet = false;

        do 
        {
			pEnv = getJNIEnv();
			if (! pEnv)
			{
				break;
			}

            jclass classID = getClassID(pEnv);

            methodID = pEnv->GetStaticMethodID(classID, methodName, paramCode);
            if (! methodID)
            {
                LOGD("Failed to find static method id of %s", methodName);
                break;
            }

			methodinfo.classID = classID;
			methodinfo.env = pEnv;
			methodinfo.methodID = methodID;

			bRet = true;
        } while (0);

        return bRet;
    }
	
	// Socket.IO
	void connectToSocketIOJNI(char const *host,int port)
	{
		JniMethodInfoEx methodInfo;
		
		if (!getStaticMethodInfo(methodInfo, "connectToSocketIO", "(Ljava/lang/String;I)V"))
		{
			return;
		}
		jstring hostArg = methodInfo.env->NewStringUTF(host);
		
		methodInfo.env->CallStaticVoidMethod(methodInfo.classID, methodInfo.methodID,hostArg,port);
		methodInfo.env->DeleteLocalRef(hostArg);
		methodInfo.env->DeleteLocalRef(methodInfo.classID);
	}
	void emitToSocketIOJNI(char const *event,char const *message)
	{
		JniMethodInfoEx methodInfo;
		
		if (!getStaticMethodInfo(methodInfo, "emitToSocketIO", "(Ljava/lang/String;Ljava/lang/String;)V"))
		{
			return;
		}
		
		jstring eventArg = methodInfo.env->NewStringUTF(event);
		jstring messageArg = methodInfo.env->NewStringUTF(message);
		
		methodInfo.env->CallStaticVoidMethod(methodInfo.classID, methodInfo.methodID, eventArg,messageArg);
		methodInfo.env->DeleteLocalRef(eventArg);
		methodInfo.env->DeleteLocalRef(messageArg);
		methodInfo.env->DeleteLocalRef(methodInfo.classID);
	}
	void disconnectFromSocketIOJNI()
	{
		JniMethodInfoEx methodInfo;
		
		if (!getStaticMethodInfo(methodInfo, "disconnectFromSocketIO", "()V"))
		{
			return;
		}
		
		methodInfo.env->CallStaticVoidMethod(methodInfo.classID, methodInfo.methodID);
		methodInfo.env->DeleteLocalRef(methodInfo.classID);
	}
	// メッセージを受け取った時の処理
	void Java_org_cocos2dx_cpp_AppActivity_nativeReceiveMessage(JNIEnv *env,jobject thiz,
																  jstring event,jstring message)
	{
		const char *eventChar = env->GetStringUTFChars(event,0);
		const char *messageChar = env->GetStringUTFChars(message,0);
		
		SocketManager::getInstance()->receiveMessage(eventChar,messageChar);
		
		env->ReleaseStringUTFChars(event, eventChar);
		env->ReleaseStringUTFChars(message, messageChar);
	}
	// Socket接続された時
	void Java_org_cocos2dx_cpp_AppActivity_nativeOnSocketConnected(JNIEnv *env,jobject thiz)
	{
		SocketManager::getInstance()->onConnected();
	}
	// Socket切断された時
	void Java_org_cocos2dx_cpp_AppActivity_nativeOnSocketDisconnected(JNIEnv *env,jobject thiz)
	{
		SocketManager::getInstance()->onDisconnected();
	}
	// Socketでエラーが発生した時
	void Java_org_cocos2dx_cpp_AppActivity_nativeOnSocketError(JNIEnv *env,jobject thiz,jstring errorMessage)
	{
		const char *errorMessageChar = env->GetStringUTFChars(errorMessage,0);
		
		SocketManager::getInstance()->onError(errorMessageChar);
		
		env->ReleaseStringUTFChars(errorMessage, errorMessageChar);
	}
}

AppActivity.java

package org.cocos2dx.cpp;

import android.util.Log;

import org.cocos2dx.lib.Cocos2dxActivity;
import org.json.JSONObject;

import java.net.MalformedURLException;

import io.socket.*;

public class AppActivity extends Cocos2dxActivity
{
    // C++側のメソッドを呼ぶ宣言
    public static native void nativeReceiveMessage(String event, String message);
    public static native void nativeOnSocketConnected();
    public static native void nativeOnSocketDisconnected();
    public static native void nativeOnSocketError(String errorMessage);

    static private SocketIO socket = null;
    private static final String TAG = "Cocos2dxSocketIOSample";


    // Socket.IO接続
    public static void connectToSocketIO(String host, int port) throws MalformedURLException
    {
        String url = String.format("http://%s:%d", host, port);
        socket = new SocketIO(url);
        socket.connect(iocallback);

        Log.d(TAG, "Socket 接続しようとします");
    }

    // Socketにイベント送信
    public static void emitToSocketIO(String event, String message)
    {
        if (socket != null && socket.isConnected())
        {
            // イベント送信
            socket.emit(event, message);
            Log.d(TAG, "emit. " + event + " , " + message);
        }
        else if(socket != null && !socket.isConnected())
        {
            // Socket接続されていないのに送信しようとした場合
            AppActivity.nativeOnSocketError("Socket is not connected.");
        }
    }

    // Socket.IO切断
    public static void disconnectFromSocketIO()
    {
        if(socket != null && socket.isConnected())
        {
            socket.disconnect();

            Log.d(TAG, "Socket 切断します");
        }
    }

    // Socket.IOコールバック関数集合
    private static IOCallback iocallback = new IOCallback()
    {
        @Override
        public void onConnect()
        {
            Log.d(TAG, "Socket 接続されました");
            AppActivity.nativeOnSocketConnected();
        }

        @Override
        public void onDisconnect()
        {
            Log.d(TAG, "Socket 切断されました");
            AppActivity.nativeOnSocketDisconnected();
        }

        @Override
        public void onMessage(JSONObject json, IOAcknowledge ack)
        {
            Log.d(TAG, "Socket onMessage");
        }

        @Override
        public void onMessage(String data, IOAcknowledge ack)
        {
            Log.d(TAG, "Socket onMessage");
        }

        @Override
        public void on(String event, IOAcknowledge ack, Object... args)
        {
            Log.d(TAG, "received . " + args[0]);
            AppActivity.nativeReceiveMessage(event, String.valueOf(args[0]));
        }

        @Override
        public void onError(SocketIOException socketIOException)
        {
            Log.d(TAG, "Socket エラー発生しました");
            socketIOException.printStackTrace();
            AppActivity.nativeOnSocketError(socketIOException.getLocalizedMessage());
        }
    };
}