网游ftp服务器权限设置的高频权限验证机制是怎样的?

后使用快捷导航没有帐号?
 论坛入口:
  |   |    |   | 
网游的跨服玩法是如何实现的?“跨域体系”架构设计思路
设计思想/框架&服务器&
作者/江贵龙
  虽然游戏市场竞争激烈,产品格局变动较大,但游戏产业一直处于稳步增长阶段,无论是在端游,页游,手游还是已经初露端倪的HTML5游戏。可以预见,游戏类型中,MMOARPG游戏仍然会是引领市场的主流趋势,贡献着大部分流水,市场上也仍然在不断涌现精品。研发团队对MMO游戏的探索从来未间断过,从付费模式的改变,到题材多元化,次时代的视觉效果,更成熟的玩法及数值体系,本文主要针对跨服玩法上的探索和实现做一些思考和分析。
  根据2016年《中国游戏产业报告》数据显示,随着游戏人口红利逐渐消失,获取用户的成本居高不下,几年来至少翻了十倍以上,目前平均导量成本页游为10~15元/人,手游在15~20元/人,其中IOS上成本30~50元/人,“洗”用户模式的效果正在变得微弱,用户流失严重。让我们先来看看滚服玩法的局限性,滚服洗量模式下存在着如下的弊端:
虽然游戏市场竞争激烈486.png (50.16 KB, 下载次数: 264)
14:35 上传
  在上述背景下,一款长留存,低流失的精品游戏就成了平台方,渠道商,研发方追捧的目标,设想一下,如果让所有服务器玩家通过“跨域体系”实现自由畅通交互,在此基础上,玩家可以体验到前所未有的“国战系统”——7×24小时昼夜不停服的国家战争,随时开战;突破单地图承载容量极限的国战对决,带来真正万人国战的刺激体验,形成全区玩家能够互动的游戏社交环境。依托平台运营来打造一款真正意义上摆脱传统游戏运营模式的全新产品,为平台吸纳足够的市场份额,大幅降低流失率。
  我们的蓝图是开创“1=1000” 模式,让所有玩家,身处一个服务器却如同同时存在于所有服务器,这种打破服务器屏障的设定,杜绝了游戏出现“被迫滚服”现象出现,玩家不用再担心鬼服人烟稀少,不用担心交易所一无所有,所有的数据共享,让玩家轻松Hold住全世界。
虽然游戏市场竞争激烈956.png (335.49 KB, 下载次数: 309)
14:35 上传
  项目组当时面临的现状是游戏各种档期计划、宣传推广安排都已经就绪,两个月后该独代项目要在腾讯平台按时上线,开发不能因引入跨服机制而导致所有完成度100%的功能都要去分别去增加跨服的支持,而技术人员在跨服功能开发这块经验的积累上也不充分。
  技术小组分析了时下项目的现状,跨服业务需求及现有的框架结构,明确了几点原则:
为了实现跨服,游戏代码从底层架构到上层业务逻辑的代码改动成本尽量降低业务逻辑里尽量少关心或者不用关心是否在本服或者跨服,降低开发人员的跨服功能开发复杂度,提高开发的效率,缩短开发周期。那么,我们需要解决哪些技术疑点呢?
  客户端直连还是服务器转发
  方案A,直连。如果直连,那么,跨服玩法时客户端要维持两个连接,在跨服里,要模拟玩家登陆,绑定session的过程,游戏服和跨服两边要同时维护两份玩家数据,如何做到数据的同步?跨服要暴露给玩家,需要有公网访问IP和端口。对客户端连接管理来说较复杂。
  方案B,转发。如果通过大区服务器消息转发,那么,服务器之间做RPC通信,连接管理,消息需额外做一步跳转,性能能否满足?跨不跨服,对于客户端来说透明,跨服隐藏在大区之后,更加安全,不需再浪费公网IP和端口。
  综合考虑了下,采用了B方案。
  1. RPC框架设计需求
  那么,我们需要先准备一套高性能轻量级的RPC框架。业界有很多典型的RPC框架,比如Motan、Thrift、gRPC、Hessian、Hprose、Wildfly、Dubbo、DubboX,为什么我们还要重复造轮子呢?综合考虑了下,框架要满足以下几点业务需求:
该框架要简单、易用、支持高并发的跨服请求;根据现有的游戏服务器框架,会有很多定制化的场景;通过NIO TCP长连接获取服务,但无需跨语言的需求;支持同步请求,异步请求,异步回调CallBack;要有服务发现的功能,要有Failfast能力;具备负载均衡,分组等路由策略;
  基于有以上的诉求,结合团队以前的开发经验,于是就决定自主研发。我们选用的技术栈有 Netty、Apache Commons Pool、Redis等。
  框架分为服务提供方(RPC Server)、服务调用方(RPC Client)、注册中心(Registry)三个角色,基于Redis为服务注册中心,通过其Pub/Sub实现服务动态的注册和发现。Server 端会在服务初始化时向Registry 注册声明所提供的服务;Client 向 Registry 订阅到具体提供服务的 Server 列表,根据需要与相关的 Server 建立连接,进行 RPC 服务调用。同时,Client 通过 Registry 感知 Server 的状态变更。三者的交互关系如图:
虽然游戏市场竞争激烈2229.png (92.2 KB, 下载次数: 280)
14:35 上传
  通过Global server提供路由策略,负载均衡策略,分组策略。具备 Failfast 能力,保障RPC服务一定程度的高可用。
  2. RPC请求的有序性
  连接池在设计过程中,比较重要的是要考虑请求的顺序性,也就是先请求的先完成。如果玩家的跨服请求通过不同的RPC连接并发执行,就有可能单个玩家请求因错序而导致逻辑矛盾,比如玩家移动,见图2:
虽然游戏市场竞争激烈2521.png (74.17 KB, 下载次数: 276)
14:35 上传
  玩家移动是很频繁的,如果A请求让玩家从位置1移动到位置2,B请求从位置2移动到位置3,有可能B请求先被跨服接收处理,这就会产生逻辑问题。
  那么,如何做到请求的有序性呢?其本质是让同一份数据的访问能串行化,方法就是让同一个玩家的跨服请求通过同一条RPC连接执行,加上逻辑上的有效性验证,如图3所示:
虽然游戏市场竞争激烈2789.png (121.47 KB, 下载次数: 281)
14:35 上传
  3. 同步RPC实现细节
  限于篇幅,这里只讲同步请求的RPC连接池实现。同步请求的时序图如图所示:
虽然游戏市场竞争激烈2957.png (150.1 KB, 下载次数: 292)
14:36 上传
  上图为进入跨服战场的一次同步请求,场景切换控制器StageControllAction发起进入跨服战场的请求applyChangeByBattlefield(),场景管理器StageControllManager首先要调用登录跨服的RPC请求GameRpcClient.loginCrossServer(LoginCrossServerReq),跨服RPC请求的工作流是这样的:
虽然游戏市场竞争激烈3266.png (55.5 KB, 下载次数: 272)
14:36 上传
  该请求第一步先从连接池里获取一个连接RpcClient rpcClient = rpcClientPool.getResource(roleId),然后发起一个同步请求RpcClient.sendWithReturn(),等待直到结果返回,然后把资源归还连接池。
  我们重点来看看sendWithReturn代码实现:
虽然游戏市场竞争激烈3544.png (297.07 KB, 下载次数: 318)
14:36 上传
  测试场景为分别在连接数在1,8,并发数1,8,数据大小在22byte,94byte,2504byte情况下,做测试,消息同步传输,原样返回,以下是针对同步请求压力测试的结果(取均值):
虽然游戏市场竞争激烈3757.png (48.08 KB, 下载次数: 264)
14:36 上传
  服务器之间主动推,还是被动拉取
  1. 被动拉取模式(Pull)
  由于我们的游戏服务器和跨服服务器代码基本一致,所以只要能在跨服中获得游戏功能所要的数据,那么,就能完成任何原有的功能,并且改造成本基本为零,我们选择了被动拉取。
  这里要提出一个概念:数据源的相对性。
  提供数据方,C向B请求一份数据,B是C的数据源,B向A请求一份数据,A是B的数据源。
虽然游戏市场竞争激烈4050.png (21.35 KB, 下载次数: 264)
14:36 上传
  一个玩家跨服过去后,往游戏原服拉取数据的细节图如图:
虽然游戏市场竞争激烈4195.png (122.24 KB, 下载次数: 282)
14:36 上传
  玩家先跨服过去,loginCrossServer(LoginCrossServerReq),然后,在用到任意数据时(主角、技能、坐骑、装备、宠物等),反向同步请求各个系统的数据。
  我们的实现如下图所示:
虽然游戏市场竞争激烈4416.png (172.48 KB, 下载次数: 288)
14:36 上传
虽然游戏市场竞争激烈4535.png (278.25 KB, 下载次数: 310)
14:36 上传
如果玩家在本服,和调整前一样的处理流程,如果玩家在跨服,客户端请求的指令,发布的事件,异步事件需要在场景Stage线程处理的,就转发到跨服,需要在其他个人业务线程(bus),公共业务线程(public)处理的,仍旧在本服处理。场景业务线程不再允许有DB操作。内部指令的转发、事件分发系统、异步事件系统要在底层支持跨服。玩家在登录本服时就会构PlayerTemplate, 场景用到的数据会实时更新,玩家去跨服,则会把场景中用到的数据PlayerTemplate主动推送给跨服。主动推送模式如下图所示。
虽然游戏市场竞争激烈5443.png (85.65 KB, 下载次数: 281)
14:36 上传
虽然游戏市场竞争激烈5562.png (585.36 KB, 下载次数: 312)
14:36 上传
  看下事件分发代码的改造:
虽然游戏市场竞争激烈5694.png (213.62 KB, 下载次数: 303)
14:36 上传
虽然游戏市场竞争激烈5812.png (28.09 KB, 下载次数: 265)
14:36 上传
  如下图,举个例子,在跨服怪物死亡后,会抛出 MonsterDeadEvent事件,在跨服进程直接处理场景的监听对应的逻辑:
  场景中道具掉落,尸体处理;其他的监听逻辑抛回游戏服处理,根据这事件,任务模块处理完成任务,获得奖励;成就模块处理完成成就,获得奖励;主角模块获得经验,金币等奖励;活动模块处理完成活动,获得奖励。
虽然游戏市场竞争激烈6091.png (149.14 KB, 下载次数: 281)
14:36 上传
  其他方面的优化1. 消息组播机制
  消息组播的优化,在跨服,来自同一服的全部玩家广播从分别单独消息转发,改成一个消息发回本服,然后再广播给玩家(比如来自同一个服n个玩家,原本广播一条消息,服务器之间之间要处理n个RPC消息,现在只需要处理1个消息,降到了原先的1/n)。
