SLG手遊Java服務器的設計與開發——網絡通信

前言

上文分析了我們這款SLG的架構,本章着重講解我們的網絡通信架構,由上文的功能分析我們可以得知,遊戲的所有功能基本上屬於非及時的通信機制,所以依靠HTTP短連接就能夠基本滿足遊戲的通信需求。

當然,我們先撇開國戰部分不說,因爲國戰部分我們正在優化開發最新版本,之前我們做的版本是想通過異步戰鬥的機制達到實時戰鬥效果,通過使用HTTP的請求機制,加上前端畫面表現,讓玩家感覺到即時戰鬥的感覺,既能在地圖上能看到其他玩家的行進隊列,又能進入城池多國混戰。可惜的是,讓異步戰鬥來實現實時戰鬥的效果,會產生很多問題,最終因爲機制的問題而商議出必須優化一版再上線。所以目前所有的功能均通過HTTP實現,如果後期國戰需要使用TCP長連接可以單獨對國戰部分使用TCP長連接實現。

通信框架

使用Netty

在開始設計通信機制時,就需要選擇合適的通信框架,當然,我們也可以自己動手寫底層通信的實現,不過在前人已有成熟框架的情況下,我們大而不必重複造輪子,由於在通信方面,我們並沒有太多的個性化需求,因此基本上成熟的通信框架都能滿足目前所需。選擇框架,無非就是看它們的底層架構是否符合需求、資料是否齊全、API文檔是否詳細、以及成熟案例有多少。上文提到,可以使用HTTP通信協議的框架中,有Servlet、Spring、Struts、Mina和Netty等常見的通信框架,在這其中我選擇了Netty,Servlet、Spring和Struts屬於同一系列,他們的底層都是Servlet的實現,在Servlet3.0以前均是BIO通信模式,而Mina和Netty均屬於基於Java NIO的通信框架,由於通信機制的不同,基於NIO的通信程序比基於BIO的通信程序能承受更多的併發連接,而在後者的框架選擇中,其實並沒有太多的誰好與不好,Mina和Netty底層都是Java NIO的封裝,並且兩者的底層框架也是大致一樣(其作者其實就是一個人),選擇Netty更多的是因爲Netty的有更多的資料可查,遇到問題可能會更容易解決,並且我個人在同時使用過Mina和Netty的情況下,認爲Netty的API更友好,使用起來更方便(個人感覺哈)。綜合種種原因,我選擇了Netty作爲我的底層通信框架。

Netty的特點

選擇了Netty,我們就應該明白Netty的一些特點,Netty具有以下特點:
1.異步、非阻塞、基於事件驅動的NIO框架
2.支持多種傳輸層通信協議,包括TCP、UDP等
3.開發異步HTTP服務端和客戶端應用程序
4.提供對多種應用層協議的支持,包括TCP私有協議、HTTP協議、WebSocket協議、文件傳輸等
5.默認提供多種編解碼能力,包括Java序列化、Google的ProtoBuf、二進制編解碼、Jboss marshalling、文本字符串、base64、簡單XML等,這些編解碼框架可以被用戶直接使用
6.提供形式多樣的編解碼基礎類庫,可以非常方便的實現私有協議棧編解碼框架的二次定製和開發
7.經典的ChannelFuture-listener機制,所有的異步IO操作都可以設置listener進行監聽和獲取操作結果
8.基於ChannelPipeline-ChannelHandler的責任鏈模式,可以方便的自定義業務攔截器用於業務邏輯定製
9.安全性:支持SSL、HTTPS
10.可靠性:流量整形、讀寫超時控制機制、緩衝區最大容量限制、資源的優雅釋放等
11.簡潔的API和啓動輔助類,簡化開發難度,減少代碼量

NIO

Netty是基於NIO的通信框架,爲什麼要使用NIO而不是用傳統的BIO通信機制呢,因爲在BIO的線程模型上,存在着致命缺陷,由於線程模型問題,接入用戶數與服務端創造線程數是1:1的關係,也就是說每一個用戶從接入斷開連接,服務端都要創造一個與之對應的線程做處理,一旦併發用戶數增多,再好配置的服務器也有可能會有因爲線程開銷問題造成服務器崩潰宕機的情況。除此之外,BIO的所有IO操作都是同步的,當IO線程處理業務邏輯時,也會出現同步阻塞,其他請求都要進入阻塞狀態。
相反,NIO的通信機制可以很好地解決BIO的線程開銷問題,NIO採用Reactor通信模式,一個Reactor線程聚合一個多路複用Selector,這個Selector可同時註冊、監聽、輪迴上百個Channel請求,這種情況下,一個IO線程就可以處理N個客戶端的同時接入,接入用戶數與線程數爲N:1的關係,並且IO總數有限,不會出現頻繁上下文切換,提高了CPU利用率,並且所有的 IO 操作都是異步的,即使業務線程直接進行IO操作,也不會被同步阻塞,系統不再依賴外部的網絡環境和外部應用程序的處理性能

