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和暗黑的代码给我提供了极大的帮助!

 

 

 

 

                         


分享到: 微信 更多