虽然游戏市场竞争激烈6344.png (88.78 KB, 下载次数: 270)
14:36 上传
  2. 通信数据量
  一个完整的PlayerTemplate模版数据由于包含了玩家在场景里用到的所有数据,比如角色、宠物、坐骑、装备、神器、法宝、时装、技能、翅膀等等, 数据量比较大,平均能达到5KB左右,需要在服务器之间传输时做zlib压缩,比如,做了压缩后,11767 Byte的玩家数据能压缩到2337Byte,压缩率可达到19.86%。
  3. 序列化/反序列化
  改造前,所有的请求都需要先在本服做AMF3反序列化,如果请求是需要转发到跨服的,再通过JSON序列化传输给跨服,在跨服通过JSON反序列化,最终该请求被处理。
  但实际上,中间过程JSON序列化和反序列化似乎是没有必要的,经过改造,对需要转发给跨服的请求,在本服先不做AMF3反序列化,发送到跨服后再处理,这样就少了一次JSON的序列化和反序列化,同时收益了另外的一个好处:降低了传输的字节。
虽然游戏市场竞争激烈6841.png (131.82 KB, 下载次数: 290)
14:36 上传
虽然游戏市场竞争激烈6960.png (258.67 KB, 下载次数: 290)
14:36 上传
  4. 内存占用优化
  Oracle JVM目前只能在JVM停止运行的时候才能做到释放占有内存,直到下次重启,所以为了防止资源浪费,各种类型的跨服服务器,游戏服务器都需要设置不同的启动参数。启动参数的设定根据我们自行设置的公式,如图所示。
虽然游戏市场竞争激烈7196.png (90.45 KB, 下载次数: 271)
14:36 上传
  但内存占用仍然会经常突破预警线90%,由于一旦系统分配内存发现不够时,就会触发自我保护机制,进行OOM killer,所以需要预留很大的内存给每个JVM进程,而且每次维护的时候去脚本修改内存也比较麻烦。
虽然游戏市场竞争激烈7416.png (142.88 KB, 下载次数: 290)
14:36 上传
  内存占用状况如上图,服务器更新维护后,内存占用一路上扬,一直到最后维持在一定的值,不会回收,除非等下次维护或者系统触发OOM killer。
  基于阿里 JVM 1.8,只要开启-XX:+DeallocateHeapPages,CMS能在不重启应用的情况下把不使用的HEAP归还给系统的物理内存,这样,我们就不需要预留很多空间给JVM,再也不用担心被误杀了。
  拿我们一款内测阶段的游戏举例,使用了ALI JVM后, 64内存配置的机器最后开到了24个新区,相比起以前64G内存的机器,单台只能放9个独立的游戏区的状况下,单区的成本将会节省62.5% 机器资源,非常可观。完美的解决了内存经常吃紧的问题,并大幅节省了成本,这里特别感谢阿里云团队和@坤谷提供黑科技,大家有需求的话,可以去申请成为种子用户。
虽然游戏市场竞争激烈7885.png (145.39 KB, 下载次数: 290)
14:36 上传
  上图就是使用了Ali JDK后的锯齿形效果,每隔一定时间闲置内存会被系统回收,这在Oracle JVM是难以做到的。
  5. 服务器分组机制
  不定向跨服是指任意游戏区的玩家都有可能匹配到一起进行游戏玩法的体验,比如跨服战场,比如跨服副本匹配,如图所示(分别表示服务器分组前和分组后)。
虽然游戏市场竞争激烈8146.png (227.38 KB, 下载次数: 294)
14:36 上传
虽然游戏市场竞争激烈8264.png (190.29 KB, 下载次数: 288)
14:36 上传
  如何在游戏正式大区中选择几个服做灰度服,又不影响不定向跨服体验,以及如何解决新老服玩家战力发展不在同一起跑线而导致的不平衡问题曾一度让人纠结。
  比如游戏产品推出了大型资料片,想先做下灰度测试,让1~4区的玩家先做下新功能的体验,同时又能防止玩家穿了一件旧版本不存在的装备而在跨服环境下报异常,根据运营需求通过分组,就很完美的解决了上述问题。
  6. 战区自动分配机制
  定向跨服是指在一定时间内会固定参与跨服玩法的几个国家,常用于战区中国家之间对战,如图所示,需要运营在后台配置;当一段时间后,随着玩家流失,又需要运营根据战力进行战区的调整,对运营人员的要求比较高。
虽然游戏市场竞争激烈8665.png (139.65 KB, 下载次数: 282)
14:36 上传
  调整后,每一种基于战区的跨服类型都可以自定义调整时间间隔,到时间点全局服务器(global server)系统自动根据全区的活跃战力匹配进行调整,让运营人员从繁杂的配置中解脱出来。
  7. 跨服断线重连机制
  比如战场系统或组队副本,由于网络状况而掉线,如果重新登录后,没法进入,将会严重影响战场的战况,顺风局马上就可能会变成逆风局,主力DPS掉线副本就有可能通不了,这个机制就弥补了这块的缺陷。
  支持的玩法
  目前,我们已经能支持任意的游戏区玩家可以到任意的跨服服务器进行游戏功能的体验。比如已经实现的跨服组队副本、跨服战场、跨服国战、跨服皇城争夺、跨服资源战、虫群入侵战、跨服押镖、挖矿争夺等。
  也支持玩家在本服就可以进行跨服互动,比如和别的区的玩家聊天、加好友、送礼等无缝交互,及国家拍卖行,世界拍卖行的跨服贸易。甚至支持玩家穿越到另外的游戏区做任意的游戏体验,比如一区的玩家听说二区服在举行抢亲活动,你可以跑到2区去观赏参与,也跑到任意的区的中央广场去显摆你的极品套装。
  跨服在线数据
  跨服定向玩法有战区国家玩法,虫群入侵,跨服押镖,挖矿争夺, 跨服皇城争夺,跨服国战等,如下图所示,我们可以看出这种玩法的规律:每次活动开启,跨服就会迎来一波波玩家涌入,活动一结束,玩家就会离开,4个跨服进程支持了7600在线的玩家。
虽然游戏市场竞争激烈9345.png (36.04 KB, 下载次数: 266)
14:36 上传
  跨服非定向性玩法有跨服组队副本,跨服战场等,支持负载均衡,可以随时动态增加跨服。这些玩法的规律是24小时随时可以体验进入,在线比较稳定,8个跨服进程支持了28000在线的玩家。
  技术架构
  下图为跨服通信拓扑图,属于整体架构的核心部分,关于这一部分的说明见图表:
虽然游戏市场竞争激烈9595.png (160.72 KB, 下载次数: 289)
14:36 上传
虽然游戏市场竞争激烈9714.png (116.65 KB, 下载次数: 275)
14:36 上传
  此套架构历经了《大闹天宫OL》、《诸神黄昏》、《暴风王座》、《惊天动地》,《三打白骨精》、《英雄领主》、《封神霸业》等先后近两万组服务器运行的验证和团队的技术积累。
  本文从当前游戏市场发展的背景出发,提出了设计自由交互的“跨域体系”的必要性,然后在实现跨服架构过程中对设计目标、原则、存在的技术难点进行了思考,实现了一套用于跨服通信的高吞吐的RPC通信框架,先后体验了被动拉取模式带来的坑,和改成主动推送模式带来的便利。并且,对该架构设计在消息组播,通信量,消息序列化/反序列化,服务器分组,战区自动分配,断线重连等进行了多方面机制的分析及深度优化,最后上线实践做了可行性验证,提供了强有力的数据支持,总体表现稳定流畅。
  关于整体架构的介绍,卖个关子,后续的文章会和大家分享。请大家关注聊聊架构公众号。
  作者介绍
  江贵龙,游戏行业从业8年,历任多款游戏项目服务器主程,上海灵娱服务器负责人。 关注游戏服务器架构及优化,监控预警,智能运维,数据统计分析等。
  相关阅读:
关注我们官方微信公众号
下载我们官方APP-游戏行
关注手游动态微信公众号
龙哥果然666
龙哥666,说俩小细节,1. 使用protobuf能使通信数据进一步压缩,序列化效率跟fastjson差不多;2. 异步回调CallBack有风险
工作三年后,我选择离开腾讯实现大逃杀手游的要点与难点顶级活动策划进阶-宣传篇,不是通知用户,当你想要做转蛋抽卡的游戏前,请再多想一想网易游戏正式进军MOBA手游市场,这个市场会地方房卡棋牌市场:一个月460多款游戏过审
微信扫一扫关注我们→君,已阅读到文档的结尾了呢~~
2016年最新百万用户级游戏服务器架构设计
扫扫二维码,随身浏览文档
手机或平板扫扫即可继续访问
2016年最新百万用户级游戏服务器架构设计
举报该文档为侵权文档。
举报该文档含有违规或不良信息。
反馈该文档无法正常浏览。
举报该文档为重复文档。
推荐理由:
将文档分享至:
分享完整地址
文档地址:
粘贴到BBS或博客
flash地址:
支持嵌入FLASH地址的网站使用
html代码:
&embed src='/DocinViewer--144.swf' width='100%' height='600' type=application/x-shockwave-flash ALLOWFULLSCREEN='true' ALLOWSCRIPTACCESS='always'&&/embed&
450px*300px480px*400px650px*490px
支持嵌入HTML代码的网站使用
您的内容已经提交成功
您所提交的内容需要审核后才能发布,请您等待!
3秒自动关闭窗口5063人阅读
本文作者:sodme
本文出处:
声明:本文可以不经作者同意任意转载、复制、传播,但任何对本文的引用都请保留作者、出处及本声明信息。谢谢!
常见的网络服务器,基本上是7*24小时运转的,对于网游来说,至少要求服务器要能连续工作一周以上的时间并保证不出现服务器崩溃这样的灾难性事件。事 实上,要求一个服务器在连续的满负荷运转下不出任何异常,要求它设计的近乎完美,这几乎是不太现实的。服务器本身可以出异常(但要尽可能少得出),但是, 服务器本身应该被设计得足以健壮,“小病小灾”打不垮它,这就要求服务器在异常处理方面要下很多功夫。
  服务器的异常处理包括的内容非常广泛,本文仅就在网络封包方面出现的异常作一讨论,希望能对正从事相关工作的朋友有所帮助。
  关于网络封包方面的异常,总体来说,可以分为两大类:一是封包格式出现异常;二是封包内容(即封包数据)出现异常。在封包格式的异常处理方面, 我们在最底端的网络数据包接收模块便可以加以处理。而对于封包数据内容出现的异常,只有依靠游戏本身的逻辑去加以判定和检验。游戏逻辑方面的异常处理,是 随每个游戏的不同而不同的,所以,本文随后的内容将重点阐述在网络数据包接收模块中的异常处理。
  为方便以下的讨论,先明确两个概念(这两个概念是为了叙述方面,笔者自行取的,并无标准可言):
  1、逻辑包:指的是在应用层提交的数据包,一个完整的逻辑包可以表示一个确切的逻辑意义。比如登录包,它里面就可以含有用户名字段和密码字段。尽管它看上去也是一段缓冲区数据,但这个缓冲区里的各个区间是代表一定的逻辑意义的。
  2、物理包:指的是使用recv(recvfrom)或wsarecv(wsarecvfrom)从网络底层接收到的数据包,这样收到的一个数据包,能不能表示一个完整的逻辑意义,要取决于它是通过UDP类的“数据报协议”发的包还是通过TCP类的“流协议”发的包。
  我们知道,TCP是流协议,“流协议”与“数据报协议”的不同点在于:“数据报协议”中的一个网络包本身就是一个完整的逻辑包,也就是说,在应 用层使用sendto发送了一个逻辑包之后,在接收端通过recvfrom接收到的就是刚才使用sendto发送的那个逻辑包,这个包不会被分开发送,也 不会与其它的包放在一起发送。但对于TCP而言,TCP会根据网络状况和neagle算法,或者将一个逻辑包单独发送,或者将一个逻辑包分成若干次发送, 或者会将若干个逻辑包合在一起发送出去。正因为TCP在逻辑包处理方面的这种粘合性,要求我们在作基于TCP的应用时,一般都要编写相应的拼包、解包代
  因此,基于TCP的上层应用,一般都要定义自己的包格式。TCP的封包定义中,除了具体的数据内容所代表的逻辑意义之外,第一步就是要确定以何种方式表示当前包的开始和结束。通常情况下,表示一个TCP逻辑包的开始和结束有两种方式:
  1、以特殊的开始和结束标志表示,比如FF00表示开始,00FF表示结束。
  2、直接以包长度来表示。比如可以用第一个字节表示包总长度,如果觉得这样的话包比较小,也可以用两个字节表示包长度。
  下面将要给出的代码是以第2种方式定义的数据包,包长度以每个封包的前两个字节表示。我将结合着代码给出相关的解释和说明。
  函数中用到的变量说明:
  CLIENT_BUFFER_SIZE:缓冲区的长度,定义为:Const int CLIENT_BUFFER_SIZE=4096。
  m_ClientDataBuf:数据整理缓冲区,每次收到的数据,都会先被复制到这个缓冲区的末尾,然后由下面的整理函数对这个缓冲区进行整理。它的定义是:char m_ClientDataBuf[2* CLIENT_BUFFER_SIZE]。
  m_DataBufByteCount:数据整理缓冲区中当前剩余的未整理字节数。
  GetPacketLen(const char*):函数,可以根据传入的缓冲区首址按照应用层协议取出当前逻辑包的长度。
  GetGamePacket(const char*, int):函数,可以根据传入的缓冲区生成相应的游戏逻辑数据包。
  AddToExeList(PBaseGamePacket):函数,将指定的游戏逻辑数据包加入待处理的游戏逻辑数据包队列中,等待逻辑处理线程对其进行处理。
  DATA_POS:指的是除了包长度、包类型等这些标志型字段之外,真正的数据包内容的起始位置。
