firefly代码流程超详细介绍 [
转自:http://www.9miao.com/thread-45205-2-1.html
net顾名思义,就是网络模块,负责接受客户端的连接,处理客户端发送过来的数据,解包转发给其它模块。整个firefly系统里面,和用户打交道的也只有这个模块(admin和master虽然提供web操作接口,但是都是服务管理员的)。
我们前面提到,子模块的功能是由config.json来配置驱动的。那么我们看看这个模块的json文件定义了哪些功能。
20 "servers": {
21 "net": {
22 "app": "app.netserver", #具有游戏功能模块,在框架启动完毕后需要import app/netserver.py 文件
23 "log": "app/logs/net.log", #日志
24 "name": "net", #该模块的名称
25 "netport": 11009, #!!重要!! 定义了netport,则firefly会为其启动一个网络服务,来监听客户端连接。
26 "remoteport": [ #需要连接其它node,这里的其它node定义的是gate。实际上我们可以有不止一个“gate”来实现分布式逻辑。
27 {
28 "rootname": "gate",
29 "rootport": 10000
30 }
31 ]
32 }
33 }
那么对应的启动代码如下:(init 和 start函数省略,只列出关键部分)
40 def config(self, config, dbconfig = None,memconfig = None,masterconf=None):
41 """配置服务器
42 """
43 netport = config.get('netport')#客户端连接
44 gatelist = config.get('remoteport',[])#remote节点配置列表
45 servername = config.get('name')#服务器名称
46 logpath = config.get('log')#日志
47 app = config.get('app')#入口模块名称
48 self.servername = servername
49
50 if masterconf:
51 masterport = masterconf.get('rootport')
52 addr = ('localhost', masterport)
53 leafnode = leafNode(servername)
54 serviceControl.initControl(leafnode.getServiceChannel())
55 leafnode.connect(addr)
56 GlobalObject().leafNode = leafnode
57
58 if netport:
59 self.netfactory = LiberateFactory()
60 netservice = services.CommandService("netservice")
61 self.netfactory.addServiceChannel(netservice)
62 reactor.listenTCP(netport, self.netfactory)
63 GlobalObject().netfactory = self.netfactory
64
65 for cnf in gatelist:
66 rname = cnf.get('rootname')
67 rport = cnf.get('rootport')
68 self.gates[rname] = leafNode(servername)
69 addr = ('localhost', rport)
70 self.gates[rname].connect(addr)
71
72 GlobalObject().remote = self.gates
73
74 if logpath:
75 log.addObserver(loogoo(logpath)) #日志处理
76 log.startLogging(sys.stdout)
77
78 if app:
79 reactor.callLater(0.1, __import__, app)
80
可以看成主要启动3个功能:
1、masterconfig部分,是说明本模块作为master模块的leafnode要连接master模块,这个和gate模块一样,可以参考前面PB的介绍章节。
2、连接gate模块,这里代码假定了不止一个需要连接的gate;但是实际上我们config.json里面只设定了一个。 这个步骤和连接master一样。
3、真正新鲜的东西是netport部分,这里 LiberateFactory实际上是protocol.ServerFactory的子类。它使用的协议是LiberateProtocol,这个是protocol.Protocol的子类。
如果大家了解twisted,那么我提到ServerFactory,和Protocol,大家就已经明白了netport部分的原理,压根不需要我废话。所以对于新人,不了解twisted,我们的主要任务是学习,并熟悉它。如下:
1、官网的介绍,这个比较简单,但是对于有经验的开发者来说,已经足够了解了(官网上面其它samples,也有不少是用protocol的)
ht空格t空格ps://twistedmatrix.com/documents/current/core/howto/servers.html
2、专门介绍twisted的书籍。有中文版的,内容我记得好像是比较浅显易懂。
h空格ttp://ww空格w.amazon.com/exec/obidos/ASIN/1449326110/jpcalsjou-20
第二章专门介绍protocol我就不多说了。
到这里我们假设大家已经熟悉twisted.protocol,那么我们看看firefly的实现部分。
大家知道twisted是事件驱动的,所以整个框架看起来很简单,我们即使对整体不熟悉,只要关注我们关心的事件处理函数部分也是ok的。
首先,
reactor.listenTCP(netport, self.netfactory)
这里建立一个服务,等待客户端connect。 每次接收到一个client连接,Factory都会调用 Factory.protocol 也就是 LiberateProtocol来处理,类似fork的概念。Factory充当了一个LiberatePtotocol的管理者的角色,干活的还是LibrateProtocol。所以我们主要关心LiberatePtotocol,代码如下:
class LiberateProtocol(protocol.Protocol):
"""协议"""
buff = ""
def connectionMade(self):
"""连接建立处理
"""
log.msg('Client %d login in.[%s,%d]' % (self.transport.sessionno,
self.transport.client[0], self.transport.client[1]))
self.factory.connmanager.addConnection(self)
self.factory.doConnectionMade(self)
self.datahandler = self.dataHandleCoroutine()
self.datahandler.next()
def connectionLost(self, reason):
"""连接断开处理
"""
log.msg('Client %d login out.'%(self.transport.sessionno))
self.factory.doConnectionLost(self)
self.factory.connmanager.dropConnectionByID(self.transport.sessionno)
def safeToWriteData(self,data,command):
"""线程安全的向客户端发送数据
@param data: str 要向客户端写的数据
"""
if not self.transport.connected or data is None:
return
senddata = self.factory.produceResult(data,command)
reactor.callFromThread(self.transport.write,senddata)
def dataHandleCoroutine(self):
"""
"""
length = self.factory.dataprotocl.getHeadLenght()#获取协议头的长度
while True:
data = yield
self.buff += data
while self.buff.__len__() >= length:
unpackdata = self.factory.dataprotocl.unpack(self.buff[:length])
if not unpackdata.get('result'):
log.msg('illegal data package --')
self.transport.loseConnection()
break
command = unpackdata.get('command')
rlength = unpackdata.get('lenght')
request = self.buff[length:length+rlength]
if request.__len__() < rlength:
log.msg('some data lose')
break
self.buff = self.buff[length+rlength:]
d = self.factory.doDataReceived(self, command, request)
if not d:
continue
d.addCallback(self.safeToWriteData, command)
d.addErrback(DefferedErrorHandle)
def dataReceived(self, data):
"""数据到达处理
@param data: str 客户端传送过来的数据
"""
self.datahandler.send(data)
可以看到主要处理了3个事件。
connectionMade #客户端连接我们
connectionLost(self, reason) #连接丢失
dataReceived(self, data) #数据到达
当客户端连接的时候,我们会调用Factory的connectmanager(也是简单的一个管理类)来保存当前连接信息。
整个系统只有一个Factory类的实例,但是每当一个连接来的时候,都会fork一个Protocol的实例。所以信息都会保存在Factory的属性里面,比如Factory.connectionManager。
同理,当连接丢失的时候,我们调用connectionManager处理一下即可。没有什么复杂的。
我们主要关注的是数据到来的处理逻辑:
1、数据到达,我们简单的发送给处理函数: self.datahandler.send(data)
2、这个处理函数包含了yield 语句所以它是一个生成器,generate。具体大家可以google python yield,会有很详细的介绍文档。
这里只是告诉大家
2.1、datahandler函数,每次调用到yield的地方就会暂停,等待下次被next,或者send唤醒,然后从yield的地方继续执行。
2.2、data = yield,这条语句在被唤醒后 data就被赋值为 send传递进来的参数。
2.3 那么这个函数改写成这样,大家就很容易理解了:
def dataHandleCoroutine(self,sendData): ########改动地方
"""
"""
length = self.factory.dataprotocl.getHeadLenght()#获取协议头的长度
data = sendData ########改动地方
self.buff += data
while self.buff.__len__() >= length:
unpackdata = self.factory.dataprotocl.unpack(self.buff[:length])
if not unpackdata.get('result'):
log.msg('illegal data package --')
self.transport.loseConnection()
break
command = unpackdata.get('command')
rlength = unpackdata.get('lenght')
request = self.buff[length:length+rlength]
if request.__len__() < rlength:
log.msg('some data lose')
break
self.buff = self.buff[length+rlength:]
d = self.factory.doDataReceived(self, command, request)
if not d:
return ########改动地方
d.addCallback(self.safeToWriteData, command)
d.addErrback(DefferedErrorHandle)
3、剩下了就简单了,利用python struct库来解包数据,
ud = struct.unpack('!sssss3I',dpack)
然后调用自己的services来处理解包后的command和参数。至于如何处理命令,就要看这个services挂什么样的处理函数了。
启动流程介绍完毕,下面就是app里面,暗黑游戏部分的具体处理内容了。
调用流程和前面一样,我们直接到关键函数:
def loadModule():
netapp.initNetApp()
import gatenodeapp
这里有些代码冗余和重复,但是没有什么太大影响,我们不提了。
这个函数的第一行代码是我改动过的(原先是import+修饰符方式),大家跟进去看,其实就是初始化一个services的子类:NetCommandService,然后给它挂上Forwarding_0 这个处理函数。
而我们仔细看NetCommandServices发现它override了原先的callTargetSingle,主要是这句改动:
target = self.getTarget(0)
说明不论啥命令过来,它都写死了调用Forwarding_0来处理。
而Forwarding_0这个函数就一句话,功能是转发前面解析的command和参数给gate模块。
OK到这里结合前面的gate模块,我们应该就可以理出一条client登录的主线。
0、暗黑客户端一启动,就会连接net模块,net模块建立一个对应的connection,并由connectionManager保存。
1、暗黑客户端发送用户名、密码和登陆命令号101: (会封包,这里忽略)
Json::FastWriter writer;
Json::Value person;
person["username"]=userName;
person["password"]=password;
std::string json_file=writer.write(person);
CCLog("%s",json_file.c_str());
SocketManager::getInstance()->sendMessage(json_file.c_str(), 101);
2、net模块接收并解析出命令号 101,参数{name:xxx,pwd:xxx}。然后调用自己的services(实际是NetCommandServices)处理。
3、NetCommandServices,不管37 21,直接写死调用Forwarding_0函数。
4、Fowarding_0函数写死了,用命令fowarding转发给gate模块。
4、gate模块收到它leafNode的调用请求,所以调用自己作为root的services。而这个services在gate/app/gate/rootservice/rservices.py里面注册了forwarding函数,所以就调用它。
5、md,fowarding函数发现这个命令号注册在loacalservices里面,见
gate/app/gate/localservice/lservices.py中:
def init():
initLocalService()
addToLocalService(loginToServer_101) ############## 这里
addToLocalService(activeNewPlayer_102)
addToLocalService(roleLogin_103)
于是调用loginToServer_101来处理这个命令
6、这个命令就不展开了,很简单,就是取数据,比对是否匹配,check是否有效。然后逐层返回,最后送给客户端
对于登录部分,流程压根没有跑到db、game1和admin模块,所以我们可以直接利用master,gate,net这3个模块做测试
顺序启动
master
gate
net
然后启动tool/clienttest.py
可以看到net模块的终端会有信息打印。
我自己也写了一个测试程序。已经放在github上面,tool/ clientTestLogin.py;
用的是twisted,很简单。如下:
1 from twisted.internet.protocol import Protocol, ClientFactory
2
3 class Echo(Protocol):
4 def connectionMade(self):
5 a = 'N%&0\t\x00\x00\x00\x00\x00\x00\x00.\x00\x00\x00e{"password":"chenee","username":"chenee"}\n'
6 self.transport.write(a)
7
8 def dataReceived(self, data):
9 print data
10 #self.transport.loseConnection()
11
12
13 class EchoClientFactory(ClientFactory):
14 def startedConnecting(self, connector):
15 print 'Started to connect.'
16
17 def buildProtocol(self, addr):
18 print 'Connected.'
19 return Echo()
20
21 def clientConnectionLost(self, connector, reason):
22 print 'Lost connection. Reason:', reason
23
24 def clientConnectionFailed(self, connector, reason):
25 print 'Connection failed. Reason:', reason
26
27 from twisted.internet import reactor
28 reactor.connectTCP("localhost", 11009, EchoClientFactory())
29 reactor.run()
很简单,就30行代码,而且都是框架 。就是在连接上server后,事件connectionMade里面往server写一条命令。这条命令是我直接copy暗黑客户端的打印,就是一条封装好的数据包。
5 a = 'N%&0\t\x00\x00\x00\x00\x00\x00\x00.\x00\x00\x00e{"password":"chenee","username":"chenee"}\n'
运行后结果如下:
bash-3.2$ python clientTestLogin.py
Started to connect.
Connected.
N%&0 We{"data": {"userId": 1915, "characterId": 1000001, "hasRole": true}, "result": true}
可以看出,server成功处理,并且返回我们登录后的角色信息。
game1 囊括了几乎所有游戏逻辑,内容很多。但是多也只是app内容多, 前面的firefly框架启动流程没有什么差别。
如果看官是一直看下来的,扫一眼代码就一目了然,这里不提。直接跳到app部分。
def loadModule():
"""
"""
load_config_data() #加载数据
registe_madmin() #注册几个表到memcache
from gatenodeapp import * #实际上是注册各种services的命令
逻辑上game/app启动逻辑分3个部分。
Load_config_data();里面东西虽然多,但是并不复杂,如下:
def getExperience_Config():
"""获取经验配置表信息"""
global tb_Experience_config
sql = "select * from tb_experience"
result = dbpool.fetchAll(sql) ########我改过了
for _item in result:
tb_Experience_config[_item['level']] = _item
只是取一些游戏中常用的数据表的内容,然后直接保存在game1的内存数据中,不是memcache,因为这是常驻内存,而不是缓存起来的。
其中 result = dbpool.fetchAll(sql) 是我改动了一下,只是把原先copy paste的代码风格整理一下。10几个地方全部是同样复制的代码看起来非常不舒服。
不过这里不但是体力活,而且是细致活,分3中fetch,具体看我改动后的github代码,没有啥技术含量就不提了。
然后是用MAdminManager,来注册管理几个表,这个和前面db章节提到的一模一样。需要注意的是,下面的代码:
def registe_madmin():
"""注册数据库与memcached对应
"""
MAdminManager().registe(memmode.tb_character_admin)
MAdminManager().registe(memmode.tb_zhanyi_record_admin)
MAdminManager().registe(memmode.tbitemadmin)
MAdminManager().registe(memmode.tb_matrix_amin)
看上去做的只是注册到MAdminManager,并没有初始化。但是其实在文件开头有一个
import memmode
而memmode.py里面是直接裸写的全局变量。所以实际上在这个python文件开头就已经初始化了。
建议把import memmode放到registe_madmin()函数开头部分。这样逻辑上清晰一些。
或者干脆吧memmode里面的内容全部封装到一个init函数里面更好。
大家明白就好,我懒得改了。
下面看第三行
from gatenodeapp import *
这个就是利用import+修饰符方式,添加一批services的命令处理函数。跟进去看:
remoteservice = CommandService("gateremote")
GlobalObject().remote["gate"].setServiceChannel(remoteservice)
def remoteserviceHandle(target):
"""
"""
remoteservice.mapTarget(target)
可以看出,实际上是给连接gate模块的leafNode的services添加的。这样gate转发过来的命令,都会被这些函数解析,处理。然后把结果返回给gate,再返回给net,最终到client端。
前面net章节已经分析了用户登录时,net到gate的数据流。这里唯一不同的是gate的消息处理不再是由localservices处理,而是有gate的root转发出去。(下面是gate模块中对应的代码)
node = VCharacterManager().getNodeByClientId(dynamicId)
return GlobalObject().root.callChild(node, key, dynamicId, data)
我们可以看出,firefly中是由gate模块来维护一个虚拟角色管理类,并由这个管理类来管理一批登陆的虚拟角色。
这些VirtualCharacter中会记录用户的虚拟角色到底是在那个game模块上;虽然我们这里只有game1一个游戏内容处理模块(游戏逻辑服务器),但是可以看出firefly的逻辑是支持多个game模块的。
同时也确定了,这些分发管理是由gate模块(或者叫gate服务器)来负责的。
这里的命令虽然很多,但是相互之间比较独立,同事也和firefly的系统架构关联性不大,独立理解起来很方便。我们就不在一一介绍了。
这个部分其实是和系统策划,游戏逻辑密切相关的,所以,如果有空,我们在后面反推暗黑的游戏逻辑的时候,结合cocos2dx客户端再详细说明上面提到的所有的services处理函数的各个功能,以此来展现一个完整的暗黑游戏框架。而不是这里讨论的firefly服务器框架。
OK,OK
这个系列基本告一段落,大概花费我3天时间码字,如果有什么疏忽的地方,冒犯的地方,或者错误的地方,还请各位看官,9miao的朋友多担待。
也请大家积极反馈,批评指点,帮助我改正错误。
最后再次感谢9miao的开源精神,毫无疑问,firefly和暗黑的代码给我提供了极大的帮助!