Post

游戏服务器框架源码学习

游戏服务器框架源码学习

服务器架构

GS流程图

定义

FBServer_Base

是大部分服务器的基类,代码复用,创建了一些基本的功能。

链接的操作

见下文IOCP的理解

win32下使用的IOCP,而linux下使用的EPOLL。两者分别都包装好了函数供调用。

  • Create() 创建。基本socket创建初始化、epoll创建初始化、端口重用、linger等待发送完毕、不使用Nagle算法,不会将小包拼接成大包,直接将小包发送出去。

  • 创建线程来开始epoll的阻塞等操作,linux下线程创建后会立即运行。线程中有epoll_wate的调用,每次获取信息判断为:

    • 新监听类型:初始化新的sockaddr_in等,进行accept。创建一个新的pConn客户端类,add到服务器的pEpoll队列中。

    • 读类型:调用读函数,写入缓冲区。

      • 实现了一个缓冲区类,进行对数据的存储。在其上进行读写操作。

        • 读之前会声明一个char*,并同时初始化可以存储的缓冲区大小。判断有空间存储,再开始读取。

        • 指针的位置来自于缓冲区类得到的空闲空间的首地址。

      • 如果读取的大小大于零。需要更新缓冲区类的属性。

      • 如果小于等于零。需要判断是否有错误信息。

    • 写类型:调用写函数,从缓冲区中读数据发送出去,和读类中缓冲区操作类似。

    • 错误类型:错误打印。退出客户端

  • Release() 判断是否已经退出。等待线程退出,关闭各种套接字、epoll。

FBServer_Base层,再将这里生成的监听类加入到map中。因为一个服务器会有多个监听端口打开。

Process

循环处理多个事件。处理事件记得事件监听。大部分事件的处理都交给了lua处理。

这里还没有搞清楚c++是怎么使用lua函数的。在OnServerEvent()这里断开了。

  • FPS事件:

  • 监听事件:包括新连接、读、写请求,包装在epoll当中

  • 客户端事件:遍历所有客户端节点,读取节点的msg,让lua进行处理。

  • 服务器事件:和客户端事件类似,遍历节点,让lua处理msg。

  • 其他服务器事件:和客户端事件类似,遍历节点,让lua处理msg。

  • 处理PlatFormMsg:(还没搞懂,难道是心跳机制?)

  • GS本身逻辑:lua处理

  • 处理定时器事件:整点报时、系统定时器处理

FBServer_GS

游戏服务器的实体类,继承于base,实现功能基础功能,创建lua接口。

###

额外知识点

IOCP

I/O完成端口:高性能的I/O模型,使用线程池处理异步I/O请求的机制。

  • 用于高效处理很多很多的客户端进行数据交换的一个模型。

  • 异步I/O操作的模型

socket

so_linger字段

iOptValue.l_onoff

iOptValue.l_linger

setsockopt(mysock, SOL_SOCKET, SO_LINGER, (char*)&iOptValue, iOptLen)

1、设置 l_onoff为0,则该选项关闭,l_linger的值被忽略,等于内核缺省情况,close调用会立即返回给调用者,如果可能将会传输任何未发送的数据;

2、设置 l_onoff为非0,l_linger为0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态;

3、设置 l_onoff 为非0,l_linger为非0,当套接口关闭时内核将拖延一段时间(由l_linger决定)。如果套接口缓冲区中仍残留数据,进程将处于睡眠状态,直 到(a)所有数据发送完且被对方确认,之后进行正常的终止序列(描述字访问计数为0)或(b)延迟时间到。此种情况下,应用程序检查close的返回值是非常重要的,如果在数据发送完并被确认前时间到,close将返回EWOULDBLOCK错误且套接口发送缓冲区中的任何数据都丢失。close的成功返回仅告诉我们发送的数据(和FIN)已由对方TCP确认,它并不能告诉我们对方应用进程是否已读了数据。如果套接口设为非阻塞的,它将不等待close完成。

程序功能逻辑

程序常用关键词

d_ms

  • require 于 ms.lua 文件中。作为require公共头文件,提供require所有文件的功能。

    • 也就是万能头文件。在所有lua文件中可以直接提取某个模块的属性和功能。

调试

GameServer进程中使用adb().

break filename.type:line_num 设置断点

n = next

c = continue

l = list 显示运行位置前后几行代码

p _L.obj.str 打印某个变量。注意要细节到变量的详细属性。