Bool SplitFun(const char* pData,const int &len)
&&& PBaseGamePacket pGamePacket=NULL;
&&& __int64 startPos=0, prePos=0, i=0;
&&& int packetLen=0;
& //先将本次收到的数据复制到整理缓冲区尾部
&&& startPos = m_DataBufByteC&&
&&& memcpy( m_ClientDataBuf+startPos, pData, len );
&&& m_DataBufByteCount +=&&&
&&& //当整理缓冲区内的字节数少于DATA_POS字节时,取不到长度信息则退出
 //注意:退出时并不置m_DataBufByteCount为0
&&& if (m_DataBufByteCount & DATA_POS+1)
&&& //根据正常逻辑,下面的情况不可能出现,为稳妥起见,还是加上
&&& if (m_DataBufByteCount && 2*CLIENT_BUFFER_SIZE)
&&&&&&& //设置m_DataBufByteCount为0,意味着丢弃缓冲区中的现有数据
&&&&&&& m_DataBufByteCount = 0;
  //可以考虑开放错误格式数据包的处理接口,处理逻辑交给上层
  //OnPacketError()
&&&& //还原起始指针
& && startPos = 0;
&&&& //只有当m_ClientDataBuf中的字节个数大于最小包长度时才能执行此语句
&&& packetLen = GetPacketLen( pIOCPClient-&m_ClientDataBuf );
&&& //当逻辑层的包长度不合法时,则直接丢弃该包
&&& if ((packetLen & DATA_POS+1) || (packetLen & 2*CLIENT_BUFFER_SIZE))
&&&&&&& m_DataBufByteCount = 0;
  //OnPacketError()
&&& //保留整理缓冲区的末尾指针
&&& __int64 oldlen = m_DataBufByteC&
&&& while ((packetLen &= m_DataBufByteCount) && (m_DataBufByteCount&0))
&&&&&&& //调用拼包逻辑,获取该缓冲区数据对应的数据包
&&&&&&& pGamePacket = GetGamePacket(m_ClientDataBuf+startPos, packetLen);&
&&&&&&& if (pGamePacket!=NULL)
&&&&&&&&&&& //将数据包加入执行队列
&&&&&&&&&&& AddToExeList(pGamePacket);
&&&&&&& pGamePacket = NULL;
  //整理缓冲区的剩余字节数和新逻辑包的起始位置进行调整
&&&&&&& m_DataBufByteCount -= packetL
&&&&&&& startPos += packetL&
&&&&&&& //残留缓冲区的字节数少于一个正常包大小时,只向前复制该包随后退出
&&&&&&& if (m_DataBufByteCount & DATA_POS+1)
&&&&&&&&&&& for(i=startP i&startPos+m_DataBufByteC ++i)
&&&&&&&&&&&&&&& m_ClientDataBuf[i-startPos] = m_ClientDataBuf[i];
&&&&&&&&&&&
&&&&&&&&packetLen = GetPacketLen(m_ClientDataBuf + startPos );
&&&&&&&& //当逻辑层的包长度不合法时,丢弃该包及缓冲区以后的包
&&&&&&& if ((packetLen&DATA_POS+1) || (packetLen&2*CLIENT_BUFFER_SIZE))
&&&&&&&&&&& m_DataBufByteCount = 0;
&&&   //OnPacketError()
&&&&&&&&&&&
&&&&&&&& if (startPos+packetLen&=oldlen)
&&&&&&&&&&& for(i=startP i&startPos+m_DataBufByteC ++i)
&&&&&&&&&&&&&&& m_ClientDataBuf[i-startPos] = m_ClientDataBuf[i];&&&&&&&&&&
&&&&&&&&&&&
&&&& }//取所有完整的包
  以上便是数据接收模块的处理函数,下面是几点简要说明:
  1、用于拼包整理的缓冲区(m_ClientDataBuf)应该比recv中指定的接收缓冲区(pData)长度(CLIENT_BUFFER_SIZE)要大,通常前者是后者的2倍(2*CLIENT_BUFFER_SIZE)或更大。
  2、为避免因为剩余数据前移而导致的额外开销,建议m_ClientDataBuf使用环形缓冲区实现。
3、为了避免出现无法拼装的包,我们约定每次发送的逻辑包,其单个逻辑包最大长度不可以超过CLIENT_BUFFER_SIZE的2倍。因为我们的整 理缓冲区只有2*CLIENT_BUFFER_SIZE这么长,更长的数据,我们将无法整理。这就要求在协议的设计上以及最终的发送函数的处理上要加上这 样的异常处理机制。
  4、对于数据包过短或过长的包,我们通常的情况是置m_DataBufByteCount为0,即舍弃当前包的处理。如果此处不设置 m_DataBufByteCount为0也可,但该客户端只要发了一次格式错误的包,则其后继发过来的包则也将连带着产生格式错误,如果设置 m_DataBufByteCount为0,则可以比较好的避免后继的包受此包的格式错误影响。更好的作法是,在此处开放一个封包格式异常的处理接口 (OnPacketError),由上层逻辑决定对这种异常如何处置。比如上层逻辑可以对封包格式方面出现的异常进行计数,如果错误的次数超过一定的值,
则可以断开该客户端的连接。
  5、建议不要在recv或wsarecv的函数后,就紧接着作以上的处理。当recv收到一段数据后,生成一个结构体或对象(它主要含有 data和len两个内容,前者是数据缓冲区,后者是数据长度),将这样的一个结构体或对象放到一个队列中由后面的线程对其使用SplitFun函数进行 整理。这样,可以最大限度地提高网络数据的接收速度,不至因为数据整理的原因而在此处浪费时间。
  代码中,我已经作了比较详细的注释,可以作为拼包函数的参考,代码是从偶的应用中提取、修改而来,本身只为演示之用,所以未作调试,应用时需要你自己去完善。如有疑问,可以我的blog上留言提出。
posted @&&暗夜教父 阅读(97) |&&|&&
本文作者:sodme 本文出处:
版权声明:本文可以不经作者同意任意转载,但转载时烦请保留文章开始前两行的版权、作者及出处信息。
提示:阅读本文前,请先读此文了解文章背景:
  让无数中国玩家为之瞩目的“魔兽世界”,随着一系列内测前期工作的逐步展开,正在一步步地走近中国玩家,但是,“魔兽”的服务器,却着实让我们为它捏了一把汗。
造成一个网游服务器当机的原因有很多,但主要有以下两种:一,服务器在线人数达到上限,服务器处理效率严重迟缓,造成当机;二,由于外挂或其它游戏作弊 工具导致的非正常数据包的出错,导致游戏服务器逻辑出现混乱,从而造成当机。在这里,我主要想说说后者如何尽可能地避免。
  要避免以上 所说到的第二种情况,我们就应该遵循一个基本原则:在网游服务器的设计中,对于具有较强逻辑关系的处理单元,服务器端和客户端应该采用“互不信任原则”, 即:服务器端即使收到了客户端的数据包,也并不是立刻就认为客户端已经达到了某种功能或者状态,客户端到达是否达到了某种功能或者状态,还必须依靠服务器 端上记载的该客户端“以往状态”来判定,也就是说:服务器端的逻辑执行并不单纯地以“当前”的这一个客户端封包来进行,它还应该广泛参考当前封包的上下文 环境,对执行的逻辑作出更进一步地判定,同时,在单个封包的处理上,服务器端应该广泛考虑当前客户端封包所需要的“前置”封包,如果没有收到该客户端应该
发过来的“前置”封包,则当前的封包应该不进行处理或进行异常处理(如果想要性能高,则可以直接忽略该封包;如果想让服务器稳定,可以进行不同的异常处 理)。
  之所以采用“互不信任”原则设计网游服务器,一个很重要的考虑是:防外挂。对于一个网络服务器(不仅仅是游戏服务器,泛指所有 服务器)而言,它所面对的对象既有属于自己系统内的合法的网络客户端,也有不属于自己系统内的非法客户端访问。所以,我们在考虑服务器向外开放的接口时, 就要同时考虑这两种情况:合法客户端访问时的逻辑走向以及非法客户端访问时的逻辑走向。举个简单的例子:一般情况下,玩家登录逻辑中,都是先向服务器发送 用户名和密码,然后再向服务器发送进入某组服务器的数据包;但在非法客户端(如外挂)中,则这些客户端则完全有可能先发进入某组服务器的数据包。当然,这
里仅仅是举个例子,也许并不妥当,但基本的意思我已经表达清楚了,即:你服务器端不要我客户端发什么你就信什么,你还得进行一系列的逻辑验证,以判定我当 前执行的操作是不是合法的。以这个例子中,服务器端可以通过以下逻辑执行验证功能:只有当客户端的用户名和密码通过验证后,该客户端才会进入在线玩家列表 中。而只有在线玩家列表中的成员,才可以在登陆服务器的引导下进入各分组服务器。
  总之,在从事网游服务器的设计过程中,要始终不移地 坚持一个信念:我们的服务器,不仅仅有自己的游戏客户端在访问,还有其它很多他人写的游戏客户端在访问,所以,我们应该确保我们的服务器是足够强壮的,任 它风吹雨打也不怕,更不会倒。如果在开发实践中,没有很好地领会这一点或者未能将这一思路贯穿进开发之中,那么,你设计出来的服务器将是无比脆弱的。
