6 从腾讯QQgame高性能服务器集群架构看“分而治之”与“自治”等分布式架构设计原则

6 从腾讯QQgame高性能服务器集群架构看“分而治之”与“自治”等分布式架构设计原则 腾讯QQGame游戏同时在线的玩家数量极其庞大,为了方便组织玩家组队游戏,腾讯设置了大量游戏室(房间),玩家可以选择进入属意的房间,并在此房间内找到可以加入的游戏组(牌桌、棋盘等)。玩家选择进入某个房间时,必须确保此房间当前人数未满(通常上限为400),否则进入步骤将会失败。玩家在登入QQGame后,会从服务器端获取某类游戏下所有房间的当前人数数据,玩家可以据此找到未满的房间以便进入。 如上篇所述的原因,如果待进入房间的人数接近上限时,玩家的进入请求可能失败,这是因为服务器在收到此进入请求之前可能有若干其他玩家也请求进入这个房间,造成房间人数达到上限。 这一问题是无法通过上篇所述调整协作分配的方法来解决的,这是因为:要进入的房间是由玩家来指定的,无法在服务器端完成此项工作,游戏软件必须将服务器端所维护的所有房间人数数据复制到玩家的客户端,并让玩家在界面上看到这些数据,以便进行选择。 这样,上篇所述的客户端与服务器端协作分配原则(谁掌握数据,谁干活),还得加上一些限制条件,并让位于另一个所谓"用户驱动客户端行为"原则–如果某个功能的执行是由用户来推动的,则这个功能的实现应当放在客户端(或者至少由客户端来控制整个协作),并且客户端必须持有此功能所依赖相关数据的副本,这个副本应当尽量与服务器端的源保持同步。 图一"进入房间"失败示意 QQGame还存在一个明显的不足,就是:玩家如果在游戏一段时间后,离开了某个房间,并且想进入其它房间,这时QQGame并不会刷新所有房间的当前人数,造成玩家据此信息所选的待进入房间往往实际上人数已满,使得进入步骤失败。笔者碰到的最糟情形是重复3、4次以上,才最后成功进入另外某个房间。此缺陷其实质是完全放弃了客户端数据副本与服务器端的源保持同步的原则。 实际上,QQGame的开发者有非常充分的理由来为此缺陷的存在进行辩护:QQGame同时在线的用户数超过百万甚至千万数量级,如果所有客户端要实时(所谓实时,就玩家的体验容忍度而言,可以定为不超过1秒的延迟)地从服务器端获取更新数据,那么最终只有一个结果–系统彻底崩溃。 设想一下每秒千万次请求的吞吐量,以普通服务器每秒上百个请求的处理能力(这个数据是根据服务请求处理过程可能涉及到I/O操作来估值的,纯内存处理的情形可能提高若干数量级),需要成千上万台服务器组成集群方能承受(高可用性挑战);而随着玩家不断地进入或退出游戏房间,相关数据一直在快速变化中, 正向来看,假设有一台中心服务器持有这些数据,那么需要让成千上万台服务器与中心保持这些动态数据的实时同步(数据一致性挑战); 相对应的,逆向来看,玩家进入房间等请求被分配给不同的服务器来处理,一旦玩家进入房间成功则对应服务器内的相关数据被改变,那么假定中的中心服务器就需要实时汇集所有工作服务器内发生的数据变动(数据完整性挑战)。 同时处理上万台服务器的数据同步,这需要什么样的中心服务器呢?即使有这样的超级服务器存在,那么Internet网较大的(而且不稳定的)网络通讯延迟又怎么解决呢? 对于软件缺陷而言,可以在不同的层面来加以解决–从设计、到需求、甚至是直接在业务层面来解决(例如,08年北京奥运会网上购票系统,为了解决订票请求拥塞而至系统崩溃的缺陷,最后放弃了原先"先到先得"的购票业务流程,改为:用户先向系统发订票申请,系统只是记录下来而不进行处理,而到了空闲时,在后台随机抽选幸运者,为他们一一完成订票业务)。当然解决方案所处的层面越高,可能就越让人不满意。 就上述进入房间可能遭遇失败的缺陷而言,最简便的解决方案就是:在需求层面调整系统的操作方式,即**增加一个类似上篇所述"自动快速加入游戏"的功能–“自动进入房间"功能。**系统在服务器端为玩家找到一个人数较多又未满的房间,并尝试进入(注意,软件需求是由用户的操作目标所驱动的,玩家在此的目标就是尽快加入一个满意的游戏组,因此由系统来替代玩家选择目标房间同样符合相关目标)。而为了方便玩家手工选择要进入的房间,则应当增加一个"刷新当前各房间人数"的功能。另外,**还可以调整房间的组织模式,**例如以地域为单位来划分房间,像深圳(长城宽带)区房间1、四川(电信)房间3、北美区房间1等,在深圳上网的玩家将被系统引导而优先进入深圳区的房间。 不管怎样,解决软件缺陷的王道还是在设计层面。要解决上述缺陷,架构设计师就必须同时面对高可用、数据一致性、完整性等方面的严峻挑战。 在思考相关解决方案时,我们将应用若干与高性能服务器集群架构设计相关的一些重要原则。首先是"分而治之"原则,即将大量客户端发出的服务请求进行适当的划分(例如,所有从深圳长城宽带上网的玩家所发出的服务请求分为一组),分别分配给不同的服务器(例如,将前述服务请求分组分配给放置于深圳数据中心的服务器)来加以处理。对于QQGame千万级的并发服务请求数而言,采用Scale Up向上扩展,即升级单个服务器处理能力的方式基本上不予考虑(没有常规的主机能处理每秒上千万的请求)。唯一可行的,只有Scale Out向外扩展,即利用大量服务器集群做负载均衡的方式,这实质上就是"分而治之"原则的具体应用。 图二 分而治之"下的QQGame游戏服务集群部署 然而,要应用"分而治之"原则进行Scale Out向外扩展,还依赖于其它的条件。如果各服务器在处理被分配的服务请求时,其行为与其它服务器的行为结果产生交叉(循环)依赖,换句话讲就是共享了某些数据(例如,服务器A处理客户端a发来的进入房间#n请求,而同时,服务器B也在处理客户端b发来的进入房间#n请求,此时服务器A与B的行为存在循环依赖–因为两者要同时访问房间#n的数据,这一共享数据会造成两者间的循环依赖),则各服务器之间必须确保这些共享数据的一致完整性,否则就可能发生逻辑错误(例如,假定房间#n的人数差一个就满了,服务器A与B在独自处理的情况下,将同时让客户端a与b的进入请求成功,于是房间#n的最终人数将超出上限)。 而要做到此点,各服务器的处理进程之间就必须保持同步(实际上就是排队按先后顺序访问共享数据,例如:服务器A先处理,让客户端a进入房间成功,此时房间#n满员;此后服务器B更新到房间#n满的数据,于是客户端b的进入请求处理结果失败),这样,原来将海量请求做负载均衡的意图就彻底失败了,多台服务器的并发处理能力在此与一台实质上并没有区别。 由此,我们导出了另外一个所谓"处理自治”(或称"行为独立")的原则,即所有参与负载均衡的服务器,其处理对应服务请求的行为应当不循环依赖于其它服务器,换句话讲,就是各服务器的行为相对独立(**注意:**在这里,非循环依赖是允许的,下文中我们来分析为什么)。 由此可见,简单的负载均衡策略对于QQGame而言是解决不了问题的。我们必须找到一种途径,使得在使用大量服务器进行"分而治之"的同时,同时有确保各个服务器"处理自治"。此间的关键就在于"分而治之"的"分"字上。前述将某个地域网段内上网的玩家所发出的服务请求分到一组,并分配给同一服务器的做法,其目的不外乎是尽可能地减少网络通讯延迟带来的负面影响。但它不能满足"处理自治"的要求,为了确保自治,应当让同一台服务器所处理的请求本身是"自治"(准确的说法是"自闭包"Closure)的。同一台服务器所处理的所有请求组成一个服务请求集合,这个集合如果与其它任何与其无交集的(请求)集合(包含此集合的父集合除外)不循环依赖,则此服务请求集合是"自闭包"的,而处理此请求集合的服务器,其"行为独立"。 我们可以将针对同一房间的进入请求划分到同一服务请求分组,这些请求相互之间当然是存在循环依赖的,但与其它分组中的请求却不存在循环依赖(本房间内人数的变化不会影响到其它房间),而将它们都分配给同一服务器(不妨命名为"房间管理服务器",简称"房间服务器")后,那个服务器将是"处理自治"的。 图三 满足"处理自治"条件的QQ游戏区域"房间管理"服务部署 那么接下来要解决的问题,就是玩家所关注的某个游戏区内,所有房间当前人数数据的实时更新问题。其解决途径与上述的方法类似,我们还是**将所有获取同一区内房间数据的服务请求归为一组,并交给同一服务器处理。**与上文所述场景不同的是,这个服务器需要实时汇集本区内所有房间服务器的房间人数数据。我们可以让每个房间服务器一旦发生数据变更时,就向此服务器(不妨命名为"游戏区域管理服务器",简称"区服务器")推送一个变更数据记录,而推送的数据只需包含房间Id和所有进入的玩家Id(房间服务器还包含其它细节数据,例如牌桌占位数据)便可。 另外,由于一个区内的玩家数可能是上十万数量级,一个服务器根本承担不了此种负荷,那么**怎么解决这一矛盾呢?**如果深入分析,我们会发现,更新区内房间数据的请求是一种数据只读类请求,它不会对服务器状态造成变更影响,因此这些请求相互间不存在依赖关系;这样,我们可以将它们再任意划分为更小的分组,而同时这些分组仍然保持"自闭包"特性,然后分配给不同的区服务器。多台区服务器来负责同一区的数据更新请求,负载瓶颈被解决。 当然,此前,还需将这些区服务器分为1台主区服务器和n台从属区服务器;主区服务器负责汇集本区内所有房间服务器的房间人数数据,从属区服务器则从主区服务器实时同步区房间数据副本。 更好的做法,则是如『图五』所示,由房间服务器来充当从属区服务器的角色,玩家进入某个房间后,在玩家进入另外一个房间之前,其客户端都将从此房间对应的房间服务器来更新区内房间数据。要注意的是,图中房间服务器的数据更新利用了所谓的"分布式对象缓存服务"。 玩家进入某个房间后,还要加入某个游戏组才能玩游戏。上篇所述的方案,是让第一个加入某个牌桌的用户,其主机自动充当本牌桌的游戏服务器;而其它玩家要加入此牌桌,其加入请求应当发往第一个加入的用户主机;此后开始游戏,其对弈过程将由第一个加入用户的主机来主导执行。 那么此途径是否同样也符合上述的前两个设计原则呢?游戏在执行的过程中,根据输赢结果,玩家要加分或减分,同时还要记录胜负场数。这些数据必须被持久化(比如在数据库中保存下来),因此游戏服务器(『图六』中的设计,是由4个部署于QQ客户端的"升级"游戏前台逻辑执行服务,加上1个"升级"游戏后台逻辑执行服务,共同组成一个牌桌的"升级"游戏服务)在处理相关游戏执行请求时,将依赖于玩家游戏账户数据服务(『图六』中的所谓"QQGame会话服务"); 不过这种依赖是非循环的,即玩家游戏账户数据服务器的行为反过来并不依赖于游戏服务器。上文中曾提到,“处理自治"原则中非循环依赖是允许的。这里游戏服务器在处理游戏收盘请求时,要调用玩家游戏账户数据服务器来更新相关数据;因为不同玩家的游戏账户数据是相互独立的,此游戏服务器在调用游戏账户数据服务器时,逻辑上不受其它游戏服务器调用游戏账户数据服务器的影响,不存在同步等待问题;所以,游戏服务器在此能够达成负载均衡的意图。 点击图片可以放大 图****四 存在"非循环依赖"的QQ游戏客户端P2P服务与交互逻辑部署 不过,在上述场景中,虽然不存在同步依赖,但是性****能依赖还是存在的,游戏账户数据服务器的处理性能不够时,会造成游戏服务器长时间等待。为此,我们**可以应用分布式数据库表水平分割的技术,**将QQ玩家用户以其登记的行政区来加以分组,并部署于对应区域的数据库中(例如,深圳的玩家数据都在深圳的游戏账户数据库中)。 点击图片可以放大 图五 满足"自闭包"条件的QQ分布式数据库(集群)部署 实际上,我们由此还可以推论出一个数据库表水平分割的原则–任何数据库表水平分割的方式,必须确保同一数据库实例中的数据记录是"自闭包"的,即不同数据库实例中的数据记录相互间不存在循环依赖。 总之,初步满足QQGame之苛刻性能要求的分布式架构现在已经是初具雏形了,但仍然有很多涉及性能方面的细节问题有待解决。例如,Internet网络通讯延迟的问题、服务器之间协作产生的性能瓶颈问题等等。笔者将在下篇中继续深入探讨这些话题。

January 11, 2021

7 QQ游戏百万人同时在线服务器架构实现

7 QQ游戏百万人同时在线服务器架构实现 QQ游戏于前几日终于突破了百万人同时在线的关口,向着更为远大的目标迈进,这让其它众多传统的棋牌休闲游戏平台黯然失色,相比之下,联众似乎已经根本不是QQ的对手,因为QQ除了这100万的游戏在线人数外,它还拥有3亿多的注册量(当然很多是重复注册的)以及QQ聊天软件900万的同时在线率,我们已经可以预见未来由QQ构建起来的强大棋牌休闲游戏帝国。 服务器程序,其可承受的同时连接数目是有理论峰值的,在实际应用中,能达到一万人的同时连接并能保证正常的数据交换已经是很不容易了,通常这个值都在2000到5000之间,据说QQ的单台服务器同时连接数目也就是在这个值这间。 如果要实现2000到5000用户的单服务器同时在线,是不难的。在windows下,比较成熟的技术是采用IOCP—完成端口。只要运用得当,一个完成端口服务器是完全可以达到2K到5K的同时在线量的。但,5K这样的数值离百万这样的数值实在相差太大了,所以,百万人的同时在线是单台服务器肯定无法实现的。 要实现百万人同时在线,首先要实现一个比较完善的完成端口服务器模型,这个模型要求至少可以承载2K到5K的同时在线率(当然,如果你MONEY多,你也可以只开发出最多允许100人在线的服务器)。在构建好了基本的完成端口服务器之后,就是有关服务器组的架构设计了。之所以说这是一个服务器组,是因为它绝不仅仅只是一台服务器,也绝不仅仅是只有一种类型的服务器。 简单地说,实现百万人同时在线的服务器模型应该是:登陆服务器+大厅服务器+房间服务器。当然,也可以是其它的模型,但其基本的思想是一样的。下面,我将逐一介绍这三类服务器的各自作用。 / 1 / **登陆服务器:**一般情况下,我们会向玩家开放若干个公开的登陆服务器,就如QQ登陆时让你选择的从哪个QQ游戏服务器登陆一样,QQ登陆时让玩家选择的六个服务器入口实际上就是登陆服务器。登陆服务器主要完成负载平衡的作用。详细点说就是,在登陆服务器的背后,有N个大厅服务器,登陆服务器只是用于为当前的客户端连接选择其下一步应该连接到哪个大厅服务器,当登陆服务器为当前的客户端连接选择了一个合适的大厅服务器后,客户端开始根据登陆服务器提供的信息连接到相应的大厅上去,同时客户端断开与登陆服务器的连接,为其他玩家客户端连接登陆服务器腾出套接字资源。 在设计登陆服务器时,至少应该有以下功能:N个大厅服务器的每一个大厅服务器都要与所有的登陆服务器保持连接,并实时地把本大厅服务器当前的同时在线人数通知给各个登陆服务器,这其中包括:用户进入时的同时在线人数增加信息以及用户退出时的同时在线人数减少信息。这里的各个大厅服务器同时在线人数信息就是登陆服务器为客户端选择某个大厅让其登陆的依据。举例来说,玩家A通过登陆服务器1连接到登陆服务器,登陆服务器开始为当前玩家在众多的大厅服务器中根据哪一个大厅服务器人数比较少来选择一个大厅,同时把这个大厅的连接IP和端口发给客户端,客户端收到这个IP和端口信息后,根据这个信息连接到此大厅,同时,客户端断开与登陆服务器之间的连接,这便是用户登陆过程中,在登陆服务器这一块的处理流程。 / 2 / 大厅服务器:是普通玩家看不到的服务器,它的连接IP和端口信息是登陆服务器通知给客户端的。也就是说,在QQ游戏的本地文件中,具体的大厅服务器连接IP和端口信息是没有保存的。大厅服务器的主要作用是向玩家发送游戏房间列表信息。 这些信息包括: 每个游戏房间的类型 名称 在线人数 连接地址以及其它如游戏帮助文件URL的信息 从****界面上看的话,大厅服务器就是我们输入用户名和密码并校验通过后进入的游戏房间列表界面。 大厅服务器,主要有以下功能: 一是向当前玩家广播各个游戏房间在线人数信息; 二是提供游戏的版本以及下载地址信息; 三是提供各个游戏房间服务器的连接IP和端口信息; 四是提供游戏帮助的URL信息; 五是提供其它游戏辅助功能。 但在这众多的功能中,有一点是最为核心的,即:**为玩家提供进入具体的游戏房间的通道,让玩家顺利进入其欲进入的游戏房间。**玩家根据各个游戏房间在线人数,判定自己进入哪一个房间,然后双击服务器列表中的某个游戏房间后玩家开始进入游戏房间服务器。 / 3 / 游戏房间服务器:具体地说就是如“斗地主1”,“斗地主2”这样的游戏房间。游戏房间服务器才是具体的负责执行游戏相关逻辑的服务器。这样的游戏逻辑分为两大类: 第一类是通用的游戏房间逻辑,如:进入房间,离开房间,进入桌子,离开桌子以及在房间内说话等; 第二类是游戏桌子逻辑,这个就是各种不同类型游戏的主要区别之处了,比如斗地主中的叫地主或不叫地主的逻辑等,当然,游戏桌子逻辑里也包括有通用的各个游戏里都存在的游戏逻辑,比如在桌子内说话等。 总之,游戏房间服务器才是真正负责执行游戏具体逻辑的服务器。 这里提到的三类服务器,均采用的是完成端口模型,每个服务器最多连接数目是5000人,但是,我在游戏房间服务器上作了逻辑层的限定,最多只允许300人同时在线。其他两个服务器仍然允许最多5000人的同时在线。 如果按照这样的结构来设计,那么要实现百万人的同时在线就应该是这样: 首先是大厅,1000000/5000=200。也就是说,至少要200台大厅服务器,但通常情况下,考虑到实际使用时服务器的处理能力和负载情况,应该至少准备250台左右的大厅服务器程序。 另外,**具体的各种类型的游戏房间服务器需要多少,**就要根据当前玩各种类型游戏的玩家数目分别计算了,比如斗地主最多是十万人同时在线,每台服务器最多允许300人同时在线,那么需要的斗地主服务器数目就应该不少于:100000/300=333,准备得充分一点,就要准备350台斗地主服务器。 除正常的玩家连接外,还要考虑到:对于登陆服务器,会有250台大厅服务器连接到每个登陆服务器上,这是始终都要保持的连接; 而对于大厅服务器而言,如果仅仅有斗地主这一类的服务器,就要有350多个连接与各个大厅服务器始终保持着。所以从这一点看,结构在某些方面还存在着需要改进的地方,但核心思想是:尽快地提供用户登陆的速度,尽可能方便地让玩家进入游戏中。

January 11, 2021

8 大型多人在线游戏服务器架构设计

8 大型多人在线游戏服务器架构设计 由于大型多人在线游戏服务器理论上需要支持无限多的玩家,所以对服务器端是一个非常大的考验。服务器必须是安全的,可维护性高的,可伸缩性高的,可负载均衡的,支持高并发请求的。面对这些需求,我们在设计服务器的时候就需要慎重考虑,特别是架构的设计,如果前期设计不好,最后面临的很可能是重构。 一款游戏服务器的架构都是慢慢从小变大的,不可能一下子就上来一个完善的服务器构架,目前流行的说法是游戏先上线,再扩展。所以说我们在做架构的时候,一定要把底层的基础组件做好,方便以后扩展,但是刚开始的时候留出一些接口,并不实现它,将来游戏业务的发展,再慢慢扩展。当然,如果前期设计的不好,后期业务扩展了,但架构没办法扩展,只能加班加点搞了。 面对庞大的数据量我们想到的唯一个解决方案就是分而治之,即采用分布式的方式去解决它。把紧凑独立的功能单独拿出来做。分担到不同的物理服务器上面去运行。而且做到可以动态扩展。这就需要我们考虑好模块的划分,尽量要业务独立,关联性低。 前期,由于游戏需要尽快上线,开发周期短,我们需要把服务尽快的跑起来,这个时候的目标应该是尽快完成测试版本开发,单台服务器支持的人数可以稍微低一些,但是当人数暴涨时,我们可以能过多开几组服务来支持新增涨的用户量,即可以平衡扩展就可以了。到后期我们再把具体的模块单独拿出来支持,比如前期逻辑服务器上包括:活动,关卡,背包,技能,好友管理等。后期我们可以把好友,背包管理或其它的单独做一个服务进程,部署在不同的物理服务器上面。我们先按分区的服务进行设计,后面在部署的时候可以部署为世界服务器,下面是一个前期的架构图,下面我们从每个服务器的功能说起: 1,登陆管理服务 负责用户的登陆验证,如果有注册功能的话,也可以放在这里。一般手机游戏直接走sdk验证。网页游戏和客户端游戏会有注册功能,也可以叫用户管理服务。 1.1 用户登陆验证 负责接收客户端的用户登陆请求,验证账号的合法性,是否在黑名单(被封号的用户),是否在白名单(一般是测试账号,服务未开启时也可以进入)。如果是sdk登陆,此服务向第三方服务发起回调请求。 1.2 登陆安全加密 使用加密的传输协议,见通信协议部分。 1.3 是否在白名单内 白名单是给内部测试人员使用的,在服务器未开启的状态下,白名单的用户可以提前进入游戏进行游戏测试。 1.4 判断是否在黑名单 黑名单的用户是禁止登陆的,一般这是一些被封号的用户,拒绝登陆。 1.5 登陆验证 服务器使用私钥解密密码,进行验证,如果是sdk登陆,则直接向第三方服务发起回调。 1.6 登陆令牌(token)生成 当用户登陆验证成功之后,服务器端需要生成一个登陆令牌token,这个token具有时效性,当用户客户端拿到这个token之后,如果在一定时间内没有登陆游戏成功,那么这个token将失败,用户需要重新申请token,token存储在登陆服务这,向外提供用户是否已登陆的接口,其它服务器想验证如果是否登陆,就拿那个服务收到的token来此验证。 1.7 显示用户角色信息 当用户登陆成功之后,显示最近登陆的角色信息。 2,显示公告 用户登陆成功之后,请求公告服务器,获取最新的公告,公告服务先根据token和Userid验证用户是否已登陆,公告有可能根据渠道的不同,显示不同的公告。所以 公告一定是要可以根据渠道编辑的。 3,选区服务 当用户登陆成功之后,请求服务器分区列表服务器,显示当前所有的大区列表。 3.1 验证用户是否已登陆 向登陆服务器请求验证是否已登陆。 3.2 大区列表显示 大区列表信息中只显示大区id和大区名称。这样做是为了安全考虑,不一次性把大区对应的网关ip和端口暴露出来,也可以减少网络的传输量。 3.3 用户点击选择某个大区,客户端拿到大区id再向选区服务请求获取此大区对应的网关ip地址和端口。根据负载算法计算得出。 3.4 网关的选择 选区服务会维护一份网关的配置列表。一个大区对应一到多个网关,当配置有多个网关时,需要定时检测各个网关是否连接正常,如果发现有网关连接不上,需要把大区对应的网关信息设置为无效,不再参与网关的分配,并发出报警。 一般对于网关的选择,可以使用用户id求余法加虚网关节点法。这样在网关节点数量固定的情况下,一个用户总是会被分配到同一个网关上面。但是如果只是使用求余法的话,可能会造成用户分布不均衡,这里可以通过增加网关的虚拟节点(其它就是增加某个网关的权重,让用户多来一些到这个网关上面),这个可以参考哈稀一致性算法。包括后面说到的一个网关对应多个逻辑服务器,也可以使用同样的方法。这部分可以抽象出来一个模块使用。 3.5 选区服务对内要提供修改服务器状态的接口,比如维护中… 4,登陆网关 4.1 建立连接 收到客户端的建立连接请求之后,记录此channel和对应的连接建立时间。并设置如果在一定时间内未收到登陆请求,则断开连接。返回给客户端登陆超时。 4.2 登陆请求 收到登陆请求后,移除记录的channelid信息,向登陆服务器验证用户是否已登陆过,并向外广播用户角色登陆成功的消息。 4.3 登陆成功后,接收网关的其它的消息 4.4 客户端消息合法性验证 在向逻辑服务器转发消息之前验证消息的合法性,具体验证方法见协议安全验 证。 4.5 将客户端消息转发送到对应的逻辑服务器。 5 通信协议 5.1协议序列化和返回序列化 可以直接使用protobuf,直接对协议进行序列化和反序列化。 5.2协议组成 5.2.1 包头构成 包总长度,加密字符串长度,加密字符串内容,userId,playerId,版本号,内包内容。 5.2.2 包体组成 请求的逻辑信息,是protobuf后对应的二进制数据。 包总长度 加密内容 UserId playerId 请求序列id 版本号 内包内容 Int 64 Long Long Long int varchar 4 64 8 8 8 4 变长 5.3 协议内容加密 如果协议明文传输的话,被篡改的风险就非常大,所以我们要对传输协议进行加密传输,由于协议内容大小不固定,为了保证效率,采用对称加密算法,首先客户端使用AES的公钥对消息内容加密(上表中userid之后的信息),客户端把加密后的报文发送到服务器端。AES的公钥在用户第一次连接时获取。 5.4 协议完整性验证 尽管我们对消息做了加密,但也不是万无一失的,为了进一步确保消息没有被篡改,我们需要对消息的完整性进行检测,使用数字摘要的方式,首先客户端对userid及之后的协议信息进行AES加密,加密之后取它的md5值,md5值用于验证数据的完整性。这个md5值会被传送到服务器,如果协议信息被修改了,那个md5就会不同。...

January 11, 2021

9 百万用户级游戏服务器架构设计

9 百万用户级游戏服务器架构设计 服务器结构探讨 – 最简单的结构 所谓服务器结构,也就是如何将服务器各部分合理地安排,以实现最初的功能需求。所以,结构本无所谓正确与错误;当然,优秀的结构更有助于系统的搭建,对系统的可扩展性及可维护性也有更大的帮助。 好的结构不是一蹴而就的,而且每个设计者心中的那把尺都不相同,所以这个优秀结构的定义也就没有定论。在这里,我们不打算对现有游戏结构做评价,而是试着从头开始搭建一个我们需要的MMOG结构。 对于一个最简单的游戏服务器来说,它只需要能够接受来自客户端的连接请求,然后处理客户端在游戏世界中的移动及交互,也即游戏逻辑处理即可。如果我们把这两项功能集成到一个服务进程中,则最终的结构很简单: 嗯,太简单了点,这样也敢叫服务器结构?好吧,现在我们来往里面稍稍加点东西,让它看起来更像是服务器结构一些。 一般来说,我们在接入游戏服务器的时候都会要提供一个帐号和密码,验证通过后才能进入。关于为什么要提供用户名和密码才能进入的问题我们这里不打算做过多讨论,云风曾对此也提出过类似的疑问,并给出了只用一个标识串就能进入的设想,有兴趣的可以去看看他们的讨论。但不管是采用何种方式进入,照目前看来我们的服务器起码得提供一个帐号验证的功能。 我们把观察点先集中在一个大区内。在大多数情况下,一个大区内都会有多组游戏服,也就是多个游戏世界可供选择。简单点来实现,我们完全可以抛弃这个大区的概念,认为一个大区也就是放在同一个机房的多台服务器组,各服务器组间没有什么关系。这样,我们可为每组服务器单独配备一台登录服。最后的结构图应该像这样: 该结构下的玩家操作流程为,先选择大区,再选择大区下的某台服务器,即某个游戏世界,点击进入后开始帐号验证过程,验证成功则进入了该游戏世界。但是,如果玩家想要切换游戏世界,他只能先退出当前游戏世界,然后进入新的游戏世界重新进行帐号验证。 早期的游戏大都采用的是这种结构,有些游戏在实现时采用了一些技术手段使得在切换游戏服时不需要再次验证帐号,但整体结构还是未做改变。 该结构存在一个服务器资源配置的问题。因为登录服处理的逻辑相对来说比较简单,就是将玩家提交的帐号和密码送到数据库进行验证,和生成会话密钥发送给游戏服和客户端,操作完成后连接就会立即断开,而且玩家在以后的游戏过程中不会再与登录服打任何交道。这样处理短连接的过程使得系统在大多数情况下都是比较空闲的,但是在某些时候,由于请求比较密集,比如开新服的时候,登录服的负载又会比较大,甚至会处理不过来。 另外在实际的游戏运营中,有些游戏世界很火爆,而有些游戏世界却非常冷清,甚至没有多少人玩的情况也是很常见的。所以,我们能否更合理地配置登录服资源,使得整个大区内的登录服可以共享就成了下一步改进的目标。 服务器结构探讨 – 登录服的负载均衡 回想一下我们在玩wow时的操作流程:运行wow.exe进入游戏后,首先就会要求我们输入用户名和密码进行验证,验证成功后才会出来游戏世界列表,之后是排队进入游戏世界,开始游戏… 可以看到跟前面的描述有个很明显的不同,那就是要先验证帐号再选择游戏世界。这种结构也就使得登录服不是固定配备给个游戏世界,而是全区共有的。 我们可以试着从实际需求的角度来考虑一下这个问题。正如我们之前所描述过的那样,登录服在大多数情况下都是比较空闲的,也许我们的一个拥有20个游戏世界的大区仅仅使用10台或更少的登录服即可满足需求。而当在开新区的时候,或许要配备40台登录服才能应付那如潮水般涌入的玩家登录请求。所以,登录服在设计上应该能满足这种动态增删的需求,我们可以在任何时候为大区增加或减少登录服的部署。 当然,在这里也不会存在要求添加太多登录服的情况。还是拿开新区的情况来说,即使新增加登录服满足了玩家登录的请求,游戏世界服的承载能力依然有限,玩家一样只能在排队系统中等待,或者是进入到游戏世界中导致大家都卡。 另外,当我们在增加或移除登录服的时候不应该需要对游戏世界服有所改动,也不会要求重启世界服,当然也不应该要求客户端有什么更新或者修改,一切都是在背后自动完成。 最后,有关数据持久化的问题也在这里考虑一下。一般来说,使用现有的商业数据库系统比自己手工技术先进要明智得多。我们需要持久化的数据有玩家的帐号及密码,玩家创建的角色相关信息,另外还有一些游戏世界全局共有数据也需要持久化。 好了,需求已经提出来了,现在来考虑如何将其实现。 对于负载均衡来说,已有了成熟的解决方案。一般最常用,也最简单部署的应该是基于DNS的负载均衡系统了,其通过在DNS中为一个域名配置多个IP地址来实现。最新的DNS服务已实现了根据服务器系统状态来实现的动态负载均衡,也就是实现了真正意义上的负载均衡,这样也就有效地解决了当某台登录服当机后,DNS服务器不能立即做出反应的问题。当然,如果找不到这样的解决方案,自己从头打造一个也并不难。而且,通过DNS来实现的负载均衡已经包含了所做的修改对登录服及客户端的透明。 而对于数据库的应用,在这种结构下,登录服及游戏世界服都会需要连接数据库。从数据库服务器的部署上来说,可以将帐号和角色数据都放在一个中心数据库中,也可分为两个不同的库分别来处理,基到从物理上分到两台不同的服务器上去也行。 但是对于不同的游戏世界来说,其角色及游戏内数据都是互相独立的,所以一般情况下也就为每个游戏世界单独配备一台数据库服务器,以减轻数据库的压力。所以,整体的服务器结构应该是一个大区有一台帐号数据库服务器,所有的登录服都连接到这里。而每个游戏世界都有自己的游戏数据库服务器,只允许本游戏世界内的服务器连接。 最后,我们的服务器结构就像这样: 这里既然讨论到了大区及帐号数据库,所以顺带也说一下关于激活大区的概念。wow中一共有八个大区,我们想要进入某个大区游戏之前,必须到官网上激活这个区,这是为什么呢? 一般来说,在各个大区帐号数据库之上还有一个总的帐号数据库,我们可以称它为中心数据库。比如我们在官网上注册了一个帐号,这时帐号数据是只保存在中心数据库上的。而当我们要到一区去创建角色开始游戏的时候,在一区的帐号数据库中并没有我们的帐号数据,所以,我们必须先到官网上做一次激活操作。这个激活的过程也就是从中心库上把我们的帐号数据拷贝到所要到的大区帐号数据库中。 服务器结构探讨 – 简单的世界服实现 讨论了这么久我们一直都还没有进入游戏世界服务器内部,现在就让我们来窥探一下里面的结构吧。 对于现在大多数MMORPG来说,游戏服务器要处理的基本逻辑有移动、聊天、技能、物品、任务和生物等,另外还有地图管理与消息广播来对其他高级功能做支撑。如纵队、好友、公会、战场和副本等,这些都是通过基本逻辑功能组合或扩展而成。 在所有这些基础逻辑中,与我们要讨论的服务器结构关系最紧密的当属地图管理方式。决定了地图的管理方式也就决定了我们的服务器结构,我们仍然先从最简单的实现方式开始说起。 回想一下我们曾战斗过无数个夜晚的暗黑破坏神,整个暗黑的世界被分为了若干个独立的小地图,当我们在地图间穿越时,一般都要经过一个叫做传送门的装置。世界中有些地图间虽然在地理上是直接相连的,但我们发现其游戏内部的逻辑却是完全隔离的。可以这样认为,一块地图就是一个独立的数据处理单元。 既然如此,我们就把每块地图都当作是一台独立的服务器,他提供了在这块地图上游戏时的所有逻辑功能,至于内部结构如何划分我们暂不理会,先把他当作一个黑盒子吧。 当两个人合作做一件事时,我们可以以对等的关系相互协商着来做,而且一般也都不会有什么问题。当人数增加到三个时,我们对等的合作关系可能会有些复杂,因为我们每个人都同时要与另两个人合作协商。正如俗语所说的那样,三个和尚可能会碰到没水喝的情况。当人数继续增加,情况就变得不那么简单了,我们得需要一个管理者来对我们的工作进行分工、协调。游戏的地图服务器之间也是这么回事。 一般来说,我们的游戏世界不可能会只有一块或者两块小地图,那顺理成章的,也就需要一个地图管理者。先称它为游戏世界的中心服务器吧,毕竟是管理者嘛,大家都以它为中心。 中心服务器主要维护一张地图ID到地图服务器地址的映射表。当我们要进入某张地图时,会从中心服上取得该地图的IP和port告诉客户端,客户端主动去连接,这样进入他想要去的游戏地图。在整个游戏过程中,客户端始终只会与一台地图服务器保持连接,当要切换地图的时候,在获取到新地图的地址后,会先与当前地图断开连接,再进入新的地图,这样保证玩家数据在服务器上只有一份。 我们来看看结构图是怎样的: 很简单,不是吗。但是简单并不表示功能上会有什么损失,简单也更不能表示游戏不能赚钱。早期不少游戏也确实采用的就是这种简单结构。 服务器结构探讨 – 继续世界服 都已经看出来了,这种每切换一次地图就要重新连接服务器的方式实在是不够优雅,而且在实际游戏运营中也发现,地图切换导致的卡号,复制装备等问题非常多,这里完全就是一个事故多发地段,如何避免这种频繁的连接操作呢? 最直接的方法就是把那个图倒转过来就行了。客户端只需要连接到中心服上,所有到地图服务器的数据都由中心服来转发。很完美的解决方案,不是吗? 这种结构在实际的部署中也遇到了一些挑战。对于一般的MMORPG服务器来说,单台服务器的承载量平均在2000左右,如果你的服务器很不幸地只能带1000人,没关系,不少游戏都是如此;如果你的服务器上跑了3000多玩家依然比较流畅,那你可以自豪地告诉你的策划,多设计些大量消耗服务器资源的玩法吧,比如大型国战、公会战争等。 2000人,似乎我们的策划朋友们不大愿意接受这个数字。我们将地图服务器分开来原来也是想将负载分开,以多带些客户端,现在要所有的连接都从中心服上转发,那连接数又遇到单台服务器的可最大承载量的瓶颈了。 这里有必要再解释下这个数字。我知道,有人一定会说,才带2000人,那是你水平不行,我随便写个TCP服务器都可带个五六千连接。问题恰恰在于你是随便写的,而MMORPG的服务器是复杂设计的。如果一个演示socket API用的echo服务器就能满足MMOG服务器的需求,那写服务器该是件多么惬意的事啊。 但我们所遇到的事实是,服务器收到一个移动包后,要向周围所有人广播,而不是echo服务器那样简单的回应;服务器在收到一个连接断开通知时要向很多人通知玩家退出事件,并将该玩家的资料写入数据库,而不是echo服务器那样什么都不需要做;服务器在收到一个物品使用请求包后要做一系列的逻辑判断以检查玩家有没有作弊;服务器上还启动着很多定时器用来更新游戏世界的各种状态…… 其实这么一比较,我们也看出资源消耗的所在了:服务器上大量的复杂的逻辑处理。再回过头来看看我们想要实现的结构,我们既想要有一个唯一的入口,使得客户端不用频繁改变连接,又希望这个唯一入口的负载不会太大,以致于接受不了多少连接。 仔细看一看这个需求,我们想要的仅仅只是一台管理连接的服务器,并不打算让他承担太多的游戏逻辑。既然如此,那五六千个连接也还有满足我们的要求。至少在现在来说,一个游戏世界内,也就是一组服务器内同时有五六千个在线的玩家还是件让人很兴奋的事。事实上,在大多数游戏的大部分时间里,这个数字也是很让人眼红的。 什么?你说梦幻、魔兽还有史先生的那个什么征途远不止这么点人了!噢,我说的是大多数,是大多数,不包括那些明星。你知道大陆现在有多少游戏在运营吗?或许你又该说,我们不该在一开始就把自己的目标定的太低!好吧,我们还是先不谈这个。 继续我们的结构讨论。一般来说,我们把这台负责连接管理的服务器称为网关服务器,因为内部的数据都要通过这个网关才能出去,不过从这台服务器提供的功能来看,称其为反向代理服务器可能更合适。我们也不在这个名字上纠缠了,就按大家通用的叫法,还是称他为网关服务器吧。 网关之后的结构我们依然可以采用之前描述的方案,只是,似乎并没有必要为每一个地图都开一个独立的监听端口了。我们可以试着对地图进行一些划分,由一个Master Server来管理一些更小的Zone Server,玩家通过网关连接到Master Server上,而实际与地图有关的逻辑是分派给更小的Zone Server去处理。 最后的结构看起来大概是这样的: 服务器结构探讨 – 最终的结构 如果我们就此打住,可能马上就会有人要嗤之以鼻了,就这点古董级的技术也敢出来现。好吧,我们还是把之前留下的问题拿出来解决掉吧。 一般来说,当某一部分能力达不到我们的要求时,最简单的解决方法就是在此多投入一点资源。既然想要更多的连接数,那就再加一台网关服务器吧。新增加了网关服后需要在大区服上做相应的支持,或者再简单点,有一台主要的网关服,当其负载较高时,主动将新到达的连接重定向到其他网关服上。 而对于游戏服来说,有一台还是多台网关服是没有什么区别的。每个代表客户端玩家的对象内部都保留一个代表其连接的对象,消息广播时要求每个玩家对象使用自己的连接对象发送数据即可,至于连接是在什么地方,那是完全透明的。当然,这只是一种简单的实现,也是普通使用的一种方案,如果后期想对消息广播做一些优化的话,那可能才需要多考虑一下。 既然说到了优化,我们也稍稍考虑一下现在结构下可能采用的优化方案。 首先是当前的Zone Server要做的事情太多了,以至于他都处理不了多少连接。这其中最消耗系统资源的当属生物的AI处理了,尤其是那些复杂的寻路算法,所以我们可以考虑把这部分AI逻辑独立出来,由一台单独的AI服务器来承担。 然后,我们可以试着把一些与地图数据无关的公共逻辑放到Master Server上去实现,这样Zone Server上只保留了与地图数据紧密相关的逻辑,如生物管理,玩家移动和状态更新等。 还有聊天处理逻辑,这部分与游戏逻辑没有任何关联,我们也完全可以将其独立出来,放到一台单独的聊天服务器上去实现。 最后是数据库了,为了减轻数据库的压力,提高数据请求的响应速度,我们可以在数据库之前建立一个数据库缓存服务器,将一些常用数据缓存在此,服务器与数据库的通信都要通过这台服务器进行代理。缓存的数据会定时的写入到后台数据库中。 好了,做完这些优化我们的服务器结构大体也就定的差不多了,暂且也不再继续深入,更细化的内容等到各个部分实现的时候再探讨。 好比我们去看一场晚会, 舞台上演员们按着预定的节目单有序地上演着,但这就是整场晚会的全部吗?显然不止,在幕后还有太多太多的人在忙碌着,甚至在晚会前和晚会后都有。我们的游戏服务器也如此。 在之前描述的部分就如同舞台上的演员,是我们能直接看到的,幕后的工作人员我们也来认识一下。 现实中有警察来维护秩序,游戏中也如此,这就是我们常说的GM。GM可以采用跟普通玩家一样的拉入方式来进入游戏,当然权限会比普通玩家高一些,也可以提供一台GM服务器专门用来处理GM命令,这样可以有更高的安全性,GM服一般接在中心服务器上。 在以时间收费的游戏中,我们还需要一台计费的服务器,这台服务器一般接在网关服务器上,注册玩家登录和退出事件以记录玩家的游戏时间。 任何为用户提供服务的地方都会有日志记录,游戏服务器当然也不例外。从记录玩家登录的时间,地址,机器信息到游戏过程中的每一项操作都可以作为日志记录下来,以备查错及数据挖掘用。至于搜集玩家机器资料所涉及到的法律问题不是我们该考虑的。 差不多就这么多了吧,接下来我们会按照这个大致的结构来详细讨论各部分的实现。 服务器结构探讨 —— 一点杂谈 再强调一下,服务器结构本无所谓好坏,只有是否适合自己。我们在前面探讨了一些在现在的游戏中见到过的结构,并尽我所知地分析了各自存在的一些问题和可以做的一些改进,希望其中没有谬误,如果能给大家也带来些启发那自然更好。 突然发现自己一旦罗嗦起来还真是没完没了。接下来先说说我在开发中遇到过的一些困惑和一基础问题探讨吧,这些问题可能有人与我一样,也曾遇到过,或者正在被困扰中,而所要探讨的这些基础问题向来也是争论比较多的,我们也不评价其中的好与坏,只做简单的描述。 首先是服务器操作系统,linux与windows之争随处可见,其实在大多数情况下这不是我们所能决定的,似乎各大公司也基本都有了自己的传统,如网易的freebsd,腾讯的linux等。如果真有权利去选择的话,选自己最熟悉的吧。 决定了OS也就基本上确定了网络IO模型,windows上的IOCP和linux下的epool,或者直接使用现有的网络框架,如ACE和asio等,其他还有些商业的网络库在国内的使用好像没有见到,不符合中国国情嘛。:) 然后是网络协议的选择,以前的选择大多倾向于UDP,为了可靠传输一般自己都会在上面实现一层封装,而现在更普通的是直接采用本身就很可靠的TCP,或者TCP与UDP的混用。早期选择UDP的主要原因还是带宽限制,现在宽带普通的情况下TCP比UDP多出来的一点点开销与开发的便利性相比已经不算什么了。当然,如果已有了成熟的可靠UDP库,那也可以继续使用着。 还有消息包格式的定义,这个曾在云风的blog上展开过激烈的争论。消息包格式定义包括三段,包长、消息码和包体,争论的焦点在于应该是消息码在前还是包长在前,我们也把这个当作是信仰问题吧,有兴趣的去云风的blog上看看,论论。 另外早期有些游戏的包格式定义是以特殊字符作分隔的,这样一个好处是其中某个包出现错误后我们的游戏还能继续。但实际上,我觉得这是完全没有必要的,真要出现这样的错误,直接断开这个客户端的连接可能更安全。而且,以特殊字符做分隔的消息包定义还加大了一点点网络数据量。 最后是一个纯技术问题,有关socket连接数的最大限制。开始学习网络编程的时候我犯过这样的错误,以为port的定义为unsigned short,所以想当然的认为服务器的最大连接数为65535,这会是一个硬性的限制。而实际上,一个socket描述符在windows上的定义是unsigned int,因此要有限制那也是四十多亿,放心好了。 在服务器上port是监听用的,想象这样一种情况,web server在80端口上监听,当一个连接到来时,系统会为这个连接分配一个socket句柄,同时与其在80端口上进行通讯;当另一个连接到来时,服务器仍然在80端口与之通信,只是分配的socket句柄不一样。这个socket句柄才是描述每个连接的唯一标识。按windows网络编程第二版上的说法,这个上限值配置影响。...