Netty架構

Netty採用經典的MVC三層架構:
1.第一層:Reactor通信調度層,它由一系列輔助類組成,包括Reactor線程NioEventLoop 以及其父類、NioSocketChannel/NioServerSocketChannel 以及其父類、ByteBuffer 以及由其衍生出來的各種 Buffer、Unsafe 以及其衍生出的各種內部子類等。
2.第二層:職責鏈ChannelPipeLine,它負責調度事件在職責鏈中的傳播,支持動態的編排職責鏈,職責鏈可以選擇性的攔截自己關心的事件,對於其它IO操作和事件忽略,Handler同時支持inbound和outbound事件
3.第三層:業務邏輯編排層,業務邏輯編排層通常有兩類:一類是純粹的業務邏輯編排,還有一類是應用層協議插件,用於協議相關的編解碼和鏈路管理,例如CMPP協議插件

基於Netty實現的HTTP Server

Netty其實更適合使用創建TCP長連接的Server,但是其也提供了HTTP的實現封裝,我們也可以很容易的實現基於Netty的HTTP服務器。Netty實現HTTP服務器主要通過HttpResponseEncoder和HttpRequestDecoder來進行HTTP請求的解碼以及HTTP響應的編碼,通過HttpRequest和HttpResponse接口來實現對請求的解析以及對響應的構造。本節先描述整個處理流程,然後通過源碼進行分享。

處理流程

使用Netty實現的HTTP Server的處理流程如下:
1.HttpServer接收到客戶端的HttpRequest,打開Channel連接
2.pipeline中的HttpInHandler調用channelRead方法讀取Channel中的ChannelHandlerContext和Object
3.channelRead中調用實現類HttpInHandlerImp中的處理,將請求按照Get或Post方式進行解析,並將數據轉爲ProtoMessage,然後轉交給MsgHandler處理
4.MsgHandler將其封裝爲Message類添加到userid哈希的消息處理隊列中,並對隊列中的消息調用handle進行遊戲的邏輯處理
5.在邏輯處理中,調用HttpInHandler的writeJSON方法構造並返回HttpResponse響應消息
6.HttpOutHandler截取消息並打印log日誌
7.HttpResponse響應消息返回給客戶端並斷開Channel連接
整個流程的流程圖如下:
網絡處理流程

HttpServer

HttpServer中負責創造並啓動Netty實例,並綁定我們的邏輯Handler到pipeline,使請求進入我們自己的邏輯處理

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
package com.kidbear._36.net.http;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.concurrent.DefaultThreadFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.Properties;
import java.util.concurrent.Executors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HttpServer {
   public static Logger log = LoggerFactory.getLogger(HttpServer.class);
   public static HttpServer inst;
   public static Properties p;
   public static String ip;
   public static int port;
   private NioEventLoopGroup bossGroup = null;
   private NioEventLoopGroup workGroup = null;

   private HttpServer() {

   }

   public static HttpServer getInstance() {
       if (inst == null) {
           inst = new HttpServer();
           inst.initData();
       }
       return inst;
   }

   public void initData() {
       try {
           p = readProperties();
           ip = p.getProperty("ip");
           port = Integer.parseInt(p.getProperty("port"));
       } catch (IOException e) {
           log.error("socket配置文件讀取錯誤");
           e.printStackTrace();
       }
   }

   public void start() {
       bossGroup = new NioEventLoopGroup(0, Executors.newCachedThreadPool());// boss線程組
       workGroup = new NioEventLoopGroup(0, Executors.newCachedThreadPool());// work線程組
       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();
               /* http request解碼 */
               pipeline.addLast("decoder", new HttpRequestDecoder());
               pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
               /* http response 編碼 */
               pipeline.addLast("encoder", new HttpResponseEncoder());
               pipeline.addLast("http-chunked", new ChunkedWriteHandler());
               /* http response handler */
               pipeline.addLast("outbound", new HttpOutHandler());
               /* http request handler */
               pipeline.addLast("inbound", new HttpInHandler());
           }
       });
       bootstrap.bind(port);
       log.info("端口{}已綁定", port);
   }

   public void shut() {
       if (bossGroup != null && workGroup != null) {
           bossGroup.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;
   }
}


