Java游戏服务器成长之路——弱联网游戏篇(源码分析)

发表于2016-01-17
评论3 981浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏程序行业精英群

711501594
前言
  前段时间由于公司的一款弱联网游戏急着上线,没能及时分享,现在基本做的差不多,剩下的就是测试阶段了(本来说元旦来分享一下服务器技术的)。公司的这款游戏已经上线一年多了,在我来之前一直都是单机版本,由于人民群众的力量太强大,各种内购破解,刷体力,刷金币,刷钻石版本的出现,公司才决定将这款游戏转型为弱联网游戏,压制百分之八十的破解用户(毕竟原则上还是属于单机游戏,不可能做到百分之百的防破解),招了我这一个服务器来进行后台的开发。什么是弱联网游戏?在这之前我也没有做过弱联网游戏的服务器,但是按照我对策划对我提出的需求的理解,就是游戏的大部分逻辑运算都是在移动端本地完成,而服务器要做的就是登录、支付验证、游戏存档读档的工作,这相对于我在上家公司做的ARPG那种强联网游戏要简单多了,那款ARPG就是所有游戏产出,逻辑运算都是在服务器端完成,服务器要完成大部分的游戏运算,而我做的这款弱联网游戏,只需要简简单单的登录、验证、读取和存储。这一类的游戏,做的最火的,就是腾讯早期手游中的《全民消消乐》《节奏大师》《天天飞车》(天天飞车后来加的实时竞赛应该还是强联网的实时数据)等,这类游戏中,服务器就只需要负责游戏数据存储和一些简单的社交功能,例如qq好友送红心送体力的等。

概括
  公司招聘我进来做服务器开发其实是为了两个项目,一个是这款单机转弱联网的游戏,另一款是公司准备拿来发家致富的SLG——战争策略游戏。从入职到现在,我一直是在支持弱联网游戏的开发,到现在,基本上这款游戏也算是差不多了,这款游戏的目前版本仍然基本属于单机,到年后会加上竞技场功能,到时候可能就会需要实时的数据交互了,现在就先来分享一下目前这个版本开发的过程。 
  要开发一个后台系统,首先要考虑的就是架构了,系统的高效稳定性,可扩展性。在游戏开发中,我认为后台服务器无非负责几个大得模块: 
  1. 网络通信 
  2. 逻辑处理 
  3. 数据存储 
  4. 游戏安全
  首先从需求分析入手,我在这款弱联网游戏中,后端需要做的事情就是,登录,支付验证,数据存储,数据读取,再加上一些简单的逻辑判断,第一眼看去,并没有任何难点,我就分别从以上几点一一介绍。

