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

前言

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

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

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

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

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

时间: 2024-10-12 20:11:02

我是如何设计游戏服务器架构的的相关文章

微信房卡麻将棋牌架设之游戏服务器架构的详细设计(一) 内核设计

题目:微信房卡麻将棋牌架设之游戏服务器架构的详细设计(一) 内核设计 今天向大家介绍一下游戏服务器的设计,着重讲解一下微信房卡麻将棋牌架设(aqiulian.com)的服务器搭建,如果有什么不懂得可以咨询我Q_212303635,欢迎大家的咨询.那么我们开始进去主题吧. 内核的几个组件被设计成Service,也就是说这几个模块都要实现如下接口: 图1  IService接口 Start方法用来启动服务. Stop 方法用来关闭服务. IsService 方法用于查询当前服务是否正在工作. 内核中

当我设计游戏服务器时,我在想些什么?(1)

机缘巧合的机会,我有幸能够从头开始设计一个游戏的服务器.中间遇到很多欢声笑语和悲伤泪水,这里分享一下. 我之前所在项目组的游戏服务器架构如下图: 这款游戏是一款MMO的端游,GateWay网关的任务是接受客户端的连接,然后通过分发策略,把玩家丢进GameSvr上去,之后玩家的所有请求都直接发给GameSvr,由GameSvr处理了.当然这里的分发策略跟一般的web服务器是不同的,web服务器一般会做成无状态的服务器,也就是对于客户端来说请求到达哪一个服务器都没有关系,都能够被处理,但是游戏服务器

当我设计游戏服务器时,我在想些什么?(2)

半年前我参与了一个手游项目,第一次能够主导整个游戏的设计,这篇文章单说服务器的架构,客户端就不提了. 对于服务端,我想从之前的端游服务器改过来肯定是走不通的(详见:),因为手游的开发周期比端游短很多,上面那一套架构开发了5年不止,当然,期间推翻重做了很多次.而这个手游项目只有快则6个月,慢则10个月的时间. 既然决定不用端游的架构,一切从0开始,那么快速搭建起一个可用的服务器是我考虑的第一个原则,第二点就是我做为程序员的一点私心,尽量使用以前没有用过的技术,这样自己能够接触更广阔的世界. 首先当

游戏服务器架构概要

声明:本文内容源自腾讯游戏学院程序公开课_服务器第二节 一.服务器架构概念解析 1,什么是“服务器架构” 对服务器软件&硬件&运行的一体化规划 框架结构:分层分块. 构建技术选择:编程语言:通信方式:存储技术. 运行质量:运行环境:部署工具方法:更新方案. 二.案例讲解:分布式服务架构设计演讲——MMORPG(大型多人在线角色扮演)<轩辕传奇> 服务器架构_分区多世界 1,运营视角 世界与世界是隔离的 世界之间的互通方式:跨服.转服.合服 2,运维视角 SET部署:每开一组服就

游戏服务器架构的思考

时间总是在不经意的时候就流走了,突然回想我已经做了四年游戏开发,经历了几个游戏项目,以前项目中的游戏服务器框架都不是我心中理想的框架,虽然不知道是不是我见识还不够.下面记录下我对游戏服务器架构的简单思考.好的游戏框架可以提高开发效率,节省人力成本.首先最简单的服务器框架,那就是只要一个网关和一个游戏服务器.如图: 图中agentserver负责客户端连接,客户端收发数据,将客户端数据转发给服务器,将服务器数据转发给客户端,几乎没有逻辑,这样可以应对大并发io.所以agent可以采用一个epoll

【前言】为什么要设计游戏服务器框架

设计游戏服务器框架: 项目设定周期:7月1日 - 12月31日 项目语言:PHP.Golang 项目成果: 1.PHP版游戏服务器框架 2.Golang版游戏服务器框架 设计目的: 1.挑战自己的毅力,遇到困难,勇敢面对解决 2.学习未涉及的领域和技术

棋牌游戏服务器架构设计

转载自:简书一位同行的文章 一,棋牌类服务器的特点 1,棋牌类不分区不分服 一般来说,棋牌游戏都是不分区不分服的.所以棋牌类服务器要满足随着用户量的增加而扩展的需要. 2,房间模式 即在同一局游戏中就是在同一个房间中,同一个房间中的人可以接收到其他人的消息. 3,每个房间的操作必须是顺序性 这个特性类似与一般游戏的回合制,每个玩家的操作都是有顺序性的. 二,需要解决的技术点 1,数据共享 因为棋牌类游戏不分区不分服,我们在设计服务器的时候,是按世界服的思想去设计,即服务器是一个n多台物理机的集群

简论游戏服务器架构设计

一.QIPAI类服务器的特点 1,QIPAI类不分区不分服 一般来说,QIPAI游戏都是不分区不分服的.所以QIPAI类服务器要满足随着用户量的增加而扩展的需要. 2,房间模式 即在同一局游戏中就是在同一个房间中,同一个房间中的人可以接收到其他人的消息. 3,每个房间的操作必须是顺序性 这个特性类似与一般游戏的回合制,每个玩家的操作都是有顺序性的. 二,需要解决的技术点 1,数据共享 因为QIPAI类游戏不分区不分服,我们在设计服务器的时候,是按世界服的思想去设计,即服务器是一个n多台物理机的集

H5十人牛牛架设游戏服务器架构: 内核设计

H5十人牛牛架设游戏服务器查看(aqiulian.com),内核设计分析Q_212303635:接下来讲解一下内核模块分析.内核的几个组件被设计成Service,也就是说这几个模块都要实现如下接口: 图1  IService接口 Start方法用来启动服务. Stop 方法用来关闭服务. IsService 方法用于查询当前服务是否正在工作. 内核中的几个Service都不能够直接创建,Applications在使用这些Service的时候首先要得到一个IServiceMgr的实例,这被实现成了