Java游戏服务器成长之路——弱联网游戏篇(源码分析)
-
http://blog.csdn.net/hjcenry/article/details/50530472
前段时间由于公司的一款弱联网游戏急着上线,没能及时分享,现在基本做的差不多,剩下的就是测试阶段了(本来说元旦来分享一下服务器技术的)。公司的这款游戏已经上线一年多了,在我来之前一直都是单机版本,由于人民群众的力量太强大,各种内购破解,刷体力,刷金币,刷钻石版本的出现,公司才决定将这款游戏转型为弱联网游戏,压制百分之八十的破解用户(毕竟原则上还是属于单机游戏,不可能做到百分之百的防破解),招了我这一个服务器来进行后台的开发。什么是弱联网游戏?在这之前我也没有做过弱联网游戏的服务器,但是按照我对策划对我提出的需求的理解,就是游戏的大部分逻辑运算都是在移动端本地完成,而服务器要做的就是登录、支付验证、游戏存档读档的工作,这相对于我在上家公司做的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通信的核心类:public class HttpServer { public static Logger log = LoggerFactory.getLogger(HttpServer.class); public static HttpServer inst; public static Properties p; public static int port; private NioEventLoopGroup bossGroup = new NioEventLoopGroup(); private NioEventLoopGroup workGroup = new NioEventLoopGroup(); public static ThreadPoolTaskExecutor handleTaskExecutor;// 处理消息线程池 private HttpServer() {// 线程池初始化 } /** * @Title: initThreadPool * @Description: 初始化线程池 * void * @throws */ public void initThreadPool() { handleTaskExecutor = new ThreadPoolTaskExecutor(); // 线程池所使用的缓冲队列 handleTaskExecutor.setQueueCapacity(Integer.parseInt(p .getProperty("handleTaskQueueCapacity"))); // 线程池维护线程的最少数量 handleTaskExecutor.setCorePoolSize(Integer.parseInt(p .getProperty("handleTaskCorePoolSize"))); // 线程池维护线程的最大数量 handleTaskExecutor.setMaxPoolSize(Integer.parseInt(p .getProperty("handleTaskMaxPoolSize"))); // 线程池维护线程所允许的空闲时间 handleTaskExecutor.setKeepAliveSeconds(Integer.parseInt(p .getProperty("handleTaskKeepAliveSeconds"))); handleTaskExecutor.initialize(); } public static HttpServer getInstance() { if (inst == null) { inst = new HttpServer(); inst.initData(); inst.initThreadPool(); } return inst; } public void initData() { try { p = readProperties(); port = Integer.parseInt(p.getProperty("port")); } catch (IOException e) { log.error("socket配置文件读取错误"); e.printStackTrace(); } } public void start() { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workGroup); bootstrap.channel(NioServerSocketChannel.class); bootstrap.childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast("decoder", new HttpRequestDecoder()); pipeline.addLast("aggregator", new HttpObjectAggregator(65536)); pipeline.addLast("encoder", new HttpResponseEncoder()); pipeline.addLast("http-chunked", new ChunkedWriteHandler()); pipeline.addLast("handler", new HttpServerHandler()); } }); log.info("端口{}已绑定", port); bootstrap.bind(port); } public void shut() { workGroup.shutdownGracefully(); workGroup.shutdownGracefully(); log.info("端口{}已解绑", port); } /** * 读配置socket文件 * * @return * @throws IOException */ protected Properties readProperties() throws IOException { Properties p = new Properties(); InputStream in = HttpServer.class .getResourceAsStream("/net.properties"); Reader r = new InputStreamReader(in, Charset.forName("UTF-8")); p.load(r); in.close(); return p; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
网络层,除了网络通信,还有就是数据传输协议了,服务器跟客户端怎么通信,传什么,怎么传。跟前端商议最终还是穿json格式的数据,前面说到了netty的编解码工具的使用,下面贴出消息处理类:
public class HttpServerHandlerImp { private static Logger log = LoggerFactory .getLogger(HttpServerHandlerImp.class); public static String DATA = "data";// 游戏数据接口 public static String PAY = "pay";// 支付接口 public static String TIME = "time";// 时间验证接口 public static String AWARD = "award";// 奖励补偿接口 public static volatile boolean ENCRIPT_DECRIPT = true; public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { HttpServer.handleTaskExecutor.execute(new Runnable() { @Override public void run() { if (!GameServer.shutdown) {// 服务器开启的情况下 DefaultFullHttpRequest req = (DefaultFullHttpRequest) msg; if (req.getMethod() == HttpMethod.GET) { // 处理get请求 } if (req.getMethod() == HttpMethod.POST) { // 处理POST请求 HttpPostRequestDecoder decoder = new HttpPostRequestDecoder( new DefaultHttpDataFactory(false), req); InterfaceHttpData postGameData = decoder .getBodyHttpData(DATA); InterfaceHttpData postPayData = decoder .getBodyHttpData(PAY); InterfaceHttpData postTimeData = decoder .getBodyHttpData(TIME); InterfaceHttpData postAwardData = decoder .getBodyHttpData(AWARD); try { if (postGameData != null) {// 存档回档 String val = ((Attribute) postGameData) .getValue(); val = postMsgFilter(val); Router.getInstance().route(val, ctx); } else if (postPayData != null) {// 支付 String val = ((Attribute) postPayData) .getValue(); val = postMsgFilter(val); Router.getInstance().queryPay(val, ctx); } else if (postTimeData != null) {// 时间 String val = ((Attribute) postTimeData) .getValue(); val = postMsgFilter(val); Router.getInstance().queryTime(val, ctx); } else if (postAwardData != null) {// 补偿 String val = ((Attribute) postAwardData) .getValue(); val = postMsgFilter(val); Router.getInstance().awardOperate(val, ctx); } } catch (Exception e) { e.printStackTrace(); } return; } } else {// 服务器已关闭 JSONObject jsonObject = new JSONObject(); jsonObject.put("errMsg", "server closed"); writeJSON(ctx, jsonObject); } } }); } private String postMsgFilter(String val) throws UnsupportedEncodingException { val = val.contains("%") ? URLDecoder.decode(val, "UTF-8") : val; String valTmp = val; val = ENCRIPT_DECRIPT ? XXTeaCoder.decryptBase64StringToString(val, XXTeaCoder.key) : val; if (Constants.MSG_LOG_DEBUG) { if (val == null) { val = valTmp; } log.info("server received : {}", val); } return val; } public static void writeJSON(ChannelHandlerContext ctx, HttpResponseStatus status, Object msg) { String sentMsg = JsonUtils.objectToJson(msg); if (Constants.MSG_LOG_DEBUG) { log.info("server sent : {}", sentMsg); } sentMsg = ENCRIPT_DECRIPT ? XXTeaCoder.encryptToBase64String(sentMsg, XXTeaCoder.key) : sentMsg; writeJSON(ctx, status, Unpooled.copiedBuffer(sentMsg, CharsetUtil.UTF_8)); ctx.flush(); } public static void writeJSON(ChannelHandlerContext ctx, Object msg) { String sentMsg = JsonUtils.objectToJson(msg); if (Constants.MSG_LOG_DEBUG) { log.info("server sent : {}", sentMsg); } sentMsg = ENCRIPT_DECRIPT ? XXTeaCoder.encryptToBase64String(sentMsg, XXTeaCoder.key) : sentMsg; writeJSON(ctx, HttpResponseStatus.OK, Unpooled.copiedBuffer(sentMsg, CharsetUtil.UTF_8)); ctx.flush(); } private static void writeJSON(ChannelHandlerContext ctx, HttpResponseStatus status, ByteBuf content/* , boolean isKeepAlive */) { if (ctx.channel().isWritable()) { FullHttpResponse msg = null; if (content != null) { msg = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content); msg.headers().set(HttpHeaders.Names.CONTENT_TYPE, "application/json; charset=utf-8"); } else { msg = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status); } if (msg.content() != null) { msg.headers().set(HttpHeaders.Names.CONTENT_LENGTH, msg.content().readableBytes()); } // not keep-alive ctx.write(msg).addListener(ChannelFutureListener.CLOSE); } } public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { } public void messageReceived(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception { } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
以上代码,由于最后商议所有接口都通过post实现,所以get请求部分代码全都注释掉了。解析json数据使用的是gson解析,因为gson是可以直接解析为JavaBean的,这一点是非常爽的。工具类中代码如下:
/** * 将json转换成bean对象 * @author fuyzh * @param jsonStr * @return */ public static Object jsonToBean(String jsonStr, Class<?> cl) { Object obj = null; if (gson != null) { obj = gson.fromJson(jsonStr, cl); } return obj; } /** * 将对象转换成json格式 * @author fuyzh * @param ts * @return */ public static String objectToJson(Object ts) { String jsonStr = null; if (gson != null) { jsonStr = gson.toJson(ts); } return jsonStr; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
逻辑处理
在这款弱联网游戏中,一个是登录逻辑,游戏有一个管理服务器,管理其他的逻辑服务器(考虑到下版本开竞技场会有分服选服),登录和支付都是在管理服务器造成的,其他接口才会通过管理服务器上获得的逻辑服务器IP去完成其他交互,在逻辑服务器上基本上也不会有什么逻辑处理,基本上是接到数据就进行解析,然后就进行存储或缓存。唯一有一点逻辑处理的就是,例如金币钻石减少到负数了,就把数据置零。逻辑上,netty接收到请求之后,就进入我的一个核心处理类,Router,由Router再将消息分发到各个功能模块。Router代码如下:
/** * @Title: route * @Description: 路由分发 * @param @param msg * @param @param ctx * @return void * @throws */ public void route(String msg, ChannelHandlerContext ctx) { GameData data = null; try { data = (GameData) JsonUtils.jsonToBean(msg, GameData.class); } catch (Exception e) { logger.error("gameData的json格式错误,{}", msg); e.printStackTrace(); HttpServerHandler.writeJSON(ctx, HttpResponseStatus.NOT_ACCEPTABLE, new BaseResp(1)); return; } if (data.getUserID() == null) { logger.error("存放/回档错误,uid为空"); HttpServerHandler.writeJSON(ctx, new BaseResp(1)); return; } long junZhuId = data.getUserID() * 1000 + GameInit.serverId; /** 回档 **/ if (JSONObject.fromObject(msg).keySet().size() == 1) { GameData ret = junZhuMgr.getMainInfo(junZhuId); ret.setTime(new Date().getTime()); ret.setPay(getPaySum(data.getUserID())); HttpServerHandler.writeJSON(ctx, ret); return; } /** 存档 **/ if (data.getDiamond() != null) {// 钻石 if (!junZhuMgr.setDiamond(junZhuId, data)) { HttpServerHandler.writeJSON(ctx, new BaseResp(1)); return; } } // 其他模块处理代码就省了 JunZhu junZhu = HibernateUtil.find(JunZhu.class, junZhuId); HttpServerHandler.writeJSON(ctx, new BaseResp(junZhu.coin, junZhu.diamond, 0)); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
GameData则是用于发送接收的消息Bean
数据存储
在这样的游戏中,逻辑运算基本由客户端操作了,因此游戏数据的持久化才是服务器的重点,必须要保证游戏的数据的完整性。数据库上,我选择了Mysql,事实上,我认为MongoDB更适合这类数据的存储,因为本身数据库就可以完全按照json格式原样存储到数据库中,但由于项目预期紧,我也不敢去尝试我没尝试过的方式,然而选择mysql,也不是什么坏事,mysql在游戏数据的处理也是相当给力,mongo虽好,却没有关系型数据库的事务管理。根据策划的需求,我将游戏数据分析完了之后,就基本理清了数据库表结构,在项目中我使用了Hibernate4作为ORM框架,相对于前面的版本,Hibernate4有一个很爽的功能,就是在JavaBean中添加一些注解,就能在构建Hibernate的session的时候,自动在数据库创建表,这样使得开发效率快了好几倍,Hibernate本身就已经够爽了,我认为至今没有什么ORM框架能跟它比,以前也用过MyBatis,个人感觉MyBatis更适合那种需要手动写很复杂的sql才用的,每一个查询都要写sql,在Hibernate中,简简单单几行代码,就能完成一个查询,一下贴出Hibernate工具类:
public class HibernateUtil { public static boolean showMCHitLog = false; public static Logger log = LoggerFactory.getLogger(HibernateUtil.class); public static Map<Class<?>, String> beanKeyMap = new HashMap<Class<?>, String>(); private static SessionFactory sessionFactory; public static void init() { sessionFactory = buildSessionFactory(); } public static SessionFactory getSessionFactory() { return sessionFactory; } public static Throwable insert(Object o) { Session session = sessionFactory.getCurrentSession(); session.beginTransaction(); try { session.save(o); session.getTransaction().commit(); } catch (Throwable e) { log.error("0要insert的数据{}", o == null ? "null" : JSONObject .fromObject(o).toString()); log.error("0保存出错", e); session.getTransaction().rollback(); return e; } return null; } /** * FIXME 不要这样返回异常,没人会关系返回的异常。 * * @param o * @return */ public static Throwable save(Object o) { Session session = sessionFactory.getCurrentSession(); Transaction t = session.beginTransaction(); boolean mcOk = false; try { if (o instanceof MCSupport) { MCSupport s = (MCSupport) o;// 需要对控制了的对象在第一次存库时调用MC.add MC.update(o, s.getIdentifier());// MC中控制了哪些类存缓存。 mcOk = true; session.update(o); } else { session.saveOrUpdate(o); } t.commit(); } catch (Throwable e) { log.error("1要save的数据{},{}", o, o == null ? "null" : JSONObject .fromObject(o).toString()); if (mcOk) { log.error("MC保存成功后报错,可能是数据库条目丢失。"); } log.error("1保存出错", e); t.rollback(); return e; } return null; } public static Throwable update(Object o) { Session session = sessionFactory.getCurrentSession(); Transaction t = session.beginTransaction(); try { if (o instanceof MCSupport) { MCSupport s = (MCSupport) o;// 需要对控制了的对象在第一次存库时调用MC.add MC.update(o, s.getIdentifier());// MC中控制了哪些类存缓存。 session.update(o); } else { session.update(o); } t.commit(); } catch (Throwable e) { log.error("1要update的数据{},{}", o, o == null ? "null" : JSONObject .fromObject(o).toString()); log.error("1保存出错", e); t.rollback(); return e; } return null; } public static <T> T find(Class<T> t, long id) { String keyField = getKeyField(t); if (keyField == null) { throw new RuntimeException("类型" + t + "没有标注主键"); } if (!MC.cachedClass.contains(t)) { return find(t, "where " + keyField + "=" + id, false); } T ret = MC.get(t, id); if (ret == null) { if (showMCHitLog) log.info("MC未命中{}#{}", t.getSimpleName(), id); ret = find(t, "where " + keyField + "=" + id, false); if (ret != null) { if (showMCHitLog) log.info("DB命中{}#{}", t.getSimpleName(), id); MC.add(ret, id); } else { if (showMCHitLog) log.info("DB未命中{}#{}", t.getSimpleName(), id); } } else { if (showMCHitLog) log.info("MC命中{}#{}", t.getSimpleName(), id); } return ret; } public static <T> T find(Class<T> t, String where) { return find(t, where, true); } public static <T> T find(Class<T> t, String where, boolean checkMCControl) { if (checkMCControl && MC.cachedClass.contains(t)) { // 请使用static <T> T find(Class<T> t,long id) throw new BaseException("由MC控制的类不能直接查询DB:" + t); } Session session = sessionFactory.getCurrentSession(); Transaction tr = session.beginTransaction(); T ret = null; try { // FIXME 使用 session的get方法代替。 String hql = "from " + t.getSimpleName() + " " + where; Query query = session.createQuery(hql); ret = (T) query.uniqueResult(); tr.commit(); } catch (Exception e) { tr.rollback(); log.error("list fail for {} {}", t, where); log.error("list fail", e); } return ret; } /** * 通过指定key值来查询对应的对象 * * @param t * @param name * @param where * @return */ public static <T> T findByName(Class<? extends MCSupport> t, String name, String where) { Class<? extends MCSupport> targetClz = t;// .getClass(); String key = targetClz.getSimpleName() + ":" + name; Object id = MC.getValue(key); T ret = null; if (id != null) { log.info("id find in cache"); ret = (T) find(targetClz, Long.parseLong((String) id)); return ret; } else { ret = (T) find(targetClz, where, false); } if (ret == null) { log.info("no record {}, {}", key, where); } else { MCSupport mc = (MCSupport) ret; long mcId = mc.getIdentifier(); log.info("found id from DB {}#{}", targetClz.getSimpleName(), mcId); MC.add(key, mcId); ret = (T) find(targetClz, mcId); } return ret; } /** * @param t * @param where * 例子: where uid>100 * @return */ public static <T> List<T> list(Class<T> t, String where) { Session session = sessionFactory.getCurrentSession(); Transaction tr = session.beginTransaction(); List<T> list = Collections.EMPTY_LIST; try { String hql = "from " + t.getSimpleName() + " " + where; Query query = session.createQuery(hql); list = query.list(); tr.commit(); } catch (Exception e) { tr.rollback(); log.error("list fail for {} {}", t, where); log.error("list fail", e); } return list; } public static SessionFactory buildSessionFactory() { log.info("开始构建hibernate"); String path = "classpath*:spring-conf/applicationContext.xml"; ApplicationContext ac = new FileSystemXmlApplicationContext(path); sessionFactory = (SessionFactory) ac.getBean("sessionFactory"); log.info("结束构建hibernate"); return sessionFactory; } public static Throwable delete(Object o) { if (o == null) { return null; } Session session = sessionFactory.getCurrentSession(); session.beginTransaction(); try { if (o instanceof MCSupport) { MCSupport s = (MCSupport) o;// 需要对控制了的对象在第一次存库时调用MC.add MC.delete(o.getClass(), s.getIdentifier());// MC中控制了哪些类存缓存。 } session.delete(o); session.getTransaction().commit(); } catch (Throwable e) { log.error("要删除的数据{}", o); log.error("出错", e); session.getTransaction().rollback(); return e; } return null; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
其中HibernateUtil中也用了SpyMemcached来做一些结果集的缓存,当然项目中也有其他地方用到了Memcache来做缓存。最开始的时候,我还纠结要不要把每个玩家的整个游戏数据(GameData)缓存起来,这样读起来会更快,但是我想了想,如果我把整个游戏数据缓存起来,那么每次存档,我都要把缓存中数据取出来,把要修改的那部分数据从数据库查询出来,再进行修改,再放回去,这样的话,每次存档就会多一次数据库操作,然而再想想,整个游戏中,读档只有进游戏的时候需要,而存档是随时都需要,权衡之下,还不如不做缓存,做了缓存反而需要更多数据库的操作。
缓存部分代码如下:/** * 对SpyMemcached Client的二次封装,提供常用的Get/GetBulk/Set/Delete/Incr/Decr函数的同步与异步操作封装. * * 未提供封装的函数可直接调用getClient()取出Spy的原版MemcachedClient来使用. * * @author 何金成 */ public class MemcachedCRUD implements DisposableBean { private static Logger logger = LoggerFactory.getLogger(MemcachedCRUD.class); private MemcachedClient memcachedClient; private long shutdownTimeout = 2500; private long updateTimeout = 2500; private static MemcachedCRUD inst; public static MemcachedCRUD getInstance() { if (inst == null) { inst = new MemcachedCRUD(); } return inst; } // Test Code public static void main(String[] args) { MemcachedCRUD.getInstance().set("test", 0, "testVal"); for (int i = 0; i < 100; i++) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } String val = MemcachedCRUD.getInstance().get("test"); Long a = MemcachedCRUD.getInstance().<Long> get("aaa"); System.out.println(a); System.out.println(val); } }).start(); } } private MemcachedCRUD() { String cacheServer = GameInit.cfg.get("cacheServer"); if (cacheServer == null) { cacheServer = "localhost:11211"; } // String cacheServer = "123.57.211.130:11211"; String host = cacheServer.split(":")[0]; int port = Integer.parseInt(cacheServer.split(":")[1]); List<InetSocketAddress> addrs = new ArrayList<InetSocketAddress>(); addrs.add(new InetSocketAddress(host, port)); try { ConnectionFactoryBuilder builder = new ConnectionFactoryBuilder(); builder.setProtocol(Protocol.BINARY); builder.setOpTimeout(1000); builder.setDaemon(true); builder.setOpQueueMaxBlockTime(1000); builder.setMaxReconnectDelay(1000); builder.setTimeoutExceptionThreshold(1998); builder.setFailureMode(FailureMode.Retry); builder.setHashAlg(DefaultHashAlgorithm.KETAMA_HASH); builder.setLocatorType(Locator.CONSISTENT); builder.setUseNagleAlgorithm(false); memcachedClient = new MemcachedClient(builder.build(), addrs); logger.info("Memcached at {}:{}", host, port); } catch (IOException e) { e.printStackTrace(); } } /** * Get方法, 转换结果类型并屏蔽异常, 仅返回Null. */ public <T> T get(String key) { try { return (T) memcachedClient.get(key); } catch (RuntimeException e) { handleException(e, key); return null; } } /** * 异步Set方法, 不考虑执行结果. * * @param expiredTime * 以秒过期时间,0表示没有延迟,如果exptime大于30天,Memcached将使用它作为UNIX时间戳过期 */ public void set(String key, int expiredTime, Object value) { memcachedClient.set(key, expiredTime, value); } /** * 安全的Set方法, 保证在updateTimeout秒内返回执行结果, 否则返回false并取消操作. * * @param expiredTime * 以秒过期时间,0表示没有延迟,如果exptime大于30天,Memcached将使用它作为UNIX时间戳过期 */ public boolean safeSet(String key, int expiration, Object value) { Future<Boolean> future = memcachedClient.set(key, expiration, value); try { return future.get(updateTimeout, TimeUnit.MILLISECONDS); } catch (Exception e) { future.cancel(false); } return false; } /** * 异步 Delete方法, 不考虑执行结果. */ public void delete(String key) { memcachedClient.delete(key); } /** * 安全的Delete方法, 保证在updateTimeout秒内返回执行结果, 否则返回false并取消操作. */ public boolean safeDelete(String key) { Future<Boolean> future = memcachedClient.delete(key); try { return future.get(updateTimeout, TimeUnit.MILLISECONDS); } catch (Exception e) { future.cancel(false); } return false; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
游戏安全
首先是数据传输的安全问题:当我们完成了接口对接之后,就会考虑一个问题,当别人进行抓包之后,就能很轻松的知道服务器和客户端传输的数据格式,这样的话,不说服务器攻击,至少会有人利用这些接口做出一大批外挂,本身我们加上弱联网就是为了杜绝作弊现象,于是,我们对传输消息做了加密,先做XXTea加密,再做Base64加密,用约定好的秘钥,进行加密解密,进行消息收发。再一个就是支付验证的安全问题,现在有人能破解内购,就是利用支付之后断网,然后模拟返回结果为true,破解内购。我们做了支付验证,在完成支付之后,必须到后台查询订单状态,状态为完成才能获得购买的物品,支付我之前也是没有做过,一点点摸索的。代码就不贴了,涉及到业务。
总结
本文章只为了记录这款弱联网游戏的后台开发历程,可能之后还会遇到很多的问题,问题都是在摸索中解决的,我还需要了解更多关于netty性能方面知识。以上代码只是项目中的部分代码,并不涉及业务部分。分享出来也是给大家一个思路,或是直接拿去用,都是可以的,因为自己踩过一些坑,所以希望将这些记录下来,下次不能踩同样的坑。到目前为止,这款游戏也经过了大概半个多月的时间,到此作为记录,作为经验分享,欢迎交流探讨。我要参与的下一款游戏是长连接的SLG,到时候我应该还会面临更多的挑战,加油!
版权声明:本文为博主原创文章,未经博主允许不得转载。