q 是退出程序。(然而并不知道为什么这么设计

类定义

class(“class_name”, fatherName)

  • 定义一个类。类名;父类名称;没有父类则表示为基类。

module “father_module.module_name”

  • 将此文件声明为模块。这样能让其只运行于该module声明语句

  • local 语句相较于其module,local在外部调用为nil

self:regist_msg_function(key, self_function)

  • key 对于通信协议写为d_ms.d_msg_def.gscl_msg_id.msg_id

  • 注册一个key到方法的事件绑定。

  • 在class_name:__init()中声明。

  • 在下方class_namd:self_function()中定义。

  • 传入参数为 : 并非所有参数都要使用

    • unit : 请求操作的角色

    • msgid : 消息的id

    • msg : 消息的内容

    • pass_time :

  • 调用d_gm。

    • on_gm_command(gm_role, param):处理角色的gm命令,如修改隐身,修改属性。
  • 调用d_log。

    • warn(“消息处理%s” , unit.name) 于普通调用print类似。打印warn信息
  • 返回时,应该处理为d_ms.d_global_def.process_result.ok。表示处理完成。

unit 角色信息与函数

  • 基础属性:id, name, level, sex, class, thread(所在线), server(所在服), lastlogout, stop

  • 扩展属性:

    • m_vehicleid

    • double_mounts

    • locked_role_id

  • 基础方法

    • is_dead()

    • clear_patch()

    • send_to_near_by_user()

    • is_vending()

    • clear_sing_state()

    • on_stop_changed()

    • on_action()

人物移动lua实现逻辑

注释:协议消息标志:C2S为 150,S2C为168。 消息处理函数名:msg_path — on_msg_path目的:用户用户的移动请求。注释:长路径移动的时候,客户端发送一次位置,并且自身不断更新位置,接受服务器的确认消息。若有其他打断操作再终止并发送给服务器。服务器这边接受一次150消息后不断寻路判断当前是否能结束移动。若不能则递归调用继续处理。

坐标(x, y)菱形蜂窝状分布

基本流程

  • 判断是否在车上 unit.vehicleid

  • 判断是否骑乘双人坐骑 double_mounts

  • 判断是否被锁定 locked_role_id

  • 判断是否死亡 unit:is_dead()

  • 判断是否在售卖操作 unit:is_vending()

  • 判断role状态能否移动 unit.m_move_state && unit.m_forbid_move

  • 检查当前状态并改变:walk2stop or stop2walk

  • 判断是否需要录像

  • 运行处理路径进程

  • 返回process_result.ok

特殊函数

unit:dispatch() 清除路径

**process_path(my, unit, pass_time)**实际的处理路径函数

  • 玩家处于双骑与锁定,直接返回

  • 玩家状态停止,直接返回

  • 判断上次处理的时间间隔,避免请求次数过多

  • role_get_walk_resultA*寻路算法求得路径,底层C包装。返回

    • rel:(???c++和lua解释不一样啊)

      • 0为被阻挡

      • 1为到达终点,没有跨格子

      • 2为忽略的障碍

      • 3为移动成功

      • 4没有跨格子

      • 5为没有走路

      • 6为垮了格子并且结束了

    • unit.m_left_length:

    • now_grid:

  • 如果还是没有移动结束,则进行递归调用继续处理

unit:on_barrier(old_grid, now_grid, my.m_old_pos)

  • 玩家被阻挡,需要把它扯回来

check_cannot_leave_area(unit, old_grid, now_grid)

  • 获取是否能通过此区域ret状态, 地图map

  • 如果不能通过。则处理当前坐标、on_barrier()、clear_patch()、取消自动打怪、发送消息给客户端、发送提醒

创建摊位消息

注释:协议消息标志:C2S为 420,S2C为436。 消息处理函数名:msg_prepare_create_vend — on_msg_prepare_create_vend目的:用户想要摆摊操作。注释:

基本流程

  • 判断是否可以安全操作,包括本身人物状态安全,环境安全

  • 判断是否为公共服,特殊状态和日常线路禁止进行摆摊?

  • 判断是否是跨服,跨服不可摆摊?

  • 获取上架商品列表

  • 判断摊位创建条件

    • ~d_op_code.success 则send_vend_event(unit, d_op_type.create, op_code)表示当前处于摊位状态, 直接进行返回商品列表信息

    • 为false则表示要新创建摊位。

      • 发送摊位名称

      • 下坐骑、回收召唤兽、变身(把自己变成一个售卖机)

      • 向周围人更新玩家状态

      • 更新摆摊剩余时间

      • 开启金钱上限通知

      • 隐藏宝宝

      • 注册一个摆摊用完的定时器

特殊函数

d_ms.d_common_fun.PubliServerSendTip(unit)

  • 查询是当前人物是否在公共服务器

IsKfServer()

  • 判断是否为跨服

check_vend_create(unit, msg.vend_name)

  • 判断玩家是否可以创建摊位can_create_vend(unit)

  • 判断摊位名是否合法check_vend)name(vend_name)

  • 返回上列函数返回值 或 d_op_code.success

can_create_vend(unit)

  • 玩家死亡、保护状态、busy状态、移动状态、不在摆摊地区 都不可创建摊位

  • 获取玩家的虚拟坐标,用于判断摆摊位置是否拥挤

  • 判断摊主等级

  • 任务判断能否摆摊

  • 摆摊剩余时间

check_vend_name(vend_name)

  • 判断str_checker.check_name(vend_name)是否为登陆代码成功

  • 判断是否过短

  • 判断是否过长