当然,安全性和效率总是相互对立的。为了实现我们所说的“互不信任”原则,难免的,就会在游戏逻辑中加入很多的异常检测机制,但异常检测又是比较耗时 的,这就需要我们在效率和安全性方面作个取舍,对于特别重要的逻辑,我们应该全面贯彻“互不信任”原则,一步扣一步,步步为营,不让游戏逻辑出现一点漏 洞。而对于并非十分重要的场合,则完全可以采用“半信任”或者根本“不须信任”的原则进行设计,以尽可能地提高服务器效率。
  本文只是对自己长期从事游戏服务器设计以来的感受加以总结,也是对魔兽的服务器有感而发。欢迎有相同感受的朋友或从事相同工作的朋友一起讨论。
posted @&&暗夜教父 阅读(122) |&&|&&
本文作者:sodme 本文出处:
版权声明:本文可以不经作者同意任意转载,但转载时烦请保留文章开始前两行的版权、作者及出处信息。
  QQ游戏于前几日终于突破了百万人同时在线的关口,向着更为远大的目标迈进,这让其它众多传统的棋牌休闲游戏平台黯然失色,相比之下,联众似乎 已经根本不是QQ的对手,因为QQ除了这100万的游戏在线人数外,它还拥有3亿多的注册量(当然很多是重复注册的)以及QQ聊天软件900万的同时在线 率,我们已经可以预见未来由QQ构建起来的强大棋牌休闲游戏帝国。
那么,在技术上,QQ游戏到底是如何实现百万人同时在线并保持游戏高效率的呢?
事实上,针对于任何单一的网络服务器程序,其可承受的同时连接数目是有理论峰值的,通过C++中对TSocket的定义类型:word,我们可以判定 这个连接理论峰值是65535,也就是说,你的单个服务器程序,最多可以承受6万多的用户同时连接。但是,在实际应用中,能达到一万人的同时连接并能保证 正常的数据交换已经是很不容易了,通常这个值都在之间,据说QQ的单台服务器同时连接数目也就是在这个值这间。
如果要实现用户的单服务器同时在线,是不难的。在windows下,比较成熟的技术是采用IOCP--完成端口。与完成端口相关的 资料在网上和CSDN论坛里有很多,感兴趣的朋友可以自己搜索一下。只要运用得当,一个完成端口服务器是完全可以达到2K到5K的同时在线量的。但,5K 这样的数值离百万这样的数值实在相差太大了,所以,百万人的同时在线是单台服务器肯定无法实现的。
要实现百万人同时在线,首先要实现一个比较完善的完成端口服务器模型,这个模型要求至少可以承载2K到5K的同时在线率(当然,如果你MONEY多, 你也可以只开发出最多允许100人在线的服务器)。在构建好了基本的完成端口服务器之后,就是有关服务器组的架构设计了。之所以说这是一个服务器组,是因 为它绝不仅仅只是一台服务器,也绝不仅仅是只有一种类型的服务器。
简单地说,实现百万人同时在线的服务器模型应该是:登陆服务器+大厅服务器+房间服务器。当然,也可以是其它的模型,但其基本的思想是一样的。下面,我将逐一介绍这三类服务器的各自作用。
登陆服务器:一般情况下,我们会向玩家开放若干个公开的登陆服务器,就如QQ登陆时让你选择的从哪个QQ游戏服务器登陆一样,QQ登陆时让玩家选择的 六个服务器入口实际上就是登陆服务器。登陆服务器主要完成负载平衡的作用。详细点说就是,在登陆服务器的背后,有N个大厅服务器,登陆服务器只是用于为当 前的客户端连接选择其下一步应该连接到哪个大厅服务器,当登陆服务器为当前的客户端连接选择了一个合适的大厅服务器后,客户端开始根据登陆服务器提供的信 息连接到相应的大厅上去,同时客户端断开与登陆服务器的连接,为其他玩家客户端连接登陆服务器腾出套接字资源。在设计登陆服务器时,至少应该有以下功
能:N个大厅服务器的每一个大厅服务器都要与所有的登陆服务器保持连接,并实时地把本大厅服务器当前的同时在线人数通知给各个登陆服务器,这其中包括:用 户进入时的同时在线人数增加信息以及用户退出时的同时在线人数减少信息。这里的各个大厅服务器同时在线人数信息就是登陆服务器为客户端选择某个大厅让其登 陆的依据。举例来说,玩家A通过登陆服务器1连接到登陆服务器,登陆服务器开始为当前玩家在众多的大厅服务器中根据哪一个大厅服务器人数比较少来选择一个 大厅,同时把这个大厅的连接IP和端口发给客户端,客户端收到这个IP和端口信息后,根据这个信息连接到此大厅,同时,客户端断开与登陆服务器之间的连
接,这便是用户登陆过程中,在登陆服务器这一块的处理流程。
大厅服务器:大厅服务器,是普通玩家看不到的服务器,它的连接IP和端口信息是登陆服务器通知给客户端的。也就是说,在QQ游戏的本地文件中,具体的 大厅服务器连接IP和端口信息是没有保存的。大厅服务器的主要作用是向玩家发送游戏房间列表信息,这些信息包括:每个游戏房间的类型,名称,在线人数,连 接地址以及其它如游戏帮助文件URL的信息。从界面上看的话,大厅服务器就是我们输入用户名和密码并校验通过后进入的游戏房间列表界面。大厅服务器,主要 有以下功能:一是向当前玩家广播各个游戏房间在线人数信息;二是提供游戏的版本以及下载地址信息;三是提供各个游戏房间服务器的连接IP和端口信息;四是
提供游戏帮助的URL信息;五是提供其它游戏辅助功能。但在这众多的功能中,有一点是最为核心的,即:为玩家提供进入具体的游戏房间的通道,让玩家顺利进 入其欲进入的游戏房间。玩家根据各个游戏房间在线人数,判定自己进入哪一个房间,然后双击服务器列表中的某个游戏房间后玩家开始进入游戏房间服务器。
游戏房间服务器:游戏房间服务器,具体地说就是如“斗地主1”,“斗地主2”这样的游戏房间。游戏房间服务器才是具体的负责执行游戏相关逻辑的服务 器。这样的游戏逻辑分为两大类:一类是通用的游戏房间逻辑,如:进入房间,离开房间,进入桌子,离开桌子以及在房间内说话等;第二类是游戏桌子逻辑,这个 就是各种不同类型游戏的主要区别之处了,比如斗地主中的叫地主或不叫地主的逻辑等,当然,游戏桌子逻辑里也包括有通用的各个游戏里都存在的游戏逻辑,比如 在桌子内说话等。总之,游戏房间服务器才是真正负责执行游戏具体逻辑的服务器。
这里提到的三类服务器,我均采用的是完成端口模型,每个服务器最多连接数目是5000人,但是,我在游戏房间服务器上作了逻辑层的限定,最多只允许 300人同时在线。其他两个服务器仍然允许最多5000人的同时在线。如果按照这样的结构来设计,那么要实现百万人的同时在线就应该是这样:首先是大 厅,0=200。也就是说,至少要200台大厅服务器,但通常情况下,考虑到实际使用时服务器的处理能力和负载情况,应该至少准备 250台左右的大厅服务器程序。另外,具体的各种类型的游戏房间服务器需要多少,就要根据当前玩各种类型游戏的玩家数目分别计算了,比如斗地主最多是十万
人同时在线,每台服务器最多允许300人同时在线,那么需要的斗地主服务器数目就应该不少于:=333,准备得充分一点,就要准备 350台斗地主服务器。
除正常的玩家连接外,还要考虑到:
对于登陆服务器,会有250台大厅服务器连接到每个登陆服务器上,这是始终都要保持的连接;
而对于大厅服务器而言,如果仅仅有斗地主这一类的服务器,就要有350多个连接与各个大厅服务器始终保持着。所以从这一点看,我的结构在某些方面还存在着需要改进的地方,但核心思想是:尽快地提供用户登陆的速度,尽可能方便地让玩家进入游戏中。
posted @&&暗夜教父 阅读(108) |&&|&&
本文作者:sodme
本文出处:
声明:本文可以不经作者同意任意转载、复制、引用。但任何对本文的引用,均须注明本文的作者、出处以及本行声明信息。
  之前,我分析过QQ游戏(特指QQ休闲平台,并非QQ堂,下同)的通信架构(),分析过魔兽世界的通信架构(),
似乎网络游戏的通信架构也就是这些了,其实不然,在网络游戏大家庭中,还有一种类型的游戏我认为有必要把它的通信架构专门作个介绍,这便是如泡泡堂、QQ 堂类的休闲类竞技游戏。曾经很多次,被网友们要求能抽时间看看泡泡堂之类游戏的通信架构,这次由于被逼交作业,所以今晚抽了一点的时间截了一下泡泡堂的 包,正巧昨日与网友就泡泡堂类游戏的通信架构有过一番讨论,于是,将这两天的讨论、截包及思考总结于本文中,希望能对关心或者正在开发此类游戏的朋友有所 帮助,如果要讨论具体的技术细节,请到我的BLOG()加我的MSN讨论..
  总体来说,泡泡堂类游戏(此下简称泡泡堂)在大厅到房间这一层的通信架构,其结构与QQ游戏相当,甚至要比QQ游戏来得简单。所以,在房间这一层的通信架构上,我不想过多讨论,不清楚的朋友请参看我对QQ游戏通信架构的分析文章()。可以这么说,如果采用与QQ游戏相同的房间和大厅架构,是完全可以组建起一套可扩展的支持百万人在线的游戏系统的。也就是说,通过负载均衡+大厅+游戏房间对游戏逻辑的分摊,完全可以实现一个可扩展的百万人在线泡泡堂。
  但是,泡泡堂与斗地主的最大不同点在于:泡泡堂对于实时性要求特别高。那么,泡泡堂是如何解决实时性与网络延迟以及大用户量之间矛盾的呢?
  阅读以下文字前,请确认你已经完全理解TCP与UDP之间的不同点。
  我们知道,TCP与UDP之间的最大不同点在于:TCP是可靠连接的,而UDP是无连接的。如果通信双方使用TCP协议,那么他们之前必须事先 通过监听+连接的方式将双方的通信管道建立起来;而如果通信双方使用的是UDP通信,则双方不用事先建立连接,发送方只管向目标地址上的目标端口发送 UDP包即可,不用管对方到底收没收到。如果要说形象点,可以用这样一句话概括:TCP是打电话,UDP是发电报。TCP通信,为了保持这样的可靠连接, 在可靠性上下了很多功夫,所以导致了它的通信效率要比UDP差很多,所以,一般地,在地实时性要求非常高的场合,会选择使用UDP协议,比如常见的动作射
击类游戏。
  通过载包,我们发现泡泡堂中同时采用了TCP和UDP两种通信协议。并且,具有以下特点:
1.当玩家未进入具体的游戏地图时,仅有TCP通信存在,而没有UDP通信;
2.进入游戏地图后,TCP的通信量远远小于UDP的通信量
3.UDP的通信IP个数,与房间内的玩家成一一对应关系(这一点,应网友疑惑而加,此前已经证实)
  以上是几个表面现象,下面我们来分析它的本质和内在。^&^
  泡泡堂的游戏逻辑,简单地可以归纳为以下几个方面:
1.玩家移动
2.玩家埋地雷(如果你觉得这种叫法比较土,你也可以叫它:下泡泡,呵呵)
3.地雷爆炸出道具或者地雷爆炸困住另一玩家
4.玩家捡道具或者玩家消灭/解救一被困的玩家
  与MMORPG一样,在上面的几个逻辑中,广播量最大的其实是玩家移动。为了保持玩家画面同步,其他玩家的每一步移动消息都要即时地发给其它玩家。
  通常,网络游戏的逻辑控制,绝大多数是在服务器端的。有时,为了保证画面的流畅性,我们会有意识地减少服务器端的逻辑判断量和广播量,当然,这 个减少,是以“不危及游戏的安全运行”为前提的。到底如何在效率、流畅性和安全性之间作取舍,很多时候是需要经验积累的,效率提高的过程,就是逻辑不断优 化的过程。不过,有一个原则是可以说的,那就是:“关键逻辑”一定要放在服务器上来判断。那么,什么是“关键逻辑”呢?
  拿泡泡堂来说,下面的这个逻辑,我认为就是关键逻辑:玩家在某处埋下一颗地雷,地雷爆炸后到底能不能炸出道具以及炸出了哪些道具,这个信息,需要服务器来给。那么,什么又是“非关键逻辑”呢?
  “非关键逻辑”,在不同的游戏中,会有不同的概念。在通常的MMORPG中,玩家移动逻辑的判断,是算作关键逻辑的,否则,如果服务器端不对客 户端发过来的移动包进行判断那就很容易造成玩家的瞬移以及其它毁灭性的灾难。而在泡泡堂中,玩家移动逻辑到底应不应该算作关键逻辑还是值得考虑的。泡泡堂 中的玩家可以取胜的方法,通常是确实因为打得好而赢得胜利,不会因为瞬移而赢得胜利,因为如果外挂要作泡泡堂的瞬移,它需要考虑的因素和判断的逻辑太多 了,由于比赛进程的瞬息万变,外挂的瞬移点判断不一定就比真正的玩家来得准确,所在,在玩家移动这个逻辑上使用外挂,在泡泡堂这样的游戏中通常是得不偿失
的(当然,那种特别变态的高智能的外挂除外)。从目前我查到的消息来看,泡泡堂的外挂多数是一些按键精灵脚本,它的本质还不是完全的游戏机器人,并不是通 过纯粹的协议接管实现的外挂功能。这也从反面验证了我以上的想法。
  说到这里,也许你已经明白了。是的!TCP通信负责“关键逻辑”,而UDP通信负责“非关键逻辑”,这里的“非关键逻辑”中就包含了玩家移动。 在泡泡堂中,TCP通信用于本地玩家与服务器之间的通信,而UDP则用于本地玩家与同一地图中的其他各玩家的通信。当本地玩家要移动时,它会同时向同一地 图内的所有玩家广播自己的移动消息,其他玩家收到这个消息后会更新自己的游戏画面以实现画面同步。而当本地玩家要在地图上放置一个炸弹时,本地玩家需要将 此消息同时通知同一地图内的其他玩家以及服务器,甚至这里,可以不把放置炸弹的消息通知给服务器,而仅仅通知其他玩家。当炸弹爆炸后,要拾取物品时才向服
务器提交拾取物品的消息。
  那么,你可能会问,“地图上某一点是否存在道具”这个消息,服务器是什么时候通知给客户端的呢?这个问题,可以有两种解决方案:
1.客户端如果在放置炸弹时,将放置炸弹的消息通知给服务器,服务器可以在收到这个消息后,告诉客户端炸弹爆炸后会有哪些道具。但我觉得这种方案不好,因为这样作会增加游戏运行过程中的数据流量。
2.而这第2种方案就是,客户端进入地图后,游戏刚开始时,就由服务器将本地图内的各道具所在点的信息传给各客户端,这样,可以省去两方面的开 销:a.客户端放炸弹时,可以不通知服务器而只通知其它玩家;b.服务器也不用在游戏运行过程中再向客户端传递有关某点有道具的信息。
但是,不管采用哪种方案,服务器上都应该保留一份本地图内道具所在点的信息。因为服务器要用它来验证一个关键逻辑:玩家拾取道具。当玩家要在某点拾取道具时,服务器必须要判定此点是否有道具,否则,外挂可以通过频繁地发拾取道具的包而不断取得道具。
  至于泡泡堂其它游戏逻辑的实现方法,我想,还是要依靠这个原则:首先判断这个逻辑是关键逻辑吗?如果不全是,那其中的哪部分是非关键逻辑呢?对 于非关键逻辑,都可以交由客户端之间(UDP)去自行完成。而对于关键逻辑,则必须要有服务器(TCP)的校验和认证。这便是我要说的。
  以上仅仅是在理论上探讨关于泡泡堂类游戏在通信架构上的可能作法,这些想法是没有事实依据的,所有结论皆来源于对封包的分析以及个人经验,文章 的内容和观点可能跟真实的泡泡堂通信架构实现有相当大的差异,但我想,这并不是主要的,因为我的目的是向大家介绍这样的TCP和UDP通信并存情况下,如 何对游戏逻辑的进行取舍和划分。无论是“关键逻辑”的定性,还是“玩家移动”的具体实施,都需要开发者在具体的实践中进行总结和优化。此文全当是一个引子 罢,如有疑问,请加Msn讨论。
posted @&&暗夜教父 阅读(142) |&&|&&
本文作者:sodme
本文出处:
声明:本文可以不经作者同意任意转载,但任何对本文的引用都须注明作者、出处及此声明信息。谢谢!!
  要了解此篇文章中引用的本人写的另一篇文章,请到以下地址:
以上的这篇文章是早在去年的时候写的了,当时正在作休闲平台,一直在想着如何实现一个可扩充的支持百万人在线的游戏平台,后来思路有了,就写了那篇总结。文章的意思,重点在于阐述一个百万级在线的系统是如何实施的,倒没真正认真地考察过QQ游戏到底是不是那样实现的。
  近日在与业内人士讨论时,提到QQ游戏的实现方式并不是我原来所想的那样,于是,今天又认真抓了一下QQ游戏的包,结果确如这位兄弟所言,QQ 游戏的架构与我当初所设想的那个架构相差确实不小。下面,我重新给出QQ百万级在线的技术实现方案,并以此展开,谈谈大型在线系统中的负载均衡机制的设 计。
  从QQ游戏的登录及游戏过程来看,QQ游戏中,也至少分为三类服务器。它们是:
第一层:登陆/账号服务器(Login Server),负责验证用户身份、向客户端传送初始信息,从QQ聊天软件的封包常识来看,这些初始信息可能包括“会话密钥”此类的信息,以后客户端与后续服务器的通信就使用此会话密钥进行身份验证和信息加密;
第二层:大厅服务器(估且这么叫吧, Game Hall Server),负责向客户端传递当前游戏中的所有房间信息,这些房间信息包括:各房间的连接IP,PORT,各房间的当前在线人数,房间名称等等。
第三层:游戏逻辑服务器(Game Logic Server),负责处理房间逻辑及房间内的桌子逻辑。
  从静态的表述来看,以上的三层结构似乎与我以前写的那篇文章相比并没有太大的区别,事实上,重点是它的工作流程,QQ游戏的通信流程与我以前的设想可谓大相径庭,其设计思想和技术水平确实非常优秀。具体来说,QQ游戏的通信过程是这样的:
  1.由Client向Login Server发送账号及密码等登录消息,Login Server根据校验结果返回相应信息。可以设想的是,如果Login Server通过了Client的验证,那么它会通知其它Game Hall Server或将通过验证的消息以及会话密钥放在Game Hall Server也可以取到的地方。总之,Login Server与Game Hall Server之间是可以共享这个校验成功消息的。一旦Client收到了Login Server返回成功校验的消息后,Login Server会主动断开与Client的连接,以腾出socket资源。Login
Server的IP信息,是存放在QQGame\config\QQSvrInfo.ini里的。
  2.Client收到Login Server的校验成功等消息后,开始根据事先选定的游戏大厅入口登录游戏大厅,各个游戏大厅Game Hall Server的IP及Port信息,是存放在QQGame\Dirconfig.ini里的。Game Hall Server收到客户端Client的登录消息后,会根据一定的策略决定是否接受Client的登录,如果当前的Game Hall Server已经到了上限或暂时不能处理当前玩家登录消息,则由Game Hall Server发消息给Client,以让Client重定向到另外的Game
Hall Server登录。重定向的IP及端口信息,本地没有保存,是通过数据包或一定的算法得到的。如果当前的Game Hall Server接受了该玩家的登录消息后,会向该Client发送房间目录信息,这些信息的内容我上面已经提到。目录等消息发送完毕后,Game Hall Server即断开与Client的连接,以腾出socket资源。在此后的时间里,Client每隔30分钟会重新连接Game Hall Server并向其索要最新的房间目录信息及在线人数信息。
  3.Client根据列出的房间列表,选择某个房间进入游戏。根据我的抓包结果分析,QQ游戏,并不是给每一个游戏房间都分配了一个单独的端口 进行处理。在QQ游戏里,有很多房间是共用的同一个IP和同一个端口。比如,在斗地主一区,前50个房间,用的都是同一个IP和Port信息。这意味着, 这些房间,在QQ游戏的服务器上,事实上,可能是同一个程序在处理!!!QQ游戏房间的人数上限是400人,不难推算,QQ游戏单个服务器程序的用户承载 量是2万,即QQ的一个游戏逻辑服务器程序最多可同时与2万个玩家保持TCP连接并保证游戏效率和品质,更重要的是,这样可以为腾讯省多少money
呀!!!哇哦!QQ确实很牛。以2万的在线数还能保持这么好的游戏品质,确实不容易!QQ游戏的单个服务器程序,管理的不再只是逻辑意义上的单个房间,而 可能是许多逻辑意义上的房间。其实,对于服务器而言,它就是一个大区服务器或大区服务器的一部分,我们可以把它理解为一个庞大的游戏地图,它实现的也是分 块处理。而对于每一张桌子上的打牌逻辑,则是有一个统一的处理流程,50个房间的50*100张桌子全由这一个服务器程序进行处理(我不知道QQ游戏的具 体打牌逻辑是如何设计的,我想很有可能也是分区域的,分块的)。当然,以上这些只是服务器作的事,针对于客户端而言,客户端只是在表现上,将一个个房间单
独罗列了出来,这样作,是为便于玩家进行游戏以及减少服务器的开销,把这个大区中的每400人放在一个集合内进行处理(比如聊天信息,“向400人广播” 和“向2万人广播”,这是完全不同的两个概念)。
  4.需要特别说明的一点。进入QQ游戏房间后,直到点击某个位置坐下打开另一个程序界面,客户端的程序,没有再创建新的socket,而仍然使 用原来大厅房间客户端跟游戏逻辑服务器交互用的socket。也就是说,这是两个进程共用的同一个socket!不要小看这一点。如果你在创建桌子客户端 程序后又新建了一个新的socket与游戏逻辑服务器进行通信,那么由此带来的玩家进入、退出、逃跑等消息会带来非常麻烦的数据同步问题,俺在刚开始的时 候就深受其害。而一旦共用了同一个socket后,你如果退出桌子,服务器不涉及释放socket的问题,所以,这里就少了很多的数据同步问题。关于多个