January 11, 2021

bind 函数重难点解析

bind 函数重难点解析 bind 函数如何选择绑定地址 bind 函数的基本用法如下: struct sockaddr_in bindaddr; bindaddr.sin_family = AF_INET; bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); bindaddr.sin_port = htons(3000); if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) { std::cout << "bind listen socket error." << std::endl; return -1; } 其中 bind 的地址我们使用了一个宏叫 INADDR_ANY ,关于这个宏的解释如下: If an application does not care what local address is assigned, specify the constant value INADDR_ANY for an IPv4 local address or the constant value in6addr_any for an IPv6 local address in the sa_data member of the name parameter. This allows the underlying service provider to use any appropriate network address, potentially simplifying application programming in the presence of multihomed hosts (that is, hosts that have more than one network interface and address)....

January 11, 2021

C++ 17 结构化绑定

C++ 17 结构化绑定 stl 的 map 容器很多读者应该都很熟悉,map 容器提供了一个 insert 方法,我们用该方法向 map 中插入元素,但是应该很少有人记得 insert 方法的返回值是什么类型,让我们来看一下 C++98/03 提供的 insert 方法的签名: std::pair<iterator,bool> insert( const value_type& value ); 这里我们仅关心其返回值,这个返回值是一个 std::pair 类型,由于 map 中的元素的 key 不允许重复,所以如果 insert 方法调用成功,T1 是被成功插入到 map 中的元素的迭代器,T2 的类型为 bool,此时其值为 true(表示插入成功);如果 insert 由于 key 重复,T1 是造成 insert 插入失败、已经存在于 map 中的元素的迭代器,此时 T2 的值为 false(表示插入失败)。 在 C++98/03 标准中我们可以使用 std::pair 的 first 和 second 属性来分别引用 T1 和 T2 的值。如下面的我们熟悉的代码所示: #include <iostream> #include <string> #include <map> int main() { std::map<std::string, int> cities; cities["beijing"] = 0; cities["shanghai"] = 1; cities["shenzhen"] = 2; cities["guangzhou"] = 3; //for (const auto& [key, value] : m) //{ // std::cout << key << ": " << value << std::endl; //} //这一行在 C++11 之前写法实在太麻烦了, //std::pair<std::map<std::string, int>::iterator, int> insertResult = cities....

