337-513-3457

9097737207Executors.newSingleThreadScheduledExecutor异常后执行停止,不显示任何报错信息

问题

使用newSingleThreadScheduledExecutor作为场景执行器执行任务时,发现客户端没有返回数据,后端日志也没有显示任何异常。

于是,在调用的方法每个阶段打印前调用打log,发现方法执行到一个阶段就停止打印了。

拍卖行竞拍的代码,执行时log只打印 1.

    /**
     *  竞拍物品
     * @param player 玩家
     * @param auctionItem  拍卖品
     * @param price 出价
     */
    private void bidAuctionItem(Player player, AuctionItem auctionItem, int price) {
        log.debug("1");
        Integer auctionMoney = auctionItem.getBiddersMap().get(player.getId());
        log.debug("2");
        if (Objects.isNull(auctionMoney)) {
            /  第一次竞拍
            auctionItem.addBidder(player,price);
            player.moneyChange(price);
        } else {
            auctionItem.addBidder(player,price);
            / 减去继续竞的差价
            player.moneyChange(-(price-auctionItem.getAuctionPrice()));
        }
        log.debug("3");
        notificationManager.notifyPlayer(player,MessageFormat.format("叫价功成。当前拍卖品价格是{0}",
                auctionItem.getAuctionPrice()));
        log.debug("4");
    }

执行这个方法的线程是一个场景执行器,


    private static ThreadFactory sceneThreadFactory = new ThreadFactoryBuilder()
            .setNameFormat("scene-loop-%d").build();
    /** 通过一个场景一个线程处理器的方法保证每个场景的指令循序 */
    ExecutorService singleThreadSchedule = Executors.newSingleThreadScheduledExecutor(sceneThreadFactory);

场景执行器开始执行任务的代码。

    / 玩家在场景内则用场景的执行器执行
    Optional.ofNullable(player).map(Player::getCurrentScene).ifPresent(
        scene -> scene.getSingleThreadSchedule().execute(
                () -> controller.handle(ctx,msg))
    );

原因

在网上查询后发现,在JDK的源码,在源码的Java doc中的发现了如下一句话:

If any execution of the task encounters an exception, subsequent executions are suppressed.Otherwise, the task will only terminate via cancellation or termination of the executor.

也就是说:

如果定时任务执行过程中遇到发生异常,则后面的任务将不再执行。

而且,异常也不会被捕获并打印信息。

解决方案