代碼中首先使用NioEventLoopGroup構造boss線程和work線程,然後構造ServerBootstrap,來設置Server的一些屬性,包括在pipeline中添加Http的編碼解碼以及邏輯處理相關類。通過調用該類的start方法即可啓動此HTTP服務器,其中端口在配置文件中配置好,啓動時從配置文件讀取。

HttpInHandler

Http請求的處理器,綁定在pipeLine中,負責請求的解析與邏輯處理,代碼如下:

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
package com.kidbear._36.net.http;

import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;

/**
* @ClassName: HttpServerHandler
* @Description: netty處理器
* @author 何金成
* @date 2015年12月18日 下午6:27:06
*
*/

public class HttpInHandler extends ChannelHandlerAdapter {

   public HttpInHandlerImp handler = new HttpInHandlerImp();

   @Override
   public void channelRead(ChannelHandlerContext ctx, Object msg)
           throws Exception {

       handler.channelRead(ctx, msg);
   }

   @Override
   public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
           throws Exception {

       handler.exceptionCaught(ctx, cause);
   }

   public static void writeJSON(ChannelHandlerContext ctx,
           HttpResponseStatus status, Object msg)
{

       HttpInHandlerImp.writeJSON(ctx, status, msg);
   }

   public static void writeJSON(ChannelHandlerContext ctx, Object msg) {
       HttpInHandlerImp.writeJSON(ctx, msg);
   }
}


其中的實現方法我都將其分離出來爲單獨的類來處理,我這樣做主要爲了我以後能通過JSP熱修復Bug(以後會講到,通過JSP熱加載的原理實現線上項目的熱修復),分離出來的實現類代碼如下:

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
package com.kidbear._36.net.http;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.util.CharsetUtil;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.kidbear._36.core.GameServer;
import com.kidbear._36.net.MsgHandler;
import com.kidbear._36.net.ProtoMessage;
import com.kidbear._36.net.ResultCode;
import com.kidbear._36.net.rpc.JsonRpcServers;
import com.kidbear._36.util.Constants;
import com.kidbear._36.util.encrypt.XXTeaCoder;

public class HttpInHandlerImp {
   private static Logger log = LoggerFactory.getLogger(HttpInHandlerImp.class);
   public static String DATA = "data";
   public static volatile boolean CODE_DEBUG = false;
   public ConcurrentHashMap<String, Future> executeMap = new ConcurrentHashMap<String, Future>();

   public void channelRead(final ChannelHandlerContext ctx, final Object msg)
           throws Exception {

       /** work線程的內容轉交線程池管理類處理,縮短work線程耗時 **/
       if (!GameServer.shutdown) {// 服務器開啓的情況下
           DefaultFullHttpRequest req = (DefaultFullHttpRequest) msg;
           if (req.getMethod() == HttpMethod.GET) { // 處理get請求
               getHandle(ctx, req);
           }
           if (req.getMethod() == HttpMethod.POST) { // 處理POST請求
               postHandle(ctx, req);
           }
       } else {// 服務器已關閉
           JSONObject jsonObject = new JSONObject();
           jsonObject.put("errMsg", "server closed");
           writeJSON(ctx, jsonObject);
       }
   }

   private void postHandle(final ChannelHandlerContext ctx,
           final DefaultFullHttpRequest req)
{

       HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(
               new DefaultHttpDataFactory(false), req);
       // 邏輯接口處理
       try {
           InterfaceHttpData data = decoder.getBodyHttpData(DATA);
           if (data != null) {
               String val = ((Attribute) data).getValue();
               val = codeFilter(val);
               log.info("ip:{},read :{}", ctx.channel().remoteAddress(),
                       val);
               ProtoMessage msg = null;
               try {
                   msg = JSON.parseObject(val, ProtoMessage.class);
               } catch (Exception e) {
                   log.error("gameData的json格式轉換錯誤");
                   HttpInHandler.writeJSON(ctx,
                           HttpResponseStatus.NOT_ACCEPTABLE,
                           "not acceptable");
                   return;
               }
               Long userid = msg.getUserid();
               // 添加到消息處理隊列
               // MsgHandler.getInstance().addMsg(userid, msg, ctx);
               // 直接處理消息
               // MsgHandler.getInstance().handle(new Message(msg, ctx));
               // 處理消息隊列
               MsgHandler.getInstance().handleMsg(userid, msg, ctx);
           }
       } catch (Exception e) {
           // 異常日誌
           log.error("post error msg:", e);
           e.printStackTrace();
           // Print our stack trace
           StringBuffer eBuffer = new StringBuffer(e.getMessage() + ",");
           StackTraceElement[] trace = e.getStackTrace();
           for (StackTraceElement traceElement : trace) {
               eBuffer.append("\r\n " + traceElement);
           }
           HttpInHandler.writeJSON(ctx, ProtoMessage.getErrorResp(
                   ResultCode.SERVER_ERR, eBuffer.toString()));
       }
   }