January 11, 2021

C++必知必会的知识点

C++必知必会的知识点 如何成为一名合格的C/C++开发者? 不定参数函数实现var_arg系列的宏 你一定要搞明白的C函数调用方式与栈原理 深入理解C/C++中的指针 详解C++11中的智能指针 C++17结构化绑定 C++必须掌握的pimpl惯用法 用Visual Studio调试Linux程序 如何使用Visual Studio管理和阅读开源项目代码 利用cmake工具生成Visual Studio工程文件

January 11, 2021

connect 函数在阻塞和非阻塞模式下的行为

connect 函数在阻塞和非阻塞模式下的行为 在 socket 是阻塞模式下 connect 函数会一直到有明确的结果才会返回(或连接成功或连接失败),如果服务器地址“较远”,连接速度比较慢,connect 函数在连接过程中可能会导致程序阻塞在 connect 函数处好一会儿(如两三秒之久),虽然这一般也不会对依赖于网络通信的程序造成什么影响,但在实际项目中,我们一般倾向使用所谓的异步的 connect 技术,或者叫非阻塞的 connect。这个流程一般有如下步骤: 1. 创建socket,并将 socket 设置成非阻塞模式; 2. 调用 connect 函数,此时无论 connect 函数是否连接成功会立即返回;如果返回-1并不表示连接出错,如果此时错误码是EINPROGRESS 3. 接着调用 select 函数,在指定的时间内判断该 socket 是否可写,如果可写说明连接成功,反之则认为连接失败。 按上述流程编写代码如下: /** * 异步的connect写法,nonblocking_connect.cpp * zhangyl 2018.12.17 */ #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <iostream> #include <string.h> #include <stdio.h> #include <fcntl.h> #include <errno.h> #define SERVER_ADDRESS "127.0.0.1" #define SERVER_PORT 3000 #define SEND_DATA "helloworld" int main(int argc, char* argv[]) { //1.创建一个socket int clientfd = socket(AF_INET, SOCK_STREAM, 0); if (clientfd == -1) { std::cout << "create client socket error." << std::endl; return -1; } //连接成功以后,我们再将 clientfd 设置成非阻塞模式, //不能在创建时就设置,这样会影响到 connect 函数的行为 int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); int newSocketFlag = oldSocketFlag | O_NONBLOCK; if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) { close(clientfd); std::cout << "set socket to nonblock error....