进程如何共享同一个socket的问题,请去google以下内容:WSADuplicateSocket。
  以上便是我根据最新的QQ游戏抓包结果分析得到的QQ游戏的通信流程,当然,这个流程更多的是客户端如何与服务器之间交互的,却没有涉及到服务器彼此之间是如何通信和作数据同步的。关于服务器之间的通信流程,我们只能基于自己的经验和猜想,得出以下想法:
  1.Login Server与Game Hall Server之前的通信问题。Login Server是负责用户验证的,一旦验证通过之后,它要设法让Game Hall Server知道这个消息。它们之前实现信息交流的途径,我想可能有这样几条:a. Login Server将通过验证的用户存放到临时数据库中;b. Login Server将验证通过的用户存放在内存中,当然,这个信息,应该是全局可访问的,就是说所有QQ的Game Hall Server都可以通过服务器之间的数据包通信去获得这样的信息。
  2.Game Hall Server的最新房间目录信息的取得。这个信息,是全局的,也就是整个游戏中,只保留一个目录。它的信息来源,可以由底层的房间服务器逐级报上来,报给谁?我认为就如保存的全局登录列表一样,它报给保存全局登录列表的那个服务器或数据库。
  3.在QQ游戏中,同一类型的游戏,无法打开两上以上的游戏房间。这个信息的判定,可以根据全局信息来判定。
  以上关于服务器之间如何通信的内容,均属于个人猜想,QQ到底怎么作的,恐怕只有等大家中的某一位进了腾讯之后才知道了。呵呵。不过,有一点是 可以肯定的,在整个服务器架构中,应该有一个地方是专门保存了全局的登录玩家列表,只有这样才能保证玩家不会重复登录以及进入多个相同类型的房间。
  在前面的描述中,我曾经提到过一个问题:当登录当前Game Hall Server不成功时,QQ游戏服务器会选择让客户端重定向到另位的服务器去登录,事实上,QQ聊天服务器和MSN服务器的登录也是类似的,它也存在登录重定向问题。
  那么,这就引出了另外的问题,由谁来作这个策略选择?以及由谁来提供这样的选择资源?这样的处理,便是负责负载均衡的服务器的处理范围了。由QQ游戏的通信过程分析派生出来的针对负责均衡及百万级在线系统的更进一步讨论,将在下篇文章中继续。
  在此,特别感谢网友tilly及某位不便透露姓名的网友的讨论,是你们让我决定认真再抓一次包探个究竟。