   private void getHandle(final ChannelHandlerContext ctx,
           DefaultFullHttpRequest req)
{

       QueryStringDecoder decoder = new QueryStringDecoder(req.getUri());
       Map<String, List<String>> params = decoder.parameters();
       List<String> typeList = params.get("type");
       if (Constants.MSG_LOG_DEBUG) {
           log.info("ip:{},read :{}", ctx.channel().remoteAddress(),
                   typeList.get(0));
       }
       writeJSON(ctx, HttpResponseStatus.NOT_IMPLEMENTED, "not implement");
   }

   /**
    * @Title: codeFilter
    * @Description: 編解碼過濾
    * @param val
    * @return
    * @throws UnsupportedEncodingException
    *             String
    * @throws
    */

   private String codeFilter(String val) throws UnsupportedEncodingException {
       val = val.contains("%") ? URLDecoder.decode(val, "UTF-8") : val;
       String valTmp = val;
       val = CODE_DEBUG ? XXTeaCoder.decryptBase64StringToString(val,
               XXTeaCoder.key) : val;
       if (Constants.MSG_LOG_DEBUG) {
           if (val == null) {
               val = valTmp;
           }
       }
       return val;
   }

   public static void writeJSON(ChannelHandlerContext ctx,
           HttpResponseStatus status, Object msg)
{

       String sentMsg = null;
       if (msg instanceof String) {
           sentMsg = (String) msg;
       } else {
           sentMsg = JSON.toJSONString(msg);
       }
       sentMsg = CODE_DEBUG ? 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 = null;
       if (msg instanceof String) {
           sentMsg = (String) msg;
       } else {
           sentMsg = JSON.toJSONString(msg);
       }
       sentMsg = CODE_DEBUG ? 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");
               msg.headers().set("userid", 101);
           } 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 {

       log.error("netty exception:", cause);
   }
}


以上代碼實現了使用Netty中封裝的Http請求的解析類對消息進行Get或Post解析,並使用了Http相應的構造類對返回消息進行Http消息格式的構造。

MsgHandler

以上代碼包含了Netty中的Get請求和Post請求的解析處理,請求消息以及響應消息的XXTea加密解密等。其中,服務器接受到請求後,會將請求交給一個消息處理類進行具體的消息處理,消息處理器MsgHandler的代碼如下:

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
package com.kidbear._36.net;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.MessageSizeEstimator.Handle;

import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.LinkedBlockingQueue;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSONObject;
import com.kidbear._36.core.GameServer;
import com.kidbear._36.core.Router;
import com.kidbear._36.manager.log.LogMgr;
import com.kidbear._36.net.http.HttpInHandler;
import com.kidbear._36.task.ExecutorPool;

/**
* @ClassName: MsgHandler
* @Description: 消息處理器
* @author 何金成
* @date 2016年8月22日 下午12:04:23
*
*/

public class MsgHandler {
   public static Logger logger = LoggerFactory.getLogger(MsgHandler.class);
   public static MsgHandler handler;

   public static MsgHandler getInstance() {
       return handler == null ? new MsgHandler() : handler;
   }

   protected MsgHandler() {

   }

   /**
    * @Fields msgMap : 併發消息處理map
    */

   public static ConcurrentMap<Long, BlockingQueue<Message>> msgMap = new ConcurrentHashMap<Long, BlockingQueue<Message>>();

   public void handleMsg(Long userid, ProtoMessage msg,
           ChannelHandlerContext ctx) throws InterruptedException
{

       // add message
       Message message = new Message();
       message.msg = msg;
       message.ctx = ctx;
       BlockingQueue<Message> queue = null;
       if (msgMap.containsKey(userid)) {
           queue = msgMap.get(userid);
           queue.put(message);
       } else {
           queue = new LinkedBlockingQueue<Message>();
           queue.put(message);
           msgMap.put(userid, queue);
       }
       // log
       LogMgr.getInstance().concurrentLog(msgMap);
       // handle message
       while (!queue.isEmpty()) {
           message = queue.take();
           if (queue.size() == 0) {
               msgMap.remove(userid);
           }
           handle(message);
       }
   }