January 11, 2021

leveldb源码分析

leveldb源码分析 leveldb源码分析1 leveldb源码分析2 leveldb源码分析3 leveldb源码分析4 leveldb源码分析5 leveldb源码分析6 leveldb源码分析7 leveldb源码分析8 leveldb源码分析9 leveldb源码分析10 leveldb源码分析11 leveldb源码分析12 leveldb源码分析13 leveldb源码分析14 leveldb源码分析15 leveldb源码分析16 leveldb源码分析17 leveldb源码分析18 leveldb源码分析19 leveldb源码分析20 leveldb源码分析21 leveldb源码分析22

January 11, 2021

leveldb源码分析1

leveldb源码分析1 本系列《leveldb源码分析》共有22篇文章,这是第一篇。 leveldb,除去测试部分,代码不超过1.5w行。这是一个单机k/v存储系统,决定看完它,并把源码分析完整的写下来,还是会很有帮助的。我比较厌烦太复杂的东西,而Leveldb的逻辑很清晰,代码不多、风格很好,功能就不用讲了,正合我的胃口。 BTW,分析Leveldb也参考了网上一些朋友写的分析blog,如【巴山独钓】。 leveldb源码分析 2012年1月21号开始研究下leveldb的代码,Google两位大牛开发的单机KV存储系统,涉及到了skip list、内存KV table、LRU cache管理、table文件存储、operation log系统等。先从边边角角的小角色开始扫。 不得不说,Google大牛的代码风格太好了,读起来很舒服,不像有些开源项目,很快就看不下去了。 开始之前先来看看Leveldb的基本框架,几大关键组件,如图1-1所示。 图1-1 leveldb是一种基于operation log的文件系统,是Log-Structured-Merge Tree的典型实现。LSM源自Ousterhout和Rosenblum在1991年发表的经典论文«The Design and Implementation of a Log-Structured File System »。 由于采用了op log,它就可以把随机的磁盘写操作,变成了对op log的append操作,因此提高了IO效率,最新的数据则存储在内存memtable中。 当op log文件大小超过限定值时,就定时做check point。Leveldb会生成新的Log文件和Memtable,后台调度会将Immutable Memtable的数据导出到磁盘,形成一个新的SSTable文件。SSTable就是由内存中的数据不断导出并进行Compaction操作后形成的,而且SSTable的所有文件是一种层级结构,第一层为Level 0,第二层为Level 1,依次类推,层级逐渐增高,这也是为何称之为LevelDb的原因。 1. 一些约定 先说下代码中的一些约定: 1.1 字节序 Leveldb对于数字的存储是little-endian的,在把int32或者int64转换为char*的函数中,是按照先低位再高位的顺序存放的,也就是little-endian的。 1.2 VarInt 把一个int32或者int64格式化到字符串中,除了上面说的little-endian字节序外,大部分还是变长存储的,也就是VarInt。对于VarInt,每byte的有效存储是7bit的,用最高的8bit位来表示是否结束,如果是1就表示后面还有一个byte的数字,否则表示结束。直接见Encode和Decode函数。 在操作log中使用的是Fixed存储格式。 1.3 字符比较 是基于unsigned char的,而非char。 2. 基本数据结构 别看是基本数据结构,有些也不是那么简单的,像LRU Cache管理和Skip list那都算是leveldb的核心数据结构。 2.1 Slice Leveldb中的基本数据结构: 包括length和一个指向外部字节数组的指针。 和string一样,允许字符串中包含’\0’。 提供一些基本接口,可以把const char和string转换为Slice;把Slice转换为string,取得数据指针const char。 2.2 Status Leveldb 中的返回状态,将错误号和错误信息封装成Status类,统一进行处理。并定义了几种具体的返回状态,如成功或者文件不存在等。 为了节省空间Status并没有用std::string来存储错误信息,而是将返回码(code), 错误信息message及长度打包存储于一个字符串数组中。 成功状态OK 是NULL state_,否则state_ 是一个包含如下信息的数组: state_[0..3] == 消息message长度 state_[4] == 消息code state_[5..] ==消息message 2.3 Arena Leveldb的简单的内存池,它所作的工作十分简单,申请内存时,将申请到的内存块放入std::vector blocks_中,在Arena的生命周期结束后,统一释放掉所有申请到的内存,内部结构如图2.3-1所示。 Arena主要提供了两个申请函数:其中一个直接分配内存,另一个可以申请对齐的内存空间。 Arena没有直接调用delete/free函数,而是由Arena的析构函数统一释放所有的内存。 应该说这是和leveldb特定的应用场景相关的,比如一个memtable使用一个Arena,当memtable被释放时,由Arena统一释放其内存。 2.4 Skip list **Skip list(跳跃表)是一种可以代替平衡树的数据结构。**Skip lists应用概率保证平衡,平衡树采用严格的旋转(比如平衡二叉树有左旋右旋)来保证平衡,因此Skip list比较容易实现,而且相比平衡树有着较高的运行效率。 从概率上保持数据结构的平衡比显式的保持数据结构平衡要简单的多。对于大多数应用,用skip list要比用树更自然,算法也会相对简单。由于skip list比较简单,实现起来会比较容易,虽然和平衡树有着相同的时间复杂度(O(logn)),但是skip list的常数项相对小很多。skip list在空间上也比较节省。一个节点平均只需要1.333个指针(甚至更少),并且不需要存储保持平衡的变量。 如图2.4-1所示。 在Leveldb中,skip list是实现memtable的核心数据结构,memtable的KV数据都存储在skip list中。...

January 11, 2021