https://github.nhnent.com/game-server-engine/sample-game-server.git
GameAnvil Server API - Java doc
-Dco.paralleluniverse.fibers.detectRunawayFibers=FALSE
-Dco.paralleluniverse.fibers.verifyInstrumentation=FALSE
-Xms6g
-Xmx6g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:+UseStringDeduplication
src/main/resources/
Mavenタブのinstallコマンドでサーバーをインストールします。この時、コンパイルタイムにAOT Instrumentationを進行します。
設定しておいた"sample_server"構成を利用してサーバーを実行します。
サーバーが正常に起動したら、以下のようにすべてのノードのonReadyログが出力されます。
http://127.0.0.1:25150/management/nodeInfoPage
URLで現在ローカルのsample_game_serverの状態を確認できます。
エラー確認
正常にサーバーが実行できなかった場合は、設定をもう一度確認してみるか、logのエラー部分を確認してお問い合わせください。
<!-- gameanvil-->
<dependency>
<groupId>com.nhn.gameanvil</groupId>
<artifactId>gameanvil</artifactId>
<version>1.1.0-jdk8</version>
</dependency>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<!-- executable jarでmainで実行されるクラス -->
<mainClass>com.nhn.gameanvil.sample.Main</mainClass>
<!-- jarファイル内のMETA-INF/MANIFEST.MFにclasspath情報が追加される -->
<addClasspath>true</addClasspath>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.nhn.gameanvil.sample.Main</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/io.netty.versions.properties</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/services/java.sql.Driver</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/LICENSE</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/NOTICE</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/services/reactor.blockhound.integration.BlockHoundIntegration</resource>
</transformer>
</transformers>
<artifactSet>
<excludes>
<exclude>javax.activation:javax.activation-*</exclude>
<exclude>org.javassist:javassist*</exclude>
</excludes>
</artifactSet>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>module-info.class</exclude>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
<exclude>META-INF/*.MF</exclude>
<exclude>META-INF/*.txt</exclude>
<exclude>about.html</exclude>
</excludes>
</filter>
</filters>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<goals>
<goal>exec</goal>
</goals>
</execution>
</executions>
<configuration>
<executable>java</executable>
<arguments>
<argument>-classpath</argument>
<!-- automatically creates the classpath using all project dependencies, also adding the project build directory -->
<classpath/>
<!-- Main class -->
<argument>com.nhn.gameanvil.sample.Main</argument>
</arguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<!-- Ant task for Quasar AOT instrumentation -->
<execution>
<id>Running AOT instrumentation</id>
<phase>compile</phase>
<configuration>
<tasks>
<taskdef name="instrumentationTask" classname="co.paralleluniverse.fibers.instrument.InstrumentationTask" classpathref="maven.dependency.classpath"/>
<instrumentationTask>
<fileset dir="${project.build.directory}/classes/" includes="**/*.class"/>
</instrumentationTask>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
<execution>
<phase>package</phase>
<configuration>
<tasks>
<copy todir="target/config/" overwrite="false">
<fileset dir="target/classes/">
<include name="logback.xml" />
<include name="mybatis-config.xml" />
<include name="GameAnvilConfig.json" />
</fileset>
</copy>
<copy todir="target/query/" overwrite="false">
<fileset dir="target/classes/query/">
<include name="*.xml" />
</fileset>
</copy>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
正常に実行できたら、./target/ フォルダにビルドされたファイルがあります。
サーバーを起動するには、sample_game_server-1.0.1.jarファイルとconfig, queryフォルダのファイルをコピーして使用してください。
コマンドプロンプトから実行
コマンドプロンプト(cmd)を実行してビルドされたtargetフォルダに移動します(自分の環境に合ったパスで進行)。
command実行
java -Dco.paralleluniverse.fibers.detectRunawayFibers=false -Dco.paralleluniverse.fibers.verifyInstrumentation=false -Dconfig.file=.\config\GameAnvilConfig.json -Dlogback.configurationFile=.\config\logback.xml -DmybatisConfig=.\config\mybatis-config.xml -Xms6g -Xmx6g -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+UseStringDeduplication -jar .\sample_game_server-1.1.0.jar
基本的に、実行時に別途オプションが指定されていない場合はビルドする時に指定されている環境ファイルを適用
com.nhn.gameanvil.sample.mybatis.GameSqlSessionFactory参考
onReadyが表示されたら正常
毎回mavenビルドでテストするのが面倒で、ローカルでテストする時はVm Optionに-javaagent:.\src\main\resources\META-INF\quasar-core-0.7.10-jdk8.jar=bm
オプションを追加し、intelliJですぐにローカルサーバーを実行できます。
<!-- Ant task for Quasar AOT instrumentation -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>Running AOT instrumentation</id>
<phase>compile</phase>
<configuration>
<tasks>
<taskdef name="instrumentationTask" classname="co.paralleluniverse.fibers.instrument.InstrumentationTask" classpathref="maven.dependency.classpath"/>
<instrumentationTask>
<fileset dir="${project.build.directory}/classes/" includes="**/*.class"/>
</instrumentationTask>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
-javaagent:.\src\main\resources\META-INF\quasar-core-0.7.10-jdk8.jar=bm
java -javaagent:.\lib\quasar-core-0.7.10-jdk8.jar=bm -Dco.paralleluniverse.fibers.detectRunawayFibers=false -Dconfig.file=.\config\GameAnvilConfig.json -Dlogback.configurationFile=.\config\logback.xml -DmybatisConfig=.\config\mybatis-config.xml -Xms6g -Xmx6g -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+UseStringDeduplication -jar .\sample_game_server-1.1.0.jar
public static void main(String[] args) {
GameAnvilBootstrap bootstrap = GameAnvilBootstrap.getInstance();
// クライアントと転送するプロトコルの定義 - 順序はクライアントと同じでなければいけない。
bootstrap.addProtoBufClass(0, Authentication.getDescriptor());
bootstrap.addProtoBufClass(1, GameMulti.getDescriptor());
bootstrap.addProtoBufClass(2, GameSingle.getDescriptor());
bootstrap.addProtoBufClass(3, Result.getDescriptor());
bootstrap.addProtoBufClass(4, User.getDescriptor());
// ゲームで使用するDBスレッドプールの指定
bootstrap.createExecutorService(GameConstants.DB_THREAD_POOL, 100);
// ゲームで使用するスレッドプールの指定
bootstrap.createExecutorService(GameConstants.REDIS_THREAD_POOL, 100);
// セッション設定
bootstrap.setGateway()
.connection(GameConnection.class)
.session(GameSession.class)
.node(GameGatewayNode.class)
.enableWhiteModules();
// ゲームスペースの設定
bootstrap.setGame(GameConstants.GAME_NAME)
.node(GameNode.class)
// シングルゲーム
.user(GameConstants.GAME_USER_TYPE, GameUser.class)
.room(GameConstants.GAME_ROOM_TYPE_SINGLE, SingleGameRoom.class)
// ルームマッチマルチゲーム - ルームに入ってゲーム:無制限タブ
.room(GameConstants.GAME_ROOM_TYPE_MULTI_ROOM_MATCH, UnlimitedTapRoom.class)
.roomMatchMaker(GameConstants.GAME_ROOM_TYPE_MULTI_ROOM_MATCH, UnlimitedTapRoomMatchMaker.class, UnlimitedTapRoomInfo.class)
// ユーザーマッチマルチゲーム - ユーザーマッチングによりゲーム同時入場:ステーキゲーム
.room(GameConstants.GAME_ROOM_TYPE_MULTI_USER_MATCH, SnakeRoom.class)
.userMatchMaker(GameConstants.GAME_ROOM_TYPE_MULTI_USER_MATCH, SnakeRoomMatchMaker.class, SnakeRoomInfo.class);
// サービス設定
bootstrap.setSupport(GameConstants.SUPPORT_NAME_LAUNCHING)
.node(LaunchingSupport.class);
bootstrap.run();
}
static private PacketDispatcher packetDispatcher = new PacketDispatcher();
static {
packetDispatcher.registerMsg(User.ChangeNicknameReq.getDescriptor(), CmdChangeNicknameReq.class); // ニックネーム変更プロトコル
packetDispatcher.registerMsg(User.ShuffleDeckReq.getDescriptor(), CmdShuffleDeckReq.class); // デッキシャッフルプロトコル
packetDispatcher.registerMsg(GameSingle.ScoreRankingReq.getDescriptor(), CmdSingleScoreRankingReq.class); // シングルスコアランキング
}
// 処理するクラスはimplements IPacketHandler<GameUser> を実装して作成する必要がある。
private static RoomPacketDispatcher dispatcher = new RoomPacketDispatcher();
static {
dispatcher.registerMsg(GameMulti.SnakeUserMsg.getDescriptor(), CmdSnakeUserMsg.class); // ユーザー位置情報
dispatcher.registerMsg(GameMulti.SnakeFoodMsg.getDescriptor(), CmdSnakeRemoveFoodMsg.class); // food削除情報処理
}
// 処理するクラスはimplements IRoomPacketHandler<SnakeRoom, GameUser>を実装して作成する必要がある。
private static RestPacketDispatcher restMsgHandler = new RestPacketDispatcher();
static {
// launching
restMsgHandler.registerMsg("/launching", RestObject.GET, CmdLaunching.class);
}
// 処理するクラスはimplements IRestPacketHandlerを実装して作成する必要がある。
// Gamebse認証
//----------------------------------- トークンの有効性を検証Gamebase
String gamebaseUrl = String.format(GameConstants.GAMEBASE_DEFAULT_URL + "/tcgb-gateway/v1.2/apps/X2bqX5du/members/%s/tokens/%s", accountId, authenticationReq.getAccessToken());
HttpRequest httpRequest = new HttpRequest(gamebaseUrl);
httpRequest.getBuilder().addHeader("Content-Type", "application/json");
httpRequest.getBuilder().addHeader("X-Secret-Key", GameConstants.GAMEBASE_SECRET_KEY);
logger.info("httpRequest url [{}]", gamebaseUrl);
HttpResponse response = httpRequest.GET();
logger.info("httpRequest response:[{}]", response.toString());
// Gamebaseレスポンスjsonデータオブジェクト解析
AuthenticationResponse gamebaseResponse = response.getContents(AuthenticationResponse.class);
if (gamebaseResponse.getHeader().isSuccessful())
{
resultCode = ErrorCode.NONE;
} else {
resultCode = ErrorCode.TOKEN_NOT_VALIDATED;
}
//------------------------------------
private RedisClusterClient clusterClient;
private StatefulRedisClusterConnection<String, String> clusterConnection;
private RedisAdvancedClusterAsyncCommands<String, String> clusterAsyncCommands;
/**
* Redis接続。使用する前に、最初に1度呼び出して接続する必要がある。
*
* @param url接続url
* @param port接続port
* @throws SuspendExecution
*/
public void connect(String url, int port) throws SuspendExecution { // Redis接続処理
RedisURI clusterURI = RedisURI.Builder.redis(url, port).build();
this.clusterClient = RedisClusterClient.create(Collections.singletonList(clusterURI));
this.clusterConnection = Lettuce.connect(GameConstants.REDIS_THREAD_POOL, clusterClient);
this.clusterAsyncCommands = clusterConnection.async();
}
/**
* 接続終了サーバーが落ちる前に呼び出される必要がある。
*/
public void shutdown() {
clusterConnection.close();
clusterClient.shutdown();
}
/**
* ユーザーデータRedisに保存
*
* @param gameUserInfoユーザー情報
* @return保存成否
* @throws SuspendExecution
*/
public boolean setUserData(GameUserInfo gameUserInfo) throws SuspendExecution {
String value = GameAnvilUtil.Gson().toJson(gameUserInfo);
boolean isSuccess = false;
try {
Lettuce.awaitFuture(clusterAsyncCommands.hset(REDIS_USER_DATA_KEY, gameUserInfo.getUuid(), value)); // この戻り値は、最初にsetする時だけtrueで、値の更新時にはfalseを返す
isSuccess = true;
} catch (TimeoutException e) {
logger.error("setUserData - timeout", e);
}
return isSuccess;
}
<!-- MySQL接続情報を指定する。 -->
<properties>
<property name="hostname" value="호스트명" />
<property name="portnumber" value="3306" />
<property name="database" value="데이터베이스명" />
<property name="username" value="유저명" />
<property name="password" value="패스워드" />
<property name="poolPingQuery" value="select 1"/>
<property name="poolPingEnabled" value="true"/>
<property name="poolPingConnectionsNotUsedFor" value="3600000"/>
</properties>
<mappers>
<!-- 定義されたSQL構文をマッピングする。基本的にリソース内にあるmapper.xmlを使用する時-->
<mapper resource="query/UserDataMapper.xml"/>
<!-- 外部のmapper.xmlファイルを指定する時は、全体パスを指定して使用する。 -->
<!--<mapper url="file:///C:/_KevinProjects/GameServerEngine/sample-game-server/target/query/UserDataMapper.xml"/>-->
</mappers>
<select id="selectUserByUuid" resultType="com.nhn.gameanvil.sample.mybatis.dto.UserDto">
SELECT uuid,
login_type AS loginType,
app_version AS appVersion,
app_store AS appStore,
device_model AS deviceModel,
device_country AS deviceCountry,
device_language AS deviceLanguage,
nickname,
heart,
coin,
ruby,
level,
exp,
high_score AS highScore,
current_deck AS currentDeck,
create_date AS createDate,
update_date AS updateDate
FROM users
WHERE uuid = #{uuid}
</select>
/**
* ゲームで使用するDB接続オブジェクト
*/
public class GameSqlSessionFactory {
private static Logger logger = LoggerFactory.getLogger(GameSqlSessionFactory.class);
private static SqlSessionFactory sqlSessionFactory;
/** XMLに記載された接続情報を読み込む。 */
// クラス初期化ブロック:クラス変数の複雑な初期化に使われる。
// クラスが初めてロードされる時に一度だけ実行される。
static {
// 接続情報を明示しているXMLのパスの読み取り
try {
// mybatis_config.xmlファイルのパス指定
String mybatisConfigPath = System.getProperty("mybatisConfig"); // パラメータが渡された場合、サーバー(実行時に-DmybatisConfig=オプションで指定)
logger.info("mybatisConfigPath : {}", mybatisConfigPath);
if (mybatisConfigPath != null) {
logger.info("load to mybatisConfigPath : {}", mybatisConfigPath);
InputStream inputStream = new FileInputStream(mybatisConfigPath);
if (sqlSessionFactory '- null) {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
} else { // パラメータがない場合は、内部ファイルで設定する
Reader reader = Resources.getResourceAsReader("mybatis/mybatis-config.xml");
logger.info("load to resource : mybatis/mybatis-config.xml");
// sqlSessionFactoryが存在しない場合は作成する。
if (sqlSessionFactory '- null) {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* データベース接続オブジェクトを使用してDATABASEに接続したセッションを返す。
*/
public static SqlSession getSqlSession() {
return sqlSessionFactory.openSession();
}
}
[2020-12-18 17:36:35,634] [INFO ] [GameAnvil-DB_THREAD_POOL-0] [GameSqlSessionFactory.java:30] mybatisConfigPath : null
[2020-12-18 17:36:35,636] [INFO ] [GameAnvil-DB_THREAD_POOL-0] [GameSqlSessionFactory.java:39] load to resource : mybatis-config.xml
[2020-12-18 17:43:37,871] [INFO ] [GameAnvil-DB_THREAD_POOL-0] [GameSqlSessionFactory.java:30] mybatisConfigPath : .\src\main\resources\mybatis-config.xml
[2020-12-18 17:43:37,871] [INFO ] [GameAnvil-DB_THREAD_POOL-0] [GameSqlSessionFactory.java:32] load to mybatisConfigPath : .\src\main\resources\mybatis-config.xml
/**
* ユーザー情報DBに保存
*
* @param gameUserInfoユーザー情報伝達
* @return保存されたレコードの数
* @throws TimeoutException
* @throws SuspendExecution
*/
public int insertUser(GameUserInfo gameUserInfo) throws TimeoutException, SuspendExecution { // Callable形式でAsyncを実行して結果を返す。
Integer resultCount = Async.callBlocking(GameConstants.DB_THREAD_POOL, new Callable<Integer>() {
@Override
public Integer call() throws Exception {
SqlSession sqlSession = GameSqlSessionFactory.getSqlSession();
try {
UserDataMapper userDataMapper = sqlSession.getMapper(UserDataMapper.class);
int resultCount = userDataMapper.insertUser(gameUserInfo.toDtoUser());
if (resultCount '- 1) { // 1件保存のため、1個なら正常にDB commit
sqlSession.commit();
}
return resultCount;
} finally {
sqlSession.close();
}
}
});
return resultCount;
}
CREATE TABLE `users` (
`uuid` varchar(40) NOT NULL,
`login_type` int(11) NOT NULL,
`app_version` varchar(45) DEFAULT NULL,
`app_store` varchar(45) DEFAULT NULL,
`device_model` varchar(45) DEFAULT NULL,
`device_country` varchar(45) DEFAULT NULL,
`device_language` varchar(45) DEFAULT NULL,
`nickname` varchar(45) DEFAULT NULL,
`heart` int(11) NOT NULL,
`coin` bigint(15) DEFAULT '0',
`ruby` bigint(15) DEFAULT '0',
`level` int(11) DEFAULT '1',
`exp` bigint(15) DEFAULT '0',
`high_score` bigint(15) DEFAULT '0',
`current_deck` varchar(45) NOT NULL,
`create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
{
//-------------------------------------------------------------------------------------
// 共通情報。
"common": {
"ip": "127.0.0.1", // ノードごとに共通で使用するIP。(マシンのIPを指定)
"meetEndPoints": ["127.0.0.1:16000"], // 対象ノードのcommon IPとcommunicatePort登録。 (該当サーバーのendpointを含められる。リストで複数可)
"ipcPort": 16000, // 他のipc nodeと通信する時に使われるport
"publisherPort" : 13300, // publish socket用のport
"debugMode": false //デバッグ時、各種timeoutが発生しないようにするオプション。リアルでは必ずfalseにする必要がある。
},
//-------------------------------------------------------------------------------------
// LocationNode設定
"location": {
"clusterSize": 1, // 複数のマシン(VM)で構成されているか?
"replicaSize": 3, // レプリケーショングループのサイズ(master + slaveの数)
"shardFactor": 3 // shardingのための引数(以下のコメント参照)
// shardの総数= clusterSize x replicaSize x shardFactor
// 1つのマシン(VM)で起動するshardの数= replicaSize x shardFactor
// 固有のshardの総数(masterシャードの数) = clusterSize x shardFactor
},
// マッチノード設定
"match": {
"nodeCnt": 1,
"useLocationDirect": true
},
//-------------------------------------------------------------------------------------
// クライアントとのコネクションを管理するノード。
"gateway": {
"nodeCnt": 4, // ノード数。(ノード番号は0から付与される)
"ip": "127.0.0.1", // クライアントと接続されるIP。
"dns": "", // クライアントと接続しているドメインのアドレス。
"maintenance": false,
"tcpNoDelay": false, // Netty Bootstrap設定時に使用される。(デフォルトでフィールド未使用および基本値false)
"connectGroup": { // コネクションの種類。
"TCP_SOCKET": {
"port": 11200, // クライアントと接続しているポート。
"idleClientTimeout": 240000 // データの送受信がない状態以降のタイムアウト。(0の場合は使用しない)
// ,"secure": { // セキュリティ設定。
// "useSelf": true
//// ,"keyCertChainPath": "gameanvil.crt" // 証明書のパス。
//// ,"privateKeyPath": "privatekey.pem" // 秘密鍵のパス。
// }
},
"WEB_SOCKET": {
"port": 11400,
"idleClientTimeout": 0
// ,"secure": {
// "useSelf": true
//// ,"keyCertChainPath": "gameanvil.crt"
//// ,"privateKeyPath": "privatekey.pem"
// }
}
}
},
//-------------------------------------------------------------------------------------
// ゲームロビーの役割をするノード。(ゲームルーム、ユーザーを含んでいる)
"game": [
{
"nodeCnt": 4,
"serviceId": 1,
"serviceName": "TapTap",
"channelIDs": ["","","","",""], // ノードごとに付与するチャンネルID。(ユニークでなくてもよい。""文字列でチャンネルの区別なく重複使用も可能)
"userTimeout": 5000 // disconnect以降のユーザーオブジェクト削除タイムアウト。
}
],
"support": [
{
"nodeCnt": 2,
"serviceId": 2,
"serviceName": "Launching",
"restIp": "127.0.0.1",
"restPort": 10080
}
],
//-------------------------------------------------------------------------------------
// JMXまたはREST API使用して他のノードの管理を行うことができるノード。(サービスポーズ、全体ユーザーカウントなど)
"management": {
"nodeCnt": 2,
"restIp": "127.0.0.1",
"restPort": 25150,
"consoleProxyPort" : 18081, // admin web console port
"logProxyPort" : 18082, // admin log download port
"db": {
"user": "root",
"password": "1234",
"url": "jdbc:h2:mem:gameanvil_admin;DB_CLOSE_DELAY'-1"
}
}
}
<logger name="com.nhn.gameanvil" level="INFO"/>
<logger name="com.nhn.gameanvil.sample" level="DEBUG"/>
<root>
<level value="WARN"/>
<appender-ref ref="ASYNC"/>
<appender-ref ref="STDOUT"/>
</root>