   /**
    * @Title: addMsg
    * @Description: 添加消息到處理隊列
    * @param userid
    * @param msg
    * @param ctx
    * @throws InterruptedException
    *             void
    * @throws
    */

   public void addMsg(Long userid, ProtoMessage msg, ChannelHandlerContext ctx)
           throws InterruptedException {

       Message message = new Message();
       message.msg = msg;
       message.ctx = ctx;
       if (msgMap.containsKey(userid)) {
           BlockingQueue<Message> queue = msgMap.get(userid);
           queue.put(message);
       } else {
           BlockingQueue<Message> queue = new LinkedBlockingQueue<Message>();
           queue.put(message);
           msgMap.put(userid, queue);
       }
       LogMgr.getInstance().concurrentLog(msgMap);
   }

   /**
    * @Title: run
    * @Description: 處理消息隊列 void
    * @throws
    */

   public void run() {
       ExecutorPool.msgHandleThread.execute(new Runnable() {
           @Override
           public void run() {
               logger.info("消息處理線程開啓");
               while (!GameServer.shutdown) {
                   for (Iterator<Long> iterator = msgMap.keySet().iterator(); iterator
                           .hasNext();) {
                       Long userid = iterator.next();
                       BlockingQueue<Message> queue = msgMap.get(userid);
                       try {
                           Message msg = queue.take();
                           if (queue.size() == 0) {
                               iterator.remove();
                           }
                           handle(msg);
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                           logger.error("msg handle err:{}", e);
                       }
                   }
               }
           }
       });
   }

   public void handle(final Message message) {
       ExecutorPool.channelHandleThread.execute(new Runnable() {
           @Override
           public void run() {
               Short typeid = message.msg.getTypeid();
               if (typeid == null) {
                   logger.error("沒有typeid");
                   HttpInHandler.writeJSON(message.ctx,
                           ProtoMessage.getErrorResp("沒有typeid"));
                   return;
               }
               JSONObject msgData = message.msg.getData();
               Router.getInstance().route(typeid, msgData,
                       message.msg.getUserid(), message.ctx);
           }
       });
   }
}


以上代碼包含handleMsg、handle和addMsg方法,msgMap中包含每個用戶的userid哈希對應的消息處理隊列,原本我的設想是在服務器啓動時,調用MsgHandler的run方法啓動消息處理,無限循環的遍歷msgMap,來處理所有玩家的消息處理隊列,請求接入時,直接添加消息到msgMap的相應玩家的消息隊列,然後由這個run方法中的線程來處理所有的消息,後來考慮到效率問題,改爲直接在HttpInHandler中調用handleMsg方法,直接處理消息請求。每個玩家分配一個消息隊列來進行處理主要是爲了考慮到單個玩家的併發請求的情況。hash使用ConcurrentMap主要是考慮到這個Map的併發使用情景,使用ConcurrentMap的桶鎖機制可以讓它在併發情境中有更高的處理效率。

Message

MsgHandle中使用的Message類是對消息的封裝包括ProtoMessage和ChannelHandlerContext,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.kidbear._36.net;

import io.netty.channel.ChannelHandlerContext;

import java.util.concurrent.BlockingQueue;

public class Message {
   public ProtoMessage msg;
   public ChannelHandlerContext ctx;

   public Message() {
   }

   public Message(ProtoMessage msg, ChannelHandlerContext ctx) {
       this.msg = msg;
       this.ctx = ctx;
   }
}


ProtoMessage

ProtoMessage是通信中對消息格式的封裝,消息格式定義爲:”{typeid:1,userid:1,data:{}}”,typeid代表遊戲中接口的協議號,userid代表玩家id,data代表具體傳輸的數據,其代碼如下:

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
package com.kidbear._36.net;

import java.io.Serializable;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

public class ProtoMessage implements Serializable {

   /**
    * @Fields serialVersionUID : TODO
    */

   private static final long serialVersionUID = -3460913241121151489L;
   private Short typeid;
   private Long userid;
   public JSONObject data;

   public ProtoMessage() {

   }

