Java遊戲服務器成長之路——弱聯網遊戲篇(源碼分析)

http://blog.csdn.net/hjcenry/article/details/50530472前端

 

前段時間因爲公司的一款弱聯網遊戲急着上線,沒能及時分享,如今基本作的差很少,剩下的就是測試階段了(原本說元旦來分享一下服務器技術的)。公司的這款遊戲已經上線一年多了,在我來以前一直都是單機版本,因爲人民羣衆的力量太強大,各類內購破解,刷體力,刷金幣,刷鑽石版本的出現,公司才決定將這款遊戲轉型爲弱聯網遊戲,壓制百分之八十的破解用戶(畢竟原則上仍是屬於單機遊戲,不可能作到百分之百的防破解),招了我這一個服務器來進行後臺的開發。什麼是弱聯網遊戲?在這以前我也沒有作過弱聯網遊戲的服務器,可是按照我對策劃對我提出的需求的理解,就是遊戲的大部分邏輯運算都是在移動端本地完成,而服務器要作的就是登陸、支付驗證、遊戲存檔讀檔的工做,這相對於我在上家公司作的ARPG那種強聯網遊戲要簡單多了,那款ARPG就是全部遊戲產出,邏輯運算都是在服務器端完成,服務器要完成大部分的遊戲運算,而我作的這款弱聯網遊戲,只須要簡簡單單的登陸、驗證、讀取和存儲。這一類的遊戲,作的最火的,就是騰訊早期手遊中的《全民消消樂》《節奏大師》《每天飛車》(每天飛車後來加的實時競賽應該仍是強聯網的實時數據)等,這類遊戲中,服務器就只須要負責遊戲數據存儲和一些簡單的社交功能,例如qq好友送紅心送體力的等。java

歸納

公司招聘我進來作服務器開發實際上是爲了兩個項目,一個是這款單機轉弱聯網的遊戲,另外一款是公司準備拿來發家致富的SLG——戰爭策略遊戲。從入職到如今,我一直是在支持弱聯網遊戲的開發,到如今,基本上這款遊戲也算是差很少了,這款遊戲的目前版本仍然基本屬於單機,到年後會加上競技場功能,到時候可能就會須要實時的數據交互了,如今就先來分享一下目前這個版本開發的過程。 
要開發一個後臺系統,首先要考慮的就是架構了,系統的高效穩定性,可擴展性。在遊戲開發中,我認爲後臺服務器無非負責幾個大得模塊: 
1. 網絡通訊 
2. 邏輯處理 
3. 數據存儲 
4. 遊戲安全mysql

首先從需求分析入手,我在這款弱聯網遊戲中,後端須要作的事情就是,登陸,支付驗證,數據存儲,數據讀取,再加上一些簡單的邏輯判斷,第一眼看去,並無任何難點,我就分別從以上幾點一一介紹spring

網絡通訊

弱聯網遊戲,基本上來講,最簡單直接的,就是使用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通訊的核心類:sql

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代碼以下:json

/** * @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則是用於發送接收的消息Beanbootstrap

數據存儲

在這樣的遊戲中,邏輯運算基本由客戶端操做了,所以遊戲數據的持久化纔是服務器的重點,必需要保證遊戲的數據的完整性。數據庫上,我選擇了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,到時候我應該還會面臨更多的挑戰,加油!

相關文章
相關標籤/搜索