网络通信
  弱联网游戏,基本上来说,最简单直接的,就是使用http短连接来进行网络层的通信,那么我又用什么来做http服务器呢,servlet还是springmvc,还是其他框架。因为之前做的ARPG用的一款nio框架——mina,然后对比servlet和springmvc的bio(实质上,springmvc只是层层封装servlet后的框架,它的本质原理还是servlet),个人还是觉得,作为需要处理大量高并发请求的业务需求来说,还是nio框架更适合,然而,我了解到netty又是比mina更好一点的框架,于是我选择了netty,然后自己写了demo测试,发现netty的处理性能确实是很可观的,netty是一个异步的,事件驱动的网络编程框架,使用netty可以快速开发出可维护的,高性能、高扩展能力的协议服务及其客户端应用。netty使用起来基本上就是傻瓜式的,它很好的封装了java的nio api。我也是刚刚接触这款网络通信框架,为此我还买了《Netty权威指南》,想系统的多了解下这款框架,以下几点,就是我使用netty作为网络层的理由: 
  1. netty的通信机制就是它本身最大的优势,nio的通信机制无论是可靠性还是吞吐量都是优于bio的。 
  2. netty使用自建的buffer API,而不是使用NIO的ByteBuffer来代表一个连续的字节序列。与ByteBuffer相比这种方式拥有明显的优势。netty使用新的buffer类型ChannelBuffer,ChannelBuffer被设计为一个可从底层解决ByteBuffer问题(netty的ByteBuf的使用跟C语言中使用对象一样,需要手动malloc和release,否则可能出现内存泄露,昨天遇到这个问题我都傻眼了,后来才知道,原来netty的ByteBuf是需要手动管理内存的,它不受java的gc机制影响,这点设定有点返璞归真的感觉!)。 
  3.  netty也提供了多种编码解码类,可以支持Google的Protobuffer,Facebook的Trift,JBoss的Marshalling以及MessagePack等编解码框架,我记得用mina的时候,当时看老大写的编解码的类,貌似是自己写protobuffer编解码工具的,mina并没有支持protobuffer。这些编解码框架的出现就是解决Java序列化后的一些缺陷。 
  4. netty不仅能进行TCP/UDP开发,更是支持Http开发,netty的api中就有支持http开发的类,以及http请求响应的编解码工具,真的可谓是人性化,我使用的就是这些工具,除此之外,netty更是支持WebSocket协议的开发,还记得之前我自己试着写过mina的WebSocket通信,我得根据WebSocket的握手协议自己来写消息编解码机制,虽然最终也写出来了,但是当我听说netty的api本身就能支持WebSocket协议开发的时候,我得内心几乎是崩溃的,为什么当初不用netty呢? 
  5. 另外,netty还有处理TCP粘包拆包的工具类! 可能对于netty的理解还是太浅,不过以上几个优势就让我觉得,我可以使用这款框架。实时也证明,它确实很高效很稳定的。 
  废话不多说,以下就贴出我使用netty作为Http通信的核心类:
  1. public class HttpServer {
  2.     public static Logger log = LoggerFactory.getLogger(HttpServer.class);
  3.     public static HttpServer inst;
  4.     public static Properties p;
  5.     public static int port;
  6.     private NioEventLoopGroup bossGroup = new NioEventLoopGroup();
  7.     private NioEventLoopGroup workGroup = new NioEventLoopGroup();
  8.     public static ThreadPoolTaskExecutor handleTaskExecutor;// 处理消息线程池

  9.     private HttpServer() {// 线程池初始化

  10.     }

  11.     /** 
  12.     * @Title: initThreadPool 
  13.     * @Description: 初始化线程池
  14.     * void
  15.     * @throws 
  16.     */
  17.     public void initThreadPool() {
  18.         handleTaskExecutor = new ThreadPoolTaskExecutor();
  19.         // 线程池所使用的缓冲队列
  20.     handleTaskExecutor.setQueueCapacity(Integer.parseInt(p
  21.                 .getProperty("handleTaskQueueCapacity")));
  22.         // 线程池维护线程的最少数量
  23.         handleTaskExecutor.setCorePoolSize(Integer.parseInt(p
  24.                 .getProperty("handleTaskCorePoolSize")));
  25.         // 线程池维护线程的最大数量
  26.         handleTaskExecutor.setMaxPoolSize(Integer.parseInt(p
  27.                 .getProperty("handleTaskMaxPoolSize")));
  28.         // 线程池维护线程所允许的空闲时间
  29.         handleTaskExecutor.setKeepAliveSeconds(Integer.parseInt(p
  30.                 .getProperty("handleTaskKeepAliveSeconds")));
  31.         handleTaskExecutor.initialize();
  32.     }

  33.     public static HttpServer getInstance() {
  34.         if (inst == null) {
  35.             inst = new HttpServer();
  36.             inst.initData();
  37.             inst.initThreadPool();
  38.         }
  39.         return inst;
  40.     }

  41.     public void initData() {
  42.         try {
  43.             p = readProperties();
  44.             port = Integer.parseInt(p.getProperty("port"));
  45.         } catch (IOException e) {
  46.             log.error("socket配置文件读取错误");
  47.             e.printStackTrace();
  48.         }
  49.     }

  50.     public void start() {
  51.         ServerBootstrap bootstrap = new ServerBootstrap();
  52.         bootstrap.group(bossGroup, workGroup);
  53.         bootstrap.channel(NioServerSocketChannel.class);
  54.         bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
  55.             @Override
  56.             protected void initChannel(SocketChannel ch) throws Exception {
  57.                 ChannelPipeline pipeline = ch.pipeline();
  58.                 pipeline.addLast("decoder", new HttpRequestDecoder());
  59.                 pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
  60.                 pipeline.addLast("encoder", new HttpResponseEncoder());
  61.                 pipeline.addLast("http-chunked", new ChunkedWriteHandler());
  62.                 pipeline.addLast("handler", new HttpServerHandler());
  63.             }
  64.         });
  65.         log.info("端口{}已绑定", port);
  66.         bootstrap.bind(port);
  67.     }

  68.     public void shut() {
  69.         workGroup.shutdownGracefully();
  70.         workGroup.shutdownGracefully();
  71.         log.info("端口{}已解绑", port);
  72.     }

  73.     /**
  74.      * 读配置socket文件
  75.      * 
  76.      * @return
  77.      * @throws IOException
  78.      */
  79.     protected Properties readProperties() throws IOException {
  80.         Properties p = new Properties();
  81.         InputStream in = HttpServer.class
  82.                 .getResourceAsStream("/net.properties");
  83.         Reader r = new InputStreamReader(in, Charset.forName("UTF-8"));
  84.         p.load(r);
  85.         in.close();
  86.         return p;
  87.     }
  88. }
  网络层,除了网络通信,还有就是数据传输协议了,服务器跟客户端怎么通信,传什么,怎么传。跟前端商议最终还是穿json格式的数据,前面说到了netty的编解码工具的使用,下面贴出消息处理类:
  1. public class HttpServerHandlerImp {
  2.     private static Logger log = LoggerFactory
  3.             .getLogger(HttpServerHandlerImp.class);
  4.     public static String DATA = "data";// 游戏数据接口
  5.     public static String PAY = "pay";// 支付接口
  6.     public static String TIME = "time";// 时间验证接口
  7.     public static String AWARD = "award";// 奖励补偿接口
  8.     public static volatile boolean ENCRIPT_DECRIPT = true;

  9.     public void channelRead(final ChannelHandlerContext ctx, final Object msg)
  10.             throws Exception {
  11.         HttpServer.handleTaskExecutor.execute(new Runnable() {
  12.             @Override
  13.             public void run() {
  14.                 if (!GameServer.shutdown) {// 服务器开启的情况下
  15.                     DefaultFullHttpRequest req = (DefaultFullHttpRequest) msg;
  16.                     if (req.getMethod() == HttpMethod.GET) { // 处理get请求
  17.                     }
  18.                     if (req.getMethod() == HttpMethod.POST) { // 处理POST请求
  19.                         HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(
  20.                                 new DefaultHttpDataFactory(false), req);
  21.                         InterfaceHttpData postGameData = decoder
  22.                                 .getBodyHttpData(DATA);
  23.                         InterfaceHttpData postPayData = decoder
  24.                                 .getBodyHttpData(PAY);
  25.                         InterfaceHttpData postTimeData = decoder
  26.                                 .getBodyHttpData(TIME);
  27.                         InterfaceHttpData postAwardData = decoder
  28.                                 .getBodyHttpData(AWARD);
  29.                         try {
  30.                             if (postGameData != null) {// 存档回档
  31.                                 String val = ((Attribute) postGameData)
  32.                                         .getValue();
  33.                                 val = postMsgFilter(val);
  34.                                 Router.getInstance().route(val, ctx);
  35.                             } else if (postPayData != null) {// 支付
  36.                                 String val = ((Attribute) postPayData)
  37.                                         .getValue();
  38.                                 val = postMsgFilter(val);
  39.                                 Router.getInstance().queryPay(val, ctx);
  40.                             } else if (postTimeData != null) {// 时间
  41.                                 String val = ((Attribute) postTimeData)
  42.                                         .getValue();
  43.                                 val = postMsgFilter(val);
  44.                                 Router.getInstance().queryTime(val, ctx);
  45.                             } else if (postAwardData != null) {// 补偿
  46.                                 String val = ((Attribute) postAwardData)
  47.                                         .getValue();
  48.                                 val = postMsgFilter(val);
  49.                                 Router.getInstance().awardOperate(val, ctx);
  50.                             }
  51.                         } catch (Exception e) {
  52.                             e.printStackTrace();
  53.                         }
  54.                         return;
  55.                     }
  56.                 } else {// 服务器已关闭
  57.                     JSONObject jsonObject = new JSONObject();
  58.                     jsonObject.put("errMsg", "server closed");
  59.                     writeJSON(ctx, jsonObject);
  60.                 }
  61.             }
  62.         });
  63.     }

  64.     private String postMsgFilter(String val)
  65.             throws UnsupportedEncodingException {
  66.         val = val.contains("%") ? URLDecoder.decode(val, "UTF-8") : val;
  67.         String valTmp = val;
  68.         val = ENCRIPT_DECRIPT ? XXTeaCoder.decryptBase64StringToString(val,
  69.                 XXTeaCoder.key) : val;
  70.         if (Constants.MSG_LOG_DEBUG) {
  71.             if (val == null) {
  72.                 val = valTmp;
  73.             }
  74.             log.info("server received : {}", val);
  75.         }
  76.         return val;
  77.     }

  78.     public static void writeJSON(ChannelHandlerContext ctx,
  79.             HttpResponseStatus status, Object msg) {
  80.         String sentMsg = JsonUtils.objectToJson(msg);
  81.         if (Constants.MSG_LOG_DEBUG) {
  82.             log.info("server sent : {}", sentMsg);
  83.         }
  84.         sentMsg = ENCRIPT_DECRIPT ? XXTeaCoder.encryptToBase64String(sentMsg,
  85.                 XXTeaCoder.key) : sentMsg;
  86.         writeJSON(ctx, status,
  87.                 Unpooled.copiedBuffer(sentMsg, CharsetUtil.UTF_8));
  88.         ctx.flush();
  89.     }

  90.     public static void writeJSON(ChannelHandlerContext ctx, Object msg) {
  91.         String sentMsg = JsonUtils.objectToJson(msg);
  92.         if (Constants.MSG_LOG_DEBUG) {
  93.             log.info("server sent : {}", sentMsg);
  94.         }
  95.         sentMsg = ENCRIPT_DECRIPT ? XXTeaCoder.encryptToBase64String(sentMsg,
  96.                 XXTeaCoder.key) : sentMsg;
  97.         writeJSON(ctx, HttpResponseStatus.OK,
  98.                 Unpooled.copiedBuffer(sentMsg, CharsetUtil.UTF_8));
  99.         ctx.flush();
  100.     }

  101.     private static void writeJSON(ChannelHandlerContext ctx,
  102.             HttpResponseStatus status, ByteBuf content/* , boolean isKeepAlive */) {
  103.         if (ctx.channel().isWritable()) {
  104.             FullHttpResponse msg = null;
  105.             if (content != null) {
  106.                 msg = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status,
  107.                         content);
  108.                 msg.headers().set(HttpHeaders.Names.CONTENT_TYPE,
  109.                         "application/json; charset=utf-8");
  110.             } else {
  111.                 msg = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status);
  112.             }
  113.             if (msg.content() != null) {
  114.                 msg.headers().set(HttpHeaders.Names.CONTENT_LENGTH,
  115.                         msg.content().readableBytes());
  116.             }
  117.             // not keep-alive
  118.             ctx.write(msg).addListener(ChannelFutureListener.CLOSE);
  119.         }
  120.     }

  121.     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
  122.             throws Exception {
  123.     }

  124.     public void messageReceived(ChannelHandlerContext ctx, FullHttpRequest msg)
  125.             throws Exception {

  126.     }
  127. }
  以上代码,由于最后商议所有接口都通过post实现,所以get请求部分代码全都注释掉了。解析json数据使用的是gson解析,因为gson是可以直接解析为JavaBean的,这一点是非常爽的。工具类中代码如下:
  1. /** 
  2.      * 将json转换成bean对象 
  3.      * @author fuyzh 
  4.      * @param jsonStr 
  5.      * @return 
  6.      */  
  7.     public static Object jsonToBean(String jsonStr, Class<?> cl) {  
  8.         Object obj = null;  
  9.         if (gson != null) {  
  10.             obj = gson.fromJson(jsonStr, cl);  
  11.         }  
  12.         return obj;  
  13.     }  
  14.     /** 
  15.      * 将对象转换成json格式 
  16.      * @author fuyzh
  17.      * @param ts 
  18.      * @return 
  19.      */  
  20.     public static String objectToJson(Object ts) {  
  21.         String jsonStr = null;  
  22.         if (gson != null) {  
  23.             jsonStr = gson.toJson(ts);  
  24.         }  
  25.         return jsonStr;  
  26.     }
