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/
Install the server using the install command of the Maven tab. At this time, process AOT Instrumentation at compile.
Run the server using the predefined "sample_server" configuration.
The onReady log is displayed in every node if the server works normally.
http://127.0.0.1:25150/management/nodeInfoPage
The status of the sample_game_server of local through URL.
Check for error
Check the settings again or the error part of the log and contact us if the server does not run normally.
<!-- 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>
<!-- The class to be executed as main in executable jar -->
<mainClass>com.nhn.gameanvil.sample.Main</mainClass>
<!-- classpath information is added to the META-INF/MANIFEST.MF in the jar file -->
<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>
The built files can be found in the ./target/ folder if it ran normally.
To operate the server, copy the sample_game_server-1.0.1.jar file and the files in the config and query folder and use them.
Run on the command prompt
Run the command prompt (cmd) and move to the built target folder(Proceed in the path that fits to one's environment).
Run 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
Apply the preference file specified at the time of build if there is no option specified when running as default
Refer to com.nhn.gameanvil.sample.mybatis.GameSqlSessionFactory
It is normal if onReady appears
If testing on local instead of testing with the maven build each time, the -javaagent:.\src\main\resources\META-INF\quasar-core-0.7.10-jdk8.jar=bm
option can be added to Vm Option and directly run the local server in 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();
// The client and the order of the protocols to be transferred - must be the same with the client.
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());
// Specify the DB thread pool used in game
bootstrap.createExecutorService(GameConstants.DB_THREAD_POOL, 100);
// Specify the Redis thread pool used in game
bootstrap.createExecutorService(GameConstants.REDIS_THREAD_POOL, 100);
// Set session
bootstrap.setGateway()
.connection(GameConnection.class)
.session(GameSession.class)
.node(GameGatewayNode.class)
.enableWhiteModules();
// Set game space
bootstrap.setGame(GameConstants.GAME_NAME)
.node(GameNode.class)
// Single-player game
.user(GameConstants.GAME_USER_TYPE, GameUser.class)
.room(GameConstants.GAME_ROOM_TYPE_SINGLE, SingleGameRoom.class)
// Room match multiplayer game - Play the game in the room: Unlimited tab
.room(GameConstants.GAME_ROOM_TYPE_MULTI_ROOM_MATCH, UnlimitedTapRoom.class)
.roomMatchMaker(GameConstants.GAME_ROOM_TYPE_MULTI_ROOM_MATCH, UnlimitedTapRoomMatchMaker.class, UnlimitedTapRoomInfo.class)
// User match multiplayer game - Matched users enter a room at the same time: Stake game
.room(GameConstants.GAME_ROOM_TYPE_MULTI_USER_MATCH, SnakeRoom.class)
.userMatchMaker(GameConstants.GAME_ROOM_TYPE_MULTI_USER_MATCH, SnakeRoomMatchMaker.class, SnakeRoomInfo.class);
// Set service
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); // Nickname change protocol
packetDispatcher.registerMsg(User.ShuffleDeckReq.getDescriptor(), CmdShuffleDeckReq.class); // Deck shuffle protocol
packetDispatcher.registerMsg(GameSingle.ScoreRankingReq.getDescriptor(), CmdSingleScoreRankingReq.class); // Single-player score rankings
}
// The class to be processed must be created by implementing implements IPacketHandler<GameUser>.
private static RoomPacketDispatcher dispatcher = new RoomPacketDispatcher();
static {
dispatcher.registerMsg(GameMulti.SnakeUserMsg.getDescriptor(), CmdSnakeUserMsg.class); // User location information
dispatcher.registerMsg(GameMulti.SnakeFoodMsg.getDescriptor(), CmdSnakeRemoveFoodMsg.class); // food Process deleted information
}
// The class to be processed must be created by implementing implements IRoomPacketHandler<SnakeRoom, GameUser>.
private static RestPacketDispatcher restMsgHandler = new RestPacketDispatcher();
static {
// launching
restMsgHandler.registerMsg("/launching", RestObject.GET, CmdLaunching.class);
}
// The class being processed must implement and create implements IRestPacketHandler.
// Authenticate Gamebase
//----------------------------------- Gamebase that authenticates the token's validity
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());
// Parse Gamebase response json data object
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;
/**
* Link to Redis, it must be initially called and linked before used.
*
* @param url Connection URL
* @param port Connection port
* @throws SuspendExecution
*/
public void connect(String url, int port) throws SuspendExecution { // Redis connection process
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();
}
/**
* It must be called before the connection end server goes down.
*/
public void shutdown() {
clusterConnection.close();
clusterClient.shutdown();
}
/**
* Store in user data Redis
*
* @param gameUserInfo User information
* @return Whether or not the storage was successful
* @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)); // The corresponding return value is true only when it is initially set, the response is false when the value is updated
isSuccess = true;
} catch (TimeoutException e) {
logger.error("setUserData - timeout", e);
}
return isSuccess;
}
<!-- Specifies MySQL connection information. -->
<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>
<!-- Map the defined SQL statement. Basically when using the mapper.xml inside a resource-->
<mapper resource="query/UserDataMapper.xml"/>
<!-- Use entire path specification when specifying an externally specified mapper.xml file. -->
<!--<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>
/**
* The DB connection object used in game
*/
public class GameSqlSessionFactory {
private static Logger logger = LoggerFactory.getLogger(GameSqlSessionFactory.class);
private static SqlSessionFactory sqlSessionFactory;
/** Reads the connection information specified in XML. */
// Class reset block: Used in the complex reset of class variable.
// Performed once when a class is initially loaded.
static {
// Read the XML path that specifies connection information
try {
// Set the path of the mybatis_config.xml file
String mybatisConfigPath = System.getProperty("mybatisConfig"); // If parameter is passed, when server (is run -DmybatisConfig= Specify as an option)
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 { // If no parameter is passed, obtain the setting from internal file
Reader reader = Resources.getResourceAsReader("mybatis/mybatis-config.xml");
logger.info("load to resource : mybatis/mybatis-config.xml");
// Creates when there is no sqlSessionFactory.
if (sqlSessionFactory == null) {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Returns the session accessed to DATABASE through a database connection object.
*/
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
/**
* Store user information in DB
*
* @param gameUserInfo Pass user information
* @return Number of stored records
* @throws TimeoutException
* @throws SuspendExecution
*/
public int insertUser(GameUserInfo gameUserInfo) throws TimeoutException, SuspendExecution { // Runs Async in the form of Callable and returns the result.
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) { // Committed normally to DB if there is only one, fitting to the single item storage
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 Information.
"common": {
"ip": "127.0.0.1", // The IP shared among nodes. (Specifies the IP of a machine)
"meetEndPoints": ["127.0.0.1:16000"], // Registers the common IP and communicatePort of the target node. (Can include the endpoint of the corresponding server, can include multiple items as a list)
"ipcPort": 16000, // The port used when communicating with a different ipc node
"publisherPort" : 13300, // The port for publish socket
"debugMode": false //An option used to prevent various timeouts from being occurred, it must be false in real.
},
//-------------------------------------------------------------------------------------
// Set LocationNode
"location": {
"clusterSize": 1, // How many machines (VM) is it consisted of?
"replicaSize": 3, // The size of the clone group (number of masters + slaves)
"shardFactor": 3 // A factor for sharding (refer to the comment below)
// Total number of shards = clusterSize x replicaSize x shardFactor
// Number of shards that can be run on a single machine (VM) = replicaSize x shardFactor
// Total number of unique shards (number of master shards) = clusterSize x shardFactor
},
// Set match node
"match": {
"nodeCnt": 1,
"useLocationDirect": true
},
//-------------------------------------------------------------------------------------
// A node that manages the connection to the client.
"gateway": {
"nodeCnt": 4, // Number of nodes. (The node number starts from 0)
"ip": "127.0.0.1", // The IP connected to the client.
"dns": "", // The domain address connected to the client.
"maintenance": false,
"tcpNoDelay": false, // Used when setting Netty Bootstrap (field unused and default value false by default)
"connectGroup": { // The type of connection.
"TCP_SOCKET": {
"port": 11200, // The port connected to the client.
"idleClientTimeout": 240000 // The timeout after there is no data transfer (not used if it is 0).
// ,"secure": { // Sets security.
// "useSelf": true
//// ,"keyCertChainPath": "gameanvil.crt" // The path of certificate.
//// ,"privateKeyPath": "privatekey.pem" // Private key path.
// }
},
"WEB_SOCKET": {
"port": 11400,
"idleClientTimeout": 0
// ,"secure": {
// "useSelf": true
//// ,"keyCertChainPath": "gameanvil.crt"
//// ,"privateKeyPath": "privatekey.pem"
// }
}
}
},
//-------------------------------------------------------------------------------------
// The node that acts as game lobby (includes game rooms and users).
"game": [
{
"nodeCnt": 4,
"serviceId": 1,
"serviceName": "TapTap",
"channelIDs": ["","","","",""], // The channel IDs that are to be assigned to each node. (They do not have to be unique. Can be used in duplicate using the "" character string without distinguishing channels)
"userTimeout": 5000 // User object removal timeout after disconnect.
}
],
"support": [
{
"nodeCnt": 2,
"serviceId": 2,
"serviceName": "Launching",
"restIp": "127.0.0.1",
"restPort": 10080
}
],
//-------------------------------------------------------------------------------------
// A node that can manage other nodes using JMX or REST API (service pause, count entire users, etc.).
"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>