我是如何设计游戏服务器架构的

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我是如何设计游戏服务器架构的相关的知识,希望对你有一定的参考价值。

前言

 现在游戏市场分为,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 }

这就是基本的游戏服务器架构设计,

这里同时提一下,之前文章里面又介绍消息解码器,

经过测试如果消息叠加,多包一起发送至服务器,服务器解析重组代码有问题,现在解码器是经过修正的

不知道各位看官有什么要指点小弟的。。

 

以上是关于我是如何设计游戏服务器架构的的主要内容,如果未能解决你的问题,请参考以下文章

一款成功的全球服游戏该如何进行架构选型与设计?

如何设计app的架构

从片段调用 Google Play 游戏服务

游戏服务器架构设计整理

大型多人在线游戏服务器架构设计

棋牌游戏服务器架构: 详细设计 应用层设计