逻辑处理
  在这款弱联网游戏中,一个是登录逻辑,游戏有一个管理服务器,管理其他的逻辑服务器(考虑到下版本开竞技场会有分服选服),登录和支付都是在管理服务器造成的,其他接口才会通过管理服务器上获得的逻辑服务器IP去完成其他交互,在逻辑服务器上基本上也不会有什么逻辑处理,基本上是接到数据就进行解析,然后就进行存储或缓存。唯一有一点逻辑处理的就是,例如金币钻石减少到负数了,就把数据置零。逻辑上,netty接收到请求之后,就进入我的一个核心处理类,Router,由Router再将消息分发到各个功能模块。Router代码如下:
  1. /**
  2.      * @Title: route
  3.      * @Description: 路由分发
  4.      * @param @param msg
  5.      * @param @param ctx
  6.      * @return void
  7.      * @throws
  8.      */
  9.     public void route(String msg, ChannelHandlerContext ctx) {
  10.         GameData data = null;
  11.         try {
  12.             data = (GameData) JsonUtils.jsonToBean(msg, GameData.class);
  13.         } catch (Exception e) {
  14.             logger.error("gameData的json格式错误,{}", msg);
  15.             e.printStackTrace();
  16.             HttpServerHandler.writeJSON(ctx, HttpResponseStatus.NOT_ACCEPTABLE,
  17.                     new BaseResp(1));
  18.             return;
  19.         }
  20.         if (data.getUserID() == null) {
  21.             logger.error("存放/回档错误,uid为空");
  22.             HttpServerHandler.writeJSON(ctx, new BaseResp(1));
  23.             return;
  24.         }
  25.         long junZhuId = data.getUserID() * 1000 + GameInit.serverId;
  26.         /** 回档 **/
  27.         if (JSONObject.fromObject(msg).keySet().size() == 1) {
  28.             GameData ret = junZhuMgr.getMainInfo(junZhuId);
  29.             ret.setTime(new Date().getTime());
  30.             ret.setPay(getPaySum(data.getUserID()));
  31.             HttpServerHandler.writeJSON(ctx, ret);
  32.             return;
  33.         }
  34.         /** 存档 **/
  35.         if (data.getDiamond() != null) {// 钻石
  36.             if (!junZhuMgr.setDiamond(junZhuId, data)) {
  37.                 HttpServerHandler.writeJSON(ctx, new BaseResp(1));
  38.                 return;
  39.             }
  40.         }
  41.         // 其他模块处理代码就省了
  42.         JunZhu junZhu = HibernateUtil.find(JunZhu.class, junZhuId);
  43.         HttpServerHandler.writeJSON(ctx, new BaseResp(junZhu.coin,
  44.                 junZhu.diamond, 0));
  45.     }
  GameData则是用于发送接收的消息Bean