   public <T> ProtoMessage(Short typeid, T data) {
       this.typeid = typeid;
       this.setData(data);
   }

   public <T> ProtoMessage(T data) {
       this.setData(data);
   }

   public static ProtoMessage getResp(String msg, int code) {
       JSONObject ret = new JSONObject();
       ret.put("code", code);
       if (msg != null) {
           ret.put("err", msg);
       }
       return new ProtoMessage(ret);
   }

   public static ProtoMessage getSucce***esp() {
       return getResp(null, ResultCode.SUCCESS);
   }

   public static ProtoMessage getEmptyResp() {
       return new ProtoMessage();
   }

   public static ProtoMessage getErrorResp(String msg) {
       return getResp(msg, ResultCode.COMMON_ERR);
   }

   public static ProtoMessage getErrorResp(short id) {
       return getResp(null, ResultCode.COMMON_ERR);
   }

   public static ProtoMessage getErrorResp(int code) {
       return getResp(null, code);
   }

   public static ProtoMessage getErrorResp(int code, String msg) {
       return getResp(msg, code);
   }

   public JSONObject getData() {
       return this.data;
   }

   public void setData(JSONObject data) {
       this.data = data;
   }

   public <T> T getData(Class<T> t) {// 轉換爲對象傳遞
       return JSON.parseObject(JSON.toJSONString(data), t);
   }

   public <T> void setData(T t) {
       this.data = JSON.parseObject(JSON.toJSONString(t), JSONObject.class);
   }

   public Short getTypeid() {
       return typeid;
   }

   public void setTypeid(Short typeid) {
       this.typeid = typeid;
   }

   public Long getUserid() {
       return userid;
   }

   public void setUserid(Long userid) {
       this.userid = userid;
   }
}


HttpOutHandler

綁定在pipeLine中,負責處理相應消息,其實響應消息的處理在HttpInHandler的writeJSON方法中已經完成,使用DefaultFullHttpResponse對響應消息進行Http格式構造,然後調用ChannelHandlerContext的write方法直接write到消息管道中,並且在完成消息傳輸後自動關閉管道。而HttpOutHandler則只是截取響應消息並進行log打印輸出一下,然後繼續調用super發送出去,其接口及實現類代碼如下:
HttpOutHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.kidbear._36.net.http;

import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;

public class HttpOutHandler extends ChannelHandlerAdapter {

   public HttpOutHandlerImp handler = new HttpOutHandlerImp();

   @Override
   public void write(ChannelHandlerContext ctx, Object msg,
           ChannelPromise promise) throws Exception
{

       super.write(ctx, msg, promise);
       handler.write(ctx, msg, promise);
   }
}


HttpOutHandlerImp:

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
package com.kidbear._36.net.http;

import java.nio.charset.Charset;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.kidbear._36.util.Constants;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.buffer.UnpooledUnsafeDirectByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;

public class HttpOutHandlerImp {
   public Logger logger = LoggerFactory.getLogger(HttpOutHandlerImp.class);

   public void write(ChannelHandlerContext ctx, Object msg,
           ChannelPromise promise) throws Exception
{

       if (Constants.MSG_LOG_DEBUG) {
           DefaultFullHttpResponse resp = (DefaultFullHttpResponse) msg;
           logger.info("ip:{},write:{}", ctx.channel().remoteAddress(), resp
                   .content().toString(Charset.forName("UTF-8")));
       }
   }
}


總結

本章內容介紹我們的這款遊戲的網絡通信的處理方式,總體來說,對目前的策劃需求,以及目前的用戶量來說,這個通信框架已經能滿足,但客觀的說,這個網絡架構還是存在很多問題的,比如通信使用JSON字符串,使得通信數據的大小沒有得到很好地處理,如果使用ProtoBuffer這樣高效的二進制數據傳輸會有更小的數據傳輸量。另外,通信完全採用Http通信,使得遊戲中一些需要實時展示的效果只能通過請求——響應式來獲取最新數據,比如遊戲中的郵件、戰報等功能,只能通過客戶端的不斷請求來獲取到最新消息,實時效果通過非實時通信來實現,會有很多冗餘的請求,浪費帶寬資源,如果以後玩家數量太多,對網絡通信這塊,我們肯定還會再進行優化。
下章內容,我們會對遊戲中的數據緩存與存儲進行介紹。

原文:http://hjcenry.github.io/2016/08/27/SLG%E6%89%8B%E6%B8%B8Java%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%BC%80%E5%8F%91%E2%80%94%E2%80%94%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章