posted @&&暗夜教父 阅读(116) |&&|&&
一直以来,flash就是我非常喜爱的平台,
因为他简单,完整,但是功能强大,
很适合游戏软件的开发,
只不过处理复杂的算法和海量数据的时候,
速度慢了一些,
但是这并不意味着flash不能做,
我们需要变通的方法去让flash做不善长的事情,
这个贴子用来专门讨论用flash作为客户端来开发网络游戏,
持续时间也不会很长,在把服务器端的源代码公开完以后,
就告一段落,
注意,仅仅用flash作为客户端,
服务器端,我们使用vc6,
我将陆续的公开服务器端的源代码和大家共享,
并且将讲解一些网络游戏开发的原理,
希望对此感兴趣的朋友能够使用今后的资源或者理论开发出完整的网络游戏。
我们从简单到复杂,
从棋牌类游戏到动作类的游戏,
从2个人的游戏到10个人的游戏,
因为工作忙的关系,我所做的一切仅仅起到抛砖引玉的作用,
希望大家能够热情的讨论,为中国的flash事业垫上一块砖,添上一片瓦。
现在的大型网络游戏(mmo game)都是基于server/client体系结构的,
server端用c(windows下我们使用vc.net+winsock)来编写,
客户端就无所谓,
在这里,我们讨论用flash来作为客户端的实现,
实践证明,flash的xml socket完全可以胜任网络传输部分,
在别的贴子中,我看见有的朋友谈论msn中的flash game
他使用msn内部的网络接口进行传输,
这种做法也是可以的,
我找很久以前对于2d图形编程的说法,&给我一个打点函数,我就能创造整个游戏世界&,
而在网络游戏开发过程中,&给我一个发送函数和一个接收函数,我就能创造网络游戏世界.&
我们抽象一个接口,就是网络传输的接口,
对于使用flash作为客户端,要进行网络连接,
一个网络游戏的客户端,
可以简单的抽象为下面的流程
1.与远程服务器建立一条长连接
2.用账号密码登陆
我们可以直接使用flash 的xml socket,也可以使用类似msn的那种方式,
这些我们先不管,我们先定义接口,
Connect( &127.0.0.1&, 20000 ); 连接远程服务器,建立一条长连接
Send( data, len ); 向服务器发送一条消息
Recv( data, len ); 接收服务器传来的消息
项目开发的基本硬件配置
一台普通的pc就可以了,
安装好windows 2000和vc6就可以了,
然后连上网,局域网和internet都可以,
接下去的东西我都简化,不去用晦涩的术语,
既然是网络,我们就需要网络编程接口,
服务器端我们用的是winsock 1.1,使用tcp连接方式,
[tcp和udp]
tcp可以理解为一条连接两个端子的隧道,提供可靠的数据传输服务,
只要发送信息的一方成功的调用了tcp的发送函数发送一段数据,
我们可以认为接收方在若干时间以后一定会接收到完整正确的数据,
不需要去关心网络传输上的细节,
而udp不保证这一点,
对于网络游戏来说,tcp是普遍的选择。
[阻塞和非阻塞]
在通过socket发送数据时,如果直到数据发送完毕才返回的方式,也就是说如果我们使用send( buffer, 100.....)这样的函数发送100个字节给别人,我们要等待,直到100个自己发送完毕,程序才往下走,这样就是阻塞的,
而非阻塞的方式,当你调用send(buffer,100....)以后,立即返回,此时send函数告诉你发送成功,并不意味着数据已经向目的地发送完 毕,甚至有可能数据还没有开始发送,只被保留在系统的缓冲里面,等待被发送,但是你可以认为数据在若干时间后,一定会被目的地完整正确的收到,我们要充分 的相信tcp。
阻塞的方式会引起系统的停顿,一般网络游戏里面使用的都是非阻塞的方式,
[有状态服务器和无状态服务器]
在c/s体系中,如果server不保存客户端的状态,称之为无状态,反之为有状态,
在这里要强调一点,
我们所说的服务器不是一台具体的机器,
而是指服务器应用程序,
一台具体的机器或者机器群组可以运行一个或者多个服务器应用程序,
我们的网络游戏使用的是有状态服务器,
保存所有玩家的数据和状态,
一些有必要了解的理论和开发工具
[开发语言]
我们首先要熟练的掌握一门开发语言,
学习c++是非常有必要的,
而vc是windows下面的软件开发工具,
为什么选择vc,可能与我本身使用vc有关,
而且网上可以找到许多相关的资源和源代码,
[操作系统]
我们使用windows2000作为服务器的运行环境,
所以我们有必要去了解windows是如何工作的,
同时对它的编程原理应该熟练的掌握
[数据结构和算法]
要写出好的程序要先具有设计出好的数据结构和算法的能力,
好的算法未必是繁琐的公式和复杂的代码,
我们要找到又好写有满足需求的算法,
有时候,最笨的方法同时也是很好的方法,
很多程序员沉迷于追求精妙的算法而忽略了宏观上的工程,
花费了大量的精力未必能够取得好的效果,
举个例子,
我当年进入游戏界工作,学习老师的代码,
发现有个函数,要对画面中的npc位置进行排序,
确定哪个先画,那个后画,
他的方法太“笨”,
任何人都会想到的冒泡,
一个一个去比较,没有任何的优化,
我当时想到的算法就有很多,
而且有一大堆优化策略,
可是,当我花了很长时间去实现我的算法时,
发现提升的那么一点效率对游戏整个运行效率而言几乎是没起到什么作用,
或者说虽然算法本身快了几倍,
可是那是多余的,老师的算法虽然“笨”,
可是他只花了几十行代码就搞定了,
他的时间花在别的更需要的地方,
这就是他可以独自完成一个游戏,
而我可以把一个函数优化100倍也只能打杂的原因
[tcp/ip的理论]
推荐数据用tcp/ip进行网际互连,tcp/ip详解,
这是两套书,共有6卷,
都是国外的大师写的,
可以说是必读的,
网络传输中的“消息”
消息是个很常见的术语,
在windows中,消息机制是个十分重要的概念,
我们在网络游戏中,也使用了消息这样的机制,
一般我们这么做,
一个数据块,头4个字节是消息名,后面接2个字节的数据长度,
再后面就是实际的数据
为什么使用消息??
我们来看看例子,
在游戏世界,
一个玩家想要和别的玩家聊天,
那么,他输入好聊天信息,
客户端生成一条聊天消息,
并把聊天的内容打包到消息中,
然后把聊天消息发送给服务器,
请求服务器把聊天信息发送给另一个玩家,
服务器接收到一条消息,
此刻,服务器并不知道当前的数据是什么东西,
对于服务器来讲,这段数据仅仅来自于网络通讯的底层,
不加以分析的话,没有任何的信息,
因为我们的通讯是基于消息机制的,
我们认为服务器接收到的任何数据都是基于消息的数据方式组织的,
4个字节消息名,2字节长度,这个是不会变的,
通过消息名,服务器发现当前数据是一条聊天数据,
通过长度把需要的数据还原,校验,
然后把这条消息发送给另一个玩家,
大家注意,消息是变长的,
关于消息的解释完全在于服务器和客户端的应用程序,
可以认为与网络传输低层无关,
比如一条私聊消息可能是这样的,
MsgID:4 byte
Length:2 byte
TargetPlayerID:2 byte
String:anybyte & 256
一条移动消息可能是这样的,
MsgID:4 byte
Length:2 byte
TargetPlayerID:2 byte
TargetPosition:4 byte (x,y)
编程者可以自定义消息的内容以满足不同的需求
队列是一个很重要的数据结构,
比如说消息队列,
服务器或者客户端,
发送的消息不一定是立即发送的,
而是等待一个适当时间,
或者系统规定的时间间隔以后才发送,
这样就需要创建一个消息队列,以保存发送的消息,
消息队列的大小可以按照实际的需求创建,
队列又可能会满,
当队列满了,可以直接丢弃消息,
如果你觉得这样不妥,
也可以预先划分一个足够大的队列,
可以使用一个系统全局的大的消息队列,
也可以为每个对象创建一个消息队列,
这个我们的一个数据队列的实现,
开发工具vc.net,使用了C++的模板,
关于队列的算法和基础知识,我就不多说了,
DataBuffer.h
#ifndef __DATABUFFER_H__
#define __DATABUFFER_H__
#include &windows.h&
#include &assert.h&
#include &g_assert.h&
#include &stdio.h&
#ifndef HAVE_BYTE
#endif // HAVE_BYTE
//数据队列管理类
template &const int _max_line, const int _max_size&
class DataBufferTPL
bool Add( byte *data ) // 加入队列数据
G_ASSERT_RET( data, false );
m_ControlStatus =
if( IsFull() )&
//assert( false );
memcpy( m_s_ptr, data, _max_size );
NextSptr();
m_NumData++;
m_ControlStatus =
bool Get( byte *data ) // 从队列中取出数据
G_ASSERT_RET( data, false );
m_ControlStatus =
if( IsNull() )&
memcpy( data, m_e_ptr, _max_size );
NextEptr();
m_NumData--;
m_ControlStatus =
bool CtrlStatus() // 获取操作成功结果
return m_ControlS
int GetNumber() // 获得现在的数据大小
return m_NumD
DataBufferTPL()
m_NumData = 0;
m_start_ptr = m_DataTeam[0];
m_end_ptr = m_DataTeam[_max_line-1];
m_s_ptr = m_start_
m_e_ptr = m_start_
~DataBufferTPL()
m_NumData = 0;
m_s_ptr = m_start_
m_e_ptr = m_start_
bool IsFull() // 是否队列满
G_ASSERT_RET( m_NumData &=0 && m_NumData &= _max_line, false );
if( m_NumData == _max_line )&
bool IsNull() // 是否队列空
G_ASSERT_RET( m_NumData &=0 && m_NumData &= _max_line, false );
if( m_NumData == 0 )
void NextSptr() // 头位置增加
assert(m_start_ptr);
assert(m_end_ptr);
assert(m_s_ptr);
assert(m_e_ptr);
m_s_ptr += _max_
if( m_s_ptr & m_end_ptr )
m_s_ptr = m_start_
void NextEptr() // 尾位置增加
assert(m_start_ptr);
assert(m_end_ptr);
assert(m_s_ptr);
assert(m_e_ptr);
m_e_ptr += _max_
if( m_e_ptr & m_end_ptr )
m_e_ptr = m_start_
byte m_DataTeam[_max_line][_max_size]; //数据缓冲
int m_NumD //数据个数
bool m_ControlS //操作结果
byte *m_start_ //起始位置
byte *m_end_ //结束位置
byte *m_s_ //排队起始位置
byte *m_e_ //排队结束位置
//////////////////////////////////////////////////////////////////////////
// 放到这里了!
//ID自动补位列表模板,用于自动列表,无间空顺序列表。
template &const int _max_count&
class IDListTPL
// 清除重置
void Reset()&
for(int i=0;i&_max_i++)
m_dwList[i] = G_ERROR;
m_counter = 0;
int MaxSize() const { return _max_ }
int Count() const { return m_ }
const DWORD operator[]( int iIndex ) {&
G_ASSERTN( iIndex &= 0 && iIndex & m_counter );
return m_dwList[ iIndex ];&
bool New( DWORD dwID )
G_ASSERT_RET( m_counter &= 0 && m_counter & _max_count, false );
//ID 唯一性,不能存在相同ID
if ( Find( dwID ) != -1 )&
m_dwList[m_counter] = dwID;
m_counter++;
// 没有Assert的加入ID功能
bool Add( DWORD dwID )
if( m_counter &0 || m_counter &= _max_count )&
//ID 唯一性,不能存在相同ID
if ( Find( dwID ) != -1 )&
m_dwList[m_counter] = dwID;
m_counter++;
bool Del( int iIndex )
G_ASSERT_RET( iIndex &=0 && iIndex & m_counter, false );
for(int k=iIk&m_counter-1;k++)
m_dwList[k] = m_dwList[k+1];
m_dwList[k] = G_ERROR;
m_counter--;
int Find( DWORD dwID )
for(int i=0;i&m_i++)
if( m_dwList[i] == dwID )&
return -1;
IDListTPL():m_counter(0)&
for(int i=0;i&_max_i++)
m_dwList[i] = G_ERROR;
virtual ~IDListTPL()&
DWORD m_dwList[_max_count];
//////////////////////////////////////////////////////////////////////////
#endif //__DATABUFFER_H__
我们采用winsock作为网络部分的编程接口,
接下去编程者有必要学习一下socket的基本知识,
不过不懂也没有关系,我提供的代码已经把那些麻烦的细节或者正确的系统设置给弄好了,
编程者只需要按照规则编写游戏系统的处理代码就可以了,
这些代码在vc6下编译通过,
是通用的网络传输底层,
这里是socket部分的代码,
我们需要安装vc6才能够编译以下的代码,
因为接下去我们要接触越来越多的c++,
所以,大家还是去看看c++的书吧,
// socket.h
#ifndef _socket_h
#define _socket_h
#pragma once
//定义最大连接用户数目 ( 最大支持 512 个客户连接 )
#define MAX_CLIENTS 512
//#define FD_SETSIZE MAX_CLIENTS
#pragma comment( lib, &wsock32.lib& )
#include &winsock.h&
class CSocketCtrl
void SetDefaultOpt();
CSocketCtrl(): m_sockfd(INVALID_SOCKET){}
BOOL StartUp();
BOOL ShutDown();
BOOL IsIPsChange();
BOOL CanWrite();
BOOL HasData();
int Recv( char* pBuffer, int nSize, int nFlag );
int Send( char* pBuffer, int nSize, int nFlag );
BOOL Create( UINT uPort );
BOOL Create(void);
BOOL Connect( LPCTSTR lpszHostAddress, UINT nHostPort );
void Close();
BOOL Listen( int nBackLog );
BOOL Accept( CSocketCtrl& sockCtrl );
BOOL RecvMsg( char *sBuf );
int SendMsg( char *sBuf,unsigned short stSize );
SOCKET GetSockfd(){ return m_ }
BOOL GetHostName( char szHostName[], int nNameLength );
protected:
static DWORD m_dwConnectO
static DWORD m_dwReadO
static DWORD m_dwWriteO
static DWORD m_dwAcceptO
static DWORD m_dwReadB
static DWORD m_dwWriteB
// socket.cpp
#include &stdio.h&
#include &msgdef.h&
#include &socket.h&
// 吊线时间
#define ALL_TIMEOUT 120000
DWORD CSocketCtrl::m_dwConnectOut = 60000;
DWORD CSocketCtrl::m_dwReadOut = ALL_TIMEOUT;
DWORD CSocketCtrl::m_dwWriteOut = ALL_TIMEOUT;
DWORD CSocketCtrl::m_dwAcceptOut = ALL_TIMEOUT;
DWORD CSocketCtrl::m_dwReadByte = 0;
DWORD CSocketCtrl::m_dwWriteByte = 0;
// 接收数据
BOOL CSocketCtrl::RecvMsg( char *sBuf )
if( !HasData() )
return FALSE;
int nbRead = this-&Recv( (char*)&header, sizeof( header ), MSG_PEEK );
if( nbRead == SOCKET_ERROR )
return FALSE;
if( nbRead & sizeof( header ) )
this-&Recv( (char*)&header, nbRead, 0 );
printf( &\ninvalid msg, skip %ld bytes.&, nbRead );
return FALSE;
if( this-&Recv( (char*)sBuf, header.stLength, 0 ) != header.stLength )
return FALSE;
return TRUE;
// 发送数据
int CSocketCtrl::SendMsg( char *sBuf,unsigned short stSize )
static char sSendBuf[ 4000 ];
memcpy( sSendBuf,&stSize,sizeof(short) );
memcpy( sSendBuf + sizeof(short),sBuf,stSize );
if( (sizeof(short) + stSize) != this-&Send( sSendBuf,stSize+sizeof(short),0 ) )
return -1;
return stS
// 启动winsock
BOOL CSocketCtrl::StartUp()
WSADATA wsaD
WORD wVersionRequested = MAKEWORD( 1, 1 );
int err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )&
return FALSE;
return TRUE;
// 关闭winsock
BOOL CSocketCtrl::ShutDown()
WSACleanup();
return TRUE;
// 得到主机名
BOOL CSocketCtrl::GetHostName( char szHostName[], int nNameLength )
if( gethostname( szHostName, nNameLength ) != SOCKET_ERROR )
return TRUE;
return FALSE;
BOOL CSocketCtrl::IsIPsChange()
return FALSE;
static int iIPNum = 0;
char sHost[300];
hostent *pH
if( gethostname(sHost,299) != 0 )
return FALSE;
pHost = gethostbyname(sHost);
psHost = pHost-&h_addr_list[i++];
if( psHost == 0 )
}while(1);
if( iIPNum != i )
return TRUE;
return FALSE;
// socket是否可以写
BOOL CSocketCtrl::CanWrite()
tout.tv_sec = 0;
tout.tv_usec = 0;
FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,NULL,&set,NULL,&tout);
if(e==SOCKET_ERROR) return FALSE;
if(e&0) return TRUE;
return FALSE;
// socket是否有数据
BOOL CSocketCtrl::HasData()
tout.tv_sec = 0;
tout.tv_usec = 0;
FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,&set,NULL,NULL,&tout);
if(e==SOCKET_ERROR) return FALSE;
if(e&0) return TRUE;
return FALSE;
int CSocketCtrl::Recv( char* pBuffer, int nSize, int nFlag )
return recv( m_sockfd, pBuffer, nSize, nFlag );
int CSocketCtrl::Send( char* pBuffer, int nSize, int nFlag )
return send( m_sockfd, pBuffer, nSize, nFlag );
BOOL CSocketCtrl::Create( UINT uPort )
m_sockfd=::socket(PF_INET,SOCK_STREAM,0);
if(m_sockfd==INVALID_SOCKET) return FALSE;
SOCKADDR_IN SockA
memset(&SockAddr,0,sizeof(SockAddr));
SockAddr.sin_family = AF_INET;
SockAddr.sin_addr.s_addr = INADDR_ANY;
SockAddr.sin_port = ::htons( uPort );
if(!::bind(m_sockfd,(SOCKADDR*)&SockAddr, sizeof(SockAddr)))&
SetDefaultOpt();
return TRUE;
return FALSE;
void CSocketCtrl::Close()
::closesocket( m_sockfd );
m_sockfd = INVALID_SOCKET;
BOOL CSocketCtrl::Connect( LPCTSTR lpszHostAddress, UINT nHostPort )
if(m_sockfd==INVALID_SOCKET) return FALSE;
SOCKADDR_IN sockA
memset(&sockAddr,0,sizeof(sockAddr));
LPSTR lpszAscii=(LPSTR)lpszHostA
sockAddr.sin_family=AF_INET;
sockAddr.sin_addr.s_addr=inet_addr(lpszAscii);
if(sockAddr.sin_addr.s_addr==INADDR_NONE)
lphost = ::gethostbyname(lpszAscii);
if(lphost!=NULL)
sockAddr.sin_addr.s_addr = ((IN_ADDR *)lphost-&h_addr)-&s_
else return FALSE;
sockAddr.sin_port = htons((u_short)nHostPort);
int r=::connect(m_sockfd,(SOCKADDR*)&sockAddr,sizeof(sockAddr));
if(r!=SOCKET_ERROR) return TRUE;
e=::WSAGetLastError();
if(e!=WSAEWOULDBLOCK) return FALSE;
tout.tv_sec = 0;
tout.tv_usec = 100000;
while( n& CSocketCtrl::m_dwConnectOut)
FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,NULL,&set,NULL, &tout);
if(e==SOCKET_ERROR) return FALSE;
if(e&0) return TRUE;
if( IsIPsChange() )
return FALSE;
n += 100;
return FALSE;
// 设置监听socket
BOOL CSocketCtrl::Listen( int nBackLog )
if( m_sockfd == INVALID_SOCKET ) return FALSE;
if( !listen( m_sockfd, nBackLog) ) return TRUE;
return FALSE;
// 接收一个新的客户连接
BOOL CSocketCtrl::Accept( CSocketCtrl& ms )
if( m_sockfd == INVALID_SOCKET ) return FALSE;
if( ms.m_sockfd != INVALID_SOCKET ) return FALSE;
tout.tv_sec = 0;
tout.tv_usec = 100000;
while(n& CSocketCtrl::m_dwAcceptOut)
//if(stop) return FALSE;
FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,&set,NULL,NULL, &tout);
if(e==SOCKET_ERROR) return FALSE;
n += 100;
if( n&= CSocketCtrl::m_dwAcceptOut ) return FALSE;
ms.m_sockfd=accept(m_sockfd,NULL,NULL);
if(ms.m_sockfd==INVALID_SOCKET) return FALSE;
ms.SetDefaultOpt();
return TRUE;
BOOL CSocketCtrl::Create(void)
m_sockfd=::socket(PF_INET,SOCK_STREAM,0);
if(m_sockfd==INVALID_SOCKET) return FALSE;
SOCKADDR_IN SockA
memset(&SockAddr,0,sizeof(SockAddr));
SockAddr.sin_family = AF_INET;
SockAddr.sin_addr.s_addr = INADDR_ANY;
SockAddr.sin_port = ::htons(0);
//if(!::bind(m_sock,(SOCKADDR*)&SockAddr, sizeof(SockAddr)))&
SetDefaultOpt();
return TRUE;
return FALSE;
// 设置正确的socket状态,
// 主要是主要是设置非阻塞异步传输模式
void CSocketCtrl::SetDefaultOpt()
ling.l_onoff=1;
ling.l_linger=0;
setsockopt( m_sockfd, SOL_SOCKET, SO_LINGER, (char *)&ling, sizeof(ling));
setsockopt( m_sockfd, SOL_SOCKET, SO_REUSEADDR, 0, 0);
int bKeepAlive = 1;
setsockopt( m_sockfd, SOL_SOCKET, SO_KEEPALIVE, (char*)&bKeepAlive, sizeof(int));
BOOL bNoDelay = TRUE;
setsockopt( m_sockfd, IPPROTO_TCP, TCP_NODELAY, (char*)&bNoDelay, sizeof(BOOL));
unsigned long nonblock=1;
::ioctlsocket(m_sockfd,FIONBIO,&nonblock);
今天晚上写了一些测试代码,
想看看flash究竟能够承受多大的网络数据传输,
我在flash登陆到服务器以后,
每隔3毫秒就发送100次100个字符的串 &6789& 给flash,
然后在flash里面接收数据的函数里面统计数据,
var g_nTotalRecvByte = 0;
var g_time = new Date();&
var g_nStartTime = g_time.getTime();
var g_nCounter = 0;
mySocket.onData=function(xmlDoc)
g_nTotalRecvByte += xmlDoc.
// 每接收超过1k字节的数据,输出一次信息,
if( g_nTotalRecvByte-g_nCounter & 1024 )
g_time = new Date();
var nPassedTime = g_time.getTime()-g_nStartT
trace( &花费时间:&+nPassedTime+&毫秒& );
g_nCounter = g_nTotalRecvB
trace( &接收总数:&+g_nTotalRecvByte+&字节& );
trace( &接收速率:&+g_nTotalRecvByte*1000/nPassedTime+&字节/秒& );
结果十分令我意外,
这是截取的一段调试信息,
花费时间:6953毫秒
接收总数:343212字节
接收速率:8988字节/秒
花费时间:7109毫秒
接收总数:344323字节
接收速率:534字节/秒
花费时间:7109毫秒
接收总数:345434字节
接收速率:3878字节/秒
花费时间:8125毫秒
接收总数:400984字节
接收速率:0769字节/秒
花费时间:8125毫秒
接收总数:402095字节
接收速率:6154字节/秒
花费时间:8125毫秒
接收总数:403206字节
接收速率:1538字节/秒
我检查了几遍源程序,没有发现逻辑错误,
如果程序没有问题的话,
那么我们得出的结论是,flash的xml socket每秒可以接收至少40K的数据,
这还没有计算xmlSocket.onData事件的触发,调试代码、信息输出占用的时间。
比我想象中快了一个数量级,
flash网络游戏我们可以继续往下走了,
有朋友问到lag的问题,
问得很好,不过也不要过于担心,
lag的产生有的是因为网络延迟,
有的是因为服务器负载过大,
对于游戏的设计者和开发者来说,
首先要从设计的角度来避免或者减少lag产生的机会,
如果lag产生了,
也不要紧,找到巧妙的办法骗过玩家的眼睛,
这也有很多成熟的方法了,
比如航行预测法,路径插值等等,
都可以产生很好的效果,
还有最后的绝招,就是提高服务器的配置和网络带宽,
从我开发网络游戏这段时间的经验来看,
我们的服务器是vc开发的,
普通pc跑几百个玩家,几百个怪物是没有问题的,
又作了一个flash发送的测试,
网络游戏的特点是,
出去的信息比较少,
进来的信息比较多,
这个很容易理解,
人操作游戏的速度是很有限的,
控制指令的产生也是随机的,
但是多人游戏的话,
因为人多,信息的流量也就区域均匀分布了,
在昨天接收数据的基础上,
我略加修改,
我在_root.enterFrame写了如下代码,
_root.onEnterFrame = function()&
for( i = 0; i & 10; i++ )
mySocket.send( ConvertToMsg( &89& ) );
服务器端要做的是,
把所有从flash客户端收到的信息原封不动的返回来,
这样,我又可以通过昨天onData里面的统计算法来从侧面估算出flash发送数据的能力,
这里是输出的数据
花费时间:30531毫秒
接收总数:200236字节
接收速率:5468字节/秒
花费时间:30937毫秒
接收总数:201290字节
接收速率:6811字节/秒
花费时间:31140毫秒
接收总数:202344字节
接收速率:9904字节/秒
花费时间:31547毫秒
接收总数:203398字节
接收速率:7208字节/秒
可以看出来,发送+接收同时做,
发送速率至少可以达到5k byte/s
有一点要注意,要非常注意,
不能让flash的网络传输满载,
所谓满载就是flash在阻塞运算的时候,
不断的有数据从网络进来,
而flash又无法在预计的时间内处理我这些信息,
或者flash发送数据过于频繁,
导致服务器端缓冲溢出导致错误,
对于5k的传输速率,
已经足够了,
因为我也想不出来有什么产生这么大的数据量,
而且如果产生了这么大的数据量,
也就意味着服务器每时每刻都要处理所有的玩家发出的海量数据,
还要把这些海量数据转发给其他的玩家,
已经引起数据爆炸了,
所以,5k的上传从设计阶段就要避免的,
我想用flash做的网络游戏,
除了动作类游戏可能需要恒定1k以内的上传速率,
其他的200个字节/秒以内就可以了,
使用于Flash的消息结构定义
我们以前讨论过,
通过消息来传递信息,
消息的结构是
struct msg
short nL // 2 byte
DWORD dwId; // 4 byte
但是在为flash开发的消息中,
不能采用这种结构,
首先Flash xmlSocket只传输字符串,
从xmlSocket的send,onData函数可以看出来,
发出去的,收进来的都应该是字符串,
而在服务器端是使用vc,java等高级语言编写的,
消息中使用的是二进制数据块,
显然,简单的使用字符串会带来问题,
所以,我们需要制定一套协议,
就是无论在客户端还是服务器端,
都用统一的字符串消息,
通过解析字符串的方式来传递信息,
我想这就是flash采用xml document来传输结构化信息的理由之一,
xml document描述了一个完整的数据结构,
而且全部使用的是字符串,
原来是这样,怪不得叫做xml socket,
本来socket和xml完全是不同的概念,
flash偏偏出了个xml socket,
一开始令我费解,
现在,渐渐理解其中奥妙。
Flash Msg结构定义源代码和相关函数
在服务器端,我们为flash定义了一种msg结构,
使用语言,vc6
#define MSGMAXSIZE 512
struct MsgHeader
MsgHeader():stLength( 0 ){}
struct Msg
short GetLength(){ return header.stL }
// flash 消息
struct MsgToFlashublic
// 一个足够大的缓冲,但是不会被整个发送,
char szString[MSGMAXSIZE];
// 计算设置好内容后,内部会计算将要发送部分的长度,
// 要发送的长度=消息头大小+字符串长度+1
void SetString( const char* pszChatString )
if( strlen( pszChatString ) & MSGMAXSIZE-1 )
strcpy( szString, pszChatString );
header.stLength = sizeof( header )+
(short)strlen( pszChatString )+1;
在发往flash的消息中,整个处理过后MsgToFlash结构将被发送,
实践证明,在flash 客户端的xmlSocket onData事件中,
接收到了正确的消息,消息的内容是MasToFlash的szString字段,
是一个字符串,
比如在服务器端,
msg.SetString( &move player0 to 100 100& );
SendMsg( msg,............. );
那么,在我们的flash客户端的onData( xmlDoc )中,
我们trace( xmlDoc )
move player0 to 100 100
然后是flash发送消息到服务器,
我们强调flash只发送字符串,
这个字符串无论是否内部拥有有效数据,
服务器都应该首先把消息收下来,
那就要保证发送给服务器的消息遵循统一的结构,
在flash客户端中,
我们定义一个函数,
这个函数把一个字符串转化为服务器可以识别的消息,
补充:现在我们约定字符串长度都不大于97个字节长度,
var num_table = new array( &0&,&1&,&2&,&3&,&4&,&5&,&6&,&7&,&8&,&9& );
function ConvertToMsg( str )
var l = str.length+3;&
var t = &&;
if( l & 10 )
t = num_table[Math.floor(l/10)]+num_table[Number(l%10)]+
t = num_table[0]+num_table[l]+
var msg = ConvertToMsg( &client login& );
我们trace( msg );
15client login
为什么是这个结果呢?
15是消息的长度,
头两个字节是整个消息的长度的asc码,意思是整个消息有15个字节长,
然后是信息client login,
最后是一个0(c语言中的字符串结束符)
当服务器收到15client login,
他首先把15给分析出来,
把&15&字符串转化为15的数字,
然后,根据15这个长度把后面的client login读出来,
这样,网络传输的底层就完成了,
client login的处理就交给逻辑层,
谢谢大家的支持,
很感谢斑竹把这个贴子置顶,
我写这文章的过程也是我自己摸索的过程,
文章可以记录我一段开发的历史,
一个思考分析的历程,
有时候甚至作

我要回帖

更多关于 文件下载权限控制机制 的文章

 

随机推荐