我是如何設計遊戲伺服器架構的
前言
現在遊戲市場分為,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 [email protected]<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 [email protected]<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 [email protected]<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 }
這就是基本的遊戲伺服器架構設計,
這裡同時提一下,之前文章裡面又介紹訊息解碼器,
經過測試如果訊息疊加,多包一起傳送至伺服器,伺服器解析重組程式碼有問題,現在解碼器是經過修正的
不知道各位看官有什麼要指點小弟的。。