状态自动机与行为树

前言

游戏开发中, 玩家操作的每次都可以理解为一个行为, 每次行为的结果会让玩家到某个状态。为了加快开发效率、让整个流程逻辑更清晰,出现了有限自动机。状态树的方法也能应用到这里。《linux高性能服务器》书中也介绍过有限自动机的概念。

我学习过后,准备在这里对一些内容进行一些总结。

有限自动机

DFA 确定的有限状态自动机。FSM有限状态自动机,对于一个流程,有开始状态与终止状态,每个操作、事件对应一个行为,行为过后会转换到另一个状态。

行为树

BT(Beahvior Tree) 是有行为节点组成的树状结构。节点是有层次的,子节点由其父节点进行控制。节点分为多种,每次会对子节点进行操作。如果满足某种条件则递归式的继续运行子节点的行为。执行返回有Success、Failure、Runing。

节点分类

  • 序列(Sequence)节点:按照序列的形式执行所有节点,如果某个节点失败则返回失败

  • 循环(Loop)节点:循环的执行所有子节点一定的次数,可以设置为无限循环

  • 条件(Condition)节点:如果满足某个条件则返回成功或者失败

  • 动作(Action)节点:根据动作的结果返回成功、失败和运行

  • 等待(Wait)节点:等待一定时间过去后返回成功

运行(Runing)状态

行为节点返回成功和失败都会通知父节点做相应的动作。

如果是返回的行状态,则父节点会继续运行这个动作,直到返回为成功,才继续运行之后的节点。

我们称之为(#`O′)伪阻塞。这是因为每次运行的时候都是跳出了子节点,再进入子节点的,只是返回的是运行的时候,每一帧的下一步操作都是进入这个节点。

假设现在有流程“走过去”,做“采矿”再做“返回”,一颗行为树就可以表达完毕,每件事情都是持续的,如果没有RUNNING,那其实就更像一颗“决策树”而非“行为树”。

解决伪阻塞

使用前置

每个节点都可以添加一个前置附件或者后置附件

添加前置附件,设置执行时机为Update或Both。name每次执行节点之前都会先执行配置里的条件。

使用parallel节点

该节点作用为“一遍检查条件、一遍执行动作”。该节点后的节点运行的时候回进行一次判断。

有限状态机和行为树

对于一系列事件,有限状态机更多表现出来的形式是一种有向图的形式,而行为树则是因为逻辑更像一个树形结构。

消息系统

总览

消息定义

消息定义+消息结构定义,能让底层传输的数据仅仅只有数据,而没有数据类型、名字、分类。两边通过消息id来区分该消息的结构,一边压缩,一边解析。极大的减少了传输的数据量。

  • const常量表

    定义一些在之后的消息定义中会使用到的常量。比如GS的最大数量、密码的长度、角色名的长度。

  • 角色事件

    定义玩家的一些操作事件,这里的操作包括了在服务器中流转的消息和在客户端内玩家操作需要服务器进一步处理的消息。

  • 角色使用物品事件

  • 确认取消框

    服务器通过传送这类消息来让客户端显示某种类型的按钮显示框,客户端接收到这类消息后会自动将用户的选择结果返回给服务器。

  • 服务器与服务器之间的公共消息

    这类的消息是最多的,一般会用是哪两种服务器之间的通信进行定义消息分类名。

  • 通用消息

    一些事件可能是临时加的、或者是非常特殊的消息,或者是所有服务器都能使用的公用消息。这类的消息分类不明显,可以放到通用消息结构内。但是这类消息应注意不要和其他类型的消息值冲突在判定消息时可能会引起错误。

  • 各类消息结构定义

    消息结构定义了某种消息的传递结构,比如“id=i,count=i”。其中等号左侧为类型名,用于将消息进行压缩和解压时候的分类,右侧为值的类型。比如(u)short、(u)int、(u)char、string以\0结尾

底层消息结构

  • 加密层

    只对客户端到BCS之间的数据进行加密。这里不讨论外网到服务器之间的传输,suchAs 支付宝接口

  • 压缩层

    对一定长度的消息进行压缩处理。lz4压缩算法。更快更高效的LZ77压缩算法。简而言之,是字典查重复字符串,记录位置长度,用了滑动窗口。lz4优化了lz77的一些地方。

  • 消息实体层

    • msgid值

    • msglen值

    • connid连接值

    • msg实体。只有值,msgid定义了msg的结构。

服务器与服务器(客户端类似)之间

sendMsg2Server(msgConnectid, msgid, msg, common_client_index)

  • 类型判定是否存在

  • 计算大小,申请char空间、消息id、消息长度、连接id加载到buffer

  • 转换消息为c++字节流

    • 判断m_basetype类型

      • buffer用于临时计算使用,最终发送的长度已经在转换前计算好了
  • connectid_id存内存指针值。通过指针获取相应服务器对象

This post is licensed under CC BY 4.0 by the author.