我的解决方式是直接在执行任务时使用try/catch。

    / 玩家在场景内则用场景的执行器执行
    Optional.ofNullable(player).map(Player::getCurrentScene).ifPresent(
        scene -> scene.getSingleThreadSchedule().execute(
                () -> {
                    log.debug("{}  {} 的 {} 在执行命令 {}",scene.getSingleThreadSchedule(),
                            scene.getName(),player.getName(),new String(msg.getContent()));
                    try {
                        controller.handle(ctx,msg);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
        })
    );

报错信息终于显示出来了。

结语

这个问题折腾了我好久,方法执行到一半不执行我就猜测是出现了异常,但是异常没有被捕获。这件事教训我以后使用ExecutorService的实现类都要注意异常处理。

Netty消息分发和业务逻辑分离

5187833163Netty消息分发和业务逻辑分离


目的

(732) 498-9243

(540) 815-0329Netty, SpringBoot, MyBatis搭建游戏服务器 - 1


(310) 538-5790各框架简介

Netty是步的、事件驱动的网络应用程序框架和工具,,很适合做游戏服务器。

使用SpringBoot可以抛弃传统Spirng框架的繁琐配置和依赖版本管理,更方便地打包与部署。这里我们主要是使用SpringBoot的依赖注入功能。

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。因为我们的数据库可能因为业务需求不断地变化,所以我们可以用MyBaits的逆向工程实时根据数据库的表生成实体类、Mapper接口,Mapper XML。或者利用注解注解写SQL语句查询。


目的

我们要使用Netty构建一个游戏服务端,她可以接受客户端的信息,并根据业务逻辑做出响应。
架构图

Spring负责依赖注入,管理对象(游戏业务对象另外由缓存模块管理),使得项目代码直之间解耦合。

MyBatis负责依据业务需求持久化各类数据。


(615) 537-9349依赖配置

项目使用Maven来管理依赖,依赖结构初步想法是

  • mmorpg
    • gameserver
      • mysql
      • 缓存模块
      • 游戏配置管理模块
      • ….
    • gameclient

下面是当前版本项目的pom文件

总项目定义文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="/maven.apache.org/POM/4.0.0" xmlns:xsi="/www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="/maven.apache.org/POM/4.0.0 /maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.wan37</groupId>
    <artifactId>mmorpg</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>

    <name>mmorpg</name>
    <description>Demo project for Spring Boot</description>

    <modules>
        <module>gameserver</module>
        <module>mysql</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--<dependency>-->
            <!--<groupId>org.springframework.boot</groupId>-->
            <!--<artifactId>spring-boot-starter-data-redis</artifactId>-->
        <!--</dependency>-->

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>/repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>/repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>/repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>/repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
</project>

持久化模块定义文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="/maven.apache.org/POM/4.0.0"
         xmlns:xsi="/www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="/maven.apache.org/POM/4.0.0 /maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>mmorpg</artifactId>
        <groupId>com.wan37</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>mysql</artifactId>
    <packaging>jar</packaging>


    <dependencies>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>


        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- /mvnrepository.com/artifact/com.alibaba/druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.26</version>
        </dependency>


        <!-- 分页插件 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>4.1.6</version>
        </dependency>

        <!--Json Support-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.1.43</version>
        </dependency>

        <!--mybatis逆向工程-->
        <!--<dependency>-->
            <!--<groupId>org.mybatis.generator</groupId>-->
            <!--<artifactId>mybatis-generator-core</artifactId>-->
            <!--<version>1.3.2</version>-->
        <!--</dependency>-->

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <!--mybatis插件-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.2</version>
                <configuration>
                    <configurationFile>src\main\resources\mybatis\generatorConfig.xml</configurationFile>
                    <!--允许移动生成的文件-->
                    <verbose>true</verbose>
                    <!--允许覆盖生成的文件-->
                    <overwrite>true</overwrite>
                </configuration>
                <executions>
                    <execution>
                        <id>GenerateMyBatis Artifacts</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
                <dependencies>
                    <dependency>
                        <groupId>org.mybatis.generator</groupId>
                        <artifactId>mybatis-generator-core</artifactId>
                        <version>1.3.2</version>
                    </dependency>
                </dependencies>
            </plugin>

        </plugins>
    </build>

</project>

服务端模块

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="/maven.apache.org/POM/4.0.0" xmlns:xsi="/www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="/maven.apache.org/POM/4.0.0 /maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>


    <parent>
        <artifactId>mmorpg</artifactId>
        <groupId>com.wan37</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>



    <artifactId>gameserver</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>gameserver</name>
    <description>A game server project for Spring Boot</description>


    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- Netty -->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.29.Final</version>
        </dependency>

        <!-- 引用基于MySQL和-->
        <dependency>
            <groupId>com.wan37</groupId>
            <artifactId>mysql</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>


844-647-8699Spring和myBatis的配置文件

gameserver模块中的application.yml

spring:
  datasource:
    url: jdbc:mysql:/localhost:3306/game?useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

mybatis:
  mapper-locations: classpath*:mapper/*.xml
  check-config-location: true
  type-aliases-package: com.wan37.mysql.pojo.entity

debug: true

MyBitis配置类(分页助手不知是否能用到)

package com.wan37.gameServer.config;

import com.github.pagehelper.PageHelper;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;


/**
 * @author 钱伟健 gonefutre
 * @date 2017/9/12 22:27.
 * @E-mail gonefuture@qq.com
 */

/*
 * 注册MyBatis分页插件PageHelper
 */

@Configuration
@MapperScan(basePackages = {"com.wan37"})
public class MybatisConfig {

    @Bean
    public PageHelper pageHelper() {
        System.out.println("MyBatisConfiguration.pageHelper()");
        PageHelper pageHelper = new PageHelper();
        Properties p = new Properties();
        p.setProperty("offsetAsPageNum", "true");
        p.setProperty("rowBoundsWithCount", "true");
        p.setProperty("reasonable", "true");
        pageHelper.setProperties(p);
        return pageHelper;
    }
}

MyBatis逆向工程文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE generatorConfiguration PUBLIC "-/mybatis.org/DTD MyBatis Generator Configuration 1.0/EN"
        "/mybatis.org/dtd/mybatis-generator-config_1_0.dtd" >
<generatorConfiguration >
    <!-- 引入配置文件 -->
    <!--     <properties resource="db.properties"/> -->
    <!-- 指定数据连接驱动jar地址 -->
    <classPathEntry location="C:\project\mmorpg\mysql\mysql-connector-java-5.1.47.jar"/>
    <!-- 一个数据库一个context -->
    <context id="msql" targetRuntime="MyBatis3" >

        <!-- generate entity时,生成hashcode和equals方法 -->
        <!--<plugin type="org.mybatis.generator.plugins.EqualsHashCodePlugin" />-->
        <!-- generate entity时,生成serialVersionUID -->
        <plugin type="org.mybatis.generator.plugins.SerializablePlugin" />
        <!-- 这个插件只会增加字符串字段映射到一个JDBC字符的方法 -->
        <!--<plugin type="org.mybatis.generator.plugins.CaseInsensitiveLikePlugin" />-->
        <!-- genenat entity时,生成toString -->
        <plugin type="org.mybatis.generator.plugins.ToStringPlugin" />


        <!-- 注释 -->
        <commentGenerator>
            <property name="suppressAllComments" value="true"/><!-- 是否取消注释 -->
            <property name="supperssDate" value="false"/><!-- 是否生成注释代码时间戳 -->
        </commentGenerator>
        <!-- jdbc连接 -->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql:/localhost:3306/game"
                        userId="root"
                        password="123456" />
        <!-- 类型转换 -->
        <javaTypeResolver>
            <!-- 是否使用bigDecimals,false可自动转化以下类型(Long,Integer,Short,ets..) -->
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>
        <!-- 生成实体类地址 -->
        <javaModelGenerator targetPackage="com.wan37.mysql.pojo.entity" targetProject="src/main/java">
            <!-- 是否在当前路径下新加一层schema,eg:
            fase路径:com.sky.ssm.po ; true路径:com.sky.ssm.po.[shemaName]
             -->
            <property name="enableSubPackages" value="true"/>
            <!-- 是否针对string类型的字段在set的时候进行trim调用 -->
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>

        <!-- 生成mapxml文件 -->
        <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
            <!-- 是否在当前路径下新加一层schema,eg:
            fase路径:com.sky.ssm.mapper ; true路径:com.sky.ssm.mapper.[shemaName]
             -->
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>
        <!-- 生成mapxml对应client,也就是接口dao -->
        <javaClientGenerator targetPackage="com.wan37.mysql.pojo.mapper" targetProject="src/main/java"
                             type="XMLMAPPER" >
            <!-- 是否在当前路径下新加一层schema,eg:
            fase路径:com.sky.ssm.mapper ; true路径:com.sky.ssm.mapper.[shemaName]
             -->
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>
        <!-- 配置表信息 -->
        <!-- schema即为数据库名; tableName为对应的数据库表 ;domainObjectName是要生成的实体类 ;enable*ByExample是否生成 example类 -->
        <!-- <table schema="store" tableName="user"
            domainObjectName="User" enableCountByExample="true"
            enableDeleteByExample="true" enableSelectByExample="true"
            enableUpdateByExample="ture">
            忽略列,不生成bean字段
             <ignoreColumn column="FRED"/>
                 指定列的java数据类型
           <columnOverride column="PRICE" javaType="double" />
          </table> -->
        <!-- 指定数据库表,要生成哪些表,就写哪些表,要和数据库中对应,不能写错! -->
        <table tableName="game_role"/>
        <table tableName="player"/>
        <table tableName="player_roles"/>

    </context>
</generatorConfiguration>

(770) 649-8393整合Netty

我使用的办法是把Netty服务的Handler,启动类都都变成由Spring管理的类。通过在类定义上加@Component注解,SpringBoot启动时会自动扫描注解并将Bean注入到容器中。使用 @PostConstruct修饰启动方法来启动Netty服务。

import com.wan37.gameServer.server.handler.ServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;


/**
 * @author gonefuture  gonefuture@qq.com
 * time 2018/9/10 11:36
 * @version 1.00
 * Description: 游戏服务端
 */
@Slf4j
@Component
@ChannelHandler.Sharable
public class GameServer {

    @Resource
    private ServerHandler serverHandler;

    /绑定端口
    private void bind(int port) throws Exception {
        / 逻辑线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        / 工作线程
        EventLoopGroup workGroup = new NioEventLoopGroup();

        ServerBootstrap bootstrap = new ServerBootstrap(); / 启动器
        bootstrap.group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024) / 最大客户端连接数为1024
                /是否启用心跳保活机制
                .childOption(ChannelOption.SO_KEEPALIVE, true)
        .childHandler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                / 这里添加业务处理handler
                ch.pipeline().addLast( serverHandler);
            }
        });

        try {
            ChannelFuture future = bootstrap.bind(port).sync();
            if (future.isSuccess()) {
                System.out.println("Server starts success at port:"+port);
            }
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 8000;
        new GameServer().bind(port);
    }


    @PostConstruct
    public void start() {
        int port = 8000;
        try {
          bind(port);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

期间遇到的各种问题

整合的时候遇到过很多问题

  1. 整合Netty时,由于使用new关键字创建Handler实例,导致在Handle中装配的服务实例为空,不能使用。
  2. MyBatis配置类写在了mysql模块而没有写在gameserver模块,导致mapper文件扫描不到。
  3. 逆向工程时产生了同名的mapper xml文件未及时删除,导致出现mapper result map 重复的错误。

结语

当前只整合了持久化模块,接下来还要编写请求分发的部分和整合缓存模块。