前言
現在游戲市場分為,pc端,移動端,瀏覽器端,而已移動端和瀏覽器端最為接近。都是短平快的特殊模式,不斷的開服,合服,換皮。如此滾雪球!
那么在游戲服務器架構的設計方面肯定是以簡單,快捷,節約成本來設計的。
來我們看一張圖:
這個呢是我了解到,並且在使用的方式,而PC端的游戲服務器而言,往往是大量的數據處理和大量的人在線,一般地圖也是無縫地圖的完整世界觀,所以不同的程序都是獨立的進程並且在不同的server中運行!
而瀏覽器端和移動終端,在上面就說過了,它主要是不斷的開服,合服,開服,合服,那么勢必一個服務器承載量和游戲設計就不符合pc段游戲的設計。而且移動終端由於存在着千差萬別的設備配置情況,也不可能
用無縫地圖,手機承受不了。美術開銷也是巨大的。為了承載着這樣短平快,並且還要承載一台物理的server,開啟幾個游戲服務器進程方式;所以早就了移動終端游戲服務器不一樣的架構;
移動終端游戲服務器我的設計
我在設計的時候,以登錄服務器為中心,設計,客戶端先請求登錄服務器,登錄后拿到一個token值然后請求服務器列表,選擇服務器,進行二次登錄,二次登錄就只需要token值了;
最早以前toeken是需要傳回數據中心進行驗證,而現在的設計是other2.0的設計模式,通過md5驗證即可。實現了一個解耦操作;
登錄服務器,數據中心和充值服務器,都是單獨的server,物理機,而游戲服務器gamesr,就可能是g1,g2一組,g3,g4一組,這是部署的程序架構;
那么程序的搭建架構呢?
每一個服務器程序會有對應的腳本程序進行控制;
邏輯服務器架構設計
來看個圖片先!
通過socket nio 進行數據傳輸,當數據進入游戲服務器以后,會按照先后順序進入隊列,然后由消息分發器線程,把對應的消息分排的對應的線程進行處理;
比如玩家登陸發配到登錄線程(我這里所說的線程也許不止一個線程,也可能是一個組),然后登錄成功,把玩家放到對應的地圖,,存儲對應的關系。
當玩家正常游戲消息來了以后會消息分發器會根據玩家對應關系獲得對應的地圖線程,分發消息到對應的地圖線程處理!這樣好處就是,分散開來多個線程處理玩家操作數據
划分地圖線程,保證在一個地圖上線程操作是安全性的。這里特別注明:由於地圖是切割后的小地圖,跨地圖是需要傳送門傳送,所以一個地圖玩家和怪物的數量不會太多,一個線程就能處理過來!
地圖線程存在了對應的定時觸發器:
PlayerAI (pai) 玩家智能
MonsterAI (mai) 怪物智能
PlayerRun (prun) 玩家移動模擬
Monster (mrun)怪物移動模擬
BuferRun buff計算
FightRun 戰斗計算
等等一系列的操作在一起!
消息處理器設計
腳本項目里面會存在兩個根目錄,一個是消息處理器handler;
另外一個根目錄才是腳本scripts;
為什么我要把消息處理器handler放在腳本里面呢?
好處就是我不能保證每一個開發人員在收到客戶端傳過來的消息的邏輯處理都是正確的;邏輯是非常嚴謹的
如果沒有放在腳本里面,上線了發現消息處理邏輯有bug,那么這個時候處理就非常麻煩;
1 package net.sz.game.proto.handler.cross; 2 3 import net.sz.engine.io.nettys.tcp.NettyTcpHandler; 4 import net.sz.engine.script.IInitBaseScript; 5 import com.game.proto.CrossMessage; 6 import net.sz.game.gamesr.server.tcp.GameTcpServer; 7 import org.apache.log4j.Logger; 8 9 /** 10 * 11 * <br> 12 * author 失足程序員<br> 13 * mail 492794628@qq.com<br> 14 * phone 13882122019<br> 15 */ 16 public final class ReqCrossCreateTeamZoneHandler extends NettyTcpHandler implements IInitBaseScript { 17 18 private static final Logger log = Logger.getLogger(ReqCrossCreateTeamZoneHandler.class); 19 20 @Override 21 public void init() { 22 net.sz.engine.io.nettys.NettyPool.getInstance().register( 23 com.game.proto.CrossMessage.Protos_Cross.CrossCreateTeamZone_VALUE,//消息消息id 24 com.game.proto.CrossMessage.ReqCrossCreateTeamZoneMessage.class,//messageClass 協議請求消息類型 25 this.getClass(), //消息執行的handler 26 GameTcpServer.TEAMTHREADEXECUTOR,//處理線程 27 com.game.proto.CrossMessage.ReqCrossCreateTeamZoneMessage.newBuilder(),//消息體 28 0 // mapThreadQueue 協議請求地圖服務器中的具體線程,默認情況下,每個地圖服務器都有切只有一個Main線程. 29 //一般情況下玩家在地圖的請求,都是Main線程處理的,然而某些地圖,可能會使用多個線程來處理大模塊的功能. 30 ); 31 } 32 33 public ReqCrossCreateTeamZoneHandler() { 34 35 } 36 37 @Override 38 public void run() { 39 // TODO 處理CrossMessage.ReqCrossCreateTeamZone消息 40 CrossMessage.ReqCrossCreateTeamZoneMessage reqMessage = (CrossMessage.ReqCrossCreateTeamZoneMessage) getMessage(); 41 //CrossMessage.ResCrossCreateTeamZoneMessage.Builder builder4Res = CrossMessage.ResCrossCreateTeamZoneMessage.newBuilder(); 42 } 43 }
這就是一個消息處理模板;
腳本在被加載的時候會調用init函數,init函數把消息處理連同消息本身一起注冊到消息中心,包含消息id,消息處理handler,消息處理應用的消息模板,已經消息的處理線程;
1 NettyPool.getInstance().setSessionAttr(ctx, NettyPool.SessionLastTime, System.currentTimeMillis()); 2 MessageHandler _msghandler = NettyPool.getInstance().getHandlerMap().get(msg.getMsgid()); 3 if (_msghandler == null) { 4 log.error("尚未注冊消息:" + msg.getMsgid()); 5 } else { 6 try { 7 NettyTcpHandler newInstance = (NettyTcpHandler) _msghandler.getHandler().newInstance(); 8 Message.Builder parseFrom = _msghandler.getMessage().clone().mergeFrom(msg.getMsgbuffer()); 9 newInstance.setSession(ctx); 10 newInstance.setMessage(parseFrom.build()); 11 if (_msghandler.getThreadId() == 0) { 12 log.error("注冊消息:" + msg.getMsgid() + ",未注冊線程,線程id:0"); 13 } else { 14 log.debug("收到消息並派發:" + msg.getMsgid() + " 線程id:" + _msghandler.getThreadId()); 15 ThreadPool.addTask(_msghandler.getThreadId(), newInstance); 16 } 17 } catch (InstantiationException | IllegalAccessException | InvalidProtocolBufferException e) { 18 log.error("工人<“" + Thread.currentThread().getName() + "”> 執行任務<" + msg.getMsgid() + "(“" + _msghandler.getMessage().getClass().getName() + "”)> 遇到錯誤: ", e); 19 } 20 }
而消息中心收到消息以后會自動解析消息,轉發消息到對應的消息handler邏輯塊
這樣就形成了一個消息循環;
提到消息,就不得不說消息編碼器和解碼器
1 package net.sz.engine.io.nettys.tcp; 2 3 import io.netty.buffer.ByteBuf; 4 import io.netty.buffer.Unpooled; 5 import io.netty.channel.ChannelHandlerContext; 6 import io.netty.handler.codec.ByteToMessageDecoder; 7 import io.netty.util.ReferenceCountUtil; 8 import java.util.ArrayList; 9 import java.util.List; 10 import net.sz.engine.io.nettys.NettyPool; 11 import org.apache.log4j.Logger; 12 13 /** 14 * 解碼器 15 * <br> 16 * author 失足程序員<br> 17 * mail 492794628@qq.com<br> 18 * phone 13882122019<br> 19 */ 20 class NettyDecoder extends ByteToMessageDecoder { 21 22 private static final Logger logger = Logger.getLogger(NettyDecoder.class); 23 24 private byte ZreoByteCount = 0; 25 private ByteBuf bytes; 26 private long secondTime = 0; 27 private int reveCount = 0; 28 29 public NettyDecoder() { 30 31 } 32 33 ByteBuf bytesAction(ByteBuf inputBuf) { 34 ByteBuf bufferLen = Unpooled.buffer(); 35 if (bytes != null) { 36 bufferLen.writeBytes(bytes); 37 bytes = null; 38 } 39 bufferLen.writeBytes(inputBuf); 40 return bufferLen; 41 } 42 43 /** 44 * 留存無法讀取的byte等待下一次接受的數據包 45 * 46 * @param bs 數據包 47 * @param startI 起始位置 48 * @param lenI 結束位置 49 */ 50 void bytesAction(ByteBuf intputBuf, int startI) { 51 bytes = Unpooled.buffer(); 52 bytes.writeBytes(intputBuf); 53 } 54 55 @Override 56 protected void decode(ChannelHandlerContext chc, ByteBuf inputBuf, List<Object> outputMessage) { 57 if (inputBuf.readableBytes() > 0) { 58 ZreoByteCount = 0; 59 //重新組裝字節數組 60 ByteBuf buffercontent = bytesAction(inputBuf); 61 List<NettyMessageBean> megsList = new ArrayList<>(0); 62 for (;;) { 63 //讀取 消息長度(short)和消息ID(int) 需要 8 個字節 64 if (buffercontent.readableBytes() >= 8) { 65 //讀取消息長度 66 int len = buffercontent.readInt(); 67 if (buffercontent.readableBytes() >= len) { 68 int messageid = buffercontent.readInt();///讀取消息ID 69 ByteBuf buf = buffercontent.readBytes(len - 4);//讀取可用字節數; 70 megsList.add(new NettyMessageBean(messageid, buf.array())); 71 } else { 72 //重新設置讀取進度 73 buffercontent.readerIndex(buffercontent.readerIndex() - 4); 74 break; 75 } 76 } else { 77 break; 78 } 79 } 80 if (buffercontent.readableBytes() > 0) { 81 ///緩存預留的字節 82 bytesAction(buffercontent, buffercontent.readerIndex()); 83 } 84 NettyPool.getInstance().setSessionAttr(chc, NettyPool.SessionLastTime, System.currentTimeMillis()); 85 if (!megsList.isEmpty()) { 86 if (System.currentTimeMillis() - secondTime < 1000L) { 87 reveCount += megsList.size(); 88 } else { 89 secondTime = System.currentTimeMillis(); 90 reveCount = 0; 91 } 92 93 if (reveCount > 50) { 94 logger.error("發送消息過於頻繁"); 95 chc.disconnect(); 96 } else { 97 outputMessage.addAll(megsList); 98 } 99 } 100 } else { 101 ZreoByteCount++; 102 if (ZreoByteCount >= 3) { 103 //todo 空包處理 考慮連續三次空包,斷開鏈接 104 logger.error("decode 空包處理 連續三次空包"); 105 NettyPool.getInstance().closeSession(chc, "decode 空包處理 連續三次空包"); 106 } 107 } 108 //釋放內存資源 109 // ReferenceCountUtil.release(inputBuf); 110 } 111 }
1 package net.sz.engine.io.nettys.tcp; 2 3 import com.google.protobuf.Message; 4 import io.netty.buffer.ByteBuf; 5 import io.netty.buffer.Unpooled; 6 import io.netty.channel.ChannelHandlerContext; 7 import io.netty.handler.codec.MessageToByteEncoder; 8 import java.nio.ByteOrder; 9 import net.sz.engine.io.nettys.NettyPool; 10 import org.apache.log4j.Logger; 11 12 /** 13 * 編碼器 14 * <br> 15 * author 失足程序員<br> 16 * mail 492794628@qq.com<br> 17 * phone 13882122019<br> 18 */ 19 class NettyEncoder extends MessageToByteEncoder<com.google.protobuf.Message> { 20 21 private static final Logger logger = Logger.getLogger(NettyEncoder.class); 22 ByteOrder endianOrder = ByteOrder.LITTLE_ENDIAN; 23 24 public NettyEncoder() { 25 26 } 27 28 @Override 29 protected void encode(ChannelHandlerContext chc, com.google.protobuf.Message build, ByteBuf out) throws Exception { 30 ByteBuf buffercontent = Unpooled.buffer(); 31 com.google.protobuf.Descriptors.EnumValueDescriptor field = (com.google.protobuf.Descriptors.EnumValueDescriptor) build.getField(build.getDescriptorForType().findFieldByNumber(1)); 32 int msgID = field.getNumber(); 33 byte[] toByteArray = build.toByteArray(); 34 buffercontent.writeInt(toByteArray.length + 4) 35 .writeInt(msgID) 36 .writeBytes(toByteArray); 37 // logger.error("發送消息長度 " + (toByteArray.length + 4)); 38 NettyPool.getInstance().setSessionAttr(chc, NettyPool.SessionLastTime, System.currentTimeMillis()); 39 out.writeBytes(buffercontent); 40 } 41 }
這就是基本的游戲服務器架構設計,
這里同時提一下,之前文章里面又介紹消息解碼器,
經過測試如果消息疊加,多包一起發送至服務器,服務器解析重組代碼有問題,現在解碼器是經過修正的
不知道各位看官有什么要指點小弟的。。