数据存储
  在这样的游戏中,逻辑运算基本由客户端操作了,因此游戏数据的持久化才是服务器的重点,必须要保证游戏的数据的完整性。数据库上,我选择了Mysql,事实上,我认为MongoDB更适合这类数据的存储,因为本身数据库就可以完全按照json格式原样存储到数据库中,但由于项目预期紧,我也不敢去尝试我没尝试过的方式,然而选择mysql,也不是什么坏事,mysql在游戏数据的处理也是相当给力,mongo虽好,却没有关系型数据库的事务管理。根据策划的需求,我将游戏数据分析完了之后,就基本理清了数据库表结构,在项目中我使用了Hibernate4作为ORM框架,相对于前面的版本,Hibernate4有一个很爽的功能,就是在JavaBean中添加一些注解,就能在构建Hibernate的session的时候,自动在数据库创建表,这样使得开发效率快了好几倍,Hibernate本身就已经够爽了,我认为至今没有什么ORM框架能跟它比,以前也用过MyBatis,个人感觉MyBatis更适合那种需要手动写很复杂的sql才用的,每一个查询都要写sql,在Hibernate中,简简单单几行代码,就能完成一个查询,一下贴出Hibernate工具类:
  1. public class HibernateUtil {
  2.     public static boolean showMCHitLog = false;
  3.     public static Logger log = LoggerFactory.getLogger(HibernateUtil.class);
  4.     public static Map<Class<?>, String> beanKeyMap = new HashMap<Class<?>, String>();
  5.     private static SessionFactory sessionFactory;

  6.     public static void init() {
  7.         sessionFactory = buildSessionFactory();
  8.     }

  9.     public static SessionFactory getSessionFactory() {
  10.         return sessionFactory;
  11.     }

  12.     public static Throwable insert(Object o) {
  13.         Session session = sessionFactory.getCurrentSession();
  14.         session.beginTransaction();
  15.         try {
  16.             session.save(o);
  17.             session.getTransaction().commit();
  18.         } catch (Throwable e) {
  19.             log.error("0要insert的数据{}", o == null ? "null" : JSONObject
  20.                     .fromObject(o).toString());
  21.             log.error("0保存出错", e);
  22.             session.getTransaction().rollback();
  23.             return e;
  24.         }
  25.         return null;
  26.     }

  27.     /**
  28.      * FIXME 不要这样返回异常,没人会关系返回的异常。
  29.      * 
  30.      * @param o
  31.      * @return
  32.      */
  33.     public static Throwable save(Object o) {
  34.         Session session = sessionFactory.getCurrentSession();
  35.         Transaction t = session.beginTransaction();
  36.         boolean mcOk = false;
  37.         try {
  38.             if (o instanceof MCSupport) {
  39.                 MCSupport s = (MCSupport) o;// 需要对控制了的对象在第一次存库时调用MC.add
  40.                 MC.update(o, s.getIdentifier());// MC中控制了哪些类存缓存。
  41.                 mcOk = true;
  42.                 session.update(o);
  43.             } else {
  44.                 session.saveOrUpdate(o);
  45.             }
  46.             t.commit();
  47.         } catch (Throwable e) {
  48.             log.error("1要save的数据{},{}", o, o == null ? "null" : JSONObject
  49.                     .fromObject(o).toString());
  50.             if (mcOk) {
  51.                 log.error("MC保存成功后报错,可能是数据库条目丢失。");
  52.             }
  53.             log.error("1保存出错", e);
  54.             t.rollback();
  55.             return e;
  56.         }
  57.         return null;
  58.     }

  59.     public static Throwable update(Object o) {
  60.         Session session = sessionFactory.getCurrentSession();
  61.         Transaction t = session.beginTransaction();
  62.         try {
  63.             if (o instanceof MCSupport) {
  64.                 MCSupport s = (MCSupport) o;// 需要对控制了的对象在第一次存库时调用MC.add
  65.                 MC.update(o, s.getIdentifier());// MC中控制了哪些类存缓存。
  66.                 session.update(o);
  67.             } else {
  68.                 session.update(o);
  69.             }
  70.             t.commit();
  71.         } catch (Throwable e) {
  72.             log.error("1要update的数据{},{}", o, o == null ? "null" : JSONObject
  73.                     .fromObject(o).toString());
  74.             log.error("1保存出错", e);
  75.             t.rollback();
  76.             return e;
  77.         }
  78.         return null;
  79.     }
  80.     public static <T> T find(Class<T> t, long id) {
  81.         String keyField = getKeyField(t);
  82.         if (keyField == null) {
  83.             throw new RuntimeException("类型" + t + "没有标注主键");
  84.         }
  85.         if (!MC.cachedClass.contains(t)) {
  86.             return find(t, "where " + keyField + "=" + id, false);
  87.         }
  88.         T ret = MC.get(t, id);
  89.         if (ret == null) {
  90.             if (showMCHitLog)
  91.                 log.info("MC未命中{}#{}", t.getSimpleName(), id);
  92.             ret = find(t, "where " + keyField + "=" + id, false);
  93.             if (ret != null) {
  94.                 if (showMCHitLog)
  95.                     log.info("DB命中{}#{}", t.getSimpleName(), id);
  96.                 MC.add(ret, id);
  97.             } else {
  98.                 if (showMCHitLog)
  99.                     log.info("DB未命中{}#{}", t.getSimpleName(), id);
  100.             }
  101.         } else {
  102.             if (showMCHitLog)
  103.                 log.info("MC命中{}#{}", t.getSimpleName(), id);
  104.         }
  105.         return ret;
  106.     }

  107.     public static <T> T find(Class<T> t, String where) {
  108.         return find(t, where, true);
  109.     }

  110.     public static <T> T find(Class<T> t, String where, boolean checkMCControl) {
  111.         if (checkMCControl && MC.cachedClass.contains(t)) {
  112.             // 请使用static <T> T find(Class<T> t,long id)
  113.             throw new BaseException("由MC控制的类不能直接查询DB:" + t);
  114.         }
  115.         Session session = sessionFactory.getCurrentSession();
  116.         Transaction tr = session.beginTransaction();
  117.         T ret = null;
  118.         try {
  119.             // FIXME 使用 session的get方法代替。
  120.             String hql = "from " + t.getSimpleName() + " " + where;
  121.             Query query = session.createQuery(hql);

  122.             ret = (T) query.uniqueResult();
  123.             tr.commit();
  124.         } catch (Exception e) {
  125.             tr.rollback();
  126.             log.error("list fail for {} {}", t, where);
  127.             log.error("list fail", e);
  128.         }
  129.         return ret;
  130.     }

  131.     /**
  132.      * 通过指定key值来查询对应的对象
  133.      * 
  134.      * @param t
  135.      * @param name
  136.      * @param where
  137.      * @return
  138.      */
  139.     public static <T> T findByName(Class<? extends MCSupport> t, String name,
  140.             String where) {
  141.         Class<? extends MCSupport> targetClz = t;// .getClass();
  142.         String key = targetClz.getSimpleName() + ":" + name;
  143.         Object id = MC.getValue(key);
  144.         T ret = null;
  145.         if (id != null) {
  146.             log.info("id find in cache");
  147.             ret = (T) find(targetClz, Long.parseLong((String) id));
  148.             return ret;
  149.         } else {
  150.             ret = (T) find(targetClz, where, false);
  151.         }
  152.         if (ret == null) {
  153.             log.info("no record {}, {}", key, where);
  154.         } else {
  155.             MCSupport mc = (MCSupport) ret;
  156.             long mcId = mc.getIdentifier();
  157.             log.info("found id from DB {}#{}", targetClz.getSimpleName(), mcId);
  158.             MC.add(key, mcId);
  159.             ret = (T) find(targetClz, mcId);
  160.         }
  161.         return ret;

  162.     }

  163.     /**
  164.      * @param t
  165.      * @param where
  166.      *            例子: where uid>100
  167.      * @return
  168.      */
  169.     public static <T> List<T> list(Class<T> t, String where) {
  170.         Session session = sessionFactory.getCurrentSession();
  171.         Transaction tr = session.beginTransaction();
  172.         List<T> list = Collections.EMPTY_LIST;
  173.         try {
  174.             String hql = "from " + t.getSimpleName() + " " + where;
  175.             Query query = session.createQuery(hql);

  176.             list = query.list();
  177.             tr.commit();
  178.         } catch (Exception e) {
  179.             tr.rollback();
  180.             log.error("list fail for {} {}", t, where);
  181.             log.error("list fail", e);
  182.         }
  183.         return list;
  184.     }
  185.     public static SessionFactory buildSessionFactory() {
  186.         log.info("开始构建hibernate");
  187.         String path = "classpath*:spring-conf/applicationContext.xml";
  188.         ApplicationContext ac = new FileSystemXmlApplicationContext(path);
  189.         sessionFactory = (SessionFactory) ac.getBean("sessionFactory");
  190.         log.info("结束构建hibernate");
  191.         return sessionFactory;
  192.     }

  193.     public static Throwable delete(Object o) {
  194.         if (o == null) {
  195.             return null;
  196.         }
  197.         Session session = sessionFactory.getCurrentSession();
  198.         session.beginTransaction();
  199.         try {
  200.             if (o instanceof MCSupport) {
  201.                 MCSupport s = (MCSupport) o;// 需要对控制了的对象在第一次存库时调用MC.add
  202.                 MC.delete(o.getClass(), s.getIdentifier());// MC中控制了哪些类存缓存。
  203.             }
  204.             session.delete(o);
  205.             session.getTransaction().commit();
  206.         } catch (Throwable e) {
  207.             log.error("要删除的数据{}", o);
  208.             log.error("出错", e);
  209.             session.getTransaction().rollback();
  210.             return e;
  211.         }
  212.         return null;
  213.     }
  其中HibernateUtil中也用了SpyMemcached来做一些结果集的缓存,当然项目中也有其他地方用到了Memcache来做缓存。最开始的时候,我还纠结要不要把每个玩家的整个游戏数据(GameData)缓存起来,这样读起来会更快,但是我想了想,如果我把整个游戏数据缓存起来,那么每次存档,我都要把缓存中数据取出来,把要修改的那部分数据从数据库查询出来,再进行修改,再放回去,这样的话,每次存档就会多一次数据库操作,然而再想想,整个游戏中,读档只有进游戏的时候需要,而存档是随时都需要,权衡之下,还不如不做缓存,做了缓存反而需要更多数据库的操作。 
  缓存部分代码如下:
  1. /**
  2.  * 对SpyMemcached Client的二次封装,提供常用的Get/GetBulk/Set/Delete/Incr/Decr函数的同步与异步操作封装.
  3.  * 
  4.  * 未提供封装的函数可直接调用getClient()取出Spy的原版MemcachedClient来使用.
  5.  * 
  6.  * @author 何金成
  7.  */
  8. public class MemcachedCRUD implements DisposableBean {

  9.     private static Logger logger = LoggerFactory.getLogger(MemcachedCRUD.class);
  10.     private MemcachedClient memcachedClient;
  11.     private long shutdownTimeout = 2500;
  12.     private long updateTimeout = 2500;
  13.     private static MemcachedCRUD inst;

  14.     public static MemcachedCRUD getInstance() {
  15.         if (inst == null) {
  16.             inst = new MemcachedCRUD();
  17.         }
  18.         return inst;
  19.     }

  20.     // Test Code
  21.     public static void main(String[] args) {
  22.         MemcachedCRUD.getInstance().set("test", 0, "testVal");
  23.         for (int i = 0; i < 100; i++) {
  24.             new Thread(new Runnable() {

  25.                 @Override
  26.                 public void run() {
  27.                     try {
  28.                         Thread.sleep(1000);
  29.                     } catch (InterruptedException e) {
  30.                         e.printStackTrace();
  31.                     }
  32.                     String val = MemcachedCRUD.getInstance().get("test");
  33.                     Long a = MemcachedCRUD.getInstance().<Long> get("aaa");
  34.                     System.out.println(a);
  35.                     System.out.println(val);
  36.                 }
  37.             }).start();
  38.         }
  39.     }

  40.     private MemcachedCRUD() {
  41.         String cacheServer = GameInit.cfg.get("cacheServer");
  42.         if (cacheServer == null) {
  43.             cacheServer = "localhost:11211";
  44.         }
  45.         // String cacheServer = "123.57.211.130:11211";
  46.         String host = cacheServer.split(":")[0];
  47.         int port = Integer.parseInt(cacheServer.split(":")[1]);
  48.         List<InetSocketAddress> addrs = new ArrayList<InetSocketAddress>();
  49.         addrs.add(new InetSocketAddress(host, port));
  50.         try {
  51.             ConnectionFactoryBuilder builder = new ConnectionFactoryBuilder();
  52.             builder.setProtocol(Protocol.BINARY);
  53.             builder.setOpTimeout(1000);
  54.             builder.setDaemon(true);
  55.             builder.setOpQueueMaxBlockTime(1000);
  56.             builder.setMaxReconnectDelay(1000);
  57.             builder.setTimeoutExceptionThreshold(1998);
  58.             builder.setFailureMode(FailureMode.Retry);
  59.             builder.setHashAlg(DefaultHashAlgorithm.KETAMA_HASH);
  60.             builder.setLocatorType(Locator.CONSISTENT);
  61.             builder.setUseNagleAlgorithm(false);
  62.             memcachedClient = new MemcachedClient(builder.build(), addrs);
  63.             logger.info("Memcached at {}:{}", host, port);
  64.         } catch (IOException e) {
  65.             e.printStackTrace();
  66.         }
  67.     }

  68.     /**
  69.      * Get方法, 转换结果类型并屏蔽异常, 仅返回Null.
  70.      */
  71.     public <T> T get(String key) {
  72.         try {
  73.             return (T) memcachedClient.get(key);
  74.         } catch (RuntimeException e) {
  75.             handleException(e, key);
  76.             return null;
  77.         }
  78.     }
  79.         /**
  80.      * 异步Set方法, 不考虑执行结果.
  81.      * 
  82.      * @param expiredTime
  83.      *            以秒过期时间,0表示没有延迟,如果exptime大于30天,Memcached将使用它作为UNIX时间戳过期
  84.      */
  85.     public void set(String key, int expiredTime, Object value) {
  86.         memcachedClient.set(key, expiredTime, value);
  87.     }

  88.     /**
  89.      * 安全的Set方法, 保证在updateTimeout秒内返回执行结果, 否则返回false并取消操作.
  90.      * 
  91.      * @param expiredTime
  92.      *            以秒过期时间,0表示没有延迟,如果exptime大于30天,Memcached将使用它作为UNIX时间戳过期
  93.      */
  94.     public boolean safeSet(String key, int expiration, Object value) {
  95.         Future<Boolean> future = memcachedClient.set(key, expiration, value);
  96.         try {
  97.             return future.get(updateTimeout, TimeUnit.MILLISECONDS);
  98.         } catch (Exception e) {
  99.             future.cancel(false);
  100.         }
  101.         return false;
  102.     }

  103.     /**
  104.      * 异步 Delete方法, 不考虑执行结果.
  105.      */
  106.     public void delete(String key) {
  107.         memcachedClient.delete(key);
  108.     }

  109.     /**
  110.      * 安全的Delete方法, 保证在updateTimeout秒内返回执行结果, 否则返回false并取消操作.
  111.      */
  112.     public boolean safeDelete(String key) {
  113.         Future<Boolean> future = memcachedClient.delete(key);
  114.         try {
  115.             return future.get(updateTimeout, TimeUnit.MILLISECONDS);
  116.         } catch (Exception e) {
  117.             future.cancel(false);
  118.         }
  119.         return false;
  120.     }
  121. }
游戏安全
  首先是数据传输的安全问题:当我们完成了接口对接之后,就会考虑一个问题,当别人进行抓包之后,就能很轻松的知道服务器和客户端传输的数据格式,这样的话,不说服务器攻击,至少会有人利用这些接口做出一大批外挂,本身我们加上弱联网就是为了杜绝作弊现象,于是,我们对传输消息做了加密,先做XXTea加密,再做Base64加密,用约定好的秘钥,进行加密解密,进行消息收发。再一个就是支付验证的安全问题,现在有人能破解内购,就是利用支付之后断网,然后模拟返回结果为true,破解内购。我们做了支付验证,在完成支付之后,必须到后台查询订单状态,状态为完成才能获得购买的物品,支付我之前也是没有做过,一点点摸索的。代码就不贴了,涉及到业务。

总结
  本文章只为了记录这款弱联网游戏的后台开发历程,可能之后还会遇到很多的问题,问题都是在摸索中解决的,我还需要了解更多关于netty性能方面知识。以上代码只是项目中的部分代码,并不涉及业务部分。分享出来也是给大家一个思路,或是直接拿去用,都是可以的,因为自己踩过一些坑,所以希望将这些记录下来,下次不能踩同样的坑。到目前为止,这款游戏也经过了大概半个多月的时间,到此作为记录,作为经验分享,欢迎交流探讨。我要参与的下一款游戏是长连接的SLG,到时候我应该还会面临更多的挑战,加油!

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

游戏学院公众号二维码
腾讯游戏学院
微信公众号

提供更专业的游戏知识学习平台