网狐核心源码阅读分析

框架结构

网狐服务器整体上分为4个部分:中转服务器,房间服务器,大厅服务器,sqlserver数据库。其中大厅服务器主要负责帐号管理器:管理用户选择服务器登录地址,校验用户数据等。必需与中转服务器保持长连接,用于更新获取最新数据。
房间服务器:用于加载处理每款子游戏逻辑与公共游戏逻辑(例如机器人整体随机进出任何游戏房间,机器人游戏信息处理等。他也必需与中转服务器保持长连接,用于传输最新游戏房间信息给中转服务器。
中转服务器:收集所有房间服务和大厅服务器的ip、端口、在线游戏人数等信息,并负责中转全局的游戏消息,所有房间服务器大厅服务器都在线连接中转服务器,定时更新状态。中转服务器通过连接平台库,获取平台的信息。
同时这三个服务器又分别和数据库进行连接,可以独立进行操作对应的数据库。这三个服务器都是依赖于爱玩内核而设计,可以看做是基于爱玩内核对不同业务需求的不同实现。

框架结构图

大厅服务器

启动流程:

  1. 点击启动服务按钮调用OnBnClickedStartService()函数,调用StartService()函数进行初始化

  2. 配置信息:在StartService()函数里先加载配置信息,配置信息包括初始化用户数据库,财富数据库,站点页面,进程当前启动目录,相关配置文件路径名称,获取本地ip,中转服务器信息等,然后配置数据库信息,设置最大连接数为2048,监听端口,udp监听端口。

  3. 创建必要组件:m_TimerEngine定时器引擎(用于做脉冲心跳定时获取与中心服务器的最新列表信息)
    m_DataBaseEngine数据库引擎(处理业务层投递信息,处理写入数据库)
    m_AttemperEngine调度引擎(用于处理业务)

m_TCPNetworkEngine网络引擎服务
m_TCPSocketTransfer 内部网络服务

  1. 创建数据库引擎和调度引擎的回调钩子函数,并且和数据库引擎调度引擎的钩子函数成员变量绑定。这样绑定是为了引擎能调用钩子函数。
  2. 把调度引擎依次和时间引擎,内部网络服务,网络引擎内部的事件接口成员变量绑定,这样绑定是为了其他引擎处理完自己的任务后能把任务投递到调度引擎。
  3. 把网络引擎和调度引擎内部的网络接口成员变量绑定,这样做唯一目的是当调度引擎处理网络引擎投递过来的任务失败时,能关闭对应的socket句柄。
  4. 把调度引擎回调钩子函数的配置参数,事件引擎,数据库引擎,网络引擎,内部网络服务依次初始化为上面创建的组件。这样绑定后重写的调度回调钩子函数就能调用内核引擎来处理任务。
  5. 始化数据库回调钩子函数的配置参数,事件接口。这样数据库引擎操作完数据库后,可以把结果投递给调度引擎。
  6. 络引擎的最大连接数,监听端口,设置好工作目录。读取内核设置,检查并创建内核日志、大厅日志目录,然后依次启动时间引擎,内部网络服务,数据库引擎,调度引擎,网络引擎,UDP引擎。
    大厅服务器主要功能:

主要功能包括:

  1. 处理玩家注册,登录大厅过程中获取一系列包括,用户资料、头像、银行信息、账号密码,任务奖励,系统赠送,用户成就和是否代理,是否绑定推广,等辅助信息系统赠送等等所有用户在我们游戏中的个人信息
  2. 定时检查与中转服务器的长连接信息,每隔30s向中转服务器发送一次SUB_CS_GET_SERVER_LIST消息用于更新列表信息,包括游戏类型信息,游戏种类信息,房间信息,如果开启网关,还有网关信息。

    定期更新的这些游戏服务器信息,会在玩家登陆成功后发送给客户端

3.处理创建桌子流程
4.处理创建俱乐部,查找俱乐部,删除俱乐部,复制俱乐部,修改俱乐部,升级俱乐部,添加俱乐部成员等等与俱乐部相关功能
5.处理玩家玩家修改个人资料,银行信息,实名认证,手机绑定相关功能
6.处理玩家游戏任务功能
7.处理用户成就功能
8.游戏商城功能

创建房间流程

玩家点击创建房间或者加入房间按钮,客户端给大厅服务器发送MDM_GP_USER/SUB_GP_TABLE_QUERY_ONLINE也就是用户信息/查询在线信息,服务执行GSP_GP_QueryOnline存储过程查询用户情况,查询结果返回给客户端,


客户端根据查询结果,从而让客户端判断玩家是走创建房间,加入房间还是走断线重连流程。
如果是创建房间流程,会弹出创建房间界面,玩家选择好要开房的游戏后,发送MDM_GP_USER/SUB_GP_TABLE_CREATE用户信息/创建桌子信息给大厅服务器,大厅服务器发送MDM_CS_PRIVATE_TABLE_MANAGE/SUB_CS_TABLE_CREATE私人房桌子/创建桌子消息给中转服务器
如果房间创建成功,会收到来自中转服务器的MDM_CS_PRIVATE_TABLE_MANAGE/
/SUB_CS_TABLE_CREATE_SUC私人房桌子/创建成功消息。

然后大厅把这个消息发送给客户端,客户端坐下后会收到来自中转服务器的私人房桌子/刷新桌子信息消息,同时大厅也会把这条消息批量转发给客户端
综合以上流程可以看出:在中转服务器和游戏服务器没启动时,只要大厅服务器启动,玩家就可以登陆到大厅,只是玩家点击创建房间时,由于没有中转服务器,所以玩家在发送创建房间信息时,消息不能通过中转服务器转发给游戏服务器,所以也就创建失败,同时如果启动中转服务器,但不启动游戏服务器,当中转服务器收到大厅服务器的建房消息后,会创建失败,然后返回创建失败消息给大厅服务器,大厅服务器也会返回给客户端创建失败结果。

登陆大厅流程

根据客户端不同登陆请求,在OnSocketMainLogon()函数里分别有账号登陆,微信登陆,注册账号,账号重连,微信重连这些不同处理流程


只看微信登陆这条流程
投递DBR_LOGON_WX数据库请求后执行GSP_GP_LogonWX存储过程进行参数校验,校验成功后会获取DBR_LogonSuccess这个结构体所有用户相关信息,接着投递DBR_LOGON_SUCCESS这个请求,紧接着会依投递数据库请求获得辅助信息,签到信息,首充信息,密码信息,公告信息,会员信息,登录成功后会发送CMD_GP_LogonSuccess这个结构体所有信息,和房间列表,广播消息等给客户端

到此玩家登录大厅成功

中转服务器

启动流程


从CCenterService::StartService()这个函数启动,初始化网络设置,配置好PlatformDB平台数据库相关参数,创建数据库引擎,调度引擎,网络引擎,定时器引擎,创建好组件接口,并把这些组件和内核绑定,设置好数据库引擎和调度引擎的回调钩子,

配置网络引擎参数,包括设置好以服务端口为文件名的内核日志文件,设置好内核设置配置文件,建立内核日志目录。
然后依次启动时间引擎,网络引擎,数据库引擎,调度引擎。

启动网络引擎:会对完成端口进行一些初始化,并且启动异步引擎服务,配置好读写线程,应答线程,检测线程,并且启动这些线程,将服务设置为启动状态。
启动数据库引擎:线程启动函数CServiceThread::ThreadFunction执行,调用OnEventThreadStrat(),OnEventThreadStrat()又调用OnAsynchronismEngineStart()启动异步引擎,在异步引擎启动时会调用异步引擎钩子函数的启动函数OnDataBaseEngineStart()。
启动调度引擎:


创建列表组件,并且设置好数据库信息,俱乐部数据库信息,加载列表,读取私人房、俱乐部配置,加载限制字符,

中转服务器主要功能

1.


每隔60s删除一次私人房,每隔180s更新一次私人房间配置,每隔5s检查一次俱乐部自动开房
2.收到大厅服务器更新列表请求时,向大厅服务器返回种类列表,房间列表,网关列表,等信息
3.处理来自大厅的创建桌子,查找桌子,删除桌子,处理来自房间服务的玩家坐下,游戏开始,玩家起立,游戏结束,游戏扣钻等消息
4.处理来自web服务的写分,充值,兑换,web转账,活动奖励,兑换房卡等消息
5.处理来自大厅的俱乐部管理相关的请求
6.房间服务启动时,处理同步私人房信息,注册房间,同步人数等请求,房间服务关闭时,处理注销房间请求

创建房间到房间解散关闭流程

玩家点击创建房间,中转服务器会收到来自大厅的MDM_CS_PRIVATE_TABLE_MANAGE/SUB_CS_TABLE_CREATE私人房桌子/创建桌子请求,


在对建房参数做一系列检查后,会给大厅发送SUB_CS_TABLE_CREATE_SUC房间创建成功消息,
接着中转服务器会收到来自游戏服务的SUB_CS_USER_SIT_DOWN玩家坐下消息。

其他玩家输入房号加入房间后会收到来自大厅服务的SUB_CS_TABLE_FIND查找房号消息,中转服务器会做参数检查,判断是否已在私人房,桌子是否存在,是否已经开始游戏,是否人数已满,房卡是否足够等条件,如果所有检查通过,会发送SUB_CS_TABLE_FIND_SUC查找房间成功消息,接着也会收到来自房间服务的玩家坐下消息
房间解散成功后,会收到来自房间服务的所所有玩家的SUB_CS_USER_STAND_UP玩家起立消息,和SUB_CS_TABLE_ENDGAME游戏结束消息。

房间服务器

初步认识


通过查看整个服务器工程,发现房间服务源码文件是最多的,通过这些源码文件命名,和预览源码,可以看到房间服务器总体上可以分为:机器人包括机器人管理模块,调度模块,因为调度模块功能较多,所以分为通用,AI,银行,中转服务,网关,比赛,移动用户,道具金币,任务,定时器这几个文件来写,还有数据库模块,启动模块,房间服务接口定义模块,局数任务模块,还有针对不同比赛类型的功能模块,回放模块,性能测试模块,异步引擎,还有游戏桌子框架模块,发送post,request请求的web模块。

同时,房间服务还包括房间加载模块

启动流程

  1. 点击启动房间按钮后,OnBnClickedStart函数会执行,初始化配置参数,可以看到房间服务会和AccountsDB,PlatformDB,TGGameScoreDB这3个数据库有联系,
    进入到CGameService::StartService()函数,首先启动日志服务,接着分别创建时间引擎,调度引擎,网络引擎,中转服务引擎,同步引擎,数据库引擎,然后调整参数,给配置变量赋值,然后加载游戏服务模块组件,进入到InitializeService函数.
  2. 配置组件:在配置组件时可以看到有大小为5的CDataBaseSinkPrimary和大小为3的CDataBaseSinkAssistant2个数据库引擎钩子数组.其中CDataBaseSinkPrimary主要用来处理和玩家相关的数据库操作,比如写分,银行等.因为这些操作需要保证顺序性,所以必须放在同一个处理数组里面.而CDataBaseSinkAssistant主要用来处理不需要顺序要求的系统请求消息.
  3. 绑定组件,绑定组件的时候,可以看到处理系统请求的数据库操作引擎只有1个,但是设置了3个数据库引擎钩子,而处理玩家相关操作的数据库引擎有5个,同时每个引擎都设置一个数据库引擎钩子.
  4. 然后就是把处理玩家请求的数据库引擎和引擎钩子和同步引擎绑定.
  5. 然后就是分别初始化CDataBaseSinkPrimary和CDataBaseSinkAssistant数组,设置他们需要操作的数据库以及一些其他绑定,注意到CDataBaseSinkPrimary绑定了同步引擎而
  6. 然后依次启动时间引擎,网络引擎,同步引擎,内核数据库引擎,记录数据库引擎,调度引擎和网络引擎。
    内核数据库引擎启动:内核数据库引擎有5个同样的数据库引擎钩子,所以会启动5个同样的线程,在钩子函数的启动函数CDataBaseSinkPrimary::OnDataBaseEngineStart()里会连接用户数据库,和进步数据库.

    记录数据库引擎启动:记录数据库引擎有3个同样的数据库引擎钩子,会启动3个同样的线程,在钩子含税启动函数里会连接用户数据库和金币数据库.

    内核数据库引擎和记录数据库引擎启动不同点:
    虽然都是连接了用户数据库和金币数据库,但是内核数据库是一共有5个数据库引擎,每个数据库引擎一个钩子函数,一个异步线程来处理任务,也就有是说这5个数据库引擎等待在5个不同的完成端口上,而记录数据库引擎是一个数据库引擎,但是有3个异步线程,这3个异步线程等待在同一个完成端口上.
    调度引擎启动:

    初始化变量,设置聊天,判断是银行支付还是,金币支付,如果是比赛,会创建比赛变量,创建玩家连接信息变量,可以看到最大520个玩家用户,256个机器人用户,调用子游戏服务中的RectifyServiceOption函数,在子游戏逻辑代码里对房间参数进行修改,

    然后创建并初始化所有游戏桌子,初始化机器人管理类,然后根据房间类型参数来对列表项描述结构进行不同初始化,然后连接中转服务器,

    然后依次设置了限制消息,系统消息,心跳检测,加载配置,加载游戏任务等定时器
    读取AI配置,加载机器人,加载任务,设置加载机器人定时器,加载银行,桌子框架加载配置。

房间服务主要功能

  1. 处理中转服务器创建完房间后来自客户端的坐下,旁观请求。
  2. 处理房间解散或游戏结束后删除房间,玩家离开房间等流程
  3. 对所有桌子的管理,分发不同桌子的游戏消息到对应桌子玩家。广播桌子消息到同桌子其他玩家。
  4. 还有处理一些客户端和游戏服务器之间的奖励,活动,等交互
  5. 玩家游戏结束后,处理对玩家写分,金币等数据库操作。
  6. 比赛功能
  7. 定时更新整个房间服务的服务器信息,网关信息,私人房信息到中转服务器。
  8. 对机器人的管理
  9. 其他功能

内核引擎分析

异步引擎

异步引擎工作流程实例

可以说整个内核能高效的进行网络传输和数据库操作,都是依赖于异步引擎,查看异步引擎头文件,有异步引擎类CAsynchronismEngine和异步引擎线程类CAsynchronismThread,异步引擎线程类继承自服务线程CServiceThread,在整个内核中有数据库引擎,调度引擎,网络引擎,这三个用到了异步引擎,通过大厅服务启动过程中网络引擎的启动,来说明异步引擎的作用。


首先通过m_TCPNetworkEngine->StartService()这个函数进入到网络引擎启动,先是创建了一个完成端口,允许机器核心个数到线程来运行,然后就是socket编程的绑定,监听操作,

接着通过SetAsynchronismSink函数创建指定个数的异步线程,并且通过SetAsynchronismEngineSink函数把异步线程的回调函数和启动异步引擎的引擎服务的钩子函数绑定或者引擎服务自身绑定。

网络引擎是创建了1个异步线程,并且绑定了这个异步线程的m_pIAsynchronismEngineSink回调接口为网络引擎自身。
然后执行CAsynchronismEngine::StartService()启动异步引擎,

创建完成端口,并且之前创建了多少个异步线程,这个完成端口就允许几个线程运行,
并且把每个异步线程都和创建的完成端口绑定,然后就启动StartThread函数每个线程


通过tagThreadParameter结构体,把主线程指针,也就是该线程自己,传递到子线程到入口函数ThreadFunction中,同时通过事件变量让主线程挂起,
进入到子线程入口函数ThreadFunction中,又通过tagThreadParameter结构体,拿到传入到参数,进入到CAsynchronismThread::OnEventThreadStrat()函数,

因为此时m_pIAsynchronismEngineSink绑定到了网络引擎,所以实际上调用到时网络引擎的OnAsynchronismEngineStart函数。设置事件变量,好让主线程重新执行。

接着进入到线程执行函数,

到这里,异步线程就在循环里不断检查完成端口状态是否有网络操作到达。当有事件完成时,会利用临界区来对线程进行同步,防止多个线程操作队列数据。

异步引擎总结

通过以上分析可以看到使用异步引擎有几个重要步骤:
  1. 先执行SetAsynchronismSink函数,将异步引擎要服务的模块和异步引擎绑定,在这个函了数中,创建了模块指定的个数的异步线程,也可以看做是工作线程,同时把每个线程都和模块绑定,也就是初始化异步引擎的的m_AsynchronismThreadArray线程对象数组,
  2. 执行StartService启动异步引擎,先是创建一个完成端口,并且把它和每个异步线程绑定,然后调用StartThread启动每个异步异步线程。
  3. 在线程启动函数里,创建线程参数,tagThreadParameter结构体,通过这个结构体传递了本线程自己的线程指针,用来标记线程是否启动成功的标记,以及一个用来保证把cpu时间片执行权限让给子线程的事件句柄。
  4. 进入到ThreadFunction线程函数,先是调用重写了CServiceThread类的OnEventThreadStrat函数,在异步线程的OnEventThreadStrat函数中,会调用第一步绑定好了的异步线程的回调钩子函数m_pIAsynchronismEngineSink,进入到对应的被绑定的服务中的OnAsynchronismEngineStart函数。做一些服务模块自定义的启动初始工作。
  5. 接着调用setEvent函数设置主线程创建的事件信号,这样主线程可以继续往下执行。然后在子线程里用一个除非执行返回为false才跳出的while循环来循环执行线程的运行函数RepetitionRun,在异步线程的运行函数里,是等待在完成端口的GetQueuedCompletionStatus函数上,这个函数会让异步线程进入到不占用cpu的睡眠状态,直到完成端口上出现需要处理的网络操作或者超出设定的等待时间限制为止。
  6. 可以看到异步引擎使用的是使用指定个数的工作线程来为其他模块服务,其他模块可以把工作交给异步线程,在异步线程里等待在在完成端口上,这样就不会让主线程阻塞。可以把异步引擎看做是具体对专为使用完成端口而设计的线程功能类

网络引擎

启动实例

1.通过大厅服务器启动过程来看网络引擎的启动,通过函数CTCPNetworkEngine::StartService()启动网络引擎,


可以看到先是创建了一个允许机器cpu核心个数线程调度的完成端口,这个就是网络引擎的主完成端口。然后就是调用和完成端口配套的函数,创建绑定,设置监听socket。

2.在异步引擎里启动网络引擎,并且只设置了一个工作线程。通过上面对异步引擎分析,异步引擎自己会启动一个完成端口,最终网络引擎在异步引擎里运行,等待挂起在完成端口完成通知函数上。

3.创建核心个数读写线程保存到网络引擎的读写线程数组m_SocketRWThreadArray中,接着初始化应答线程,把应答线程与上面创建的主完成端口,监听socket,以及网络引擎指针绑定。
4. 启动每个读写线程,最终也是等待挂起在主完成端口的GetQueuedCompletionStatus函数上,读写线程重写了父类线程RepetitionRun运行函数,在运行函数里有完成通知到来时先是对重叠IO和单句柄数据也就是连接子项类进行一些检查。

5.当完成通知到来时,用CONTAINING_RECORD宏获取重叠io对应的数据包,然后根据数据包的类型进行处理。
6.数据发送:进入到CTCPNetworkItem::OnSendCompleted函数,因为是完成端口,所以看这个函数前,先看一下CTCPNetworkItem::SendData发送数据函数,

其中有一个GetSendOverLapped函数用来获取发送重叠IO结构,

可以看到优先使用m_OverLappedSendActive重叠结构发送数据,不够用或者剩余可用长度不够时就用m_OverLappedSendBuffer重叠结构,最后才使用new来创建对象。这样可以保证服务器运行过程中基本不调用new开辟内存,避免产生内存碎片。可以看到一次最大发送2046个字节数据。接着进行加密,封包,然后使用WSASend函数,投递异步发送请求。
接下来再看CTCPNetworkItem::OnSendCompleted函数,先是释放掉上一次发送用的重叠IO结构,并且保存到m_OverLappedSendBuffer缓冲重叠结构中,如果还有需要发送的数据,就再次投递异步发送请求。
数据接收:进入CTCPNetworkItem::OnRecvCompleted函数,同样看接收数据要先看是如何投递连接请求的,我们通过看

接收重叠IO的数据结构定义,发现并未提供接收数据的内存,构造函数里WSABUF接收缓冲区指针也是直接指向NULL,第一个接收数据请求是在应答线程里投递的,后面会提到。
正因为如此,所以在接收数据处理时,是直接调用阻塞的recv函数来从读取数据,接收完后就是对数据进行检查,做粘包拆包处理,然后再进行校验,最后合法的完整数进入到网络引擎的CTCPNetworkEngine::OnEventSocketRead函数,这个函数再调用事件接口的OnEventTCPNetworkRead函数,让调度引擎进行处理。
7. 接着启动检测线程,每隔10s对所有和网络引擎连接的socket进行心跳,合法检测。
8. 接着启动应答线程,在应答线程的线程运行函数里,调用的是WSAAccept函数,这个阻塞函数也会让应答线程挂起。当有连接请求到来时,先判断最大连接数,然后创建新的连接子项,并与之绑定好新连接的ip、socket,然后把他和主完成端口绑定。接着投递一个异步接受数据请求
9.注意用WSARecv,WSASend投递异步收发数据请求时,WSA_IO_PENDING表示数据暂且还没收或者发送完毕,需要等待后续通知,所以也不能执行关闭socket操作。

网络引擎总结

通过以上分析,发现网络引擎读写线程和应答线程绑定的是同一个完成端口,采取的是单线程处理socket接入,多线程处理数据收发,检测线程是另外一个线程。发送数据是异步,接收数据虽然收到通知使用了完成端口,但是投递的接收数据请求使用0缓冲区,然后等待有完成通知时,再真正接收数据,搜索网上资料,说这种设计是因为投递请求后,会锁定缓冲区内存,即使你 WSARecv 的缓存大小是 1 字节, 被锁定的内存也将会是 4k. 非分页内存池是由整个系统共用的, 如果用完的话最坏的情况就是系统崩溃。使用0内存,所以就不会被锁定,但是这样读数据效率肯定会降低。
另外虽然单独启动了一个线程用来处理接入请求,但是效率应该不会比投递异步的acceptEX请求去接入效率高。

调度引擎

启动实例

还是以大厅服务器的调度引擎启动过程说明:调度引擎启动很简单,使用一个工作线程的异步引擎绑定自己,然后启动异步引擎。
异步引擎启动后,在调度引擎的异步引擎启动函数CAttemperEngine::OnAsynchronismEngineStart()中,调用调度钩子函数启动函数,在调度钩子启动函数中,先把网络引擎和房间列表绑定,然后用内部网络服务连接中转服务。
当有完成通知到来时,会进入到CAttemperEngine::OnAsynchronismEngineData函数,


可以看到在这里处理了来自内核其他引擎服务的所有消息。

调度引擎总结

  1. 调度引擎是处理整个内核所有消息的调度中心,尽管网络引擎,数据库引擎,时间引擎,TcpSocket服务都至少各自占用一个线程,他们都独立工作,但是最终处理这些消息的还是调度引擎一个线程。
  2. 调度引擎能处理来自其他引擎的消息,是因为在初始化时,其他需要投递消息给调度引擎处理的线程都绑定了调度引擎的事件接口,当其他引擎自己线程处理完自己的工作后,通过事件接口回调调度引擎的异步线程的CAsynchronismEngine::PostAsynchronismData函数,通过完成端口的PostQueuedCompletionStatus函数主动投递完成通知。
  3. 总的来看其他引擎相当于生产者,调度引擎相当于消费者,期间使用了数据队列来交换数据,保证消息的先进先出,使用临界区锁来进行线程同步保证队列的消息准确的被某一个线程处理。

    观察到异步引擎在从队列拿消息时,只对拿的过程进行保护,并不是处理完一条消息后加锁,可以想到在写业务逻辑时,是需要考虑到函数能否重入的。假如客户端以很快的速度发送多条同样的消息,这时候服务器可能会出问题。

TcpSocketService内部网络服务

启动流程

通过大厅服启动来说明:

内部网络服务内部有一个重写的CTCPSocketServiceThread网络线程类,内部网络服务启动时,直接启动了网络线程。
在线程启动函数CTCPSocketServiceThread::OnEventThreadStrat()里,创建了一个窗口句柄,窗口不显示。
然后再线程运行函数里,会调用阻塞的GetMessage函数让线程挂起直到有消息到来。


同时在调度引擎的异步引擎启动函数CAttemperEngineSink::OnAttemperEngineStart里,会使用CTCPSocketService::Connect函数,给内部网络服务网络线程绑定的窗口句柄投递一个连接请求。

此时会在网络线程的线程运行函数里处理消息。调用CTCPSocketServiceThread::PerformConnect执行连接请求。
在执行函数CTCPSocketServiceThread::PerformConnect里
Uploading file...

建立socket,并调用WSAAsyncSelect函数,把socket设置为非阻塞模式,并且绑定socket和窗口句柄,以及注册感兴趣的事件类型。这样当socket上有感兴趣事件到来时,WM_SOCKET_NOTIFY消息就会发送给m_hWnd窗口句柄,在线程运行函数里处理。

内部网络服务总结

1.使用内部网络服务发送数据:通过调用CTCPSocketService::SendData函数,这个函数有2个版本一个发送不带数据消息,一个发送带数据消息。最终都是在网络线程中调用PostMessage函数异步地把消息加入到绑定的窗口句柄的消息队列里。从而在线程运行函数里可以取出队列中的消息。接着在CTCPSocketServiceThread::OnServiceRequest函数里进行发送消息处理。最终调用到CTCPSocketServiceThread::SendBuffer函数,调用的是send函数进行消息发送。
2.总的来说内部网络服务会处理两种大类型的消息,一种是主动用网络线程调用PostMessage函数投递的WM_SERVICE_REQUEST服务请求,另外一种是绑定在窗口句柄上的socket收到的WM_SOCKET_NOTIFY网络消息。
服务请求:
1.连接请求:连接请求会使用WSAAsyncSelect异步选择模型,把socket绑定到窗口句柄上。
并且尝试connect一个地址,如果连接失败,网络线程会读到WM_SOCKET_NOTIFY/FD_CONNECT网络消息/网络连接消息。把连接的ErrorCode发送给调度引擎。调度引擎判断是连接错误,就设置一个定时器,进行下一次连接尝试。
2.发送请求:用于内部网络服务发送数据。当调用send函数发送数据返回WSAEWOULDBLOCK错误后,表示socket缓冲区已满不可写时,会调用AmortizeBuffer函数把数据缓存,这个时候,网络线程会收到WM_SOCKET_NOTIFY/FD_READ网络消息/数据读取消息,在CTCPSocketServiceThread::OnSocketNotifyWrite函数中再次发送数据。
网络消息:
FD_READ:
1:当调用WSAAsyncSelect函数时,如果当前有数据可读。
2:当数据到达并且没有发送FD_READ网络事件时。
3:调用recv()或这recvfrom,如果仍有数据可读里。
FD_WRITE:
1:调用WSAAsyncSelect函数时,如果能够发送数据时。
2:connect或者accept函数后,连接已经建立时。
3:调用send或者sendto函数,返回WSAWOULDBLOCK错误后,再次调用send()或者sendto函数可能成功时。因为此时可能是套接字还处于不可写状态,多次调用直到调用成功为止。
FD_CONNECT:当connect成功或者失败时都会收到这个消息,都会把错误码投递给调度引擎,如果成功调度引擎会立即发送一次列表消息请求,设置定时获取列表消息定时器。
3. 内部网络服务是专为服务器之间内部发送接收消息而设计的。异步选择模型虽然接收读写通知是异步的,但是读写数据其实还是同步的,所以性能应该比不上完成端口。

框架源码读后感

优点:

  1. 采用组件模块化,从而高度复用,可扩展性强。绝大多数功能可通过读取配置或者组件配置。
  2. 整个架构利用面向对象多态性,调用方保存被调用方基础接口指针,调用方直接调用接口指针内声明的纯虚方法,而此纯虚函数的具体逻辑由该接口的派生类实现。从而很好的遵循了开闭原则和依赖倒置原则等设计原则。
  3. 代码注释清晰,变量,函数名,类名命名使用规范,代码可读性强。
  4. 整个服务器上使用多个进程,每个进程又采用多线程,底层采用完成端口高性能通讯模型,充分发挥cpu性能,从而保证了整个服务器的高效。
  5. 使用c++,有c++编程强类型检查,高执行效率,vs编辑器强大调试功能的优点。

可能存在的缺点

  1. 大量的同步调用直接操作数据库,会影响性能,可以采用使用redis。
  2. 虽然使用了完成端口,但是可能一些使用方法,并不是完成端口推荐使用的最佳方案。但这个可能和业务需求有关,需要考虑到具体使用场景。
  3. 大量使用存储过程,使得对数据库的操作和存储不能分离,开发时也不方便。较为方便的设计是数据库只负责存储数据,业务逻辑把要存储的数据处理好后用专门数据持久化服务来处理。
  4. 虽然可配置性强,但是房间人数也提前配置好,这个不够合理,桌子人数可以在建房规则确定后动态初始化
  5. 大厅服,房间服收发中转服数据时,使用了单线程的阻塞收发数据的TcpSocketService,这是2个服务和中转服务维持长连接的唯一tcp连接。有检测心跳,广播中转,消息收发等功能。如果创建房间并发量高,这里可能也会是性能瓶颈。
  6. 考虑到客户端是lua,如果让服务器游戏逻辑也用lua来完成,可能会提高开发效率。同时也可以利用脚本语言的特性,实现游戏的热更新。
  7. 调度引擎是处理所有其他引擎的请求的唯一消费者,但是只有一个线程,虽然调度引擎只对从队列取消息加锁,在处理完取出的消息前就已经释放锁,但是像房间服务,能不能根据房间划分,把每个房间内部再加一个队列存储本房间的消息,这样形成一个2级队列,1级队列存储的是需要处理的房间,而每个房间里又存储了这个房间的所有需要处理的消息.然后调度引擎也用多线程并发.每次一个线程从1级队列pop一个房间,然后再从2级队列pop一条消息,这样可以保证同一时刻只有一个线程处理一个房间的消息.但是不同线程能处理不同房间的消息.

本文由博客一文多发平台 OpenWrite 发布!

热门相关:艳遇传说   妖夏   花都保镖   佣兵之王都市行   花开春暖