﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-教父的告白-随笔分类-Game Development</title><link>http://www.cppblog.com/keigoliye/category/11702.html</link><description>一切都是纸老虎</description><language>zh-cn</language><lastBuildDate>Sat, 16 Jan 2010 23:38:43 GMT</lastBuildDate><pubDate>Sat, 16 Jan 2010 23:38:43 GMT</pubDate><ttl>60</ttl><item><title>我的游戏开发收藏夹</title><link>http://www.cppblog.com/keigoliye/archive/2010/01/13/105556.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 13 Jan 2010 04:32:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2010/01/13/105556.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/105556.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2010/01/13/105556.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/105556.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/105556.html</trackback:ping><description><![CDATA[国内站点:<br><a href="http://www.gameres.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.gameres.com/</a>&nbsp;中国游戏开发技术资源网(国内知名游戏技术站)<br><a href="http://bbs.gamedev.csdn.net/web/default.aspx" style="color: rgb(0, 44, 153); text-decoration: none; ">http://bbs.gamedev.csdn.net/web/default.aspx</a>&nbsp;中国游戏开发者CGD(论坛)<br><a href="http://www.chaosstars.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.chaosstars.com/</a>&nbsp;北京混沌星辰科技有限公司-ChaosStars(之前的开发GBA程序的小组)<br><a href="http://www.cgfront.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.cgfront.com/</a>&nbsp; 中国游戏@图形前线(ver6.0) by inova-Tech<br><a href="http://www.chinaai.org/index.asp" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.chinaai.org/index.asp</a>&nbsp;人工智能|模式识别|数字图像处理—中国人工智能网<br><a href="http://www.cad.zju.edu.cn/chinagraph/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.cad.zju.edu.cn/chinagraph/</a>&nbsp;中国计算机图形学教学研究会主页<br><a href="http://www.vrforum.cn/home.php" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.vrforum.cn/home.php</a>&nbsp;中国VR技术社区 -&nbsp;<a href="http://www.vrforum.cn/" style="color: rgb(0, 44, 153); text-decoration: none; ">www.vrforum.cn</a><br><a href="http://www.modchina.com/ceshi/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.modchina.com/ceshi/</a>&nbsp;中国MOD制作同盟社<br><a href="http://bbs.99nets.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://bbs.99nets.com/</a>&nbsp;99NETS网游模拟中文站<br><a href="http://www.codingnow.com/2000/index.html" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.codingnow.com/2000/index.html</a>&nbsp;云风工作室<br><a href="http://blog.codingnow.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://blog.codingnow.com/</a>&nbsp;云风的 BLOG<br><a href="http://gd.91.com/Modules/index.aspx" style="color: rgb(0, 44, 153); text-decoration: none; ">http://gd.91.com/Modules/index.aspx</a>&nbsp;91游戏制作联盟 -- 专业游戏开发权威网站、游戏制作人社区<br><a href="http://www.npc6.com/rc/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.npc6.com/rc/</a>&nbsp;游戏人才网<br><a href="http://www.ylog.net/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.ylog.net/</a>&nbsp;异次元空间-首页 游戏制作　游戏开发　BLOG空间<br><a href="http://www.tuyasoft.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.tuyasoft.com/</a>&nbsp;涂鸦软件--国内第一家商业3D引擎公司<br><a href="http://lightwing.diy.myrice.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://lightwing.diy.myrice.com/</a>&nbsp;琴心剑胆<br><a href="http://www.dreamwork.cn/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.dreamwork.cn/</a>&nbsp;梦工厂软件有限公司—DreamWork.CN<br><a href="http://www.sf.org.cn/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.sf.org.cn/</a>&nbsp;开发视界<br><a href="http://www.hyzgame.org/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.hyzgame.org/</a>&nbsp;绝情电脑游戏创作群<br><a href="http://www.cnblogs.com/team/CG.html" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.cnblogs.com/team/CG.html</a>&nbsp;博客园 - 计算机图形学&nbsp;<br><a href="http://www.chinagcn.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.chinagcn.com/</a>&nbsp;欢迎来到游戏创造网！<br><a href="http://www.npc6.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.npc6.com/</a>&nbsp;何苦做游戏-游戏制作de文化...<br><a href="http://www.d2-life.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.d2-life.com/</a>&nbsp;第二人生游戏开发俱乐部<br><a href="http://www.gameassassin.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.gameassassin.com/</a>&nbsp;代码空间<br><a href="http://creativesoft.home.shangdu.net/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://creativesoft.home.shangdu.net/</a>&nbsp;创新软件编程乐园<br><a href="http://www.gpgame.net/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.gpgame.net/</a>&nbsp;金点工作组<br><a href="http://www.cmzj.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.cmzj.com/</a>&nbsp;Welken Game<br><a href="http://gd.91.com/zt/ogre/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://gd.91.com/zt/ogre/</a>&nbsp;OGRE引擎研究站--游戏制作联盟<br><a href="http://www.ogdev.net/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.ogdev.net/</a>&nbsp;OGDEV.NET-网络游戏研发网<br><a href="http://www.azure.com.cn/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.azure.com.cn/</a>&nbsp;Azure Product(游戏技术,3D图形学)<br><a href="http://www.csie.ntu.edu.tw/~r89004/hive/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.csie.ntu.edu.tw/~r89004/hive/</a>&nbsp;Hotball的小屋(计算机图形学)<br><a href="http://www.sycini.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.sycini.com/</a>&nbsp;中国demoscene开发制作资源网<br><a href="http://www.image2003.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.image2003.com/</a>&nbsp;图像图形网<br><a href="http://www.gamaura.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.gamaura.com/</a>&nbsp;Gamaura - 属于游戏人的专业信息交流平台<br><a href="http://www.3donline.cn/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.3donline.cn/</a>　（DxhViewer官方网站）<br>外国站点:<br><a href="http://www.gamedev.net/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.gamedev.net/</a>&nbsp;(国外权威游戏开发技术站点)&nbsp;<br><a href="http://www.gdmag.com/homepage.htm" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.gdmag.com/homepage.htm</a>&nbsp;(知名游戏开发杂志)&nbsp;<br><a href="http://www.flipcode.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.flipcode.com/</a>&nbsp;(知名图形学站点,现在只剩下文章)<br><a href="http://www.gameprogrammer.org/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.gameprogrammer.org/</a>&nbsp;(游戏开发教程)<br><a href="http://www.gametutorials.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.gametutorials.com/</a>&nbsp;(游戏教程网,好像要收费)<br><a href="http://www.garagegames.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.garagegames.com/</a>&nbsp;(游戏引擎公司)<br><a href="http://www.humus.ca/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.humus.ca/</a>&nbsp;(牛的网站,3D图形编程,有很多源码)<br><a href="http://www.hugi.scene.org/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.hugi.scene.org/</a>&nbsp;(很好的编程电子杂志,多数是DEMO INTRO技术)<br><a href="http://www.gpgpu.org/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.gpgpu.org/</a>&nbsp;(GPU通用编程)<br><a href="http://www.iddevnet.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.iddevnet.com/</a>&nbsp;(ID SOFT的SDK网站)<br><a href="http://irrlicht.sourceforge.net/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://irrlicht.sourceforge.net/</a>&nbsp;(很好的开源3D引擎)<br><a href="http://student.kuleuven.be/~m0216922/CG/index.html" style="color: rgb(0, 44, 153); text-decoration: none; ">http://student.kuleuven.be/~m0216922/CG/index.html</a>&nbsp;(图形学教程)<br><a href="http://nehe.gamedev.net/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://nehe.gamedev.net/</a>&nbsp;(知名OPENGL编程教程)<br><a href="http://www.xmission.com/~nate/index.html" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.xmission.com/~nate/index.html</a>&nbsp;(OPENGL例子教学)<br><a href="http://www.novodex.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.novodex.com/</a>&nbsp;(物理引擎)<br><a href="http://developer.nvidia.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://developer.nvidia.com/</a>&nbsp;(显卡大厂的开发资源网)<br><a href="http://www.ogre3d.org/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.ogre3d.org/</a>&nbsp;(知名开源3D引擎)<br><a href="http://www.openal.org/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.openal.org/</a>&nbsp;(优秀的音频编程库)<br><a href="http://www.opengl.org/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.opengl.org/</a>&nbsp;(OpenGL)<br><a href="http://www.msi.unilim.fr/~porquet/glexts/index.html" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.msi.unilim.fr/~porquet/glexts/index.html</a>&nbsp;NVIDIA OpenGL Extension Specifications<br><a href="http://www.typhoonlabs.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.typhoonlabs.com/</a>&nbsp;(SHADER编程)<br><a href="http://www.clockworkcoders.com/oglsl/extensions.htm" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.clockworkcoders.com/oglsl/extensions.htm</a>&nbsp;(GLSL教程)<br><a href="http://www.cs.unc.edu/~davemc/Particle/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.cs.unc.edu/~davemc/Particle/</a>&nbsp;(粒子引擎)<br><a href="http://www.markmark.net/clouds/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.markmark.net/clouds/</a>&nbsp;(实时云的渲染)<br><a href="http://www.scene.org/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.scene.org/</a>&nbsp;(Demo Intro的大站)<br><a href="http://www.sgi.com/products/software/opengl/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.sgi.com/products/software/opengl/</a>&nbsp;(SGI公司的OPENGL编程资源)<br><a href="http://www.shadertech.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.shadertech.com/</a>&nbsp;(SHADER技术)<br><a href="http://libsh.org/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://libsh.org/</a>&nbsp;(一种新图形硬件编程语言)<br><a href="http://demo-effects.sourceforge.net/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://demo-effects.sourceforge.net/</a>&nbsp;(开源的图形特效例子合集合)<br><a href="http://www.zfx.info/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.zfx.info/</a>&nbsp;(3D图形学,Demo Intro)<br><a href="http://www.ultimategameprogramming.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.ultimategameprogramming.com/</a>&nbsp;(游戏编程网,很多代码)<br><a href="http://www.beyond3d.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.beyond3d.com/</a>&nbsp;(3D硬件资迅站)<br><a href="http://www.codesampler.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.codesampler.com</a>&nbsp;(大量图形编程例子)<br><a href="http://www.devmaster.net/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.devmaster.net/</a>&nbsp;(游戏引擎数据库,3D图形编程文章)<br><a href="http://astronomy.swin.edu.au/~pbourke/geometry/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://astronomy.swin.edu.au/~pbourke/geometry/</a>&nbsp;(计算机图形学)<br><a href="http://ps2dev.org/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://ps2dev.org/</a>&nbsp;(PS2,PSP开发)<br><a href="http://pspdev.ofcode.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://pspdev.ofcode.com/</a>&nbsp;(PSP开发)<br><a href="http://www.yaz0r.net/blogs/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.yaz0r.net/blogs/</a>&nbsp;（FF10,FF12,王国之心系列游戏模型查看器作者的ＢＬＯＧ）<br><a href="http://home.gna.org/cal3d/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://home.gna.org/cal3d/</a>　（开源的３Ｄ模型动画库）<br><a href="http://www.ozone3d.net/index.php" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.ozone3d.net/index.php</a>　（Realtime 3D Programming）<br><a href="http://www.rakkarsoft.com/" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.rakkarsoft.com/</a>&nbsp;(Multiplayer game network engine)<br><a href="http://www.student.kuleuven.ac.be/~m0216922/CG/index.html" style="color: rgb(0, 44, 153); text-decoration: none; ">http://www.student.kuleuven.ac.be/~m0216922/CG/index.html</a>　
<img src ="http://www.cppblog.com/keigoliye/aggbug/105556.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2010-01-13 12:32 <a href="http://www.cppblog.com/keigoliye/archive/2010/01/13/105556.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>小谈Online-game服务器端设计</title><link>http://www.cppblog.com/keigoliye/archive/2009/10/29/99754.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Thu, 29 Oct 2009 09:47:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/10/29/99754.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/99754.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/10/29/99754.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/99754.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/99754.html</trackback:ping><description><![CDATA[<p><font face=Tahoma size=2>　　谈这个话题之前，首先要让大家知道，什么是服务器。在网络游戏中，服务器所扮演的角色是同步，广播和服务器主动的一些行为，比如说天气，NPC AI之类的，之所以现在的很多网络游戏服务器都需要负担一些游戏逻辑上的运算是因为为了防止客户端的作弊行为。了解到这一点，那么本系列的文章将分为两部分来谈谈网络游戏服务器的设计，一部分是讲如何做好服务器的网络连接，同步，广播以及NPC的设置，另一部分则将着重谈谈哪些逻辑放在服务器比较合适，并且用什么样的结构来安排这些逻辑。<br><br><br><strong>服务器的网络连接</strong><br><br>　　大多数的网络游戏的服务器都会选择非阻塞select这种结构，为什么呢？因为网络游戏的服务器需要处理的连接非常之多，并且大部分会选择在Linux/Unix下运行，那么为每个用户开一个线程实际上是很不划算的，一方面因为在Linux/Unix下的线程是用进程这么一个概念模拟出来的，比较消耗系统资源，另外除了I/O之外，每个线程基本上没有什么多余的需要并行的任务，而且网络游戏是互交性非常强的，所以线程间的同步会成为很麻烦的问题。由此一来，对于这种含有大量网络连接的单线程服务器，用阻塞显然是不现实的。对于网络连接，需要用一个结构来储存，其中需要包含一个向客户端写消息的缓冲，还需要一个从客户端读消息的缓冲，具体的大小根据具体的消息结构来定了。另外对于同步，需要一些时间校对的值，还需要一些各种不同的值来记录当前状态，下面给出一个初步的连接的结构：<br><br></font><font face=Fixedsys size=2>typedef connection_s {<br><br>&nbsp;&nbsp;&nbsp; user_t *ob; /* 指向处理服务器端逻辑的结构 */<br><br>&nbsp;&nbsp;&nbsp; int fd; /* socket连接 */<br><br>&nbsp;&nbsp;&nbsp; struct sockaddr_in addr; /* 连接的地址信息 */<br><br>&nbsp;&nbsp;&nbsp; char text[MAX_TEXT]; /* 接收的消息缓冲 */<br><br>&nbsp;&nbsp;&nbsp; int text_end; /* 接收消息缓冲的尾指针 */<br><br>&nbsp;&nbsp;&nbsp; int text_start; /* 接收消息缓冲的头指针 */<br><br>&nbsp;&nbsp;&nbsp; int last_time; /* 上一条消息是什么时候接收到的 */<br><br>&nbsp;&nbsp;&nbsp; struct timeval latency; /* 客户端本地时间和服务器本地时间的差值 */<br><br>&nbsp;&nbsp;&nbsp; struct timeval last_confirm_time; /* 上一次验证的时间 */<br><br>&nbsp;&nbsp;&nbsp; short is_confirmed; /* 该连接是否通过验证过 */ <br><br>&nbsp;&nbsp;&nbsp; int ping_num; /* 该客户端到服务器端的ping值 */<br><br>&nbsp;&nbsp;&nbsp; int ping_ticker; /* 多少个IO周期处理更新一次ping值 */<br><br>&nbsp;&nbsp;&nbsp; int message_length; /* 发送缓冲消息长度 */<br><br>&nbsp;&nbsp;&nbsp; char message_buf[MAX_TEXT]; /* 发送缓冲区 */<br><br>&nbsp;&nbsp;&nbsp; int iflags; /* 该连接的状态 */<br><br>} connection_t;</font><font face=Tahoma><br><br><font size=2>　　服务器循环的处理所有连接，是一个死循环过程，每次循环都用select检查是否有新连接到达，然后循环所有连接，看哪个连接可以写或者可以读，就处理该连接的读写。由于所有的处理都是非阻塞的，所以所有的Socket IO都可以用一个线程来完成。<br><br>　　由于网络传输的关系，每次recv()到的数据可能不止包含一条消息，或者不到一条消息，那么怎么处理呢？所以对于接收消息缓冲用了两个指针，每次接收都从text_start开始读起，因为里面残留的可能是上次接收到的多余的半条消息，然后text_end指向消息缓冲的结尾。这样用两个指针就可以很方便的处理这种情况，另外有一点值得注意的是：解析消息的过程是一个循环的过程，可能一次接收到两条以上的消息在消息缓冲里面，这个时候就应该执行到消息缓冲里面只有一条都不到的消息为止，大体流程如下：<br><br></font></font><font face=Fixedsys size=2>while ( text_end &#8211; text_start &gt; 一条完整的消息长度 )<br><br>{<br><br>&nbsp;&nbsp;&nbsp; 从text_start处开始处理;<br><br>&nbsp;&nbsp;&nbsp; text_start += 该消息长度;<br><br>}<br><br>memcpy ( text, text + text_start, text_end &#8211; text_start );</font><font face=Tahoma><br><br><font size=2>　　对于消息的处理，这里首先就需要知道你的游戏总共有哪些消息，所有的消息都有哪些，才能设计出比较合理的消息头。一般来说，消息大概可分为主角消息，场景消息，同步消息和界面消息四个部分。其中主角消息包括客户端所控制的角色的所有动作，包括走路，跑步，战斗之类的。场景消息包括天气变化，一定的时间在场景里出现一些东西等等之类的，这类消息的特点是所有消息的发起者都是服务器，广播对象则是场景里的所有玩家。而同步消息则是针对发起对象是某个玩家，经过服务器广播给所有看得见他的玩家，该消息也是包括所有的动作，和主角消息不同的是该种消息是服务器广播给客户端的，而主角消息一般是客户端主动发给服务器的。最后是界面消息，界面消息包括是服务器发给客户端的聊天消息和各种属性及状态信息。<br><br>　　下面来谈谈消息的组成。一般来说，一个消息由消息头和消息体两部分组成，其中消息头的长度是不变的，而消息体的长度是可变的，在消息体中需要保存消息体的长度。由于要给每条消息一个很明显的区分，所以需要定义一个消息头特有的标志，然后需要消息的类型以及消息ID。消息头大体结构如下：<br><br></font></font><font face=Fixedsys size=2>type struct message_s {<br><br>&nbsp;&nbsp;&nbsp; unsigned short message_sign;<br><br>&nbsp;&nbsp;&nbsp; unsigned char message_type;<br><br>&nbsp;&nbsp;&nbsp; unsigned short message_id<br><br>&nbsp;&nbsp;&nbsp; unsigned char message_len<br><br>}message_t;<br><br></font><font face=Tahoma><br><font size=2><strong>服务器的广播</strong><br><br>　　服务器的广播的重点就在于如何计算出广播的对象。很显然，在一张很大的地图里面，某个玩家在最东边的一个动作，一个在最西边的玩家是应该看不到的，那么怎么来计算广播的对象呢？最简单的办法，就是把地图分块，分成大小合适的小块，然后每次只象周围几个小块的玩家进行广播。那么究竟切到多大比较合适呢？一般来说，切得块大了，内存的消耗会增大，切得块小了，CPU的消耗会增大（原因会在后面提到）。个人觉得切成一屏左右的小块比较合适，每次广播广播周围九个小块的玩家，由于广播的操作非常频繁，那么遍利周围九块的操作就会变得相当的频繁，所以如果块分得小了，那么遍利的范围就会扩大，CPU的资源会很快的被吃完。<br><br>　　切好块以后，怎么让玩家在各个块之间走来走去呢？让我们来想想在切换一次块的时候要做哪些工作。首先，要算出下个块的周围九块的玩家有哪些是现在当前块没有的，把自己的信息广播给那些玩家，同时也要算出下个块周围九块里面有哪些物件是现在没有的，把那些物件的信息广播给自己，然后把下个块的周围九快里没有的，而现在的块周围九块里面有的物件的消失信息广播给自己，同时也把自己消失的消息广播给那些物件。这个操作不仅烦琐而且会吃掉不少CPU资源，那么有什么办法可以很快的算出这些物件呢？一个个做比较？显然看起来就不是个好办法，这里可以参照二维矩阵碰撞检测的一些思路，以自己周围九块为一个矩阵，目标块周围九块为另一个矩阵，检测这两个矩阵是否碰撞，如果两个矩阵相交，那么没相交的那些块怎么算。这里可以把相交的块的坐标转换成内部坐标，然后再进行运算。<br><br>　　对于广播还有另外一种解决方法，实施起来不如切块来的简单，这种方法需要客户端来协助进行运算。首先在服务器端的连接结构里面需要增加一个广播对象的队列，该队列在客户端登陆服务器的时候由服务器传给客户端，然后客户端自己来维护这个队列，当有人走出客户端视野的时候，由客户端主动要求服务器给那个物件发送消失的消息。而对于有人总进视野的情况，则比较麻烦了。<br><br>　　首先需要客户端在每次给服务器发送update position的消息的时候，服务器都给该连接算出一个视野范围，然后在需要广播的时候，循环整张地图上的玩家，找到坐标在其视野范围内的玩家。使用这种方法的好处在于不存在转换块的时候需要一次性广播大量的消息，缺点就是在计算广播对象的时候需要遍历整个地图上的玩家，如果当一个地图上的玩家多得比较离谱的时候，该操作就会比较的慢。<br><br><br><strong>服务器的同步</strong><br><br>　　同步在网络游戏中是非常重要的，它保证了每个玩家在屏幕上看到的东西大体是一样的。其实呢，解决同步问题的最简单的方法就是把每个玩家的动作都向其他玩家广播一遍，这里其实就存在两个问题：1，向哪些玩家广播，广播哪些消息。2，如果网络延迟怎么办。事实上呢，第一个问题是个非常简单的问题，不过之所以我提出这个问题来，是提醒大家在设计自己的消息结构的时候，需要把这个因素考虑进去。而对于第二个问题，则是一个挺麻烦的问题，大家可以来看这么个例子：<br><br>　　比如有一个玩家A向服务器发了条指令，说我现在在P1点，要去P2点。指令发出的时间是T0，服务器收到指令的时间是T1，然后向周围的玩家广播这条消息，消息的内容是&#8220;玩家A从P1到P2&#8221;有一个在A附近的玩家B，收到服务器的这则广播的消息的时间是T2，然后开始在客户端上画图，A从P1到P2点。这个时候就存在一个不同步的问题，玩家A和玩家B的屏幕上显示的画面相差了T2-T1的时间。这个时候怎么办呢？<br><br><br><br>　　有个解决方案，我给它取名叫 预测拉扯，虽然有些怪异了点，不过基本上大家也能从字面上来理解它的意思。要解决这个问题，首先要定义一个值叫：预测误差。然后需要在服务器端每个玩家连接的类里面加一项属性，叫latency，然后在玩家登陆的时候，对客户端的时间和服务器的时间进行比较，得出来的差值保存在latency里面。还是上面的那个例子，服务器广播消息的时候，就根据要广播对象的latency，计算出一个客户端的CurrentTime，然后在消息头里面包含这个CurrentTime，然后再进行广播。并且同时在玩家A的客户端本地建立一个队列，保存该条消息，只到获得服务器验证就从未被验证的消息队列里面将该消息删除，如果验证失败，则会被拉扯回P1点。然后当玩家B收到了服务器发过来的消息&#8220;玩家A从P1到P2&#8221;这个时候就检查消息里面服务器发出的时间和本地时间做比较，如果大于定义的预测误差，就算出在T2这个时间，玩家A的屏幕上走到的地点P3，然后把玩家B屏幕上的玩家A直接拉扯到P3，再继续走下去，这样就能保证同步。更进一步，为了保证客户端运行起来更加smooth，我并不推荐直接把玩家拉扯过去，而是算出P3偏后的一点P4，然后用(P4-P1)/T(P4-P3)来算出一个很快的速度S，然后让玩家A用速度S快速移动到P4，这样的处理方法是比较合理的，这种解决方案的原形在国际上被称为（Full plesiochronous），当然，该原形被我篡改了很多来适应网络游戏的同步，所以而变成所谓的：预测拉扯。<br><br>　　另外一个解决方案，我给它取名叫 验证同步，听名字也知道，大体的意思就是每条指令在经过服务器验证通过了以后再执行动作。具体的思路如下：首先也需要在每个玩家连接类型里面定义一个latency，然后在客户端响应玩家鼠标行走的同时，客户端并不会先行走动，而是发一条走路的指令给服务器，然后等待服务器的验证。服务器接受到这条消息以后，进行逻辑层的验证，然后计算出需要广播的范围，包括玩家A在内，根据各个客户端不同的latency生成不同的消息头，开始广播，这个时候这个玩家的走路信息就是完全同步的了。这个方法的优点是能保证各个客户端之间绝对的同步，缺点是当网络延迟比较大的时候，玩家的客户端的行为会变得比较不流畅，给玩家带来很不爽的感觉。该种解决方案的原形在国际上被称为（Hierarchical master-slave synchronization），80年代以后被广泛应用于网络的各个领域。<br><br>　　最后一种解决方案是一种理想化的解决方案，在国际上被称为Mutual synchronization，是一种对未来网络的前景的良好预测出来的解决方案。这里之所以要提这个方案，并不是说我们已经完全的实现了这种方案，而只是在网络游戏领域的某些方面应用到这种方案的某些思想。我对该种方案取名为：半服务器同步。大体的设计思路如下：<br><br>　　首先客户端需要在登陆世界的时候建立很多张广播列表，这些列表在客户端后台和服务器要进行不及时同步，之所以要建立多张列表，是因为要广播的类型是不止一种的，比如说有local message,有remote message,还有global message 等等，这些列表都需要在客户端登陆的时候根据服务器发过来的消息建立好。在建立列表的同时，还需要获得每个列表中广播对象的latency，并且要维护一张完整的用户状态列表在后台，也是不及时的和服务器进行同步，根据本地的用户状态表，可以做到一部分决策由客户端自己来决定，当客户端发送这部分决策的时候，则直接将最终决策发送到各个广播列表里面的客户端，并对其时间进行校对，保证每个客户端在收到的消息的时间是和根据本地时间进行校对过的。那么再采用预测拉扯中提到过的计算提前量，提高速度行走过去的方法，将会使同步变得非常的smooth。该方案的优点是不通过服务器，客户端自己之间进行同步，大大的降低了由于网络延迟而带来的误差，并且由于大部分决策都可以由客户端来做，也大大的降低了服务器的资源。由此带来的弊端就是由于消息和决策权都放在客户端本地，所以给外挂提供了很大的可乘之机。<br><br>　　下面我想来谈谈关于服务器上NPC的设计以及NPC智能等一些方面涉及到的问题。首先，我们需要知道什么是NPC，NPC需要做什么。NPC的全称是（Non-Player Character），很显然，他是一个character，但不是玩家，那么从这点上可以知道，NPC的某些行为是和玩家类似的，他可以行走，可以战斗，可以呼吸（这点将在后面的NPC智能里面提到），另外一点和玩家物件不同的是，NPC可以复生（即NPC被打死以后在一定时间内可以重新出来）。其实还有最重要的一点，就是玩家物件的所有决策都是玩家做出来的，而NPC的决策则是由计算机做出来的，所以在对NPC做何种决策的时候，需要所谓的NPC智能来进行决策。<br><br>　　下面我将分两个部分来谈谈NPC，首先是NPC智能，其次是服务器如何对NPC进行组织。之所以要先谈NPC智能是因为只有当我们了解清楚我们需要NPC做什么之后，才好开始设计服务器来对NPC进行组织。<br><br><br><strong>NPC智能</strong><br><br>　　NPC智能分为两种，一种是被动触发的事件，一种是主动触发的事件。对于被动触发的事件，处理起来相对来说简单一些，可以由事件本身来呼叫NPC身上的函数，比如说NPC的死亡，实际上是在NPC的HP小于一定值的时候，来主动呼叫NPC身上的OnDie() 函数，这种由事件来触发NPC行为的NPC智能，我称为被动触发。这种类型的触发往往分为两种：<br><br>一种是由别的物件导致的NPC的属性变化，然后属性变化的同时会导致NPC产生一些行为。由此一来，NPC物件里面至少包含以下几种函数：<br><br><font face=Fixedsys size=2>class NPC {<br><br>public:<br><br>&nbsp;&nbsp;&nbsp; // 是谁在什么地方导致了我哪项属性改变了多少。<br><br>&nbsp;&nbsp;&nbsp; OnChangeAttribute(object_t *who, int which, int how, int where);<br><br>Private:<br><br>&nbsp;&nbsp;&nbsp; OnDie();<br><br>&nbsp;&nbsp;&nbsp; OnEscape();<br><br>&nbsp;&nbsp;&nbsp; OnFollow();<br><br>&nbsp;&nbsp;&nbsp; OnSleep();<br><br>&nbsp;&nbsp;&nbsp; // 一系列的事件。<br><br>}<br></font><font face=Tahoma size=2><br>　　这是一个基本的NPC的结构，这种被动的触发NPC的事件，我称它为NPC的反射。但是，这样的结构只能让NPC被动的接收一些信息来做出决策，这样的NPC是愚蠢的。那么，怎么样让一个NPC能够主动的做出一些决策呢？这里有一种方法：呼吸。那么怎么样让NPC有呼吸呢？<br><br>　　一种很简单的方法，用一个计时器，定时的触发所有NPC的呼吸，这样就可以让一个NPC有呼吸起来。这样的话会有一个问题，当NPC太多的时候，上一次NPC的呼吸还没有呼吸完，下一次呼吸又来了，那么怎么解决这个问题呢。这里有一种方法，让NPC异步的进行呼吸，即每个NPC的呼吸周期是根据NPC出生的时间来定的，这个时候计时器需要做的就是隔一段时间检查一下，哪些NPC到时间该呼吸了，就来触发这些NPC的呼吸。<br><br>　　上面提到的是系统如何来触发NPC的呼吸，那么NPC本身的呼吸频率该如何设定呢？这个就好象现实中的人一样，睡觉的时候和进行激烈运动的时候，呼吸频率是不一样的。同样，NPC在战斗的时候，和平常的时候，呼吸频率也不一样。那么就需要一个Breath_Ticker来设置NPC当前的呼吸频率。<br><br>　　那么在NPC的呼吸事件里面，我们怎么样来设置NPC的智能呢？大体可以概括为检查环境和做出决策两个部分。首先，需要对当前环境进行数字上的统计，比如说是否在战斗中，战斗有几个敌人，自己的HP还剩多少，以及附近有没有敌人等等之类的统计。统计出来的数据传入本身的决策模块，决策模块则根据NPC自身的性格取向来做出一些决策，比如说野蛮型的NPC会在HP比较少的时候仍然猛扑猛打，又比如说智慧型的NPC则会在HP比较少的时候选择逃跑。等等之类的。<br><br>　　至此，一个可以呼吸，反射的NPC的结构已经基本构成了，那么接下来我们就来谈谈系统如何组织让一个NPC出现在世界里面。<br><br><br><strong>NPC的组织</strong><br><br>　　这里有两种方案可供选择，其一：NPC的位置信息保存在场景里面，载入场景的时候载入NPC。其二，NPC的位置信息保存在NPC身上，有专门的事件让所有的NPC登陆场景。这两种方法有什么区别呢？又各有什么好坏呢？<br><br>　　前一种方法好处在于场景载入的时候同时载入了NPC，场景就可以对NPC进行管理，不需要多余的处理，而弊端则在于在刷新的时候是同步刷新的，也就是说一个场景里面的NPC可能会在同一时间内长出来。而对于第二种方法呢，设计起来会稍微麻烦一些，需要一个统一的机制让NPC登陆到场景，还需要一些比较麻烦的设计，但是这种方案可以实现NPC异步的刷新，是目前网络游戏普遍采用的方法，下面我们就来着重谈谈这种方法的实现：<br><br>　　首先我们要引入一个&#8220;灵魂&#8221;的概念，即一个NPC在死后，消失的只是他的肉体，他的灵魂仍然在世界中存在着，没有呼吸，在死亡的附近漂浮，等着到时间投胎，投胎的时候把之前的所有属性清零，重新在场景上构建其肉体。那么，我们怎么来设计这样一个结构呢？首先把一个场景里面要出现的NPC制作成图量表，给每个NPC一个独一无二的标识符，在载入场景之后，根据图量表来载入属于该场景的NPC。在NPC的OnDie() 事件里面不直接把该物件destroy 掉，而是关闭NPC的呼吸，然后打开一个重生的计时器，最后把该物件设置为invisable。这样的设计，可以实现NPC的异步刷新，在节省服务器资源的同时也让玩家觉得更加的真实。<br><br>（这一章节已经牵扯到一些服务器脚本相关的东西，所以下一章节将谈谈服务器脚本相关的一些设计）<br><br>补充的谈谈启发式搜索（heuristic searching）在NPC智能中的应用。<br><br>　　其主要思路是在广度优先搜索的同时，将下一层的所有节点经过一个启发函数进行过滤，一定范围内缩小搜索范围。众所周知的寻路A*算法就是典型的启发式搜索的应用，其原理是一开始设计一个Judge(point_t* point)函数，来获得point这个一点的代价，然后每次搜索的时候把下一步可能到达的所有点都经过Judge()函数评价一下，获取两到三个代价比较小的点，继续搜索，那些没被选上的点就不会在继续搜索下去了，这样带来的后果的是可能求出来的不是最优路径，这也是为什么A*算法在寻路的时候会走到障碍物前面再绕过去，而不是预先就走斜线来绕过该障碍物。如果要寻出最优化的路径的话，是不能用A*算法的，而是要用动态规划的方法，其消耗是远大于A*的。<br><br>　　那么，除了在寻路之外，还有哪些地方可以应用到启发式搜索呢？其实说得大一点，NPC的任何决策都可以用启发式搜索来做，比如说逃跑吧，如果是一个2D的网络游戏，有八个方向，NPC选择哪个方向逃跑呢？就可以设置一个Judge(int direction)来给定每个点的代价，在Judge里面算上该点的敌人的强弱，或者该敌人的敏捷如何等等，最后选择代价最小的地方逃跑。下面，我们就来谈谈对于几种NPC常见的智能的启发式搜索法的设计：<br><br>Target select （选择目标）：<br><br>　　首先获得地图上离该NPC附近的敌人列表。设计Judge() 函数，根据敌人的强弱，敌人的远近，算出代价。然后选择代价最小的敌人进行主动攻击。<br><br>Escape（逃跑）：<br><br>　　在呼吸事件里面检查自己的HP，如果HP低于某个值的时候，或者如果你是远程兵种，而敌人近身的话，则触发逃跑函数，在逃跑函数里面也是对周围的所有的敌人组织成列表，然后设计Judge() 函数，先选择出对你构成威胁最大的敌人，该Judge() 函数需要判断敌人的速度，战斗力强弱，最后得出一个主要敌人，然后针对该主要敌人进行路径的Judge() 的函数的设计，搜索的范围只可能是和主要敌人相反的方向，然后再根据该几个方向的敌人的强弱来计算代价，做出最后的选择。<br><br>Random walk（随机走路）：<br><br>　　这个我并不推荐用A*算法，因为NPC一旦多起来，那么这个对CPU的消耗是很恐怖的，而且NPC大多不需要长距离的寻路，只需要在附近走走即可，那么，就在附近随机的给几个点，然后让NPC走过去，如果碰到障碍物就停下来，这样几乎无任何负担。<br><br>Follow Target（追随目标）：<br><br>　　这里有两种方法，一种方法NPC看上去比较愚蠢，一种方法看上去NPC比较聪明，第一种方法就是让NPC跟着目标的路点走即可，几乎没有资源消耗。而后一种则是让NPC在跟随的时候，在呼吸事件里面判断对方的当前位置，然后走直线，碰上障碍物了用A*绕过去，该种设计会消耗一定量的系统资源，所以不推荐NPC大量的追随目标，如果需要大量的NPC追随目标的话，还有一个比较简单的方法：让NPC和目标同步移动，即让他们的速度统一，移动的时候走同样的路点，当然，这种设计只适合NPC所跟随的目标不是追杀的关系，只是跟随着玩家走而已了。<br><br>　　在这一章节，我想谈谈关于服务器端的脚本的相关设计。因为在上一章节里面，谈NPC智能相关的时候已经接触到一些脚本相关的东东了。还是先来谈谈脚本的作用吧。<br>　　在基于编译的服务器端程序中，是无法在程序的运行过程中构建一些东西的，那么这个时候就需要脚本语言的支持了，由于脚本语言涉及到逻辑判断，所以光提供一些函数接口是没用的，还需要提供一些简单的语法和文法解析的功能。其实说到底，任何的事件都可以看成两个部分：第一是对自身，或者别的物件的数值的改变，另外一个就是将该事件以文字或者图形的方式广播出去。那么，这里牵扯到一个很重要的话题，就是对某一物件进行寻址。恩，谈到这，我想将本章节分为三个部分来谈，首先是服务器如何来管理动态创建出来的物件（服务器内存管理），第二是如何对某一物件进行寻址，第三则是脚本语言的组织和解释。其实之所以到第四章再来谈服务器的内存管理是因为在前几章谈这个的话，大家对其没有一个感性的认识，可能不知道服务器的内存管理究竟有什么用。<br><br><strong>4.1、服务器内存管理</strong><br>　　对于服务器内存管理我们将采用内存池的方法，也称为静态内存管理。其概念为在服务器初始化的时候，申请一块非常大的内存，称为内存池（<span class=English>Memory pool</span>），同时也申请一小块内存空间，称为垃圾回收站（<span class=English>Garbage recollecting station</span>）。其大体思路如下：当程序需要申请内存的时候，首先检查垃圾回收站是否为空，如果不为空的话，则从垃圾回收站中找一块可用的内存地址，在内存池中根据地址找到相应的空间，分配给程序用，如果垃圾回收站是空的话，则直接从内存池的当前指针位置申请一块内存；当程序释放空间的时候，给那块内存打上已经释放掉的标记，然后把那块内存的地址放入垃圾回收站。<br>　　下面具体谈谈该方法的详细设计，首先，我们将采用类似于操作系统的段页式系统来管理内存，这样的好处是可以充分的利用内存池，其缺点是管理起来比较麻烦。嗯，下面来具体看看我们怎么样来定义页和段的结构：<br><br><font face=Fixedsys><font size=3>　　</font><font size=3><span class=ColorCode>typedef struct m_segment_s<br>　　{<br>　　　　struct m_segment_s *next;</span>　</font><font size=3><font color=#808080><span class=ColorCatchword>/* 双线链表 + 静态内存可以达到随机访问和顺序访问的目的，<br>　　　　　　　　　　　　　　　　　　　真正的想怎么访问，就怎么访问。 */</span></font><br>　　　　<span class=ColorCode>struct m_segment_s *pre; int flags;</span>　　</font><font size=3><span class=ColorCatchword><font color=#808080>// 该段的一些标记。</font><br></span>　　　　<span class=ColorCode>int start;</span>　　　　　　　　　　　　　　 </font><font size=3><span class=ColorCatchword><font color=#808080>// 相对于该页的首地址。</font><br></span>　　　　<span class=ColorCode>int size;</span>　　　　　　　　　　　　　　　<font color=#808080><span class=ColorCatchword>// 长度。</span></font><br>　　　　<span class=ColorCode>struct m_page_s *my_owner;</span>　　　　　　 <font color=#808080><span class=ColorCatchword>// 我是属于哪一页的。</span></font><br>　　　　<span class=ColorCode>char *data;</span>　　　　　　　　　　　　　　<font color=#808080><span class=ColorCatchword>// 内容指针。</span></font><br>　　</font><font size=3><span class=ColorCode>}m_segment_t;<br><br>　　typedef struct m_page_s<br>　　{<br>　　　　unsigned int flags;</span>　　　 </font><font size=3><span class=ColorCatchword><font color=#808080>/* 使用标记，是否完全使用，是否还有空余 */</font><br></span>　　　　<span class=ColorCode>int size;</span>　　　　　　　　 <font color=#808080><span class=ColorCatchword>/* 该页的大小，一般都是统一的，最后一页除外 */</span></font><br>　　　　<span class=ColorCode>int end;</span>　　　　　　　 　</font><font size=3><span class=ColorCatchword> <font color=#808080>/* 使用到什么地方了 */</font><br></span>　　　　<span class=ColorCode>int my_index;</span>　　　　　　 </font><font size=3><span class=ColorCatchword><font color=#808080>/* 提供随机访问的索引 */</font><br></span>　　　　<span class=ColorCode>m_segment_t *segments;</span>　　</font><font size=3><span class=ColorCatchword><font color=#808080>// 页内段的头指针。</font><br></span>　</font><span class=ColorCode><font size=3>　}m_page_t;<br></font></span></font><font face=Arial size=2><br>　　那么内存池和垃圾回收站怎么构建呢？下面也给出一些构建相关的伪代码：<br><br></font><font face=Fixedsys><font size=3>　　</font><font size=3><span class=ColorCode>static m_page_t *all_pages;<br></span>　　</font><font size=3><span class=ColorCatchword><font color=#808080>// total_size是总共要申请的内存数，num_pages是总共打算创建多少个页面。</font><br></span>　　</font><font size=3><span class=ColorCode>void initialize_memory_pool( int total_size, int num_pages )<br>　　{<br>　　　　int i, page_size, last_size;</span>　　　　 </font><font size=3><span class=ColorCatchword><font color=#808080>// 算出每个页面的大小。</font><br></span>　　　　<span class=ColorCode>page_size = total_size / num_pages;</span>　</font><font size=3><span class=ColorCatchword><font color=#808080>// 分配足够的页面。</font><br></span>　　　　</font><font size=3><span class=ColorCode>all_pages = (m_page_t*) calloc( num_pages, sizeof(m_page_t*) );<br>　　　　for ( i = 0; i &lt; num_pages; i ++ )<br>　　　　{<br></span>　　　　　　</font><font size=3><span class=ColorCatchword><font color=#808080>// 初始化每个页面的段指针。</font><br></span>　　　　　　</font><font size=3><span class=ColorCode>all_pages[i].m_segment_t = (m_segment_t*) malloc( page_size );<br></span>　　　　　　</font><font size=3><span class=ColorCatchword><font color=#808080>// 初始化该页面的标记。</font><br></span>　　　　　　</font><font size=3><span class=ColorCode>all_pages[i].flags |= NEVER_USED;<br></span>　　　　　　</font><font size=3><span class=ColorCatchword><font color=#808080>// 除了最后一个页面，其他的大小都是page_size 大小。</font><br></span>　　　　　　</font><font size=3><span class=ColorCode>all_pages[i].size = page_size;<br></span>　　　　　　</font><font size=3><span class=ColorCatchword><font color=#808080>// 初始化随机访问的索引。</font><br></span>　　　　　　</font><font size=3><span class=ColorCode>all_pages[i].my_index = i;<br></span>　　　　　　</font><font size=3><span class=ColorCatchword><font color=#808080>// 由于没有用过，所以大小都是0</font><br></span>　　　　　　</font><font size=3><span class=ColorCode>all_pages[i].end = 0;<br>　　　　}<br></span><br>　　　　<font color=#808080><span class=ColorCatchword>// 设置最后一个页面的大小。</span></font><br>　　　　</font><span class=ColorCode><font size=3>if ( (last_size = total_size % num_pages) != 0 )<br>　　　　　　all_pages[i].size = last_size;<br>　　}<br></font></span></font><font face=Arial size=2><br>　　下面看看垃圾回收站怎么设计：<br><br></font><font face=Fixedsys><font size=3>　　</font><font size=3><span class=ColorCode>int **garbage_station;<br>　　void init_garbage_station( int num_pages, int page_size )<br>　　{<br>　　　　int i;<br>　　　　garbage_station = (int**) calloc( num_pages, sizeof( int* ) );<br>　　　　for ( i = 0; i &lt; num_pages; i ++)<br>　　　　{<br></span>　　　　　　</font><font size=3><span class=ColorCatchword><font color=#808080>// 这里用unsigned short的高8位来储存首相对地址，低8位来储存长度。</font><br></span>　　　　　　</font></font><span class=ColorCode><font face=Fixedsys size=3>garbage_station[i] = (int*) calloc( page_size, sizeof( unsigned short ));<br>　　　　　　memset( garbage_station[i], 0, sizeof( garbage_station[i] ));<br>　　　　}<br>　　}</font><font face=Arial><br></font></span><font face=Arial size=2><br>　　也许这样的贴代码会让大家觉得很不明白，嗯，我的代码水平确实不怎么样，那么下面我来用文字方式来叙说一下大体的概念吧。对于段页式内存管理，首先分成N个页面，这个是固定的，而对于每个页面内的段则是动态的，段的大小事先是不知道的，那么我们需要回收的不仅仅是页面的内存，还包括段的内存，那么我们就需要一个二维数组来保存是哪个页面的那块段的地址被释放了。然后对于申请内存的时候，则首先检查需要申请内存的大小，如果不够一个页面大小的话，则在垃圾回收站里面寻找可用的段空间分配，如果找不到，则申请一个新的页面空间。<br>　　这样用内存池的方法来管理整个游戏世界的内存可以有效的减少内存碎片，一定程度的提高游戏运行的稳定性和效率。<br><br><strong>4.2、游戏中物件的寻址<br></strong>　　第一个问题，我们为什么要寻址？加入了脚本语言的概念之后，游戏中的一些逻辑物件，比如说NPC，某个ITEM之类的都是由脚本语言在游戏运行的过程中动态生成的，那么我们通过什么样的方法来对这些物件进行索引呢？说得简单一点，就是如何找到他们呢？有个很简单的方法，全部遍历一次。当然，这是个简单而有效的方法，但是效率上的消耗是任何一台服务器都吃不消的，特别是在游戏的规模比较大之后。<br>　　那么，我们怎么来在游戏世界中很快的寻找这些物件呢？我想在谈这个之前，说一下Hash Table这个数据结构，它叫哈希表，也有人叫它散列表，其工作原理是不是顺序访问，也不是随机访问，而是通过一个散列函数对其key进行计算，算出在内存中这个key对应的value的地址，而对其进行访问。好处是不管面对多大的数据，只需要一次计算就能找到其地址，非常的快捷，那么弊端是什么呢？当两个key通过散列函数计算出来的地址是同一个地址的时候，麻烦就来了，会产生碰撞，其的解决方法非常的麻烦，这里就不详细谈其解决方法了，否则估计再写个四，五章也未必谈得清楚，不过如果大家对其感兴趣的话，欢迎讨论。<br>　　嗯，我们将用散列表来对游戏中的物件进行索引，具体怎么做呢？首先，在内存池中申请一块两倍大于游戏中物件总数的内存，为什么是两倍大呢？防止散列表碰撞。然后我们选用物件的名称作为散列表的索引key，然后就可以开始设计散列函数了。下面来看个例子：<br><br></font></font><font face=Fixedsys size=2>　　<span class=ColorCode>static int T[] =<br>　　{<br>　　　　1, 87, 49, 12, 176, 178, 102, 166, 121, 193, 6, 84, 249, 230, 44, 163,<br>　　　　14, 197, 213, 181, 161, 85, 218, 80, 64, 239, 24, 226, 236, 142, 38, 200,<br>　　　　110, 177, 104, 103, 141, 253, 255, 50, 77, 101, 81, 18, 45, 96, 31, 222,<br>　　　　25, 107, 190, 70, 86, 237, 240, 34, 72, 242, 20, 214, 244, 227, 149, 235,<br>　　　　97, 234, 57, 22, 60, 250, 82, 175, 208, 5, 127, 199, 111, 62, 135, 248,<br>　　　　174, 169, 211, 58, 66, 154, 106, 195, 245, 171, 17, 187, 182, 179, 0, 243,<br>　　　　132, 56, 148, 75, 128, 133, 158, 100, 130, 126, 91, 13, 153, 246, 216, 219,<br>　　　　119, 68, 223, 78, 83, 88, 201, 99, 122, 11, 92, 32, 136, 114, 52, 10,<br>　　　　138, 30, 48, 183, 156, 35, 61, 26, 143, 74, 251, 94, 129, 162, 63, 152,<br>　　　　170, 7, 115, 167, 241, 206, 3, 150, 55, 59, 151, 220, 90, 53, 23, 131,<br>　　　　125, 173, 15, 238, 79, 95, 89, 16, 105, 137, 225, 224, 217, 160, 37, 123,<br>　　　　118, 73, 2, 157, 46, 116, 9, 145, 134, 228, 207, 212, 202, 215, 69, 229,<br>　　　　27, 188, 67, 124, 168, 252, 42, 4, 29, 108, 21, 247, 19, 205, 39, 203,<br>　　　　233, 40, 186, 147, 198, 192, 155, 33, 164, 191, 98, 204, 165, 180, 117, 76,<br>　　　　140, 36, 210, 172, 41, 54, 159, 8, 185, 232, 113, 196, 231, 47, 146, 120,<br>　　　　51, 65, 28, 144, 254, 221, 93, 189, 194, 139, 112, 43, 71, 109, 184, 209,<br>　　};<br><br></span>　　<span class=ColorCatchword><font color=#808080>// s是需要进行索引的字符串指针，maxn是字符串可能的最大长度，返回值是相对地址。</font><br></span>　　</font><span class=ColorCode><font face=Fixedsys size=2>inline int whashstr(char *s, int maxn)<br>　　{<br>　　　　register unsigned char oh, h;<br>　　　　register unsigned char *p;<br>　　　　register int i;<br><br>　　　　if (!*s)<br>　　　　　　return 0;<br>　　　　p = (unsigned char *) s;<br>　　　　oh = T[*p]; h = (*(p++) + 1) &amp; 0xff;<br>　　　　for (i = maxn - 1; *p &amp;&amp; --i &gt;= 0; )<br>　　　　{<br>　　　　　　oh = T[oh ^ *p]; h = T[h ^ *(p++)];<br>　　　　}<br>　　　　return (oh &lt;&lt; 8) + h;<br>　　}</font><font face=Arial size=2><br></font></span><font face=Arial><font face=Arial size=2><br>　　具体的算法就不说了，上面的那一大段东西不要问我为什么，这个算法的出处是CACM 33-6中的一个叫Peter K.Pearson的鬼子写的论文中介绍的算法，据说速度非常的快。有了这个散列函数，我们就可以通过它来对世界里面的任意物件进行非常快的寻址了。<br><br><strong>4.3、脚本语言解释</strong><br>　　在设计脚本语言之前，我们首先需要明白，我们的脚本语言要实现什么样的功能？否则随心所欲的做下去写出个C的解释器之类的也说不定。我们要实现的功能只是简单的逻辑判断和循环，其他所有的功能都可以由事先提供好的函数来完成。嗯，这样我们就可以列出一张工作量的表单：设计物件在底层的保存结构，提供脚本和底层间的访问接口，设计支持逻辑判断和循环的解释器。<br>　　下面先来谈谈物件在底层的保存结构。具体到每种不同属性的物件，需要采用不同的结构，当然，如果你愿意的话，你可以所有的物件都采同同样的结构，然后在结构里面设计一个散列表来保存各种不同的属性。但这并不是一个好方法，过分的依赖散列表会让你的游戏的逻辑变得繁杂不清。所以，尽量的区分每种不同的物件采用不同的结构来设计。但是有一点值得注意的是，不管是什么结构，有一些东西是统一的，就是我们所说的物件头，那么我们怎么来设计这样一个物件头呢？<br><br></font></font><font face=Fixedsys size=2>　　</font><span class=ColorCode><font face=Fixedsys size=2>typedef struct object_head_s<br>　　{<br>　　　　char* name;<br>　　　　char* prog;<br>　　}object_head_t;</font><font face=Arial size=2><br></font></span><font face=Arial><font face=Arial size=2><br>　　其中name是在散列表中这个物件的索引号，prog则是脚本解释器需要解释的程序内容。下面我们就以NPC为例来设计一个结构：<br><br></font></font><font face=Fixedsys size=2>　　<span class=ColorCode>typedef struct npc_s<br>　　{<br>　　　　object_head_t header;</span>　　　　<span class=ColorCatchword><font color=#808080>// 物件头</font><br></span>　　　　<span class=ColorCode>int hp;</span>　　　　　　　　　　　<span class=ColorCatchword><font color=#808080>// NPC的hp值。</font><br></span>　　　　<span class=ColorCode>int level;</span>　　　　　　　 　　<span class=ColorCatchword><font color=#808080>// NPC的等级。</font><br></span>　　　　<span class=ColorCode>struct position_s position;</span>　<span class=ColorCatchword><font color=#808080>// 当前的位置信息。</font><br></span>　　　　<span class=ColorCode>unsigned int personality;</span>　　<font color=#808080><span class=ColorCatchword>// NPC的个性，一个unsigned int可以保存24种个性。</span></font><br>　　<span class=ColorCode>}npc_t;</span></font><font face=Arial><font face=Arial size=2><br><br>　　OK，结构设计完成，那么我们怎么来设计脚本解释器呢？这里有两种法，一种是用虚拟机的模式来解析脚本语言，另外一中则是用类似汇编语言的那种结构来设计，设置一些条件跳转和循环就可以实现逻辑判断和循环了，比如：<br><br></font></font><font face=Fixedsys size=2>　　<span class=ColorCode>set name, "路人甲";<br>　　CHOOSE: random_choose_personality;</span>　　<span class=ColorCatchword><font color=#808080>// 随机选择NPC的个性</font><br></span>　　<span class=ColorCode>compare hp, 100;</span>　　　　　　　　　　　<span class=ColorCatchword><font color=#808080>// 比较气血，比较出的值可以放在一个固定的变量里面</font><br></span>　　<span class=ColorCode>ifless LESS;</span>　　　　　　　　　　　　　<span class=ColorCatchword><font color=#808080>// hp &lt; 100的话，则返回。</font><br></span>　　<span class=ColorCode>jump CHOOSE;</span>　　　　　　　　　　　　　<font color=#808080><span class=ColorCatchword>// 否则继续选择，只到选到一个hp &lt; 100的。</span></font><br>　</font><span class=ColorCode><font face=Fixedsys size=2>　LESS: return success;</font><font face=Arial size=2><br></font></span><font face=Arial><font face=Arial size=2><br>　　这种脚本结构就类似CPU的指令的结构，一条一条指令按照顺序执行，对于脚本程序员（<span class=English>Script Programmer</span>）也可以培养他们汇编能力的说。<br>　　那么怎么来模仿这种结构呢？我们拿CPU的指令做参照，首先得设置一些寄存器，CPU的寄存器的大小和数量是受硬件影响的，但我们是用内存来模拟寄存器，所以想要多大，就可以有多大。然后提供一些指令，包括四则运算，寻址，判断，循环等等。接下来针对不同的脚本用不同的解析方法，比如说对NPC就用NPC固定的脚本，对ITEM就用ITEM固定的脚本，解析完以后就把结果生成底层该物件的结构用于使用。<br>　　而如果要用虚拟机来实现脚本语言的话呢，则会将工程变得无比之巨大，强烈不推荐使用，不过如果你想做一个通用的网络游戏底层的话，则可以考虑设计一个虚拟机。虚拟机大体的解释过程就是进行两次编译，第一次对关键字进行编译，第二次生成汇编语言，然后虚拟机在根据编译生成的汇编语言进行逐行解释，如果大家对这个感兴趣的话，可以去<a class=cLink href="http://www.mudos.org/" target=_blank><u><font color=#0000ff>www.mudos.org</font></u></a>上下载一份MudOS的原码来研究研究。 大体的思路讲到这里已经差不多了，下面将用unreal（虚幻）为实例，谈一谈网络游戏服务器的设计。</font></font></font></font></font></p>
<img src ="http://www.cppblog.com/keigoliye/aggbug/99754.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-10-29 17:47 <a href="http://www.cppblog.com/keigoliye/archive/2009/10/29/99754.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>小谈网络游戏同步</title><link>http://www.cppblog.com/keigoliye/archive/2009/10/29/99742.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Thu, 29 Oct 2009 06:18:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/10/29/99742.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/99742.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/10/29/99742.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/99742.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/99742.html</trackback:ping><description><![CDATA[<font face=Verdana size=2>　　同步在网络游戏中是非常重要的，它保证了每个玩家在屏幕上看到的东西大体是一样的。其实呢，解决同步问题的最简单的方法就是把每个玩家的动作都向其他玩家广播一遍，这里其实就存在两个问题：1，向哪些玩家广播，广播哪些消息。2，如果网络延迟怎么办。事实上呢，第一个问题是个非常简单的问题，不过之所以我提出这个问题来，是提醒大家在设计自己的消息结构的时候，需要把这个因素考虑进去。而对于第二个问题，则是一个挺麻烦的问题，大家可以来看这么个例子： </font>
<p>　　比如有一个玩家A向服务器发了条指令，说我现在在P1点，要去P2点。指令发出的时间是T0，服务器收到指令的时间是T1，然后向周围的玩家广播这条消息，消息的内容是&#8220;玩家A从P1到P2&#8221;有一个在A附近的玩家B，收到服务器的这则广播的消息的时间是T2，然后开始在客户端上画图，A从P1到P2点。这个时候就存在一个不同步的问题，玩家A和玩家B的屏幕上显示的画面相差了T2-T1的时间。这个时候怎么办呢？</p>
<p>　　有个解决方案，我给它取名叫 预测拉扯，虽然有些怪异了点，不过基本上大家也能从字面上来理解它的意思。要解决这个问题，首先要定义一个值叫：预测误差。然后需要在服务器端每个玩家连接的类里面加一项属性，叫TimeModified，然后在玩家登陆的时候，对客户端的时间和服务器的时间进行比较，得出来的差值保存在TimeModified里面。还是上面的那个例子，服务器广播消息的时候，就根据要广播对象的TimeModified，计算出一个客户端的CurrentTime，然后在消息头里面包含这个CurrentTime，然后再进行广播。并且同时在玩家A的客户端本地建立一个队列，保存该条消息，只到获得服务器验证就从未被验证的消息队列里面将该消息删除，如果验证失败，则会被拉扯回P1点。然后当玩家B收到了服务器发过来的消息&#8220;玩家A从P1到P2&#8221;这个时候就检查消息里面服务器发出的时间和本地时间做比较，如果大于定义的预测误差，就算出在T2这个时间，玩家A的屏幕上走到的地点P3，然后把玩家B屏幕上的玩家A直接拉扯到P3，再继续走下去，这样就能保证同步。更进一步，为了保证客户端运行起来更加smooth，我并不推荐直接把玩家拉扯过去，而是算出P3偏后的一点P4，然后用(P4-P1)/T(P4-P3)来算出一个很快的速度S，然后让玩家A用速度S快速移动到P4，这样的处理方法是比较合理的，这种解决方案的原形在国际上被称为（Full plesiochronous），当然，该原形被我篡改了很多来适应网络游戏的同步，所以而变成所谓的：预测拉扯。</p>
<p>　　另外一个解决方案，我给它取名叫 验证同步，听名字也知道，大体的意思就是每条指令在经过服务器验证通过了以后再执行动作。具体的思路如下：首先也需要在每个玩家连接类型里面定义一个TimeModified，然后在客户端响应玩家鼠标行走的同时，客户端并不会先行走动，而是发一条走路的指令给服务器，然后等待服务器的验证。服务器接受到这条消息以后，进行逻辑层的验证，然后计算出需要广播的范围，包括玩家A在内，根据各个客户端不同的TimeModified生成不同的消息头，开始广播，这个时候这个玩家的走路信息就是完全同步的了。这个方法的优点是能保证各个客户端之间绝对的同步，缺点是当网络延迟比较大的时候，玩家的客户端的行为会变得比较不流畅，给玩家带来很不爽的感觉。该种解决方案的原形在国际上被称为（Hierarchical master-slave synchronization），80年代以后被广泛应用于网络的各个领域。</p>
<p>　　最后一种解决方案是一种理想化的解决方案，在国际上被称为Mutual synchronization，是一种对未来网络的前景的良好预测出来的解决方案。这里之所以要提这个方案，并不是说我们已经完全的实现了这种方案，而只是在网络游戏领域的某些方面应用到这种方案的某些思想。我对该种方案取名为：半服务器同步。大体的设计思路如下：</p>
<p>　　首先客户端需要在登陆世界的时候建立很多张广播列表，这些列表在客户端后台和服务器要进行不及时同步，之所以要建立多张列表，是因为要广播的类型是不止一种的，比如说有local message,有remote message,还有global message 等等，这些列表都需要在客户端登陆的时候根据服务器发过来的消息建立好。在建立列表的同时，还需要获得每个列表中广播对象的TimeModified，并且要维护一张完整的用户状态列表在后台，也是不及时的和服务器进行同步，根据本地的用户状态表，可以做到一部分决策由客户端自己来决定，当客户端发送这部分决策的时候，则直接将最终决策发送到各个广播列表里面的客户端，并对其时间进行校对，保证每个客户端在收到的消息的时间是和根据本地时间进行校对过的。那么再采用预测拉扯中提到过的计算提前量，提高速度行走过去的方法，将会使同步变得非常的smooth。该方案的优点是不通过服务器，客户端自己之间进行同步，大大的降低了由于网络延迟而带来的误差，并且由于大部分决策都可以由客户端来做，也大大的降低了服务器的资源。由此带来的弊端就是由于消息和决策权都放在客户端本地，所以给外挂提供了很大的可乘之机。</p>
<p>　　综合以上三种关于网络同步派系的优缺点，综合出一套关于网络游戏传输同步的较完整的解决方案，我称它为综合同步法（colligate synchronization）。大体设计思路如下：</p>
<p>　　首先将服务器需要同步的所有消息从划分一个优先等级，然后按照3/4的比例划分出重要消息和非重要消息，对于非重要消息，把决策权放在客户端，在客户端逻辑上建立相关的决策机构和各种消息缓存区，以及相关的消息缓存区管理机构，如下图所示：</p>
<p><img height=247 src="http://dev.gameres.com/Program/Abstract/DeadReckoning1.gif" width=508 border=0></p>
<p>　　上图简单说明了对于非重要消息，客户端的大体处理流程，其中有一个客户端被动行为值得大家注意，其中包括对服务器发过来的某些验证代码做返回，来确保消息缓存中的消息和服务器端是一致的，从而有效的防止外挂来篡改本地消息缓存。其中的消息来源是包括本地的客户端响应玩家的消息以及远程服务器传递过来的消息。</p>
<p>　　对于重要消息，比如说战斗或者是某些牵扯到玩家一些比较敏感数据的操作，则采用另外一套方案，该方案首先需要在服务器和客户端之间建立一套Ping System，然后服务器保存和用户的及时的ping值，当ping比较小的时候，响应玩家消息的同时先不进行动作，而是先把该消息反馈给服务器，并且阻塞，服务器收到该消息，进行逻辑验证之后向所有该详细广播的有效对象进行广播（包括消息发起者），然后客户端收到该消息的验证，才开始执行动作。而当ping比较大的时候，客户端响应玩家消息的同时立刻进行动作，并且同时把该消息反馈给服务器，值得注意的是这个时候还需要在本地建立一个无验证消息的队列，把该消息入队，执行动作的同时等待服务器的验证，还需要保存当前状态。服务器收到客户端的请求后，进行逻辑验证，并把消息反馈到各个客户端，带上各个客户端校对过的本地时间。如果验证通过不过，则通知消息发起者，该消息验证失败，然后客户端自动把已经在进行中的动作取消，恢复原来状态。如果验证通过，则广播到的各个客户端根据从服务器获得校对时间进行对其进行拉扯，保证在该行为完成之前完成同步。</p>
<p><img height=396 src="http://dev.gameres.com/Program/Abstract/DeadReckoning2.gif" width=587 border=0></p>
<p>　　至此，一个比较成熟的网络游戏的同步机制已经初步建立起来了，接下来的逻辑代码就根据各自不同的游戏风格以及侧重点来写了。</p>
<p>　　同步是网络游戏最重要的问题，如何同步也牵扯到各个方面的问题，比如说游戏的规模，游戏的类型以及各种各样的方面，对于规模比较大的游戏，在同步方面可以下很多的工夫，把消息分得十分的细腻，对于不同的消息采用不同的同步机制，而对于规模比较小的游戏，则可以采用大体上一样的同步机制，究竟怎么样同步，没有个定式，是需要根据自己的不同情况来做出不同的同步决策的</p>
<font size=2><strong><br><br><font face=Arial>网游同步算法之导航推测（Dead Reckoning）算法：</font></strong><br><br><font face=Arial>　　在了解该算法前，我们先来谈谈该算法的一些背景资料。大家都知道，在网络传输的时候，延迟现象是很普遍的，而在基于Server/Client结构下的网络游戏的同步也就成了很头疼的问题，在保证客户端响应用户本地指令流畅的情况下，没法有效的保证的同步的及时性。同样，在军方也有类似的事情发生，即使是同一LAN里面的机器，也会因为传输的延迟，导致一些运算的失误，介于此，美国国防部投入了大量的资金用于研究一种比较的好的方案来解决分布式系统中的延迟问题，特别是一个叫分布式模拟运动（Distributed Interactive Simulation）的系统，这套系统呢，其中就提出了一套号称是Latency Hiding &amp; Bandwidth Reduction的方案，命名为Dead Reckoning。呵呵，来头很大吧，恩，那么我们下面就来看看这套系统的一些观点，以及我们如何把它运用到我们的网络游戏的同步中。<br><br>　　首先，这套同步方案是基于我那篇《网络游戏的同步》一文中的Mutual Synchronization同步方案的，也就是说，它并不是Server/Client结构的，而是基于客户端之间的同步的。下面我们先来说一些本文中将用到的名词概念：<br>　　网状网络：客户端之间构成的网络<br>　　节点：网状网络中的每个客户端<br>　　极限误差：进行同步的时候可能产生的误差的极值<br><br>　　恩，在探讨其原理的之前，我们先来看看我们需要一个什么样的环境。首先，需要一个网状网络，网状网络如何构成呢？当有新节点进入的时候，通知该网络里面的所有节点，各节点为该客户端在本地创建一个副本，登出的时候，则通知所有节点销毁本地关于该节点的副本。然后每个节点该保存一些什么数据呢？首先有一个很重要的包需要保存，叫做协议数据包（PDU Protocol Data Unit），PDU包含节点的一些相关的运动信息，比如当前位置，速度，运动方向，或者还有加速度等一些信息。除PDU之外，还有其他信息需要保存，比如说节点客户端人物的HP，MP之类的。然后，保证每个节点在最少8秒之内要向其它节点广播一次PDU信息。最后，设置一个极限误差值。到此，其环境就算搭建完成了。下面，我们就来看看相关的具体算法：<br><br>　　假设在节点A有一个小人（路人甲），开始跑路了，这个时候，就像所有的节点广播一次他的PDU信息，包括：速度（S），方向（O），加速度（A）。那么所有的节点就开始模拟路人甲的运动轨迹和路线，包括节点A本身（这点很重要），同时，路人甲在某某玩家的控制下，会不时的改变一下方向，让其跑路的路线变得不是那么正规。在跑路的过程中，节点A有一个值在不停的记录着其真实坐标和在后台模拟运动的坐标的差值，当差值大于极限误差的时候，则计算出当前的速度S，方向O和速度A（算法将在后面介绍），并广播给网络中其他所有节点。其他节点在收到这条消息之后呢，就可以用一些很平滑的移动把路人甲拉扯过去，然后重新调整模拟跑路的数据，让其继续在后台模拟跑路。<br><br>　　很显然，如果极限误差定义得大了，其他节点看到的偏差就会过大，如果极限偏差定义得小了，网络带宽就会增大。如果定义这个极限误差，就该根据各种数据的重要性来设计了。如果是回合制的网络游戏，那么在走路上把极限误差定义得大些无所谓，可以减少带宽。但是如果是及时打斗的网络游戏，那么就得把极限误差定义得小一些，否则会出现某人看到某人老远把自己给砍死的情况。<br><br>　　Dead Reckoning的主要算法有9种，但是只有两种是解决主要问题的，其他的基本上只是针对不同的坐标系的一些不同的算法，这里就不一一介绍了。好，那么我们下面来看传说中的最主要的两种算法：<br>　　　　<font color=#99ccff>第一：目标点 = 原点 + 速度 * 时间差</font><br>　　　　<font color=#99ccff>第二：目标点 = 原点 + 速度 * 时间差 + 1/2 * 加速度 * 时间差</font><br>呵呵，传说中的算法都是很经典的，虽然我们早在初中物理的时候就学过。<br><br>　　该算法的好处呢，正如它开始所说的，Latency Hiding &amp; Bandwidth Reduction，从原则上解决了网络延迟导致的不同步的问题，并且有效的减少了带宽，不好的地方就是该算法基本上只能使用于移动中的同步，当然，移动的同步是网络游戏中同步的最大的问题。<br><br>　　该方法结合我在《网络游戏的同步》一文中提出的综合同步法的构架可以基本上解决掉网络游戏中走路同步的问题。相关问题欢迎大家一起讨论。<br><br><br><strong>有关导航推测算法（Dead Reckoning）中的平滑处理：</strong><br><br>　　根据我上篇文章所介绍的，在节点A收到节点B新的PDU包时，如果和A本地的关于B的模拟运动的坐标不一致时，怎么样在A的屏幕上把B拽到新的PDU包所描叙的点上面去呢，上文中只提了用&#8220;很平滑的移动&#8221;把B&#8220;拉扯&#8221;过去，那么实际中应该怎么操作呢？这里介绍四种方法。 <br><br>　　第一种方法，我取名叫直接拉扯法，大家听名字也知道，就是直接把B硬生生的拽到新的PDU包所描叙的坐标上去，该方法的好处是：简单。坏处是：看了以下三种方法之后你就不会用这种方法了。<br><br>　　第二种方法，叫直线行走（Linear），即让B从它的当前坐标走直线到新的PDU包所描叙的坐标，行走速度用上文中所介绍的经典算法：<br>　　　　<font color=#99ccff>目标点 = 原点 + 速度 * 时间差 + 1/2 * 加速度 * 时间差算出：</font><br>　　首先算出从当前坐标到PDU包中描叙的坐标所需要的时间：<br>　　　　<font color=#99ccff>T = Dest( TargetB &#8211; OriginB ) / Speed</font><br>　　然后根据新PDU包中所描叙的坐标信息模拟计算出在时间T之后，按照新的PDU包中的运动信息所应该达到的位置：<br>　　　　<font color=#99ccff>_TargetB = NewPDU.Speed * T</font><br>　　然后根据当前模拟行动中的B和_TargetB的距离配合时间T算出一个修正过的速度_S：<br>　　　　<font color=#99ccff>_S = Dest( _TargetB &#8211; OriginB ) / T</font><br>　　然后在画面上让B以速度_S走直线到Target_B，并且在走到之后调整其速度，方向，加速度等信息为新的PDU包中所描叙的。<br><br>　　这种方法呢，非常的土，会让物体在画面上移动起来变得非常的不现实，经常会出现很生硬的拐角，而且对于经常要修改的速度_S，在玩家A的画面上，玩家B的行动会变得非常的诡异。其好处是：比第一种方法要好。<br><br>　　第三种方法，叫二次方程行走（Quadratic），该方法的原理呢，就是在直线行走的过程中，加入二次方程来计算一条曲线路径，让Dest( _TargetB &#8211; OriginB )的过程是一条曲线，而不是一条直线，恩，具体的实现方法，就是在Linear方法的计算中，设定一个二次方程，在Dest函数计算距离的时候根据设定的二次方程来计算，这样一来，可以使B在玩家A屏幕上的移动变得比较的有人性化一些。但是该方法的考虑也是不周全的，仅仅只考虑了TargetB到_TargetB的方向，而没有考虑新的PDU包中的方向描叙，那么从_TargetB开始模拟行走的时候，仍然是会出现比较生硬的拐角，那么下面提出的最终解决方案，将彻底解决这个问题。<br><br>　　最后一种方法叫：立方体抖动（Cubic Splines），这个东东比较复杂，它需要四个坐标信息作为它的参数来进行运算，第一个参数Pos1是OriginB，第二个参数Pos2是OriginB在模拟运行一秒以后的位置，第三个参数Pos3是到达_TargetB前一秒的位置，第四个参数pos4是_TargetB的位置。<br><br></font></font><font face=宋体 color=#99ccff size=2>Struct pos {<br>&nbsp;&nbsp;&nbsp; Coordinate X;<br>&nbsp;&nbsp;&nbsp; Coordinate Y;<br>}</font><font face=Arial size=2><br><br><font color=#99ccff>　　　Pos1 = OriginB<br>　　　Pos2 = OriginB + V<br>　　　Pos3 = _TargetB &#8211; V<br>　　　Pos4 = _TargetB</font><br>运动轨迹中(x, y)的坐标。<br><font color=#99ccff>　　　x = At^3 + Bt^2 + Ct + D<br>　　　y = Et^3 + Ft^2 + Gt + H</font><br>（其中时间t的取值范围为0-1，在Pos1的时候为0，在Pos4的时候为1）<br><br>x(0-3)代表Pos1-Pos4中x的值，y(0-3)代表Pos1-Pos4中y的值<br><font color=#99ccff>　　　A = x3 &#8211; 3 * x2 +3 * x1 &#8211; x0<br>　　　B = 3 * x2 &#8211; 6 * x1 + 3 * x0<br>　　　C = 3 * x1 &#8211; 3 * x0<br>　　　D = x0<br><br>　　　E = y3 &#8211; 3 * y2 +3 * y1 &#8211; y0<br>　　　F = 3 * y2 &#8211; 6 * y1 + 3 * y0<br>　　　G = 3 * y1 &#8211; 3 * y0<br>　　　H = y0</font><br><br>　　上面是公式，那么下面我们来看看如何获得Pos1-Pos4：首先，Pos1和 Pos2的取值会比较容易获得，根据OriginB配合当前的速度和方向可以获得，然而Pos3和Pos4呢，怎么获得呢？如果在从Pos1到Pos4的过程中有新的PDU到达，那么我们定义它为NewPackage。<br><br><font color=#99ccff>　　　Pos3 = NewPackage.X + NewPackage.Y * t + 1/2 * NewPackage.a * t^2<br>　　　Pos4 = Pos3 &#8211; (NewPackage.V + NewPackage.a * t)</font><br><br>如果没有NewPackage的情况下,则Pos3和Pos4按照开始所规定的方法获得。<br><br>至此，关于导航推测的算法大致介绍完毕。<br><br>欢迎讨论，联系作者：QQ 181194&nbsp;&nbsp; MSN: xiataiyi@hotmail.com<br>参考文献《Defeating Lag with Cubic Splines》</font></font>
<img src ="http://www.cppblog.com/keigoliye/aggbug/99742.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-10-29 14:18 <a href="http://www.cppblog.com/keigoliye/archive/2009/10/29/99742.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>负载均衡－－大型在线系统实现的关键(下篇)（服务器集群架构的设计与选择）</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/23/97073.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 23 Sep 2009 15:48:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/23/97073.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/97073.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/23/97073.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/97073.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/97073.html</trackback:ping><description><![CDATA[本文作者：sodme<br>本文出处：<a  href="http://blog.csdn.net/sodme">http://blog.csdn.net/sodme</a><br>声明：本文可以不经作者同意任意转载，但任何对本文的引用都须注明作者、出处及此声明信息。谢谢！！<br><br>　　在网络应用中，&#8220;负载均衡&#8221;已经不能算是什么新鲜话题了，从硬件到软件，也都有了很多的方法来实现负载均衡。我们这里讨论的负载均衡，并不是指依靠DNS转向或其它硬件设备等所作的负载均衡，而是指在应用层所作的负载均衡。<br><br>　　一般而言，只有在大型在线系统当中才有必要引入负载均衡，那么，多大的系统才能被称为大型系统呢？比如动辄同时在线数十万的网络游戏，比如同时在线数在10万以上的WEB应用，这些我们都可以理解为大型系统，这本身就是一个宽泛的概念。<br><br>
设计再好的服务器程序，其单个程序所能承载的同时访问量也是有限的，面对一个庞大且日益增长的网络用户群，如何让我们的架构能适应未来海量用户访问，这
自然就牵涉到了负载均衡问题。支持百万级以上的大型在线系统，它的架构核心就是如何将&#8220;百万&#8221;这么大的一个同时在线量分摊到每个单独的服务器程序上去。真
正的逻辑处理应该是在这最终的底层的服务器程序（如QQ游戏平台的游戏房间服务器）上的，而在此之前所存在的那些服务器，都可以被称为&#8220;引路者&#8221;，它们的
作用就是将客户端一步步引导到这最终的负责真正逻辑的底层服务器上去，我们计算&#8220;百万级在线&#8221;所需要的服务器数量，也是首先考虑这底层的逻辑服务器单个可
承载的客户端连接量。<br><br>　　比如：按上篇我们所分析QQ游戏架构而言，假设每个服务器程序最高支持2W的用户在线（假设一台机子只运行一个
服务器程序），那么实现150万的在线量至少需要多少台服务器呢？如果算得简单一点的话，就应该是：150/2=75台。当然，这样算起来，可能并不能代
表真正的服务器数量，因为除了这底层的服务器外，还要包括登录/账号服务器以及大厅服务器。但是，由于登录/账号服务器和大厅服务器，它们与客户端的连接
都属于短连接（即：取得所需要信息后，客户端与服务器即断开连接），所以，客户端给这两类服务器带来的压力相比于长连接（即：客户端与服务器始终保持连
接）而言就要轻得多，它们的压力主要在处理瞬间的并发访问上。<br><br>　　&#8220;短连接&#8221;，是实现应用层负载均衡的基本手段！！！如果客户端要始终与登录/账号服务器以及大厅服务器保持连接，那么这样作的分层架构将是无意义的，这也没有办法从根本上解决用户量不断增长与服务器数量有限之间的矛盾。<br><br>
当然，短连接之所以可以被使用并能维护正常的游戏逻辑，是因为在玩家看不到的地方，服务器与服务器之间进行了大量的数据同步操作。如果一个玩家没有登录
到登录服务器上去而是直接连接进了游戏房间服务器并试图进行游戏，那么，由于游戏房间服务器与大厅服务器和登录/账号服务器之间都会有针对于玩家登录的逻
辑维护，游戏房间服务器会检测出来该玩家之前并没有到登录服务器进行必要的账号验证工作，它便会将玩家踢下线。由此看来，各服务器之间的数据同步，又是实
现负载均衡的又一必要条件了。<br><br>　　服务器之间的数据同步问题，依据应用的不同，也会呈现不同的实现方案。比如，我们在处理玩家登录这个问
题上。我们首先可以向玩家开放一些默认的登录服务器（服务器的IP及PORT信息），当玩家连接到当前的登录服务器后，由该服务器首先判断自己同时连接的
玩家是不是超过了自定义的上限，如果是，由向与该服务器连接着的&#8220;登录服务器管理者&#8221;（一般是一个内部的服务器，不直接向玩家开放）申请仲裁，由&#8220;登录服
务器管理者&#8221;根据当前各登录服务器的负载情况选择一个新的服务器IP和PORT信息传给客户端，客户端收到这个IP和PORT信息之后重定向连接到这个新
的登录服务器上去，完成后续的登录验证过程。<br><br>　　这种方案的一个特点是，在面向玩家的一侧，会提供一个外部访问接口，而在服务器集群的内部，会提供一个&#8220;服务器管理者&#8221;及时记录各登录服务器的负载情况以便客户端需要重定向时根据策略选择一个新的登录接口给客户端。<br><br>
采用分布式结构的好处是可以有效分摊整个系统的压力，但是，不足点就是对于全局信息的索引将会变得比较困难，因为每个单独的底层逻辑服务器上都只是存放
了自己这一个服务器上的用户数据，它没有办法查找到其它服务器上的用户数据。解决这个问题，简单一点的作法，就是在集群内部，由一个中介者，提供一个全局
的玩家列表。这个全局列表，根据需要，可以直接放在&#8220;服务器管理者&#8221;上，也可以存放在数据库中。<br><br>　　对于逻辑相对独立的应用，全局列表的
使用机会其实并不多，最主要的作用就是用来检测玩家是不是重复登录了。但如果有其它的某些应用，要使用这样的全局列表，就会使数据同步显得比较复杂。比
如，我们在超大无缝地图的MMORPG里，如果允许跨服操作(如跨服战斗、跨服交易等)的话，这时的数据同步将会变得异常复杂，也容易使处理逻辑出现不可
预测性。<br><br>　　我认为，对于休闲平台而言，QQ游戏的架构已经是比较合理的，也可以称之为休闲平台的标准架构了。那么，MMORPG一般的架构是什么样的呢？<br><br>
MMORPG一般是把整个游戏分成若干个游戏世界组，每个组内其实就是一个单独的游戏世界。而不同的组之间，其数据皆是相互独立的，并不象QQ休闲平台
一样所有的用户都会有一个集中的数据存放点，MMORPG的游戏数据是按服务器组的不同而各自存放的。玩家在登录QQ游戏时，QQ游戏相关的服务器会自动
为玩家的登录进行负载均衡，选择相对不忙的服务器为其执行用户验证并最终让用户选择进入哪一个游戏房间。但是，玩家在登录MMORPG时，却没有这样的自
动负载均衡，一般是由玩家人为地去选择要进入哪一个服务器组，之所以这样，是因为各服务器组之间的数据是不相通的。其实，细致想来，MMORPG的服务器
架构思想与休闲平台的架构思想有异曲同工之妙，MMORPG的思想是：可以为玩家无限地开独立的游戏世界（即服务器组），以满足大型玩家在线；而休闲平台
的思想则是：可以为玩家无限地开游戏房间以满足大量玩家在线。这两种应用，可以无限开的都是&#8220;具有完整游戏性的游戏世界&#8221;，对于MMORPG而言，它的一
个完整的游戏地图就是一个整体的&#8220;游戏世界&#8221;，而对于休闲平台，它的一个游戏房间就可以描述为一个&#8220;游戏世界&#8221;。如果MMORPG作成了休闲平台那样的全
服皆通，也不是不可以，但随之而来的，就是要解决众多跨服问题，比如：好友、组队、帮派等等的问题，所有在传统MMORPG里所定义的这些玩家组织形式的
规则可能都会因为&#8220;全服皆通&#8221;而改变。<br><br>　　架构的选择是多样性的，确实没有一种可以称得上是最好的所谓的架构，适合于当前项目的，不一定就适合于另一个项目。针对于特定的应用，会灵活选用不同的架构。但有一点，是可以说的：不管你如何架构，你所要作的就是－－要以尽可能简单的方案实现尽可能的稳定、高效！<img src ="http://www.cppblog.com/keigoliye/aggbug/97073.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-23 23:48 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/23/97073.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>逆向思维--魔兽世界封包分析</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/23/97074.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 23 Sep 2009 15:48:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/23/97074.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/97074.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/23/97074.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/97074.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/97074.html</trackback:ping><description><![CDATA[<p>本文作者：sodme<br>本文出处：<a  href="http://blog.csdn.net/sodme">http://blog.csdn.net/sodme</a><br>声明：本文可以不经作者同意任意转载，但任何对本文的引用都须注明作者、出处及此声明信息。谢谢！！</p>
<p>　　特别声明：<br>　　本人非常欣赏暴雪及他们的游戏，之所以写这个文章，是想让大家了解一些网络封包分析方面的常见方法以及学习暴雪游戏在网
络处理方面的经验，偶认为作为一个网络编程者，熟练掌握封包分析的工具和方法应该是其基本功之一。本文所列的所有封包分析内容，全部是采用普通黑箱方式即
可得来的，并未涉及对魔兽世界可执行程序的逆向工程。同时，除此文涉及的内容外，本人拒绝向任何人透露更详细的关于魔兽世界封包方面的更多内容，有兴趣者
请自己进行相关的试验，本人在此文中也将尽量避免公开敏感的封包内容及相关加解密算法。谨以此文献给忠爱的暴雪！</p>
<p>　　一、登录模块流程及封包分析<br><br>　　我们先看登录流程。从封包流程来看，魔兽的登录流程是这样的：<br><br>　　1.由
Client向登录/账号服务器(Login
Server)发送用户名及密码等信息。此数据包的最后部分是用户名(明文表示，未加密)，在用户名的前一个字节表示的是用户名的长度。登录/账号服务器
向Client返回登录成功及后续连接到游戏服务器服务器所必备的信息等。这中间的两个来往数据包，我还没有看出具体有什么作用。在这个交互过程中，由登
录/账号服务器向Client发送所有的游戏服务器列表，服务器列表数据包的内容包括：ip, port,
服务器上所拥有的角色个数等信息，因服务器列表内容过多，被客户端分为两次接收完毕。<br><br>　　2.Client收到Login
Server的服务器列表后，根据最近访问的服务器标识(这个信息应该是包含在那个服务器列表数据包中)，连接到最近游戏的那个游戏服务器(Game
Server)。连接成功后，Game
Server首先向Client发送一个8字节的数据包，据以往的常识判断，这个数据包的内容很可能是以后客户端与服务器通信的加密密钥。<br><br>
3.Client向Game Server再次发送自己的账号信息。Game
Server与Client经过两个数据包的交互后，向Client发送角色数据包，此包中包括了玩家在该Game
Server所创建的所有角色信息，当然这个信息只是部分的，并不是该角色的所有信息。<br><br>　　4.在此后的通信过程中，Client每隔30秒向Game Server发送一个保持连接的包，该包长度为10字节，包的最后四字节是一个递增数字，前面6字节暂时未看出规律。<br><br>
5.只要Client没有点击某个角色进入最终的Game Server，则Client要始终与Login
Server保持连接。当Client点击角色进入Game Server时，Client才与Login
Server断开连接。在以后的游戏过程中，Client始终与且仅与该Game Server进行数据通信。</p>
<p>　　通过对登录流程中的数据包初步分析，可以得出以下几个结论：<br>　　1.Client向Login
Server发的第一个数据包，用户名部分是采用明文的，且该数据包的内容，每次登录都一样，并没有因时间的不同而发生改变。由此可以推算：针对于此数据
包中的密码加密算法是固定不变的，换句话说，密码的加密算法是比较容易通过逆向工程被找到的。偶认为，针对于此处，服务器也应该先向客户端发送一个加密密
钥，以后的通信可以用该密钥作为安全验证的依据。但暴雪没有这样作，最大的可能是为了提高服务器的效率，在登录服务器上，如果每个客户端一旦连接成功，登
录服务器都得向客户端广播一个数据包的话，可能这个量还是比较大的，这可能延长了玩家的登录等待时间，所以他们没有在这块作。<br><br>
2.Client在登录Login Server的地址，每次Login
Server的登录地址都可能是不一样的。偶没有在客户端目录里找到这些地址，只在客户端目录里找到了四个大区的四个域名，我猜想，魔兽世界是用的DNS
解析的简单方法来实现Login Server的简单动态均衡的。不知道这个猜想是否正确。<br><br>　　3.&#8220;根据玩家最近在玩的哪个游戏，由客户端和服务器自动为玩家选择进入这个游戏服务器&#8221;，这一项设定充分体现了暴雪一贯的风格：为玩家着想，最大限度地提高游戏的舒适度。再次对暴雪的态度予以肯定！<br><br>　　4.一旦玩家进入了游戏世界，客户端与服务器的通信端口会一直保持不变。即：魔兽世界的游戏世界服务器群设计结构采用的是带网关的服务器集群。<br><br>
5.偶觉得在整个的登录流程中，让我产生最大疑问的就是Login Server与Client的连接保持逻辑。当Client与Game
Server连接了之后，Client并未与Login
Server断开，是一直保持连接的。后来，经进一步的抓包分析，Client之所以要与Login
Server保持这样的连接，是为了当Client重新选择服务器时，不至于重新连接Login
Server。当Client点击了"选择服务器"按纽后，Login
Server会每隔5秒向Client发一个当前所有的服务器列表数据包。要知道，这个服务器列表数据包的内容可是非常大的，如果有玩家就打开了这个窗口
不关闭，Login
Server向这种情况的所有玩家每5秒钟就发一个服务器列表数据包，这个广播量可是很大的哦(2k左右，这可是一个用户是2k哦)。偶认为这里的逻辑设
计是相当不合理的。Login
Server如果为了给客户端提供一个最新的全局服务器列表，可以保持连接，但也没必要每隔5秒就向客户端发一个服务器列表，最多只在客户端在某个服务器
上创建了不同的角色后再更新这个列表也是可以的，但只用更新这个列表中的变化内容即可，不用发全部的完整包，这样，在通信量上就小了很多。据说，魔兽刚开
始的时候，产生DOWN机的原因就是登录模块没有处理好，偶不知道现在的这个情况是不是已经经过改良的了。但偶还是认为每隔5秒就向客户端发送一个2K的
包，这一点是不可以被接受的。</p>
<p>　　以上只是针对于魔兽世界登录流程的简单分析，没有多少技术含量，拿出来跟大家相互讨论讨论，看看有没有可以借鉴的地方，后面还会有其它部分的封包分析。欢迎继续关注偶的Blog: <a  href="http://blog.csdn.net/sodme">http://blog.csdn.net/sodme</a>。</p>
<p>　　偶在文章前面部分说过，作为一个网络编程人员，熟练使用截包软件和掌握基本的封包分析方法是其基本能力之一，发此文的目的一个原因也是希望向正在作网络编程的兄弟介绍一下相关工具的使用和常见的分析方法。下面补充一下关于封包分析的基本方法和相关工具：<br><br>　　1.你需要一个截包工具，偶推荐：commview，小巧但功能强大，支持自定义的封包分析插件以DLL形式装载，也就是说只要你愿意，你可以写个DLL对某类特殊形式的包进行显示、记录、解密等特别处理。<br><br>
2.如何查看真正的封包数据。在commview里，会详细列出自网卡级别以上的各层封包数据，包括Ethernet层，IP层和TCP层。而我们作封
包分析时，只需要关注TCP层。但TCP层里也有很多内容，对于我们的分析需求来说，我们需要关注的是其Data字段（在协议目录里可以看到"data
length标识，点击即可查看data段"）的内容。<br><br>　　3.TCP的几个状态对于我们分析所起的作用。在TCP层，有个FLAGS字
段，这个字段有以下几个标识：SYN, FIN, ACK, PSH, RST,
URG.其中，对于我们日常的分析有用的就是前面的五个字段。它们的含义是：SYN表示建立连接，FIN表示关闭连接，ACK表示响应，PSH表示有
DATA数据传输，RST表示连接重置。其中，ACK是可能与SYN，FIN等同时使用的，比如SYN和ACK可能同时为1，它表示的就是建立连接之后的
响应，如果只是单个的一个SYN，它表示的只是建立连接。TCP的几次握手就是通过这样的ACK表现出来的。但SYN与FIN是不会同时为1的，因为前者
表示的是建立连接，而后者表示的是断开连接。RST一般是在FIN之后才会出现为1的情况，表示的是连接重置。一般地，当出现FIN包或RST包时，我们
便认为客户端与服务器端断开了连接；而当出现SYN和SYN＋ACK包时，我们认为客户端与服务器建立了一个连接。PSH为1的情况，一般只出现在
DATA内容不为0的包中，也就是说PSH为1表示的是有真正的TCP数据包内容被传递。TCP的连接建立和连接关闭，都是通过请求－响应的模式完成的。</p>
<p><br></p>
<p>　　封包分析的手段，说简单也挺简单的，那就是：比较！要不断地从不同的思维角度对封包进行对比分析，要充分发挥你的想象力不断地截取自己需要的包进行比较。不仅要作横向（同类）的比较，还要作纵向（不同类）的比较。即时对于同一个包，也要不断地反复研究。</p>
<p>　　初涉封包分析的新手，一般会不知道封包分析究竟该从何入手。基于经验，本文将告诉你一般会从哪些类型的包入手进行分析以及应该怎样对封包进行初
步的分析。需要指出的是：封包分析是一件非常有趣但同时也非常考验耐心的事，通常，半天的封包分析下来，会让你眼前全是诸如&#8220;B0 EF 58 02
10 72....&#8221;之类的网络数据，而且附带有头疼、头晕症状，所以，没有充分的心理准备，还请不要轻易尝试。呵呵。</p>
<p>　　从事封包分析的基本前提是：应该了解和熟悉TCP协议，并知道数据包&#8220;粘合&#8221;是怎么一回事。当然，我们平常截获到的包，从数量上来看，只有一小
部分是属于&#8220;粘合&#8221;的情况。但如果不了解它，将可能会对你的分析思路产生误导和困惑。关于&#8220;粘包&#8221;的更详细解释，请参考我的另外一篇文章&#8220;拼包函数及网络
封包的异常处理(含代码) (<a  href="http://blog.csdn.net/sodme/archive/2005/07/10/419233.aspx">http://blog.csdn.net/sodme/archive/2005/07/10/419233.aspx</a>)&#8221;。</p>
<p>　　上一篇有关魔兽世界封包分析的文章（<a  href="http://blog.csdn.net/sodme/archive/2005/06/18/397371.aspx">http://blog.csdn.net/sodme/archive/2005/06/18/397371.aspx</a>）中，我根据客户端与服务器端连接及断开事件的处理流程以及登录过程中的一些数据包分析了魔兽的架构和登录逻辑。这篇文章中，我将结合聊天数据包的分析，来阐述魔兽世界封包的大体结构。&nbsp;&nbsp; </p>
<p>　　首先解释一下我们的目标：封包的大体结构。封包的大体结构包含哪些内容呢？一般情况下，封包的大体结构至少包括两方面的信息：<br>　　1、一个封包是如何表示它的长度的？封包长度是由哪个字段表示的？（或者说：如何表示封包的开始和结束的）<br>　　2、各种不同的封包类型是通过哪个字段表示的？</p>
<p>　　是不是所有游戏的封包都必然会有表示&#8220;长度&#8221;信息的&#8220;字段&#8221;呢？答案是否定的。有的游戏确实没有采用这种方式，它们的作法设定特殊的包开始和包
结束标志。但是，从应用的角度来看，偶推荐使用&#8220;长度&#8221;这样的方法，因为不管在网络底层的处理效率以及上层应用的处理便捷性来说，使用&#8220;长度&#8221;字段标识一
个完整的逻辑包都是比较好的办法。在确定了封包的大体结构后，我们才方便分析具体类型包（比如聊天、行走等）的详细结构。</p>
<p>　　作数据包分析，在单纯采用黑箱分析的阶段，我们选取的数据包，须要是具有这种性质的，即：在数据包发送前客户端未进行加密等处理时，这个数据包
中的部分内容，我们是已经知道的。这样的包，就可以作为封包分析的突破口。这样看来，我们拿&#8220;聊天封包&#8221;作为第一个分析对象也就不难理解了，因为我们说的
话，打上去的字，我们自己是知道的，但是我们说的话经过客户端的处理后，发到网络上的可能就是已经加了密的或者加了校验码的。站在黑箱分析的角度，我们能
作的，就是不断截取各种&#8220;聊天包&#8221;进行对比、判断和总结。</p>
<p>　　OK，打开你的commview。让我们从&#8220;聊天封包&#8221;开始。</p>
<p>　　分析&#8220;聊天包&#8221;的前提，是我们能够正常判断哪种类型的数据包是属于聊天的，不要误把行走或其它的数据包当作了聊天数据包。为了减小分析难度，建
议新手到游戏中人少或周围没有玩家的地方进行封包分析。这样一来没人打扰，二来你的网络通信量会相对小得多，比较容易进行一些封包判定。</p>
<p>　　第一步，我们需要确定客户端与服务器通信所用的端口，然后在commview的rules-&gt;ports中设定服务器端口，截获与该端口
通信的所有数据包。服务器端口的确定方法：不要使用其它网络通信工具，打开commview，进游戏，截包，观察其通信端口。进行封包分析时，特别是初期
的封包分析时，你的网络通信应该尽可能是单一的，即：除了游戏，其它的通信软件尽可能不要开。但当你确定了服务器的IP和端口后，就可以照常使用其它网络
软件了。</p>
<p>　　第二步，如前面述，在游戏中找个人少或没人的地方，开始&#8220;自言自语&#8221;，呵呵。说话的内容，建议以字母和数字为宜，不要说中文。因为中文是双字节
的，而字母和数字是单字节的，对于单字节的信息内容，截包软件会以单字节的文本信息显示，但对于双字节的汉字而言，截包软件在对其进行显示时由于换行等原
因会造成部分中文显示有乱码，不容易直接看出中文内容。如果执意要说中文，偶也不拦你，给你推荐一个工具：String Demander(下载地址：<a  href="http://www.cnxhacker.com/Download/show/395.html">http://www.cnxhacker.com/Download/show/395.html</a>)，这个软件，可以查询中文所对应的编码。</p>
<p>　　第三步，设定好commview的rules并使之生效，开始截包。</p>
<p>　　观察通过以上的过程所截的包，可以发现，魔兽世界的聊天封包的说话内容是明文的！这一点，用不着大惊小怪，呵呵。聊天封包本身并不会对游戏的关
键逻辑造成损害，所以，即使让其明文显示也不足为奇。但是，我们还是不太相信自己的眼睛，于是再截若干个包，发现包中的说话内容确实是明文的！但是，包的
其它字段却是我们一时看不懂的&#8220;密文&#8221;。</p>
<p>　　看来，下面的事情就是对这些包里的&#8220;密文&#8221;进行研究了。一般情况下，这种&#8220;密文&#8221;的加密方法，通过封包分析是分析不出来的，但，我们仍然可以通过封包分析来推论一些与&#8220;密文&#8221;生成算法有关的问题。我们可以作以下的对比分析：<br>　　1、连续三次输入&#8220;a&#8221;，并分别观察及保存封包数据；<br>　　2、连续三次输入&#8220;aa&#8221;，并分别观察及保存封包数据；<br>　　3、连续三次输入&#8220;aaa&#8221;，并分别观察及保存封包数据。 </p>
<p>　　输入的封包用例，我们选择了字母"a"，它的ASCII码是61。输入的规律是：每种情况连续输入三次，然后逐次增加a字母的个数。于是，我们发现这样一个有趣的现象：<br>　　1、包中有关说话的内容是明文的；<br>　　2、即使针对于同样的说话内容，比如&#8220;a&#8221;，客户端所发出去的包也是不一样的；<br>　　3、当一次说话的字母个数增加1时，封包的总体长度也随之增加1；<br>　　4、除每个封包的前面6个字节以及说话的字节外，其余的封包内容每次都一样；<br>　　5、每个聊天封包的结尾字节都是0。</p>
<p>　　于是，我们可以试着得出如下结论：<br>　　1、包是没有压缩的，它所使用的加密算法应该是按字节进行的，并没有改变封包的长度使之看上去使用统一的长度；<br>　　2、包是以0结尾的（尽管我们不知道它是以什么表示开头的，呵呵）；<br>
3、封包加密算法中所使用的密钥是可变的，即针对于相同的数据包内容由于加密的密钥不同，所以产生的密文也不同。由于客户端的数据传到服务器端后，服务
器端还要对数据进行解密。所以，客户端的加密算法与服务器端的解密算法应该共用了前6字节中的某些内容，以此作为解密算法的密钥。如果这6字节中没有包含
有关封包加、解密所需要的同步数据，那客户端和服务器之间应该会通过其它的方式同步这样的数据。不过，偶倾向于前者，即：这6字节中应该含有加、解密所需
要的密钥信息。</p>
<p>　　回头看我们上面观察到的有趣现象，针对于第2点，反过来想，这应该也是最起码的功能了。就是说，即使客户端作出的是同样的动作，在客户端发出的包中，包的内容也是不一样的。这样，外挂就不能靠单纯的重复发相同的包而达到其目的了。</p>
<p>　　分析来分析去，我们还是没能确定魔兽封包的大体结构。其实，到现在，我觉得我此文的目的已经达到了，即向大家展示封包分析的思维角度和思维方
式。至于具体结果，偶觉得倒真的不重要的了。可以肯定地告诉大家的是，魔兽的封包结构偶大致已经掌握了。偶仅在此公布我的分析结果：<br>　　1、魔兽的封包长度字段是每个封包的前两字节，它的表示方式是：前两字节的数值＋2。之所以加这个2，是因为封包长度字段本身占用了两个字节的长度。<br>　　2、魔兽的封包类型偶推断是第三和第四字节，其中普通聊天的类型标识是&#8220;95 00&#8221;。</p>
<p>　　请不要来信向我询问任何有关魔兽封包破解的内容，偶能说的都已经在文章里说了，偶之所以写这个系列的文章不是想破解魔兽，而是想以这样优秀的一
款游戏作为案例来向大家展示它在封包设计方面值得我们学习和讨论的地方，同时向更多的朋友普及有关封包分析的常识、工具以及思维方式，仅此而已。</p>
<p>　　ps:由于每次封包分析的内容都很多，所以，一有了点结论后，要及时记录和总结，并与之前取得的总结进行对比，及时更新相关的记录文档。</p><img src ="http://www.cppblog.com/keigoliye/aggbug/97074.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-23 23:48 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/23/97074.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>从“魔兽世界”的服务器崩溃说开去......</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/23/97071.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 23 Sep 2009 15:47:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/23/97071.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/97071.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/23/97071.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/97071.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/97071.html</trackback:ping><description><![CDATA[本文作者：sodme　本文出处：<a  href="http://blog.csdn.net/sodme"><font color="#223355">http://blog.csdn.net/sodme</font></a><br>版权声明：本文可以不经作者同意任意转载，但转载时烦请保留文章开始前两行的版权、作者及出处信息。<br>
<p>提示：阅读本文前，请先读此文了解文章背景：<a  href="http://data.gameres.com/message.asp?TopicID=27236">http://data.gameres.com/message.asp?TopicID=27236</a><br><br>　　让无数中国玩家为之瞩目的&#8220;魔兽世界&#8221;，随着一系列内测前期工作的逐步展开，正在一步步地走近中国玩家，但是，&#8220;魔兽&#8221;的服务器，却着实让我们为它捏了一把汗。<br><br>
造成一个网游服务器当机的原因有很多，但主要有以下两种：一，服务器在线人数达到上限，服务器处理效率严重迟缓，造成当机；二，由于外挂或其它游戏作弊
工具导致的非正常数据包的出错，导致游戏服务器逻辑出现混乱，从而造成当机。在这里，我主要想说说后者如何尽可能地避免。<br><br>　　要避免以上
所说到的第二种情况，我们就应该遵循一个基本原则：在网游服务器的设计中，对于具有较强逻辑关系的处理单元，服务器端和客户端应该采用&#8220;互不信任原则&#8221;，
即：服务器端即使收到了客户端的数据包，也并不是立刻就认为客户端已经达到了某种功能或者状态，客户端到达是否达到了某种功能或者状态，还必须依靠服务器
端上记载的该客户端&#8220;以往状态&#8221;来判定，也就是说：服务器端的逻辑执行并不单纯地以&#8220;当前&#8221;的这一个客户端封包来进行，它还应该广泛参考当前封包的上下文
环境，对执行的逻辑作出更进一步地判定，同时，在单个封包的处理上，服务器端应该广泛考虑当前客户端封包所需要的&#8220;前置&#8221;封包，如果没有收到该客户端应该
发过来的&#8220;前置&#8221;封包，则当前的封包应该不进行处理或进行异常处理（如果想要性能高，则可以直接忽略该封包；如果想让服务器稳定，可以进行不同的异常处
理）。<br><br>　　之所以采用&#8220;互不信任&#8221;原则设计网游服务器，一个很重要的考虑是：防外挂。对于一个网络服务器（不仅仅是游戏服务器，泛指所有
服务器）而言，它所面对的对象既有属于自己系统内的合法的网络客户端，也有不属于自己系统内的非法客户端访问。所以，我们在考虑服务器向外开放的接口时，
就要同时考虑这两种情况：合法客户端访问时的逻辑走向以及非法客户端访问时的逻辑走向。举个简单的例子：一般情况下，玩家登录逻辑中，都是先向服务器发送
用户名和密码，然后再向服务器发送进入某组服务器的数据包；但在非法客户端（如外挂）中，则这些客户端则完全有可能先发进入某组服务器的数据包。当然，这
里仅仅是举个例子，也许并不妥当，但基本的意思我已经表达清楚了，即：你服务器端不要我客户端发什么你就信什么，你还得进行一系列的逻辑验证，以判定我当
前执行的操作是不是合法的。以这个例子中，服务器端可以通过以下逻辑执行验证功能：只有当客户端的用户名和密码通过验证后，该客户端才会进入在线玩家列表
中。而只有在线玩家列表中的成员，才可以在登陆服务器的引导下进入各分组服务器。<br><br>　　总之，在从事网游服务器的设计过程中，要始终不移地
坚持一个信念：我们的服务器，不仅仅有自己的游戏客户端在访问，还有其它很多他人写的游戏客户端在访问，所以，我们应该确保我们的服务器是足够强壮的，任
它风吹雨打也不怕，更不会倒。如果在开发实践中，没有很好地领会这一点或者未能将这一思路贯穿进开发之中，那么，你设计出来的服务器将是无比脆弱的。<br><br>
当然，安全性和效率总是相互对立的。为了实现我们所说的&#8220;互不信任&#8221;原则，难免的，就会在游戏逻辑中加入很多的异常检测机制，但异常检测又是比较耗时
的，这就需要我们在效率和安全性方面作个取舍，对于特别重要的逻辑，我们应该全面贯彻&#8220;互不信任&#8221;原则，一步扣一步，步步为营，不让游戏逻辑出现一点漏
洞。而对于并非十分重要的场合，则完全可以采用&#8220;半信任&#8221;或者根本&#8220;不须信任&#8221;的原则进行设计，以尽可能地提高服务器效率。<br><br>　　本文只是对自己长期从事游戏服务器设计以来的感受加以总结，也是对魔兽的服务器有感而发。欢迎有相同感受的朋友或从事相同工作的朋友一起讨论。</p><img src ="http://www.cppblog.com/keigoliye/aggbug/97071.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-23 23:47 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/23/97071.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>拼包函数及网络封包的异常处理(含代码)</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/23/97072.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 23 Sep 2009 15:47:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/23/97072.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/97072.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/23/97072.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/97072.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/97072.html</trackback:ping><description><![CDATA[<p>本文作者：sodme<br>本文出处：<a  href="http://blog.csdn.net/sodme">http://blog.csdn.net/sodme</a><br>声明：本文可以不经作者同意任意转载、复制、传播，但任何对本文的引用都请保留作者、出处及本声明信息。谢谢！<br><br>
常见的网络服务器，基本上是7*24小时运转的，对于网游来说，至少要求服务器要能连续工作一周以上的时间并保证不出现服务器崩溃这样的灾难性事件。事
实上，要求一个服务器在连续的满负荷运转下不出任何异常，要求它设计的近乎完美，这几乎是不太现实的。服务器本身可以出异常（但要尽可能少得出），但是，
服务器本身应该被设计得足以健壮，&#8220;小病小灾&#8221;打不垮它，这就要求服务器在异常处理方面要下很多功夫。</p>
<p>　　服务器的异常处理包括的内容非常广泛，本文仅就在网络封包方面出现的异常作一讨论，希望能对正从事相关工作的朋友有所帮助。</p>
<p>　　关于网络封包方面的异常，总体来说，可以分为两大类：一是封包格式出现异常；二是封包内容（即封包数据）出现异常。在封包格式的异常处理方面，
我们在最底端的网络数据包接收模块便可以加以处理。而对于封包数据内容出现的异常，只有依靠游戏本身的逻辑去加以判定和检验。游戏逻辑方面的异常处理，是
随每个游戏的不同而不同的，所以，本文随后的内容将重点阐述在网络数据包接收模块中的异常处理。</p>
<p>　　为方便以下的讨论，先明确两个概念（这两个概念是为了叙述方面，笔者自行取的，并无标准可言）：<br>　　1、逻辑包：指的是在应用层提交的数据包，一个完整的逻辑包可以表示一个确切的逻辑意义。比如登录包，它里面就可以含有用户名字段和密码字段。尽管它看上去也是一段缓冲区数据，但这个缓冲区里的各个区间是代表一定的逻辑意义的。<br>　　2、物理包：指的是使用recv(recvfrom)或wsarecv(wsarecvfrom)从网络底层接收到的数据包，这样收到的一个数据包，能不能表示一个完整的逻辑意义，要取决于它是通过UDP类的&#8220;数据报协议&#8221;发的包还是通过TCP类的&#8220;流协议&#8221;发的包。</p>
<p>　　我们知道，TCP是流协议，&#8220;流协议&#8221;与&#8220;数据报协议&#8221;的不同点在于：&#8220;数据报协议&#8221;中的一个网络包本身就是一个完整的逻辑包，也就是说，在应
用层使用sendto发送了一个逻辑包之后，在接收端通过recvfrom接收到的就是刚才使用sendto发送的那个逻辑包，这个包不会被分开发送，也
不会与其它的包放在一起发送。但对于TCP而言，TCP会根据网络状况和neagle算法，或者将一个逻辑包单独发送，或者将一个逻辑包分成若干次发送，
或者会将若干个逻辑包合在一起发送出去。正因为TCP在逻辑包处理方面的这种粘合性，要求我们在作基于TCP的应用时，一般都要编写相应的拼包、解包代
码。</p>
<p>　　因此，基于TCP的上层应用，一般都要定义自己的包格式。TCP的封包定义中，除了具体的数据内容所代表的逻辑意义之外，第一步就是要确定以何种方式表示当前包的开始和结束。通常情况下，表示一个TCP逻辑包的开始和结束有两种方式：<br>　　1、以特殊的开始和结束标志表示，比如FF00表示开始，00FF表示结束。<br>　　2、直接以包长度来表示。比如可以用第一个字节表示包总长度，如果觉得这样的话包比较小，也可以用两个字节表示包长度。</p>
<p>　　下面将要给出的代码是以第2种方式定义的数据包，包长度以每个封包的前两个字节表示。我将结合着代码给出相关的解释和说明。</p>
<p>　　函数中用到的变量说明：</p>
<p>　　CLIENT_BUFFER_SIZE：缓冲区的长度，定义为：Const int CLIENT_BUFFER_SIZE=4096。<br>　　m_ClientDataBuf：数据整理缓冲区，每次收到的数据，都会先被复制到这个缓冲区的末尾，然后由下面的整理函数对这个缓冲区进行整理。它的定义是：char m_ClientDataBuf[2* CLIENT_BUFFER_SIZE]。<br>　　m_DataBufByteCount：数据整理缓冲区中当前剩余的未整理字节数。<br>　　GetPacketLen(const char*)：函数，可以根据传入的缓冲区首址按照应用层协议取出当前逻辑包的长度。<br>　　GetGamePacket(const char*, int)：函数，可以根据传入的缓冲区生成相应的游戏逻辑数据包。<br>　　AddToExeList(PBaseGamePacket)：函数，将指定的游戏逻辑数据包加入待处理的游戏逻辑数据包队列中，等待逻辑处理线程对其进行处理。<br>　　DATA_POS：指的是除了包长度、包类型等这些标志型字段之外，真正的数据包内容的起始位置。</p>
<p>Bool SplitFun(const char* pData,const int &amp;len)<br>{<br>&nbsp;&nbsp;&nbsp; PBaseGamePacket pGamePacket=NULL;<br>&nbsp;&nbsp;&nbsp; __int64 startPos=0, prePos=0, i=0;<br>&nbsp;&nbsp;&nbsp; int packetLen=0;</p>
<p>&nbsp;　//先将本次收到的数据复制到整理缓冲区尾部<br>&nbsp;&nbsp;&nbsp; startPos = m_DataBufByteCount;&nbsp;&nbsp;<br>&nbsp;&nbsp;&nbsp; memcpy( m_ClientDataBuf+startPos, pData, len );<br>&nbsp;&nbsp;&nbsp; m_DataBufByteCount += len;&nbsp;&nbsp;&nbsp; </p>
<p>&nbsp;&nbsp;&nbsp; //当整理缓冲区内的字节数少于DATA_POS字节时，取不到长度信息则退出<br>　//注意：退出时并不置m_DataBufByteCount为0<br>&nbsp;&nbsp;&nbsp; if (m_DataBufByteCount &lt; DATA_POS+1)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return false;&nbsp;</p>
<p>&nbsp;&nbsp;&nbsp; //根据正常逻辑，下面的情况不可能出现，为稳妥起见，还是加上<br>&nbsp;&nbsp;&nbsp; if (m_DataBufByteCount &gt;&nbsp; 2*CLIENT_BUFFER_SIZE)<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //设置m_DataBufByteCount为0，意味着丢弃缓冲区中的现有数据<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_DataBufByteCount = 0;</p>
<p>　　//可以考虑开放错误格式数据包的处理接口，处理逻辑交给上层<br>　　//OnPacketError()<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return false;<br>&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp; //还原起始指针<br>&nbsp; &nbsp;&nbsp; startPos = 0;</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp; //只有当m_ClientDataBuf中的字节个数大于最小包长度时才能执行此语句<br>&nbsp;&nbsp;&nbsp; packetLen = GetPacketLen( pIOCPClient-&gt;m_ClientDataBuf );</p>
<p>&nbsp;&nbsp;&nbsp; //当逻辑层的包长度不合法时，则直接丢弃该包<br>&nbsp;&nbsp;&nbsp; if ((packetLen &lt; DATA_POS+1) || (packetLen &gt; 2*CLIENT_BUFFER_SIZE))<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_DataBufByteCount = 0;</p>
<p>　　//OnPacketError()<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return false;<br>&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp; //保留整理缓冲区的末尾指针<br>&nbsp;&nbsp;&nbsp; __int64 oldlen = m_DataBufByteCount;&nbsp;</p>
<p>&nbsp;&nbsp;&nbsp; while ((packetLen &lt;= m_DataBufByteCount) &amp;&amp; (m_DataBufByteCount&gt;0))<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //调用拼包逻辑，获取该缓冲区数据对应的数据包<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pGamePacket = GetGamePacket(m_ClientDataBuf+startPos, packetLen);&nbsp;</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (pGamePacket!=NULL)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //将数据包加入执行队列<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; AddToExeList(pGamePacket);<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pGamePacket = NULL;<br>&nbsp;<br>　　//整理缓冲区的剩余字节数和新逻辑包的起始位置进行调整<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_DataBufByteCount -= packetLen;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; startPos += packetLen;&nbsp;</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //残留缓冲区的字节数少于一个正常包大小时，只向前复制该包随后退出<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (m_DataBufByteCount &lt; DATA_POS+1)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for(i=startPos; i&lt;startPos+m_DataBufByteCount; ++i)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_ClientDataBuf[i-startPos] = m_ClientDataBuf[i];</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return true;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;packetLen = GetPacketLen(m_ClientDataBuf + startPos );</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //当逻辑层的包长度不合法时，丢弃该包及缓冲区以后的包<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ((packetLen&lt;DATA_POS+1) || (packetLen&gt;2*CLIENT_BUFFER_SIZE))<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_DataBufByteCount = 0;</p>
<p>&nbsp;&nbsp;&nbsp; 　　//OnPacketError()<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return false;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (startPos+packetLen&gt;=oldlen)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for(i=startPos; i&lt;startPos+m_DataBufByteCount; ++i)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_ClientDataBuf[i-startPos] = m_ClientDataBuf[i];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return true;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp;&nbsp; }//取所有完整的包</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp; return true;<br>}<br><br>　　以上便是数据接收模块的处理函数，下面是几点简要说明：</p>
<p>　　1、用于拼包整理的缓冲区(m_ClientDataBuf)应该比recv中指定的接收缓冲区(pData)长度(CLIENT_BUFFER_SIZE)要大，通常前者是后者的2倍(2*CLIENT_BUFFER_SIZE)或更大。<br><br>　　2、为避免因为剩余数据前移而导致的额外开销，建议m_ClientDataBuf使用环形缓冲区实现。<br><br>
3、为了避免出现无法拼装的包，我们约定每次发送的逻辑包，其单个逻辑包最大长度不可以超过CLIENT_BUFFER_SIZE的2倍。因为我们的整
理缓冲区只有2*CLIENT_BUFFER_SIZE这么长，更长的数据，我们将无法整理。这就要求在协议的设计上以及最终的发送函数的处理上要加上这
样的异常处理机制。</p>
<p><br>　　4、对于数据包过短或过长的包，我们通常的情况是置m_DataBufByteCount为0，即舍弃当前包的处理。如果此处不设置
m_DataBufByteCount为0也可，但该客户端只要发了一次格式错误的包，则其后继发过来的包则也将连带着产生格式错误，如果设置
m_DataBufByteCount为0，则可以比较好的避免后继的包受此包的格式错误影响。更好的作法是，在此处开放一个封包格式异常的处理接口
(OnPacketError)，由上层逻辑决定对这种异常如何处置。比如上层逻辑可以对封包格式方面出现的异常进行计数，如果错误的次数超过一定的值，
则可以断开该客户端的连接。</p>
<p>　　5、建议不要在recv或wsarecv的函数后，就紧接着作以上的处理。当recv收到一段数据后，生成一个结构体或对象(它主要含有
data和len两个内容，前者是数据缓冲区，后者是数据长度)，将这样的一个结构体或对象放到一个队列中由后面的线程对其使用SplitFun函数进行
整理。这样，可以最大限度地提高网络数据的接收速度，不至因为数据整理的原因而在此处浪费时间。<br><br>　　代码中，我已经作了比较详细的注释，可以作为拼包函数的参考，代码是从偶的应用中提取、修改而来，本身只为演示之用，所以未作调试，应用时需要你自己去完善。如有疑问，可以我的blog上留言提出。</p><img src ="http://www.cppblog.com/keigoliye/aggbug/97072.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-23 23:47 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/23/97072.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>泡泡堂、QQ堂游戏通信架构分析</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/23/97069.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 23 Sep 2009 15:44:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/23/97069.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/97069.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/23/97069.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/97069.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/97069.html</trackback:ping><description><![CDATA[<div id="blog_text" class="cnt">
<p>本文作者：sodme<br>
本文出处：<a  href="http://blog.csdn.net/sodme">http://blog.csdn.net/sodme</a><br>
声明：本文可以不经作者同意任意转载、复制、引用。但任何对本文的引用，均须注明本文的作者、出处以及本行声明信息。</p>
<p>　　之前，我分析过QQ游戏（特指QQ休闲平台，并非QQ堂，下同）的通信架构（<a  href="http://blog.csdn.net/sodme/archive/2005/06/12/393165.aspx">http://blog.csdn.net/sodme/archive/2005/06/12/393165.aspx</a>），分析过魔兽世界的通信架构（<a  href="http://blog.csdn.net/sodme/archive/2005/06/18/397371.aspx">http://blog.csdn.net/sodme/archive/2005/06/18/397371.aspx</a>），
似乎网络游戏的通信架构也就是这些了，其实不然，在网络游戏大家庭中，还有一种类型的游戏我认为有必要把它的通信架构专门作个介绍，这便是如泡泡堂、QQ
堂类的休闲类竞技游戏。曾经很多次，被网友们要求能抽时间看看泡泡堂之类游戏的通信架构，这次由于被逼交作业，所以今晚抽了一点的时间截了一下泡泡堂的
包，正巧昨日与网友就泡泡堂类游戏的通信架构有过一番讨论，于是，将这两天的讨论、截包及思考总结于本文中，希望能对关心或者正在开发此类游戏的朋友有所
帮助，如果要讨论具体的技术细节，请到我的BLOG（<a  href="http://blog.csdn.net/sodme">http://blog.csdn.net/sodme</a>）加我的MSN讨论..</p>
<p>　　总体来说，泡泡堂类游戏（此下简称泡泡堂）在大厅到房间这一层的通信架构，其结构与QQ游戏相当，甚至要比QQ游戏来得简单。所以，在房间这一层的通信架构上，我不想过多讨论，不清楚的朋友请参看我对QQ游戏通信架构的分析文章（<a  href="http://blog.csdn.net/sodme/archive/2005/06/12/393165.aspx">http://blog.csdn.net/sodme/archive/2005/06/12/393165.aspx</a>）。可以这么说，如果采用与QQ游戏相同的房间和大厅架构，是完全可以组建起一套可扩展的支持百万人在线的游戏系统的。也就是说，通过负载均衡＋大厅＋游戏房间对游戏逻辑的分摊，完全可以实现一个可扩展的百万人在线泡泡堂。</p>
<p>　　但是，泡泡堂与斗地主的最大不同点在于：泡泡堂对于实时性要求特别高。那么，泡泡堂是如何解决实时性与网络延迟以及大用户量之间矛盾的呢？</p>
<p>　　阅读以下文字前，请确认你已经完全理解TCP与UDP之间的不同点。</p>
<p>　　我们知道，TCP与UDP之间的最大不同点在于：TCP是可靠连接的，而UDP是无连接的。如果通信双方使用TCP协议，那么他们之前必须事先
通过监听＋连接的方式将双方的通信管道建立起来；而如果通信双方使用的是UDP通信，则双方不用事先建立连接，发送方只管向目标地址上的目标端口发送
UDP包即可，不用管对方到底收没收到。如果要说形象点，可以用这样一句话概括：TCP是打电话，UDP是发电报。TCP通信，为了保持这样的可靠连接，
在可靠性上下了很多功夫，所以导致了它的通信效率要比UDP差很多，所以，一般地，在地实时性要求非常高的场合，会选择使用UDP协议，比如常见的动作射
击类游戏。</p>
<p>　　通过载包，我们发现泡泡堂中同时采用了TCP和UDP两种通信协议。并且，具有以下特点：<br>
1.当玩家未进入具体的游戏地图时，仅有TCP通信存在，而没有UDP通信；<br>
2.进入游戏地图后，TCP的通信量远远小于UDP的通信量<br>
3.UDP的通信IP个数，与房间内的玩家成一一对应关系(这一点，应网友疑惑而加，此前已经证实)</p>
<p>　　以上是几个表面现象，下面我们来分析它的本质和内在。^&amp;^</p>
<p>　　泡泡堂的游戏逻辑，简单地可以归纳为以下几个方面：<br>
1.玩家移动<br>
2.玩家埋地雷（如果你觉得这种叫法比较土，你也可以叫它：下泡泡，呵呵）<br>
3.地雷爆炸出道具或者地雷爆炸困住另一玩家<br>
4.玩家捡道具或者玩家消灭/解救一被困的玩家</p>
<p>　　与MMORPG一样，在上面的几个逻辑中，广播量最大的其实是玩家移动。为了保持玩家画面同步，其他玩家的每一步移动消息都要即时地发给其它玩家。</p>
<p>　　通常，网络游戏的逻辑控制，绝大多数是在服务器端的。有时，为了保证画面的流畅性，我们会有意识地减少服务器端的逻辑判断量和广播量，当然，这
个减少，是以&#8220;不危及游戏的安全运行&#8221;为前提的。到底如何在效率、流畅性和安全性之间作取舍，很多时候是需要经验积累的，效率提高的过程，就是逻辑不断优
化的过程。不过，有一个原则是可以说的，那就是：&#8220;关键逻辑&#8221;一定要放在服务器上来判断。那么，什么是&#8220;关键逻辑&#8221;呢？</p>
<p>　　拿泡泡堂来说，下面的这个逻辑，我认为就是关键逻辑：玩家在某处埋下一颗地雷，地雷爆炸后到底能不能炸出道具以及炸出了哪些道具，这个信息，需要服务器来给。那么，什么又是&#8220;非关键逻辑&#8221;呢？</p>
<p>　　&#8220;非关键逻辑&#8221;，在不同的游戏中，会有不同的概念。在通常的MMORPG中，玩家移动逻辑的判断，是算作关键逻辑的，否则，如果服务器端不对客
户端发过来的移动包进行判断那就很容易造成玩家的瞬移以及其它毁灭性的灾难。而在泡泡堂中，玩家移动逻辑到底应不应该算作关键逻辑还是值得考虑的。泡泡堂
中的玩家可以取胜的方法，通常是确实因为打得好而赢得胜利，不会因为瞬移而赢得胜利，因为如果外挂要作泡泡堂的瞬移，它需要考虑的因素和判断的逻辑太多
了，由于比赛进程的瞬息万变，外挂的瞬移点判断不一定就比真正的玩家来得准确，所在，在玩家移动这个逻辑上使用外挂，在泡泡堂这样的游戏中通常是得不偿失
的（当然，那种特别变态的高智能的外挂除外）。从目前我查到的消息来看，泡泡堂的外挂多数是一些按键精灵脚本，它的本质还不是完全的游戏机器人，并不是通
过纯粹的协议接管实现的外挂功能。这也从反面验证了我以上的想法。</p>
<p>　　说到这里，也许你已经明白了。是的！TCP通信负责&#8220;关键逻辑&#8221;，而UDP通信负责&#8220;非关键逻辑&#8221;，这里的&#8220;非关键逻辑&#8221;中就包含了玩家移动。
在泡泡堂中，TCP通信用于本地玩家与服务器之间的通信，而UDP则用于本地玩家与同一地图中的其他各玩家的通信。当本地玩家要移动时，它会同时向同一地
图内的所有玩家广播自己的移动消息，其他玩家收到这个消息后会更新自己的游戏画面以实现画面同步。而当本地玩家要在地图上放置一个炸弹时，本地玩家需要将
此消息同时通知同一地图内的其他玩家以及服务器，甚至这里，可以不把放置炸弹的消息通知给服务器，而仅仅通知其他玩家。当炸弹爆炸后，要拾取物品时才向服
务器提交拾取物品的消息。</p>
<p>　　那么，你可能会问，&#8220;地图上某一点是否存在道具&#8221;这个消息，服务器是什么时候通知给客户端的呢？这个问题，可以有两种解决方案：<br>
1.客户端如果在放置炸弹时，将放置炸弹的消息通知给服务器，服务器可以在收到这个消息后，告诉客户端炸弹爆炸后会有哪些道具。但我觉得这种方案不好，因为这样作会增加游戏运行过程中的数据流量。<br>
2.而这第2种方案就是，客户端进入地图后，游戏刚开始时，就由服务器将本地图内的各道具所在点的信息传给各客户端，这样，可以省去两方面的开
销：a.客户端放炸弹时，可以不通知服务器而只通知其它玩家；b.服务器也不用在游戏运行过程中再向客户端传递有关某点有道具的信息。<br>
<br>
但是，不管采用哪种方案，服务器上都应该保留一份本地图内道具所在点的信息。因为服务器要用它来验证一个关键逻辑：玩家拾取道具。当玩家要在某点拾取道具时，服务器必须要判定此点是否有道具，否则，外挂可以通过频繁地发拾取道具的包而不断取得道具。</p>
<p>　　至于泡泡堂其它游戏逻辑的实现方法，我想，还是要依靠这个原则：首先判断这个逻辑是关键逻辑吗？如果不全是，那其中的哪部分是非关键逻辑呢？对
于非关键逻辑，都可以交由客户端之间（UDP）去自行完成。而对于关键逻辑，则必须要有服务器(TCP)的校验和认证。这便是我要说的。</p>
<p>　　以上仅仅是在理论上探讨关于泡泡堂类游戏在通信架构上的可能作法，这些想法是没有事实依据的，所有结论皆来源于对封包的分析以及个人经验，文章
的内容和观点可能跟真实的泡泡堂通信架构实现有相当大的差异，但我想，这并不是主要的，因为我的目的是向大家介绍这样的TCP和UDP通信并存情况下，如
何对游戏逻辑的进行取舍和划分。无论是&#8220;关键逻辑&#8221;的定性，还是&#8220;玩家移动&#8221;的具体实施，都需要开发者在具体的实践中进行总结和优化。此文全当是一个引子
罢，如有疑问，请加Msn讨论。</p>
</div><img src ="http://www.cppblog.com/keigoliye/aggbug/97069.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-23 23:44 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/23/97069.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>类似于QQ游戏百万人同时在线的服务器架构实现</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/23/97070.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 23 Sep 2009 15:44:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/23/97070.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/97070.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/23/97070.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/97070.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/97070.html</trackback:ping><description><![CDATA[<div id="blog_text" class="cnt">
<p>本文作者：sodme　本文出处：<a  href="http://blog.csdn.net/sodme">http://blog.csdn.net/sodme</a><br>
版权声明：本文可以不经作者同意任意转载，但转载时烦请保留文章开始前两行的版权、作者及出处信息。</p>
<p>　　QQ游戏于前几日终于突破了百万人同时在线的关口，向着更为远大的目标迈进，这让其它众多传统的棋牌休闲游戏平台黯然失色，相比之下，联众似乎
已经根本不是QQ的对手，因为QQ除了这100万的游戏在线人数外，它还拥有3亿多的注册量（当然很多是重复注册的）以及QQ聊天软件900万的同时在线
率，我们已经可以预见未来由QQ构建起来的强大棋牌休闲游戏帝国。<br>
那么，在技术上，QQ游戏到底是如何实现百万人同时在线并保持游戏高效率的呢？<br>
事实上，针对于任何单一的网络服务器程序，其可承受的同时连接数目是有理论峰值的，通过C＋＋中对TSocket的定义类型：word，我们可以判定
这个连接理论峰值是65535，也就是说，你的单个服务器程序，最多可以承受6万多的用户同时连接。但是，在实际应用中，能达到一万人的同时连接并能保证
正常的数据交换已经是很不容易了，通常这个值都在2000到5000之间，据说QQ的单台服务器同时连接数目也就是在这个值这间。<br>
如果要实现2000到5000用户的单服务器同时在线，是不难的。在windows下，比较成熟的技术是采用IOCP－－完成端口。与完成端口相关的
资料在网上和CSDN论坛里有很多，感兴趣的朋友可以自己搜索一下。只要运用得当，一个完成端口服务器是完全可以达到2K到5K的同时在线量的。但，5K
这样的数值离百万这样的数值实在相差太大了，所以，百万人的同时在线是单台服务器肯定无法实现的。<br>
要实现百万人同时在线，首先要实现一个比较完善的完成端口服务器模型，这个模型要求至少可以承载2K到5K的同时在线率（当然，如果你MONEY多，
你也可以只开发出最多允许100人在线的服务器）。在构建好了基本的完成端口服务器之后，就是有关服务器组的架构设计了。之所以说这是一个服务器组，是因
为它绝不仅仅只是一台服务器，也绝不仅仅是只有一种类型的服务器。<br>
简单地说，实现百万人同时在线的服务器模型应该是：登陆服务器＋大厅服务器＋房间服务器。当然，也可以是其它的模型，但其基本的思想是一样的。下面，我将逐一介绍这三类服务器的各自作用。<br>
登陆服务器：一般情况下，我们会向玩家开放若干个公开的登陆服务器，就如QQ登陆时让你选择的从哪个QQ游戏服务器登陆一样，QQ登陆时让玩家选择的
六个服务器入口实际上就是登陆服务器。登陆服务器主要完成负载平衡的作用。详细点说就是，在登陆服务器的背后，有N个大厅服务器，登陆服务器只是用于为当
前的客户端连接选择其下一步应该连接到哪个大厅服务器，当登陆服务器为当前的客户端连接选择了一个合适的大厅服务器后，客户端开始根据登陆服务器提供的信
息连接到相应的大厅上去，同时客户端断开与登陆服务器的连接，为其他玩家客户端连接登陆服务器腾出套接字资源。在设计登陆服务器时，至少应该有以下功
能：N个大厅服务器的每一个大厅服务器都要与所有的登陆服务器保持连接，并实时地把本大厅服务器当前的同时在线人数通知给各个登陆服务器，这其中包括：用
户进入时的同时在线人数增加信息以及用户退出时的同时在线人数减少信息。这里的各个大厅服务器同时在线人数信息就是登陆服务器为客户端选择某个大厅让其登
陆的依据。举例来说，玩家A通过登陆服务器1连接到登陆服务器，登陆服务器开始为当前玩家在众多的大厅服务器中根据哪一个大厅服务器人数比较少来选择一个
大厅，同时把这个大厅的连接IP和端口发给客户端，客户端收到这个IP和端口信息后，根据这个信息连接到此大厅，同时，客户端断开与登陆服务器之间的连
接，这便是用户登陆过程中，在登陆服务器这一块的处理流程。<br>
大厅服务器：大厅服务器，是普通玩家看不到的服务器，它的连接IP和端口信息是登陆服务器通知给客户端的。也就是说，在QQ游戏的本地文件中，具体的
大厅服务器连接IP和端口信息是没有保存的。大厅服务器的主要作用是向玩家发送游戏房间列表信息，这些信息包括：每个游戏房间的类型，名称，在线人数，连
接地址以及其它如游戏帮助文件URL的信息。从界面上看的话，大厅服务器就是我们输入用户名和密码并校验通过后进入的游戏房间列表界面。大厅服务器，主要
有以下功能：一是向当前玩家广播各个游戏房间在线人数信息；二是提供游戏的版本以及下载地址信息；三是提供各个游戏房间服务器的连接IP和端口信息；四是
提供游戏帮助的URL信息；五是提供其它游戏辅助功能。但在这众多的功能中，有一点是最为核心的，即：为玩家提供进入具体的游戏房间的通道，让玩家顺利进
入其欲进入的游戏房间。玩家根据各个游戏房间在线人数，判定自己进入哪一个房间，然后双击服务器列表中的某个游戏房间后玩家开始进入游戏房间服务器。<br>
游戏房间服务器：游戏房间服务器，具体地说就是如&#8220;斗地主1&#8221;，&#8220;斗地主2&#8221;这样的游戏房间。游戏房间服务器才是具体的负责执行游戏相关逻辑的服务
器。这样的游戏逻辑分为两大类：一类是通用的游戏房间逻辑，如：进入房间，离开房间，进入桌子，离开桌子以及在房间内说话等；第二类是游戏桌子逻辑，这个
就是各种不同类型游戏的主要区别之处了，比如斗地主中的叫地主或不叫地主的逻辑等，当然，游戏桌子逻辑里也包括有通用的各个游戏里都存在的游戏逻辑，比如
在桌子内说话等。总之，游戏房间服务器才是真正负责执行游戏具体逻辑的服务器。<br>
这里提到的三类服务器，我均采用的是完成端口模型，每个服务器最多连接数目是5000人，但是，我在游戏房间服务器上作了逻辑层的限定，最多只允许
300人同时在线。其他两个服务器仍然允许最多5000人的同时在线。如果按照这样的结构来设计，那么要实现百万人的同时在线就应该是这样：首先是大
厅，1000000/5000＝200。也就是说，至少要200台大厅服务器，但通常情况下，考虑到实际使用时服务器的处理能力和负载情况，应该至少准备
250台左右的大厅服务器程序。另外，具体的各种类型的游戏房间服务器需要多少，就要根据当前玩各种类型游戏的玩家数目分别计算了，比如斗地主最多是十万
人同时在线，每台服务器最多允许300人同时在线，那么需要的斗地主服务器数目就应该不少于：100000/300=333，准备得充分一点，就要准备
350台斗地主服务器。<br>
除正常的玩家连接外，还要考虑到：<br>
对于登陆服务器，会有250台大厅服务器连接到每个登陆服务器上，这是始终都要保持的连接；<br>
而对于大厅服务器而言，如果仅仅有斗地主这一类的服务器，就要有350多个连接与各个大厅服务器始终保持着。所以从这一点看，我的结构在某些方面还存在着需要改进的地方，但核心思想是：尽快地提供用户登陆的速度，尽可能方便地让玩家进入游戏中。</p>
</div><img src ="http://www.cppblog.com/keigoliye/aggbug/97070.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-23 23:44 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/23/97070.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>闪客帝国-flash多人网络游戏的实现的讨论</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/23/97067.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 23 Sep 2009 15:43:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/23/97067.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/97067.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/23/97067.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/97067.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/97067.html</trackback:ping><description><![CDATA[一直以来，flash就是我非常喜爱的平台，<br>
因为他简单，完整，但是功能强大，<br>
很适合游戏软件的开发，<br>
只不过处理复杂的算法和海量数据的时候，<br>
速度慢了一些，<br>
但是这并不意味着flash不能做，<br>
我们需要变通的方法去让flash做不善长的事情，<br>
<br>
这个贴子用来专门讨论用flash作为客户端来开发网络游戏，<br>
持续时间也不会很长，在把服务器端的源代码公开完以后，<br>
就告一段落，<br>
注意，仅仅用flash作为客户端，<br>
服务器端，我们使用vc6，<br>
我将陆续的公开服务器端的源代码和大家共享，<br>
并且将讲解一些网络游戏开发的原理，<br>
希望对此感兴趣的朋友能够使用今后的资源或者理论开发出完整的网络游戏。<br>
我们从简单到复杂，<br>
从棋牌类游戏到动作类的游戏，<br>
从2个人的游戏到10个人的游戏，<br>
因为工作忙的关系，我所做的一切仅仅起到抛砖引玉的作用，<br>
希望大家能够热情的讨论，为中国的flash事业垫上一块砖，添上一片瓦。<br>
<br>
现在的大型网络游戏(mmo game)都是基于server/client体系结构的，<br>
server端用c（windows下我们使用vc.net+winsock）来编写，<br>
客户端就无所谓，<br>
在这里，我们讨论用flash来作为客户端的实现，<br>
<br>
实践证明，flash的xml socket完全可以胜任网络传输部分，<br>
在别的贴子中，我看见有的朋友谈论msn中的flash game<br>
他使用msn内部的网络接口进行传输，<br>
这种做法也是可以的，<br>
我找很久以前对于2d图形编程的说法，"给我一个打点函数，我就能创造整个游戏世界",<br>
而在网络游戏开发过程中，"给我一个发送函数和一个接收函数，我就能创造网络游戏世界."<br>
<br>
我们抽象一个接口，就是网络传输的接口，<br>
对于使用flash作为客户端，要进行网络连接，<br>
一个网络游戏的客户端，<br>
可以简单的抽象为下面的流程<br>
1.与远程服务器建立一条长连接<br>
2.用账号密码登陆<br>
3.循环<br>
接收消息<br>
发送消息<br>
4.关闭<br>
<br>
我们可以直接使用flash 的xml socket，也可以使用类似msn的那种方式，<br>
这些我们先不管，我们先定义接口，<br>
Connect( "127.0.0.1", 20000 ); 连接远程服务器，建立一条长连接<br>
Send( data, len ); 向服务器发送一条消息<br>
Recv( data, len ); 接收服务器传来的消息<br>
<hr>
<p>项目开发的基本硬件配置<br>
一台普通的pc就可以了，<br>
安装好windows 2000和vc6就可以了，<br>
然后连上网，局域网和internet都可以，<br>
<br>
接下去的东西我都简化，不去用晦涩的术语，<br>
<br>
既然是网络，我们就需要网络编程接口，<br>
服务器端我们用的是winsock 1.1，使用tcp连接方式，<br>
<br>
[tcp和udp]<br>
tcp可以理解为一条连接两个端子的隧道，提供可靠的数据传输服务，<br>
只要发送信息的一方成功的调用了tcp的发送函数发送一段数据，<br>
我们可以认为接收方在若干时间以后一定会接收到完整正确的数据，<br>
不需要去关心网络传输上的细节，<br>
而udp不保证这一点，<br>
对于网络游戏来说，tcp是普遍的选择。<br>
<br>
[阻塞和非阻塞]<br>
在通过socket发送数据时，如果直到数据发送完毕才返回的方式，也就是说如果我们使用send( buffer, 100.....)这样的函数发送100个字节给别人，我们要等待，直到100个自己发送完毕，程序才往下走，这样就是阻塞的，<br>
而非阻塞的方式，当你调用send(buffer,100....)以后，立即返回，此时send函数告诉你发送成功，并不意味着数据已经向目的地发送完
毕，甚至有可能数据还没有开始发送，只被保留在系统的缓冲里面，等待被发送，但是你可以认为数据在若干时间后，一定会被目的地完整正确的收到，我们要充分
的相信tcp。<br>
阻塞的方式会引起系统的停顿，一般网络游戏里面使用的都是非阻塞的方式，</p>
<p> </p>
<hr>
<p> </p>
<p>[有状态服务器和无状态服务器]<br>
在c/s体系中，如果server不保存客户端的状态，称之为无状态，反之为有状态，<br>
<br>
在这里要强调一点，<br>
我们所说的服务器不是一台具体的机器，<br>
而是指服务器应用程序，<br>
一台具体的机器或者机器群组可以运行一个或者多个服务器应用程序，<br>
<br>
我们的网络游戏使用的是有状态服务器，<br>
保存所有玩家的数据和状态，</p>
<hr>
<strong>一些有必要了解的理论和开发工具</strong>
<p> </p>
<p>[开发语言]<br>
vc6<br>
我们首先要熟练的掌握一门开发语言，<br>
学习c++是非常有必要的，<br>
而vc是windows下面的软件开发工具，<br>
为什么选择vc，可能与我本身使用vc有关，<br>
而且网上可以找到许多相关的资源和源代码，<br>
<br>
[操作系统]<br>
我们使用windows2000作为服务器的运行环境，<br>
所以我们有必要去了解windows是如何工作的，<br>
同时对它的编程原理应该熟练的掌握<br>
<br>
[数据结构和算法]<br>
要写出好的程序要先具有设计出好的数据结构和算法的能力，<br>
好的算法未必是繁琐的公式和复杂的代码，<br>
我们要找到又好写有满足需求的算法，<br>
有时候，最笨的方法同时也是很好的方法，<br>
很多程序员沉迷于追求精妙的算法而忽略了宏观上的工程，<br>
花费了大量的精力未必能够取得好的效果，<br>
<br>
举个例子，<br>
我当年进入游戏界工作，学习老师的代码，<br>
发现有个函数，要对画面中的npc位置进行排序，<br>
确定哪个先画，那个后画，<br>
他的方法太&#8220;笨&#8221;，<br>
任何人都会想到的冒泡，<br>
一个一个去比较，没有任何的优化，<br>
我当时想到的算法就有很多，<br>
而且有一大堆优化策略，<br>
可是，当我花了很长时间去实现我的算法时，<br>
发现提升的那么一点效率对游戏整个运行效率而言几乎是没起到什么作用，<br>
或者说虽然算法本身快了几倍，<br>
可是那是多余的，老师的算法虽然&#8220;笨&#8221;，<br>
可是他只花了几十行代码就搞定了，<br>
他的时间花在别的更需要的地方，<br>
这就是他可以独自完成一个游戏，<br>
而我可以把一个函数优化100倍也只能打杂的原因<br>
<br>
[tcp/ip的理论]<br>
推荐数据用tcp/ip进行网际互连，tcp/ip详解，<br>
这是两套书，共有6卷，<br>
都是国外的大师写的，<br>
可以说是必读的，</p>
<hr>
<strong>网络传输中的&#8220;消息&#8221;</strong>
<p> </p>
<p>[消息]<br>
消息是个很常见的术语，<br>
在windows中，消息机制是个十分重要的概念，<br>
我们在网络游戏中，也使用了消息这样的机制，<br>
<br>
一般我们这么做，<br>
一个数据块，头4个字节是消息名，后面接2个字节的数据长度，<br>
再后面就是实际的数据<br>
<br>
为什么使用消息？？<br>
我们来看看例子，<br>
<br>
在游戏世界，<br>
一个玩家想要和别的玩家聊天，<br>
那么，他输入好聊天信息，<br>
客户端生成一条聊天消息，<br>
并把聊天的内容打包到消息中，<br>
然后把聊天消息发送给服务器，<br>
请求服务器把聊天信息发送给另一个玩家，<br>
<br>
服务器接收到一条消息，<br>
此刻，服务器并不知道当前的数据是什么东西，<br>
对于服务器来讲，这段数据仅仅来自于网络通讯的底层，<br>
不加以分析的话，没有任何的信息，<br>
因为我们的通讯是基于消息机制的，<br>
我们认为服务器接收到的任何数据都是基于消息的数据方式组织的，<br>
4个字节消息名，2字节长度，这个是不会变的，<br>
<br>
通过消息名，服务器发现当前数据是一条聊天数据，<br>
通过长度把需要的数据还原，校验，<br>
然后把这条消息发送给另一个玩家，<br>
<br>
大家注意，消息是变长的，<br>
关于消息的解释完全在于服务器和客户端的应用程序，<br>
可以认为与网络传输低层无关，<br>
比如一条私聊消息可能是这样的，<br>
<br>
MsgID:4 byte<br>
Length:2 byte<br>
TargetPlayerID:2 byte<br>
String:anybyte &lt; 256<br>
<br>
一条移动消息可能是这样的，<br>
MsgID:4 byte<br>
Length:2 byte<br>
TargetPlayerID:2 byte<br>
TargetPosition:4 byte (x,y)<br>
<br>
编程者可以自定义消息的内容以满足不同的需求</p>
<hr>
<p> </p>
<strong>队列</strong>
<p>[队列]<br>
队列是一个很重要的数据结构，<br>
比如说消息队列，<br>
服务器或者客户端，<br>
发送的消息不一定是立即发送的，<br>
而是等待一个适当时间，<br>
或者系统规定的时间间隔以后才发送，<br>
这样就需要创建一个消息队列，以保存发送的消息，<br>
<br>
消息队列的大小可以按照实际的需求创建，<br>
队列又可能会满，<br>
当队列满了，可以直接丢弃消息，<br>
如果你觉得这样不妥，<br>
也可以预先划分一个足够大的队列，<br>
<br>
可以使用一个系统全局的大的消息队列，<br>
也可以为每个对象创建一个消息队列，<br>
<br>
<br>
这个我们的一个数据队列的实现，<br>
开发工具vc.net，使用了C++的模板，<br>
关于队列的算法和基础知识，我就不多说了，<br>
<br>
DataBuffer.h<br>
<br>
#ifndef __DATABUFFER_H__<br>
#define __DATABUFFER_H__<br>
<br>
#include &lt;windows.h&gt;<br>
#include &lt;assert.h&gt;<br>
#include "g_assert.h"<br>
#include &lt;stdio.h&gt;<br>
<br>
#ifndef HAVE_BYTE<br>
typedef unsigned char byte;<br>
#endif // HAVE_BYTE<br>
<br>
//数据队列管理类<br>
template &lt;const int _max_line, const int _max_size&gt;<br>
class DataBufferTPL<br>
{<br>
public:<br>
<br>
bool Add( byte *data ) // 加入队列数据<br>
{<br>
G_ASSERT_RET( data, false );<br>
m_ControlStatus = false;<br>
<br>
if( IsFull() ) <br>
{<br>
//assert( false );<br>
return false;<br>
}<br>
<br>
memcpy( m_s_ptr, data, _max_size );<br>
<br>
NextSptr();<br>
m_NumData++;<br>
<br>
m_ControlStatus = true;<br>
return true;<br>
} <br>
<br>
<br>
bool Get( byte *data ) // 从队列中取出数据<br>
{<br>
G_ASSERT_RET( data, false );<br>
m_ControlStatus = false;<br>
<br>
if( IsNull() ) <br>
return false;<br>
<br>
memcpy( data, m_e_ptr, _max_size );<br>
<br>
NextEptr();<br>
m_NumData--;<br>
<br>
m_ControlStatus = true;<br>
return true;<br>
}<br>
<br>
<br>
bool CtrlStatus() // 获取操作成功结果<br>
{<br>
return m_ControlStatus;<br>
}<br>
<br>
<br>
int GetNumber() // 获得现在的数据大小<br>
{<br>
return m_NumData;<br>
}<br>
<br>
public:<br>
<br>
DataBufferTPL()<br>
{<br>
m_NumData = 0;<br>
m_start_ptr = m_DataTeam[0];<br>
m_end_ptr = m_DataTeam[_max_line-1];<br>
m_s_ptr = m_start_ptr;<br>
m_e_ptr = m_start_ptr;<br>
}<br>
~DataBufferTPL()<br>
{<br>
m_NumData = 0;<br>
m_s_ptr = m_start_ptr;<br>
m_e_ptr = m_start_ptr;<br>
}<br>
<br>
private:<br>
<br>
bool IsFull() // 是否队列满<br>
{<br>
G_ASSERT_RET( m_NumData &gt;=0 &amp;&amp; m_NumData &lt;= _max_line, false );<br>
if( m_NumData == _max_line ) <br>
return true; <br>
else <br>
return false;<br>
}<br>
bool IsNull() // 是否队列空<br>
{<br>
G_ASSERT_RET( m_NumData &gt;=0 &amp;&amp; m_NumData &lt;= _max_line, false );<br>
if( m_NumData == 0 )<br>
return true;<br>
else<br>
return false;<br>
}<br>
void NextSptr() // 头位置增加<br>
{<br>
assert(m_start_ptr);<br>
assert(m_end_ptr);<br>
assert(m_s_ptr);<br>
assert(m_e_ptr);<br>
m_s_ptr += _max_size;<br>
if( m_s_ptr &gt; m_end_ptr )<br>
m_s_ptr = m_start_ptr;<br>
}<br>
void NextEptr() // 尾位置增加<br>
{<br>
assert(m_start_ptr);<br>
assert(m_end_ptr);<br>
assert(m_s_ptr);<br>
assert(m_e_ptr);<br>
m_e_ptr += _max_size;<br>
if( m_e_ptr &gt; m_end_ptr )<br>
m_e_ptr = m_start_ptr;<br>
}<br>
<br>
private:<br>
<br>
byte m_DataTeam[_max_line][_max_size]; //数据缓冲<br>
int m_NumData; //数据个数<br>
bool m_ControlStatus; //操作结果<br>
<br>
byte *m_start_ptr; //起始位置<br>
byte *m_end_ptr; //结束位置<br>
byte *m_s_ptr; //排队起始位置<br>
byte *m_e_ptr; //排队结束位置<br>
};<br>
<br>
<br>
//////////////////////////////////////////////////////////////////////////<br>
// 放到这里了！<br>
<br>
//ID自动补位列表模板，用于自动列表，无间空顺序列表。<br>
template &lt;const int _max_count&gt;<br>
class IDListTPL<br>
{<br>
public:<br>
// 清除重置<br>
void Reset() <br>
{ <br>
for(int i=0;i&lt;_max_count;i++)<br>
m_dwList[i] = G_ERROR;<br>
m_counter = 0;<br>
}<br>
<br>
int MaxSize() const { return _max_count; }<br>
int Count() const { return m_counter; }<br>
const DWORD operator[]( int iIndex ) { <br>
<br>
G_ASSERTN( iIndex &gt;= 0 &amp;&amp; iIndex &lt; m_counter );<br>
<br>
return m_dwList[ iIndex ]; <br>
}<br>
bool New( DWORD dwID )<br>
{<br>
G_ASSERT_RET( m_counter &gt;= 0 &amp;&amp; m_counter &lt; _max_count, false );<br>
<br>
//ID 唯一性，不能存在相同ID<br>
if ( Find( dwID ) != -1 ) <br>
return false;<br>
<br>
m_dwList[m_counter] = dwID;<br>
m_counter++;<br>
<br>
return true;<br>
}<br>
// 没有Assert的加入ID功能<br>
bool Add( DWORD dwID )<br>
{<br>
if( m_counter &lt;0 || m_counter &gt;= _max_count ) <br>
return false;<br>
<br>
//ID 唯一性，不能存在相同ID<br>
if ( Find( dwID ) != -1 ) <br>
return false;<br>
<br>
m_dwList[m_counter] = dwID;<br>
m_counter++;<br>
return true;<br>
}<br>
bool Del( int iIndex )<br>
{<br>
G_ASSERT_RET( iIndex &gt;=0 &amp;&amp; iIndex &lt; m_counter, false );<br>
<br>
for(int k=iIndex;k&lt;m_counter-1;k++)<br>
{<br>
m_dwList[k] = m_dwList[k+1];<br>
}<br>
<br>
m_dwList[k] = G_ERROR;<br>
m_counter--;<br>
return true;<br>
}<br>
int Find( DWORD dwID )<br>
{<br>
for(int i=0;i&lt;m_counter;i++)<br>
{<br>
if( m_dwList[i] == dwID ) <br>
return i;<br>
}<br>
<br>
return -1;<br>
}<br>
<br>
IDListTPL():m_counter(0) <br>
{<br>
for(int i=0;i&lt;_max_count;i++)<br>
m_dwList[i] = G_ERROR;<br>
}<br>
virtual ~IDListTPL() <br>
{}<br>
<br>
private:<br>
<br>
DWORD m_dwList[_max_count];<br>
int m_counter;<br>
<br>
};<br>
<br>
//////////////////////////////////////////////////////////////////////////<br>
<br>
<br>
#endif //__DATABUFFER_H__</p>
<hr>
<strong>socket</strong>
<p> </p>
<p>我们采用winsock作为网络部分的编程接口，<br>
<br>
接下去编程者有必要学习一下socket的基本知识，<br>
不过不懂也没有关系，我提供的代码已经把那些麻烦的细节或者正确的系统设置给弄好了，<br>
编程者只需要按照规则编写游戏系统的处理代码就可以了，<br>
<br>
这些代码在vc6下编译通过，<br>
是通用的网络传输底层，<br>
这里是socket部分的代码，<br>
<br>
我们需要安装vc6才能够编译以下的代码，<br>
因为接下去我们要接触越来越多的c++，<br>
所以，大家还是去看看c++的书吧，<br>
<br>
// socket.h<br>
#ifndef _socket_h<br>
#define _socket_h<br>
#pragma once<br>
<br>
//定义最大连接用户数目 ( 最大支持 512 个客户连接 )<br>
#define MAX_CLIENTS 512<br>
//#define FD_SETSIZE MAX_CLIENTS<br>
<br>
#pragma comment( lib, "wsock32.lib" )<br>
<br>
#include &lt;winsock.h&gt;<br>
<br>
class CSocketCtrl<br>
{<br>
void SetDefaultOpt();<br>
public:<br>
CSocketCtrl(): m_sockfd(INVALID_SOCKET){}<br>
BOOL StartUp();<br>
BOOL ShutDown();<br>
BOOL IsIPsChange();<br>
<br>
BOOL CanWrite();<br>
BOOL HasData();<br>
int Recv( char* pBuffer, int nSize, int nFlag );<br>
int Send( char* pBuffer, int nSize, int nFlag );<br>
BOOL Create( UINT uPort );<br>
BOOL Create(void);<br>
BOOL Connect( LPCTSTR lpszHostAddress, UINT nHostPort );<br>
void Close();<br>
<br>
BOOL Listen( int nBackLog );<br>
BOOL Accept( CSocketCtrl&amp; sockCtrl );<br>
<br>
BOOL RecvMsg( char *sBuf );<br>
int SendMsg( char *sBuf,unsigned short stSize );<br>
SOCKET GetSockfd(){ return m_sockfd; }<br>
<br>
BOOL GetHostName( char szHostName[], int nNameLength );<br>
<br>
protected:<br>
SOCKET m_sockfd;<br>
<br>
static DWORD m_dwConnectOut;<br>
static DWORD m_dwReadOut;<br>
static DWORD m_dwWriteOut;<br>
static DWORD m_dwAcceptOut;<br>
static DWORD m_dwReadByte;<br>
static DWORD m_dwWriteByte;<br>
};<br>
<br>
<br>
#endif<br>
<br>
// socket.cpp<br>
<br>
#include &lt;stdio.h&gt;<br>
#include "msgdef.h"<br>
#include "socket.h"<br>
// 吊线时间<br>
#define ALL_TIMEOUT 120000<br>
DWORD CSocketCtrl::m_dwConnectOut = 60000;<br>
DWORD CSocketCtrl::m_dwReadOut = ALL_TIMEOUT;<br>
DWORD CSocketCtrl::m_dwWriteOut = ALL_TIMEOUT;<br>
DWORD CSocketCtrl::m_dwAcceptOut = ALL_TIMEOUT;<br>
DWORD CSocketCtrl::m_dwReadByte = 0;<br>
DWORD CSocketCtrl::m_dwWriteByte = 0;<br>
<br>
// 接收数据<br>
BOOL CSocketCtrl::RecvMsg( char *sBuf )<br>
{<br>
if( !HasData() )<br>
return FALSE;<br>
MsgHeader header;<br>
int nbRead = this-&gt;Recv( (char*)&amp;header, sizeof( header ), MSG_PEEK );<br>
if( nbRead == SOCKET_ERROR )<br>
return FALSE;<br>
if( nbRead &lt; sizeof( header ) )<br>
{<br>
this-&gt;Recv( (char*)&amp;header, nbRead, 0 );<br>
printf( "\ninvalid msg, skip %ld bytes.", nbRead );<br>
return FALSE;<br>
}<br>
<br>
if( this-&gt;Recv( (char*)sBuf, header.stLength, 0 ) != header.stLength )<br>
return FALSE;<br>
<br>
return TRUE;<br>
}<br>
<br>
// 发送数据<br>
int CSocketCtrl::SendMsg( char *sBuf,unsigned short stSize )<br>
{<br>
static char sSendBuf[ 4000 ];<br>
memcpy( sSendBuf,&amp;stSize,sizeof(short) );<br>
memcpy( sSendBuf + sizeof(short),sBuf,stSize );<br>
<br>
if( (sizeof(short) + stSize) != this-&gt;Send( sSendBuf,stSize+sizeof(short),0 ) )<br>
return -1;<br>
return stSize;<br>
}<br>
<br>
<br>
// 启动winsock<br>
BOOL CSocketCtrl::StartUp()<br>
{<br>
WSADATA wsaData;<br>
WORD wVersionRequested = MAKEWORD( 1, 1 );<br>
<br>
int err = WSAStartup( wVersionRequested, &amp;wsaData );<br>
if ( err != 0 ) <br>
{<br>
return FALSE;<br>
}<br>
<br>
<br>
return TRUE;<br>
<br>
}<br>
// 关闭winsock<br>
BOOL CSocketCtrl::ShutDown()<br>
{<br>
WSACleanup();<br>
return TRUE;<br>
}<br>
<br>
// 得到主机名<br>
BOOL CSocketCtrl::GetHostName( char szHostName[], int nNameLength )<br>
{<br>
if( gethostname( szHostName, nNameLength ) != SOCKET_ERROR )<br>
return TRUE;<br>
return FALSE;<br>
}<br>
<br>
BOOL CSocketCtrl::IsIPsChange()<br>
{<br>
return FALSE;<br>
static int iIPNum = 0;<br>
char sHost[300];<br>
<br>
hostent *pHost;<br>
if( gethostname(sHost,299) != 0 )<br>
return FALSE;<br>
pHost = gethostbyname(sHost);<br>
int i;<br>
char *psHost;<br>
i = 0;<br>
do<br>
{<br>
psHost = pHost-&gt;h_addr_list[i++];<br>
if( psHost == 0 )<br>
break;<br>
<br>
}while(1);<br>
if( iIPNum != i )<br>
{<br>
iIPNum = i;<br>
return TRUE;<br>
}<br>
return FALSE;<br>
}<br>
<br>
// socket是否可以写<br>
BOOL CSocketCtrl::CanWrite()<br>
{<br>
int e;<br>
<br>
fd_set set;<br>
timeval tout;<br>
tout.tv_sec = 0;<br>
tout.tv_usec = 0;<br>
<br>
FD_ZERO(&amp;set);<br>
FD_SET(m_sockfd,&amp;set);<br>
e=::select(0,NULL,&amp;set,NULL,&amp;tout);<br>
if(e==SOCKET_ERROR) return FALSE;<br>
if(e&gt;0) return TRUE;<br>
return FALSE;<br>
}<br>
<br>
// socket是否有数据<br>
BOOL CSocketCtrl::HasData()<br>
{<br>
int e;<br>
fd_set set;<br>
timeval tout;<br>
tout.tv_sec = 0;<br>
tout.tv_usec = 0;<br>
<br>
FD_ZERO(&amp;set);<br>
FD_SET(m_sockfd,&amp;set);<br>
e=::select(0,&amp;set,NULL,NULL,&amp;tout);<br>
if(e==SOCKET_ERROR) return FALSE;<br>
if(e&gt;0) return TRUE;<br>
return FALSE;<br>
}<br>
<br>
int CSocketCtrl::Recv( char* pBuffer, int nSize, int nFlag )<br>
{<br>
return recv( m_sockfd, pBuffer, nSize, nFlag );<br>
}<br>
<br>
int CSocketCtrl::Send( char* pBuffer, int nSize, int nFlag )<br>
{<br>
return send( m_sockfd, pBuffer, nSize, nFlag );<br>
}<br>
<br>
BOOL CSocketCtrl::Create( UINT uPort )<br>
{<br>
m_sockfd=::socket(PF_INET,SOCK_STREAM,0);<br>
if(m_sockfd==INVALID_SOCKET) return FALSE;<br>
SOCKADDR_IN SockAddr;<br>
memset(&amp;SockAddr,0,sizeof(SockAddr));<br>
SockAddr.sin_family = AF_INET;<br>
SockAddr.sin_addr.s_addr = INADDR_ANY;<br>
SockAddr.sin_port = ::htons( uPort );<br>
if(!::bind(m_sockfd,(SOCKADDR*)&amp;SockAddr, sizeof(SockAddr))) <br>
{<br>
SetDefaultOpt();<br>
return TRUE;<br>
}<br>
Close();<br>
return FALSE;<br>
<br>
}<br>
<br>
void CSocketCtrl::Close()<br>
{<br>
::closesocket( m_sockfd );<br>
m_sockfd = INVALID_SOCKET;<br>
}<br>
<br>
BOOL CSocketCtrl::Connect( LPCTSTR lpszHostAddress, UINT nHostPort )<br>
{<br>
if(m_sockfd==INVALID_SOCKET) return FALSE;<br>
<br>
SOCKADDR_IN sockAddr;<br>
<br>
memset(&amp;sockAddr,0,sizeof(sockAddr));<br>
LPSTR lpszAscii=(LPSTR)lpszHostAddress;<br>
sockAddr.sin_family=AF_INET;<br>
sockAddr.sin_addr.s_addr=inet_addr(lpszAscii);<br>
if(sockAddr.sin_addr.s_addr==INADDR_NONE)<br>
{<br>
HOSTENT * lphost;<br>
lphost = ::gethostbyname(lpszAscii);<br>
if(lphost!=NULL)<br>
sockAddr.sin_addr.s_addr = ((IN_ADDR *)lphost-&gt;h_addr)-&gt;s_addr;<br>
else return FALSE;<br>
}<br>
sockAddr.sin_port = htons((u_short)nHostPort);<br>
<br>
int r=::connect(m_sockfd,(SOCKADDR*)&amp;sockAddr,sizeof(sockAddr));<br>
if(r!=SOCKET_ERROR) return TRUE;<br>
<br>
int e;<br>
e=::WSAGetLastError();<br>
if(e!=WSAEWOULDBLOCK) return FALSE;<br>
<br>
fd_set set;<br>
timeval tout;<br>
tout.tv_sec = 0;<br>
tout.tv_usec = 100000;<br>
<br>
UINT n=0;<br>
while( n&lt; CSocketCtrl::m_dwConnectOut)<br>
{<br>
FD_ZERO(&amp;set);<br>
FD_SET(m_sockfd,&amp;set);<br>
e=::select(0,NULL,&amp;set,NULL, &amp;tout);<br>
<br>
if(e==SOCKET_ERROR) return FALSE;<br>
if(e&gt;0) return TRUE;<br>
<br>
if( IsIPsChange() )<br>
return FALSE;<br>
n += 100;<br>
}<br>
<br>
return FALSE;<br>
<br>
}<br>
// 设置监听socket<br>
BOOL CSocketCtrl::Listen( int nBackLog )<br>
{<br>
if( m_sockfd == INVALID_SOCKET ) return FALSE;<br>
if( !listen( m_sockfd, nBackLog) ) return TRUE;<br>
return FALSE;<br>
}<br>
<br>
// 接收一个新的客户连接<br>
BOOL CSocketCtrl::Accept( CSocketCtrl&amp; ms )<br>
{<br>
if( m_sockfd == INVALID_SOCKET ) return FALSE;<br>
if( ms.m_sockfd != INVALID_SOCKET ) return FALSE;<br>
<br>
int e;<br>
fd_set set;<br>
timeval tout;<br>
tout.tv_sec = 0;<br>
tout.tv_usec = 100000;<br>
<br>
UINT n=0;<br>
while(n&lt; CSocketCtrl::m_dwAcceptOut)<br>
{<br>
//if(stop) return FALSE;<br>
FD_ZERO(&amp;set);<br>
FD_SET(m_sockfd,&amp;set);<br>
e=::select(0,&amp;set,NULL,NULL, &amp;tout);<br>
if(e==SOCKET_ERROR) return FALSE;<br>
if(e==1) break;<br>
n += 100;<br>
}<br>
if( n&gt;= CSocketCtrl::m_dwAcceptOut ) return FALSE;<br>
<br>
ms.m_sockfd=accept(m_sockfd,NULL,NULL);<br>
if(ms.m_sockfd==INVALID_SOCKET) return FALSE;<br>
ms.SetDefaultOpt();<br>
<br>
return TRUE;<br>
}<br>
<br>
BOOL CSocketCtrl::Create(void)<br>
{<br>
m_sockfd=::socket(PF_INET,SOCK_STREAM,0);<br>
if(m_sockfd==INVALID_SOCKET) return FALSE;<br>
SOCKADDR_IN SockAddr;<br>
<br>
memset(&amp;SockAddr,0,sizeof(SockAddr));<br>
SockAddr.sin_family = AF_INET;<br>
SockAddr.sin_addr.s_addr = INADDR_ANY;<br>
SockAddr.sin_port = ::htons(0);<br>
//if(!::bind(m_sock,(SOCKADDR*)&amp;SockAddr, sizeof(SockAddr))) <br>
{<br>
SetDefaultOpt();<br>
return TRUE;<br>
}<br>
Close();<br>
return FALSE;<br>
}<br>
<br>
// 设置正确的socket状态，<br>
// 主要是主要是设置非阻塞异步传输模式<br>
void CSocketCtrl::SetDefaultOpt()<br>
{<br>
struct linger ling;<br>
ling.l_onoff=1;<br>
ling.l_linger=0;<br>
setsockopt( m_sockfd, SOL_SOCKET, SO_LINGER, (char *)&amp;ling, sizeof(ling));<br>
setsockopt( m_sockfd, SOL_SOCKET, SO_REUSEADDR, 0, 0);<br>
int bKeepAlive = 1;<br>
setsockopt( m_sockfd, SOL_SOCKET, SO_KEEPALIVE, (char*)&amp;bKeepAlive, sizeof(int));<br>
BOOL bNoDelay = TRUE;<br>
setsockopt( m_sockfd, IPPROTO_TCP, TCP_NODELAY, (char*)&amp;bNoDelay, sizeof(BOOL));<br>
unsigned long nonblock=1;<br>
::ioctlsocket(m_sockfd,FIONBIO,&amp;nonblock);<br>
}</p>
<hr>
<p> </p>
<p>今天晚上写了一些测试代码，<br>
想看看flash究竟能够承受多大的网络数据传输，<br>
<br>
我在flash登陆到服务器以后，<br>
每隔3毫秒就发送100次100个字符的串
"0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
给flash，<br>
然后在flash里面接收数据的函数里面统计数据，<br>
<br>
<br>
var g_nTotalRecvByte = 0;<br>
var g_time = new Date(); <br>
var g_nStartTime = g_time.getTime();<br>
var g_nCounter = 0;<br>
<br>
mySocket.onData=function(xmlDoc)<br>
{<br>
g_nTotalRecvByte += xmlDoc.length;<br>
// 每接收超过1k字节的数据，输出一次信息，<br>
if( g_nTotalRecvByte-g_nCounter &gt; 1024 )<br>
{<br>
g_time = new Date();<br>
var nPassedTime = g_time.getTime()-g_nStartTime;<br>
trace( "花费时间:"+nPassedTime+"毫秒" );<br>
g_nCounter = g_nTotalRecvByte;<br>
trace( "接收总数:"+g_nTotalRecvByte+"字节" );<br>
trace( "接收速率:"+g_nTotalRecvByte*1000/nPassedTime+"字节/秒" );<br>
<br>
}<br>
结果十分令我意外，<br>
这是截取的一段调试信息，<br>
//<br>
花费时间:6953毫秒<br>
接收总数:343212字节<br>
接收速率:49361.7143678988字节/秒<br>
花费时间:7109毫秒<br>
接收总数:344323字节<br>
接收速率:48434.800956534字节/秒<br>
花费时间:7109毫秒<br>
接收总数:345434字节<br>
接收速率:48591.0817273878字节/秒<br>
。。。<br>
。。。<br>
。。。<br>
。。。<br>
花费时间:8125毫秒<br>
接收总数:400984字节<br>
接收速率:49351.8769230769字节/秒<br>
花费时间:8125毫秒<br>
接收总数:402095字节<br>
接收速率:49488.6153846154字节/秒<br>
花费时间:8125毫秒<br>
接收总数:403206字节<br>
接收速率:49625.3538461538字节/秒<br>
<br>
我检查了几遍源程序，没有发现逻辑错误，<br>
如果程序没有问题的话，<br>
那么我们得出的结论是，flash的xml socket每秒可以接收至少40K的数据，<br>
这还没有计算xmlSocket.onData事件的触发，调试代码、信息输出占用的时间。<br>
<br>
比我想象中快了一个数量级，<br>
够用了，<br>
flash网络游戏我们可以继续往下走了，</p>
<hr>
<p> </p>
<p>有朋友问到lag的问题，<br>
问得很好，不过也不要过于担心，<br>
lag的产生有的是因为网络延迟，<br>
有的是因为服务器负载过大，<br>
对于游戏的设计者和开发者来说，<br>
首先要从设计的角度来避免或者减少lag产生的机会，<br>
如果lag产生了，<br>
也不要紧，找到巧妙的办法骗过玩家的眼睛，<br>
这也有很多成熟的方法了，<br>
比如航行预测法，路径插值等等，<br>
都可以产生很好的效果，<br>
还有最后的绝招，就是提高服务器的配置和网络带宽，<br>
<br>
从我开发网络游戏这段时间的经验来看，<br>
我们的服务器是vc开发的，<br>
普通pc跑几百个玩家，几百个怪物是没有问题的，<br>
<br>
<br>
又作了一个flash发送的测试，<br>
<br>
网络游戏的特点是，<br>
出去的信息比较少，<br>
进来的信息比较多，<br>
<br>
这个很容易理解，<br>
人操作游戏的速度是很有限的，<br>
控制指令的产生也是随机的，<br>
离散的，<br>
<br>
但是多人游戏的话，<br>
因为人多，信息的流量也就区域均匀分布了，<br>
<br>
在昨天接收数据的基础上，<br>
我略加修改，<br>
这次，<br>
我在_root.enterFrame写了如下代码，<br>
_root.onEnterFrame = function() <br>
{<br>
var i;<br>
for( i = 0; i &lt; 10; i++ )<br>
mySocket.send( ConvertToMsg( "01234567890123456789012345678901234567890123456789" ) );<br>
return;<br>
}<br>
<br>
服务器端要做的是，<br>
把所有从flash客户端收到的信息原封不动的返回来，<br>
<br>
这样，我又可以通过昨天onData里面的统计算法来从侧面估算出flash发送数据的能力，<br>
这里是输出的数据<br>
//<br>
花费时间:30531毫秒<br>
接收总数:200236字节<br>
接收速率:6558.44878975468字节/秒<br>
花费时间:30937毫秒<br>
接收总数:201290字节<br>
接收速率:6506.44858906811字节/秒<br>
花费时间:31140毫秒<br>
接收总数:202344字节<br>
接收速率:6497.88053949904字节/秒<br>
花费时间:31547毫秒<br>
接收总数:203398字节<br>
接收速率:6447.45934637208字节/秒<br>
<br>
可以看出来，发送+接收同时做，<br>
发送速率至少可以达到5k byte/s<br>
<br>
有一点要注意，要非常注意，<br>
不能让flash的网络传输满载，<br>
所谓满载就是flash在阻塞运算的时候，<br>
不断的有数据从网络进来，<br>
而flash又无法在预计的时间内处理我这些信息，<br>
或者flash发送数据过于频繁，<br>
导致服务器端缓冲溢出导致错误，<br>
<br>
对于5k的传输速率，<br>
已经足够了，<br>
因为我也想不出来有什么产生这么大的数据量，<br>
而且如果产生了这么大的数据量，<br>
也就意味着服务器每时每刻都要处理所有的玩家发出的海量数据，<br>
还要把这些海量数据转发给其他的玩家，<br>
已经引起数据爆炸了，<br>
所以，5k的上传从设计阶段就要避免的，<br>
我想用flash做的网络游戏，<br>
除了动作类游戏可能需要恒定1k以内的上传速率，<br>
其他的200个字节/秒以内就可以了，</p>
<hr>
<strong>使用于Flash的消息结构定义</strong>
<p> </p>
<p>我们以前讨论过，<br>
通过消息来传递信息，<br>
消息的结构是<br>
struct msg<br>
{<br>
short nLength; // 2 byte<br>
DWORD dwId; // 4 byte<br>
<br>
....<br>
data<br>
}<br>
<br>
但是在为flash开发的消息中，<br>
不能采用这种结构，<br>
<br>
首先Flash xmlSocket只传输字符串，<br>
从xmlSocket的send,onData函数可以看出来，<br>
发出去的，收进来的都应该是字符串，<br>
<br>
而在服务器端是使用vc,java等高级语言编写的，<br>
消息中使用的是二进制数据块，<br>
显然，简单的使用字符串会带来问题，<br>
<br>
所以，我们需要制定一套协议，<br>
就是无论在客户端还是服务器端，<br>
都用统一的字符串消息，<br>
通过解析字符串的方式来传递信息，<br>
<br>
我想这就是flash采用xml document来传输结构化信息的理由之一，<br>
xml document描述了一个完整的数据结构，<br>
而且全部使用的是字符串，<br>
原来是这样，怪不得叫做xml socket，<br>
本来socket和xml完全是不同的概念，<br>
flash偏偏出了个xml socket，<br>
一开始令我费解，<br>
现在，渐渐理解其中奥妙。</p>
<hr>
<strong>Flash Msg结构定义源代码和相关函数</strong>
<p> </p>
<p>在服务器端，我们为flash定义了一种msg结构，<br>
使用语言，vc6<br>
#define MSGMAXSIZE 512<br>
// 消息头<br>
struct MsgHeader<br>
{<br>
short stLength;<br>
MsgHeader():stLength( 0 ){}<br>
<br>
};<br>
// 消息<br>
struct Msg<br>
{<br>
MsgHeader header;<br>
short GetLength(){ return header.stLength; }<br>
};<br>
// flash 消息<br>
struct MsgToFlash<img  src="http://www.flashempire.net/images/smilies/tongue.gif" title="点击新窗口查看大图" border="0">ublic Msg<br>
{<br>
// 一个足够大的缓冲，但是不会被整个发送，<br>
char szString[MSGMAXSIZE];<br>
// 计算设置好内容后，内部会计算将要发送部分的长度，<br>
// 要发送的长度=消息头大小+字符串长度+1<br>
void SetString( const char* pszChatString )<br>
{<br>
if( strlen( pszChatString ) &lt; MSGMAXSIZE-1 )<br>
{<br>
strcpy( szString, pszChatString );<br>
header.stLength = sizeof( header )+<br>
(short)strlen( pszChatString )+1;<br>
}<br>
}<br>
<br>
};<br>
<br>
在发往flash的消息中，整个处理过后MsgToFlash结构将被发送，<br>
实践证明，在flash 客户端的xmlSocket onData事件中，<br>
接收到了正确的消息，消息的内容是MasToFlash的szString字段，<br>
是一个字符串，<br>
<br>
比如在服务器端，<br>
MsgToFlash msg;<br>
msg.SetString( "move player0 to 100 100" );<br>
SendMsg( msg,............. );<br>
那么，在我们的flash客户端的onData( xmlDoc )中，<br>
我们trace( xmlDoc )<br>
结果是<br>
move player0 to 100 100<br>
<br>
<br>
然后是flash发送消息到服务器，<br>
我们强调flash只发送字符串，<br>
这个字符串无论是否内部拥有有效数据，<br>
服务器都应该首先把消息收下来，<br>
那就要保证发送给服务器的消息遵循统一的结构，<br>
在flash客户端中，<br>
我们定义一个函数，<br>
这个函数把一个字符串转化为服务器可以识别的消息，<br>
<br>
补充：现在我们约定字符串长度都不大于97个字节长度，<br>
<br>
<br>
var num_table = new array( "0","1","2","3","4","5","6","7","8","9" );<br>
function ConvertToMsg( str )<br>
{<br>
var l = str.length+3; <br>
var t = "";<br>
if( l &gt; 10 )<br>
t = num_table[Math.floor(l/10)]+num_table[Number(l%10)]+str;<br>
else<br>
t = num_table[0]+num_table[l]+str;<br>
return t;<br>
}<br>
<br>
比如<br>
var msg = ConvertToMsg( "client login" );<br>
我们trace( msg );<br>
看到的是<br>
15client login<br>
<br>
为什么是这个结果呢？<br>
15是消息的长度，<br>
头两个字节是整个消息的长度的asc码，意思是整个消息有15个字节长，<br>
然后是信息client login，<br>
最后是一个0（c语言中的字符串结束符）<br>
<br>
当服务器收到15client login，<br>
他首先把15给分析出来，<br>
把"15"字符串转化为15的数字，<br>
然后，根据15这个长度把后面的client login读出来，<br>
这样，网络传输的底层就完成了，<br>
client login的处理就交给逻辑层，</p>
<hr>
谢谢大家的支持，<br>
很感谢斑竹把这个贴子置顶，<br>
我写这文章的过程也是我自己摸索的过程，<br>
文章可以记录我一段开发的历史，<br>
一个思考分析的历程，<br>
有时候甚至作为日志来写，<br>
<br>
由于我本身有杂务在身，<br>
所以贴子的更新有点慢，<br>
请大家见谅，<br>
<br>
我喜爱flash，<br>
虽然我在帝国中，但我并不能称之为闪客，<br>
因为我制作flash的水平实在很低，<br>
但是我想设计开发出让其他人能更好的使用flash的工具，<br>
<br>
前阵子我开发了Match3D，<br>
一个可以把三维动画输出成为swf的工具，<br>
而且实现了swf渲染的实时三维角色动画，<br>
这可以说是我真正推出的第一个flash第三方软件，<br>
其实这以前，<br>
我曾经开发过几个其他的flash第三方软件，<br>
都中途停止了，<br>
因为不实用或者市场上有更好的同类软件，<br>
<br>
随着互联网的发展，<br>
flash的不断升级，<br>
我的flash第三方软件目光渐渐的从美术开发工具转移到网络互连，<br>
web应用上面来，<br>
如今已经到了2004版本，<br>
flash的种种新特性让我眼前发光，<br>
<br>
我最近在帝国的各个板块看了很多贴子，<br>
分析里面潜在的用户需求，<br>
总结了以下的几个我认为比较有意义的选题，<br>
可能很片面，<br>
<br>
flash源代码保护，主要是为了抵御asv之类的软件进行反编译和萃取<br>
flash与远端数据库的配合，应该出现一个能够方便快捷的对远程数据库进行操作的方法或者控件，<br>
flash网际互连，我认为flash网络游戏是一块金子，<br>
<br>
这里我想谈谈flash网络游戏，<br>
我要谈的不仅仅是技术，而是一个概念，<br>
用flash网络游戏，<br>
我本身并不想把flash游戏做成rpg或者其他剧烈交互性的游戏，<br>
而是想让flash实现那些节奏缓慢，玩法简单的游戏，<br>
把网络的概念带进来，<br>
<br>
你想玩游戏的时候，登上flash网络游戏的网站，<br>
选择你想玩的网络游戏，<br>
因为现在几乎所有上网的电脑都可以播放swf，<br>
所以，我们几乎不用下载任何插件，<br>
输入你的账号和密码，<br>
就可以开始玩了，<br>
<br>
我觉得battle.net那种方式很适合flash，<br>
开房间或者进入别人开的房间，<br>
然后2个人或者4个人就可以交战了，<br>
<br>
这种游戏可以是棋类，这是最基本的，<br>
用户很广泛，<br>
我脑海中的那种是类似与宠物饲养的，<br>
就像当年的电子宠物，<br>
每个玩家都可以到服务器认养宠物，<br>
然后在线养成宠物，<br>
还可以邀请别的玩家进行宠物比武，<br>
看谁的宠物厉害，<br>
<br>
就这样简简单单的模式，<br>
配合清新可爱的画面，<br>
趣味的玩法，<br>
加入网络的要素，<br>
也许可以取得以想不到的效果，<br>
<br>
今天就说到这里吧，<br>
想法那么多，要实现的话还有很多路要走，<br>
<br>
希望大家多多支持，积极参与，<br>
让我们的想法不仅仅停留于纸上。 <hr>
大家好，<br>
非常抱歉，<br>
都很长时间没有回贴了，<br>
因为手头项目的原因，<br>
几乎没有时间做flash multiplayer的研究，<br>
<br>
很感谢大家的支持，<br>
现在把整个flash networking的源代码共享出来，<br>
大家可以任意的使用，<br>
其实里面也没有多少东西，<br>
相信感兴趣的朋友还是可以从中找到一些有用的东西，<br>
<br>
这一次的源代码做的事情很简单，<br>
服务器运行，<br>
客户端登陆到服务器，<br>
然后客户端不断的发送字符串给服务器，<br>
服务器收到后，在发还给客户端，<br>
客户端统计一些数据，<img src ="http://www.cppblog.com/keigoliye/aggbug/97067.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-23 23:43 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/23/97067.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>负载均衡－－大型在线系统实现的关键(上篇)（再谈QQ游戏百万人在线的技术实现）</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/23/97068.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 23 Sep 2009 15:43:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/23/97068.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/97068.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/23/97068.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/97068.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/97068.html</trackback:ping><description><![CDATA[<p>本文作者：sodme<br>
本文出处：<a  href="http://blog.csdn.net/sodme">http://blog.csdn.net/sodme</a><br>
声明：本文可以不经作者同意任意转载，但任何对本文的引用都须注明作者、出处及此声明信息。谢谢！！</p>
<p>　　要了解此篇文章中引用的本人写的另一篇文章，请到以下地址：<br>
<a  href="http://blog.csdn.net/sodme/archive/2004/12/12/213995.aspx">http://blog.csdn.net/sodme/archive/2004/12/12/213995.aspx</a><br>
以上的这篇文章是早在去年的时候写的了，当时正在作休闲平台，一直在想着如何实现一个可扩充的支持百万人在线的游戏平台，后来思路有了，就写了那篇总结。文章的意思，重点在于阐述一个百万级在线的系统是如何实施的，倒没真正认真地考察过QQ游戏到底是不是那样实现的。</p>
<p>　　近日在与业内人士讨论时，提到QQ游戏的实现方式并不是我原来所想的那样，于是，今天又认真抓了一下QQ游戏的包，结果确如这位兄弟所言，QQ
游戏的架构与我当初所设想的那个架构相差确实不小。下面，我重新给出QQ百万级在线的技术实现方案，并以此展开，谈谈大型在线系统中的负载均衡机制的设
计。</p>
<p>　　从QQ游戏的登录及游戏过程来看，QQ游戏中，也至少分为三类服务器。它们是：<br>
第一层：登陆/账号服务器(Login Server)，负责验证用户身份、向客户端传送初始信息，从QQ聊天软件的封包常识来看，这些初始信息可能包括&#8220;会话密钥&#8221;此类的信息，以后客户端与后续服务器的通信就使用此会话密钥进行身份验证和信息加密；<br>
第二层：大厅服务器(估且这么叫吧, Game Hall Server)，负责向客户端传递当前游戏中的所有房间信息，这些房间信息包括：各房间的连接IP，PORT，各房间的当前在线人数，房间名称等等。<br>
第三层：游戏逻辑服务器(Game Logic Server)，负责处理房间逻辑及房间内的桌子逻辑。</p>
<p>　　从静态的表述来看，以上的三层结构似乎与我以前写的那篇文章相比并没有太大的区别，事实上，重点是它的工作流程，QQ游戏的通信流程与我以前的设想可谓大相径庭，其设计思想和技术水平确实非常优秀。具体来说，QQ游戏的通信过程是这样的：</p>
<p>　　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里的。</p>
<p>　　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并向其索要最新的房间目录信息及在线人数信息。</p>
<p>　　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人放在一个集合内进行处理（比如聊天信息，&#8220;向400人广播&#8221;
和&#8220;向2万人广播&#8221;，这是完全不同的两个概念）。</p>
<p>　　4.需要特别说明的一点。进入QQ游戏房间后，直到点击某个位置坐下打开另一个程序界面，客户端的程序，没有再创建新的socket，而仍然使
用原来大厅房间客户端跟游戏逻辑服务器交互用的socket。也就是说，这是两个进程共用的同一个socket！不要小看这一点。如果你在创建桌子客户端
程序后又新建了一个新的socket与游戏逻辑服务器进行通信，那么由此带来的玩家进入、退出、逃跑等消息会带来非常麻烦的数据同步问题，俺在刚开始的时
候就深受其害。而一旦共用了同一个socket后，你如果退出桌子，服务器不涉及释放socket的问题，所以，这里就少了很多的数据同步问题。关于多个
进程如何共享同一个socket的问题，请去google以下内容：WSADuplicateSocket。</p>
<p>　　以上便是我根据最新的QQ游戏抓包结果分析得到的QQ游戏的通信流程，当然，这个流程更多的是客户端如何与服务器之间交互的，却没有涉及到服务器彼此之间是如何通信和作数据同步的。关于服务器之间的通信流程，我们只能基于自己的经验和猜想，得出以下想法：</p>
<p>　　1.Login Server与Game Hall Server之前的通信问题。Login
Server是负责用户验证的，一旦验证通过之后，它要设法让Game Hall
Server知道这个消息。它们之前实现信息交流的途径，我想可能有这样几条：a. Login
Server将通过验证的用户存放到临时数据库中；b.　Login
Server将验证通过的用户存放在内存中，当然，这个信息，应该是全局可访问的，就是说所有QQ的Game Hall
Server都可以通过服务器之间的数据包通信去获得这样的信息。</p>
<p>　　2.Game Hall Server的最新房间目录信息的取得。这个信息，是全局的，也就是整个游戏中，只保留一个目录。它的信息来源，可以由底层的房间服务器逐级报上来，报给谁？我认为就如保存的全局登录列表一样，它报给保存全局登录列表的那个服务器或数据库。</p>
<p>　　3.在QQ游戏中，同一类型的游戏，无法打开两上以上的游戏房间。这个信息的判定，可以根据全局信息来判定。</p>
<p>　　以上关于服务器之间如何通信的内容，均属于个人猜想，QQ到底怎么作的，恐怕只有等大家中的某一位进了腾讯之后才知道了。呵呵。不过，有一点是
可以肯定的，在整个服务器架构中，应该有一个地方是专门保存了全局的登录玩家列表，只有这样才能保证玩家不会重复登录以及进入多个相同类型的房间。</p>
<p>　　在前面的描述中，我曾经提到过一个问题：当登录当前Game Hall Server不成功时，QQ游戏服务器会选择让客户端重定向到另位的服务器去登录，事实上，QQ聊天服务器和MSN服务器的登录也是类似的，它也存在登录重定向问题。</p>
<p>　　那么，这就引出了另外的问题，由谁来作这个策略选择？以及由谁来提供这样的选择资源？这样的处理，便是负责负载均衡的服务器的处理范围了。由QQ游戏的通信过程分析派生出来的针对负责均衡及百万级在线系统的更进一步讨论，将在下篇文章中继续。</p>
<p>　　在此，特别感谢网友tilly及某位不便透露姓名的网友的讨论，是你们让我决定认真再抓一次包探个究竟。</p><img src ="http://www.cppblog.com/keigoliye/aggbug/97068.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-23 23:43 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/23/97068.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>转：网络游戏封包基础</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/23/97065.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 23 Sep 2009 15:39:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/23/97065.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/97065.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/23/97065.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/97065.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/97065.html</trackback:ping><description><![CDATA[原文：http://game.chinaitlab.com/freshmen/783449.html<br><br>
<p>&nbsp;&nbsp;&nbsp;
要想在修改游戏中做到百战百胜，是需要相当丰富的计算机知识的。有很多计算机高手就是从玩游戏，修改游戏中，逐步对计算机产生浓厚的兴趣，逐步成长起来
的。不要在羡慕别人能够做到的，因为别人能够做的你也能够！我相信你们看了本教程后，会对游戏有一个全新的认识，呵呵，因为我是个好老师！（别拿鸡蛋砸我
呀，救命啊！#￥%&#8230;&#8230;*）</p>
<p>&nbsp;&nbsp;&nbsp; 不过要想从修改游戏中学到知识，增加自己的计算机水平，可不能只是靠修改游戏呀！
要知道，修改游戏只是一个验证你对你所了解的某些计算机知识的理解程度的场所，只能给你一些发现问题、解决问题的机会，只能起到帮助你提高学习计算机的兴
趣的作用，而决不是学习计算机的捷径。</p>
<p>&nbsp;&nbsp;&nbsp; 一：什么叫外挂？</p>
<p>&nbsp;&nbsp;&nbsp; 现在的网络游戏多是基于Internet上客户／<span class="channel_keylink">服务器</span>模式，服务端程序运行在游戏<span class="channel_keylink">服务器</span>上，游戏的设计者在其中创造一个庞大的游戏空间，各地的玩家可以通过运行客户端程序同时登录到游戏中。简单地说，网络游戏实际上就是由<span class="channel_keylink">游戏开发</span>商
提供一个游戏环境，而玩家们就是在这个环境中相对自由和开放地进行游戏操作。那么既然在网络游戏中有了服务器这个概念，我们以前传统的修改游戏方法就显得
无能为力了。记得我们在单机版的游戏中，随心所欲地通过内存搜索来修改角色的各种属性，这在网络游戏中就没有任何用处了。因为我们在网络游戏中所扮演角色
的各种属性及各种重要资料都存放在服务器上，在我们自己机器上（客户端）只是显示角色的状态，所以通过修改客户端内存里有关角色的各种属性是不切实际的。
那么是否我们就没有办法在网络游戏中达到我们修改的目的？回答是"否".我们知道Internet客户／服务器模式的通讯一般采用TCP/IP通信<span class="channel_keylink">协议</span>，数据<span class="channel_keylink">交换</span>是通过IP数据包的传输来实现的，一般来说我们客户端向服务器发出某些请求，比如移动、战斗等指令都是通过封包的形式和服务器<span class="channel_keylink">交换</span>数
据。那么我们把本地发出消息称为SEND，意思就是发送数据，服务器收到我们SEND的消息后，会按照既定的程序把有关的信息反馈给客户端，比如，移动的
坐标，战斗的类型。那么我们把客户端收到服务器发来的有关消息称为RECV.知道了这个道理，接下来我们要做的工作就是分析客户端和服务器之间往来的数据
（也就是封包），这样我们就可以提取到对我们有用的数据进行修改，然后模拟服务器发给客户端，或者模拟客户端发送给服务器，这样就可以实现我们修改游戏的
目的了。</p>
<p>&nbsp;&nbsp;&nbsp;
目前除了修改游戏封包来实现修改游戏的目的，我们也可以修改客户端的有关程序来达到我们的要求。我们知道目前各个服务器的运算能力是有限的，特别在游戏
中，游戏服务器要计算游戏中所有玩家的状况几乎是不可能的，所以有一些运算还是要依靠我们客户端来完成，这样又给了我们修改游戏提供了一些便利。比如我们
可以通过将客户端程序脱壳来发现一些程序的判断分支，通过跟踪调试我们可以把一些对我们不利的判断去掉，以此来满足我们修改游戏的需求。
在下几个章节中，我们将给大家讲述封包的概念，和修改跟踪客户端的有关知识。大家准备好了吗？</p>
<p>&nbsp;&nbsp;&nbsp; 游戏数据格式和<span class="channel_keylink">存储</span>：</p>
<p>&nbsp;&nbsp;&nbsp;
在进行我们的工作之前，我们需要掌握一些关于计算机中储存数据方式的知识和游戏中储存数据的特点。本章节是提供给菜鸟级的玩家看的，如果你是高手就可以跳
过了，如果，你想成为无坚不摧的剑客，那么，这些东西就会花掉你一些时间；如果，你只想作个江湖的游客的话，那么这些东西，了解与否无关紧要。是作剑客，
还是作游客，你选择吧！</p>
<p>&nbsp;&nbsp;&nbsp; 现在我们开始！首先，你要知道游戏中储存数据的几种格式，这几种格式是：字节（BYTE）、字（WORD）和双字（DOUBLE
WORD），或者说是8位、16位和32位储存方式。字节也就是8位方式能储存0~255的数字；字或说是16位储存方式能储存0~65535的数；双字
即32位方式能储存0~4294967295的数。</p>
<p>&nbsp;&nbsp;&nbsp;
为何要了解这些知识呢？在游戏中各种参数的最大值是不同的，有些可能100左右就够了，比如，金庸群侠传中的角色的等级、随机遇敌个数等等。而有些却需要
大于255甚至大于65535，象金庸群侠传中角色的金钱值可达到数百万。所以，在游戏中各种不同的数据的类型是不一样的。在我们修改游戏时需要寻找准备
修改的数据的封包，在这种时候，正确判断数据的类型是迅速找到正确地址的重要条件。</p>
<p>&nbsp;&nbsp;&nbsp; 在计算机中数据以字节为基本的储存单位，每个字节被赋予一个编号，以确定各自的位置。这个编号我们就称为地址。</p>
<p>&nbsp;&nbsp;&nbsp; 在需要用到字或双字时，计算机用连续的两个字节来组成一个字，连续的两个字组成一个双字。而一个字或双字的地址就是它们的低位字节的地址。 现在我们常用的<span class="channel_keylink">Windows</span> 9x操作系统中，地址是用一个32位的二进制数表示的。而在平时我们用到内存地址时，总是用一个8位的16进制数来表示它。</p>
<p>&nbsp;&nbsp;&nbsp; 二进制和十六进制又是怎样一回事呢？</p>
<p>&nbsp;&nbsp;&nbsp;
简单说来，二进制数就是一种只有0和1两个数码，每满2则进一位的计数进位法。同样，16进制就是每满十六就进一位的计数进位法。16进制有0——F十六
个数字，它为表示十到十五的数字采用了A、B、C、D、E、F六个数字，它们和十进制的对应关系是：A对应于10，B对应于11，C对应于12，D对应于
13，E对应于14，F对应于15.而且，16进制数和二进制数间有一个简单的对应关系，那就是；四位二进制数相当于一位16进制数。比如，一个四位的二
进制数1111就相当于16进制的F，1010就相当于A.了解这些基础知识对修改游戏有着很大的帮助，下面我就要谈到这个问题。由于在计算机中数据是以
二进制的方式储存的，同时16进制数和二进制间的转换关系十分简单，所以大部分的修改工具在显示计算机中的数据时会显示16进制的代码，而且在你修改时也
需要输入16进制的数字。你清楚了吧？</p>
<p>&nbsp;&nbsp;&nbsp; 在游戏中看到的数据可都是十进制的，在要寻找并修改参数的值时，可以使用<span class="channel_keylink">Windows</span>提供的计算器来进行十进制和16进制的换算，我们可以在开始菜单里的程序组中的附件中找到它。</p>
<p>&nbsp;&nbsp;&nbsp; 现在要了解的知识也差不多了！不过，有个问题在游戏修改中是需要注意的。在计算机中数据的储存方式一般是低位数储存在低位字节，高位数储存在高位字节。比如，十进制数41715转换为16进制的数为A2F3，但在计算机中这个数被存为F3A2.</p>
<p>&nbsp;&nbsp;&nbsp; 看了以上内容大家对数据的存贮和数据的对应关系都了解了吗？ 好了，接下来我们要告诉大家在游戏中，封包到底是怎么一回事了，来！大家把袖口卷起来，让我们来干活吧！</p>
<p>&nbsp;&nbsp;&nbsp; 二：什么是封包？</p>
<p>&nbsp;&nbsp;&nbsp; 怎么截获一个游戏的封包？怎么去检查游戏服务器的ip地址和端口号？
Internet用户使用的各种信息服务，其通讯的信息最终均可以归结为以IP包为单位的信息传送，IP包除了包括要传送的数据信息外，还包含有信息要发
送到的目的IP地址、信息发送的源IP地址、以及一些相关的控制信息。当一台<span class="channel_keylink">路由</span>器收到一个IP数据包时，它将根据数据包中的目的IP地址项查找路由表，根据查找的结果将此IP数据包送往对应端口。下一台IP路由器收到此数据包后继续转发，直至发到目的地。路由器之间可以通过路由<span class="channel_keylink">协议</span>来进行路由信息的交换，从而更新路由表。</p>
<p>&nbsp;&nbsp;&nbsp; 那么我们所关心的内容只是IP包中的数据信息，我们可以使用许多监听网络的工具来截获客户端与服务器之间的交换数据，下面就向你介绍其中的一种工具：WPE. WPE使用方法：执行WPE会有下列几项功能可选择：</p>
<p>&nbsp;&nbsp;&nbsp; SELECT GAME选择目前在记忆体中您想拦截的程式，您只需双击该程式名称即可。</p>
<p>&nbsp;&nbsp;&nbsp; TRACE追踪功能。用来追踪撷取程式送收的封包。WPE必须先完成点选欲追踪的程式名称，才可以使用此项目。
按下Play键开始撷取程式收送的封包。您可以随时按下 | | 暂停追踪，想继续时请再按下 | |
.按下正方形可以停止撷取封包并且显示所有已撷取封包内容。若您没按下正方形停止键，追踪的动作将依照OPTION里的设定值自动停止。如果您没有撷取到
资料，试试将OPTION里调整为Winsock Version 2.WPE 及 Trainers 是设定在显示至少16 bits
颜色下才可执行。</p>
<p>&nbsp;&nbsp;&nbsp; FILTER过滤功能。用来分析所撷取到的封包，并且予以修改。</p>
<p>&nbsp;&nbsp;&nbsp; SEND PACKET送出封包功能。能够让您送出假造的封包。</p>
<p>&nbsp;&nbsp;&nbsp; TRAINER MAKER制作修改器。</p>
<p>&nbsp;&nbsp;&nbsp; OPTIONS设定功能。让您调整WPE的一些设定值。</p>
<p>&nbsp;&nbsp;&nbsp; FILTER的详细教学</p>
<p>&nbsp;&nbsp;&nbsp; - 当FILTER在启动状态时 ，ON的按钮会呈现红色。-
当您启动FILTER时，您随时可以关闭这个视窗。FILTER将会留在原来的状态，直到您再按一次 on / off 钮。-
只有FILTER启用钮在OFF的状态下，才可以勾选Filter前的方框来编辑修改。-
当您想编辑某个Filter，只要双击该Filter的名字即可。</p>
<p>&nbsp;&nbsp;&nbsp; NORMAL MODE：</p>
<p>&nbsp;&nbsp;&nbsp; 范例：</p>
<p>&nbsp;&nbsp;&nbsp; 当您在 Street Fighter Online
﹝快打旋风线上版﹞游戏中，您使用了两次火球而且击中了对方，这时您会撷取到以下的封包：SEND-&gt; 0000 08 14 21 06 01
04 SEND-&gt; 0000 02 09 87 00 67 FF A4 AA 11 22 00 00 00 00 SEND-&gt;
0000 03 84 11 09 11 09 SEND-&gt; 0000 0A 09 C1 10 00 00 FF 52 44
SEND-&gt; 0000 0A 09 C1 10 00 00 66 52 44您的第一个火球让对方减了16滴﹝16 =
10h﹞的生命值，而您观察到第4跟第5个封包的位置4有10h的值出现，应该就是这里了。</p>
<p>&nbsp;&nbsp;&nbsp; 您观察10h前的0A 09 C1在两个封包中都没改变，可见得这3个数值是发出火球的关键。</p>
<p>&nbsp;&nbsp;&nbsp; 因此您将0A 09 C1 10填在搜寻列﹝SEARCH﹞，然后在修改列﹝MODIFY﹞的位置4填上FF.如此一来，当您再度发出火球时，FF会取代之前的10，也就是攻击力为255的火球了！</p>
<p>&nbsp;&nbsp;&nbsp; ADVANCED MODE：范例：
当您在一个游戏中，您不想要用真实姓名，您想用修改过的假名传送给对方。在您使用TRACE后，您会发现有些封包里面有您的名字出现。假设您的名字是
Shadow，换算成16进位则是﹝53 68 61 64 6F 77﹞；而您打算用moon﹝6D6F 6F 6E 20 20﹞来取代他。1）
SEND-&gt; 0000 08 14 21 06 01 042） SEND-&gt; 0000 01 06 99 53 68 61 64
6F 77 00 01 05 3） SEND-&gt; 0000 03 84 11 09 11 094） SEND-&gt; 0000 0A
09 C1 10 00 53 68 61 64 6F 77 00 11 5） SEND-&gt; 0000 0A 09 C1 10 00 00
66 52 44但是您仔细看，您的名字在每个封包中并不是出现在相同的位置上- 在第2个封包里，名字是出现在第4个位置上-
在第4个封包里，名字是出现在第6个位置上在这种情况下，您就需要使用ADVANCED MODE- 您在搜寻列﹝SEARCH﹞填上：53 68
61 64 6F 77 ﹝请务必从位置1开始填﹞-
您想要从原来名字Shadow的第一个字母开始置换新名字，因此您要选择从数值被发现的位置开始替代连续数值﹝from the position
of the chain found﹞.- 现在，在修改列﹝MODIFY﹞000的位置填上：6D 6F 6F 6E 20 20
﹝此为相对应位置，也就是从原来搜寻栏的+001位置开始递换﹞- 如果您想从封包的第一个位置就修改数值，请选择﹝from the
beginning of the packet﹞了解一点TCP/IP<span class="channel_keylink">协议</span>常识的人都知道，<span class="channel_keylink">互联网</span>是
将信息数据打包之后再传送出去的。每个数据包分为头部信息和数据信息两部分。头部信息包括数据包的发送地址和到达地址等。数据信息包括我们在游戏中相关操
作的各项信息。那么在做截获封包的过程之前我们先要知道游戏服务器的IP地址和端口号等各种信息，实际上最简单的是看看我们游戏目录下，是否有一个
SERVER.INI的配置文件，这个文件里你可以查看到个游戏服务器的IP地址，比如金庸群侠传就是如此，那么除了这个我们还可以在DOS下使用
NETSTAT这个命令，
NETSTAT命令的功能是显示网络连接、路由表和网络接口信息，可以让用户得知目前都有哪些网络连接正在运作。或者你可以使用木马客星等工具来查看网络
连接。工具是很多的，看你喜欢用哪一种了。</p>
<p>&nbsp;&nbsp;&nbsp; NETSTAT命令的一般格式为：NETSTAT [选项]命令中各选项的含义如下：-a 显示所有socket，包括正在监听的。-c 每隔1秒就重新显示一遍，直到用户中断它。</p>
<p>&nbsp;&nbsp;&nbsp; -i 显示所有网络接口的信息。-n 以网络IP地址代替名称，显示出网络连接情形。-r 显示核心路由表，格式同"route -e".-t 显示TCP协议的连接情况。-u 显示UDP协议的连接情况。-v 显示正在进行的工作。</p>
<p>&nbsp;&nbsp;&nbsp; 三：怎么来分析我们截获的封包？</p>
<p>&nbsp;&nbsp;&nbsp;
首先我们将WPE截获的封包保存为文本文件，然后打开它，这时会看到如下的数据（这里我们以金庸群侠传里PK店小二客户端发送的数据为例来讲解）：第一个
文件：SEND-&gt; 0000 E6 56 0D 22 7E 6B E4 17 13 13 12 13 12 13 67
1BSEND-&gt; 0010 17 12 DD 34 12 12 12 12 17 12 0E 12 12 12 9BSEND-&gt;
0000 E6 56 1E F1 29 06 17 12 3B 0E 17 1ASEND-&gt; 0000 E6 56 1B C0 68
12 12 12 5ASEND-&gt; 0000 E6 56 02 C8 13 C9 7E 6B E4 17 10 35 27 13 12
12SEND-&gt; 0000 E6 56 17 C9 12第二个文件：SEND-&gt; 0000 83 33 68 47 1B 0E
81 72 76 76 77 76 77 76 02 7ESEND-&gt; 0010 72 77 07 1C 77 77 77 77 72
77 72 77 77 77 6DSEND-&gt; 0000 83 33 7B 94 4C 63 72 77 5E 6B 72
F3SEND-&gt; 0000 83 33 7E A5 21 77 77 77 3FSEND-&gt; 0000 83 33 67 AD
76 CF 1B 0E 81 72 75 50 42 76 77 77SEND-&gt; 0000 83 33 72 AC
77我们发现两次PK店小二的数据格式一样，但是内容却不相同，我们是PK的同一个NPC，为什么会不同呢？
原来金庸群侠传的封包是经过了加密运算才在网路上传输的，那么我们面临的问题就是如何将密文解密成明文再分析了。</p>
<p>&nbsp;&nbsp;&nbsp; 因为一般的数据包加密都是异或运算，所以这里先讲一下什么是异或。
简单的说，异或就是"相同为0，不同为1"（这是针对二进制按位来讲的），举个例子，0001和0010异或，我们按位对比，得到异或结果是0011，计
算的方法是：0001的第4位为0，0010的第4位为0，它们相同，则异或结果的第4位按照"相同为0，不同为1"的原则得到0，0001的第3位为
0，0010的第3位为0，则异或结果的第3位得到0，0001的第2位为0，0010的第2位为1，则异或结果的第2位得到1，0001的第1位为
1，0010的第1位为0，则异或结果的第1位得到1，组合起来就是0011.异或运算今后会遇到很多，大家可以先熟悉熟悉，熟练了对分析很有帮助的。</p>
<p>&nbsp;&nbsp;&nbsp; 下面我们继续看看上面的两个文件，按照常理，数据包的数据不会全部都有值的，<span class="channel_keylink">游戏开发</span>时会预留一些字节空间来便于日后的扩充，也就是说数据包里会存在一些"00"的字节，观察上面的文件，我们会发现文件一里很多"12"，文件二里很多"77"，那么这是不是代表我们说的"00"呢？推理到这里，我们就开始行动吧！</p>
<p>&nbsp;&nbsp;&nbsp; 我们把文件一与"12"异或，文件二与"77"异或，当然用手算很费事，我们使用"M2M 1.0
加密封包分析工具"来计算就方便多了。得到下面的结果：第一个文件：1 SEND-&gt; 0000 F4 44 1F 30 6C 79 F6
05 01 01 00 01 00 01 75 09SEND-&gt; 0010 05 00 CF 26 00 00 00 00 05 00
1C 00 00 00 892 SEND-&gt; 0000 F4 44 0C E3 3B 13 05 00 29 1C 05 083
SEND-&gt; 0000 F4 44 09 D2 7A 00 00 00 484 SEND-&gt; 0000 F4 44 10 DA
01 DB 6C 79 F6 05 02 27 35 01 00 005 SEND-&gt; 0000 F4 44 05 DB
00第二个文件：1 SEND-&gt; 0000 F4 44 1F 30 6C 79 F6 05 01 01 00 01 00 01 75
09SEND-&gt; 0010 05 00 70 6B 00 00 00 00 05 00 05 00 00 00 1A2
SEND-&gt; 0000 F4 44 0C E3 3B 13 05 00 29 1C 05 843 SEND-&gt; 0000 F4
44 09 D2 56 00 00 00 484 SEND-&gt; 0000 F4 44 10 DA 01 B8 6C 79 F6 05
02 27 35 01 00 005 SEND-&gt; 0000 F4 44 05 DB
00哈，这一下两个文件大部分都一样啦，说明我们的推理是正确的，上面就是我们需要的明文！</p>
<p>&nbsp;&nbsp;&nbsp; 接下来就是搞清楚一些关键的字节所代表的含义，这就需要截获大量的数据来分析。</p>
<p>&nbsp;&nbsp;&nbsp; 首先我们会发现每个数据包都是"F4
44"开头，第3个字节是变化的，但是变化很有规律。我们来看看各个包的长度，发现什么没有？对了，第3个字节就是包的长度！
通过截获大量的数据包，我们判断第4个字节代表指令，也就是说客户端告诉服务器进行的是什么操作。例如向服务器请求战斗指令为"30"，战斗中移动指令
为"D4"等。 接下来，我们就需要分析一下上面第一个包"F4 44 1F 30 6C 79 F6 05 01 01 00 01 00 01
75 09 05 00 CF 26 00 00 00 00 05 00 1C 00 00 00
89"，在这个包里包含什么信息呢？应该有通知服务器你PK的哪个NPC吧，我们就先来找找这个店小二的代码在什么地方。
我们再PK一个小喽罗（就是大理客栈外的那个咯）：SEND-&gt; 0000 F4 44 1F 30 D4 75 F6 05 01 01 00
01 00 01 75 09SEND-&gt; 0010 05 00 8A 19 00 00 00 00 11 00 02 00 00 00
C0
我们根据常理分析，游戏里的NPC种类虽然不会超过65535（FFFF），但开发时不会把自己限制在字的范围，那样不利于游戏的扩充，所以我们在双字里
看看。通过"店小二"和"小喽罗"两个包的对比，我们把目标放在"6C 79 F6 05"和"CF 26 00
00"上。（对比一下很容易的，但你不能太迟钝咯，呵呵）我们再看看后面的包，在后面的包里应该还会出现NPC的代码，比如移动的包，游戏允许观战，服务
器必然需要知道NPC的移动坐标，再广播给观战的其他玩家。在后面第4个包"SEND-&gt; 0000 F4 44 10 DA 01 DB 6C
79 F6 05 02 27 35 01 00 00"里我们又看到了"6C 79 F6
05"，初步断定店小二的代码就是它了！（这分析里边包含了很多工作的，大家可以用WPE截下数据来自己分析分析）</p>
<p>&nbsp;&nbsp;&nbsp; 第一个包的分析暂时就到这里（里面还有的信息我们暂时不需要完全清楚了）</p>
<p>&nbsp;&nbsp;&nbsp; 我们看看第4个包"SEND-&gt; 0000 F4 44 10 DA 01 DB 6C 79 F6 05 02 27 35
01 00 00"，再截获PK黄狗的包，（狗会出来2只哦）看看包的格式：SEND-&gt; 0000 F4 44 1A DA 02 0B 4B
7D F6 05 02 27 35 01 00 00SEND-&gt; 0010 EB 03 F8 05 02 27 36 01 00
00根据上面的分析，黄狗的代码为"4B 7D F6 05"（100040011），不过两只黄狗服务器怎样分辨呢？看看"EB 03 F8
05"（100140011），是上一个代码加上100000，呵呵，这样服务器就可以认出两只黄狗了。我们再通过野外遇敌截获的数据包来证实，果然如
此。</p>
<p>&nbsp;&nbsp;&nbsp; 那么，这个包的格式应该比较清楚了：第3个字节为包的长度，"DA"为指令，第5个字节为NPC个数，从第7个字节开始的10个字节代表一个NPC的信息，多一个NPC就多10个字节来表示。</p>
<p>&nbsp;&nbsp;&nbsp; 大家如果玩过网金，必然知道随机遇敌有时会出现增援，我们就利用游戏这个增援来让每次战斗都会出现增援的NPC吧。</p>
<p>&nbsp;&nbsp;&nbsp; 通过在战斗中出现增援截获的数据包，我们会发现服务器端发送了这样一个包：F4 44 12 E9 EB 03 F8 05 02 00
00 03 00 00 00 00 00 00 第5-第8个字节为增援NPC的代码（这里我们就简单的以黄狗的代码来举例）。
那么，我们就利用单机代理技术来同时欺骗客户端和服务器吧！</p>
<p>&nbsp;&nbsp;&nbsp; 好了，呼叫NPC的工作到这里算是完成了一小半，接下来的事情，怎样修改封包和发送封包，我们下节继续讲解吧。</p>
<p>&nbsp;&nbsp;&nbsp; 四：怎么冒充"客户端"向"服务器"发我们需要的封包？</p>
<p>&nbsp;&nbsp;&nbsp;
这里我们需要使用一个工具，它位于客户端和服务器端之间，它的工作就是进行数据包的接收和转发，这个工具我们称为代理。如果代理的工作单纯就是接收和转发
的话，这就毫无意义了，但是请注意：所有的数据包都要通过它来传输，这里的意义就重大了。我们可以分析接收到的数据包，或者直接转发，或者修改后转发，或
者压住不转发，甚至伪造我们需要的封包来发送。</p>
<p>&nbsp;&nbsp;&nbsp; 下面我们继续讲怎样来同时欺骗服务器和客户端，也就是修改封包和伪造封包。 通过我们上节的分析，我们已经知道了打多个NPC的封包格式，那么我们就动手吧！</p>
<p>&nbsp;&nbsp;&nbsp; 首先我们要查找客户端发送的包，找到战斗的特征，就是请求战斗的第1个包，我们找"F4 44 1F
30"这个特征，这是不会改变的，当然是要解密后来查找哦。 找到后，表示客户端在向服务器请求战斗，我们不动这个包，转发。
继续向下查找，这时需要查找的特征码不太好办，我们先查找"DA"，这是客户端发送NPC信息的数据包的指令，那么可能其他包也有"DA"，没关系，我们
看前3个字节有没有"F4 44"就行了。找到后，我们的工作就开始了！</p>
<p>&nbsp;&nbsp;&nbsp; 我们确定要打的NPC数量。这个数量不能很大，原因在于网金的封包长度用一个字节表示，那么一个包可以有255个字节，我们上面分析过，增加一个NPC要增加10个字节，所以大家算算就知道，打20个NPC比较合适。</p>
<p>&nbsp;&nbsp;&nbsp; 然后我们要把客户端原来的NPC代码分析计算出来，因为增加的NPC代码要加上100000哦。再把我们增加的NPC代码计算出来，并且组合成新的封包，注意代表包长度的字节要修改啊，然后转发到服务器，这一步在编写程序的时候要注意算法，不要造成较大延迟。</p>
<p>&nbsp;&nbsp;&nbsp; 上面我们欺骗服务器端完成了，欺骗客户端就简单了。</p>
<p>&nbsp;&nbsp;&nbsp; 发送了上面的封包后，我们根据新增NPC代码构造封包马上发给客户端，格式就是"F4 44 12 E9 NPC代码 02 00 00
03 00 00 00 00 00 00"，把每个新增的NPC都构造这样一个包，按顺序连在一起发送给客户端，客户端也就被我们骗过了，很简单吧。</p>
<p>&nbsp;&nbsp;&nbsp; 以后战斗中其他的事我们就不管了，尽情地开打吧。</p>
<p>&nbsp;&nbsp;&nbsp; 游戏外挂基本原理及实现</p>
<p>&nbsp;&nbsp;&nbsp; 解释游戏外挂的基本原理和实现方法</p>
<p>&nbsp;&nbsp;&nbsp; 游戏外挂已经深深地影响着众多网络游戏玩家，今天在网上看到了一些关于游戏外挂编写的技术，于是转载上供大家参考</p>
<p>&nbsp;&nbsp;&nbsp; 1、游戏外挂的原理</p>
<p>&nbsp;&nbsp;&nbsp; 外挂现在分为好多种，比如模拟键盘的，鼠标的，修改数据包的，还有修改本地内存的，但好像没有修改服务器内存的哦，呵呵。其实修改服务器也是有办法的，只是技术太高一般人没有办法入手而已。（比如请GM去夜总会、送礼、收黑钱等等办法都可以修改服务器数据，哈哈）</p>
<p>&nbsp;&nbsp;&nbsp;
修改游戏无非是修改一下本地内存的数据，或者截获API函数等等。这里我把所能想到的方法都作一个介绍，希望大家能做出很好的外挂来使游戏厂商更好的完善
自己的技术。我见到一篇文章是讲魔力宝贝的理论分析，写得不错，大概是那个样子。下来我就讲解一下技术方面的东西，以作引玉之用。</p>
<p><br></p>
<p>&nbsp;&nbsp; 2 技术分析部分
</p>
<p>&nbsp;&nbsp;&nbsp; 2.1 模拟键盘或鼠标的响应</p>
<p>&nbsp;&nbsp;&nbsp; 我们一般使用：</p>
<p>UINT SendInput(<br>　　　　UINT nInputs,　　 // count of input events<br>　　　PINPUT pInputs,　// array of input events<br>　　　　int cbSize　　　　// size of structure<br>　　);<br>&nbsp;&nbsp;&nbsp; API函数。第一个参数是说明第二个参数的矩阵的维数的，第二个参数包含了响应事件，这个自己填充就可以，最后是这个结构的大小，非常简单，这是最简单的方法模拟键盘鼠标了，呵呵。注意，这个函数还有个替代函数：</p>
<p>VOID keybd_event(<br>　　　　BYTE bVk,　　　　　　　// 虚拟键码<br>　　　　BYTE bScan,　　　　　　// 扫描码<br>　　　　DWORD dwFlags,<br>　　　　ULONG_PTR dwExtraInfo　// 附加键状态<br>　　);<br>　　与<br>　　VOID mouse_event(<br>　　　　DWORD dwFlags,　　　　　 // motion and click options<br>　　　　DWORD dx,　　　　 　　　 // horizontal position or change<br>　　　　DWORD dy,　　　　　　　　// vertical position or change<br>　　　　DWORD dwData,　　　　　　// wheel movement<br>　　　　ULONG_PTR dwExtraInfo　　// application-defined information<br>　　);<br>&nbsp;&nbsp;&nbsp;
这两个函数非常简单了，我想那些按键精灵就是用的这个吧。上面的是模拟键盘，下面的是模拟鼠标的。这个仅仅是模拟部分，要和游戏联系起来我们还需要找到游
戏的窗口才行，或者包含快捷键，就象按键精灵的那个激活键一样，我们可以用GetWindow函数来枚举窗口，也可以用Findwindow函数来查找制
定的窗口（注意，还有一个FindWindowEx），FindwindowEx可以找到窗口的子窗口，比如按钮，等什么东西。当游戏切换场景的时候我们
可以用FindWindowEx来确定一些当前窗口的特征，从而判断是否还在这个场景，方法很多了，比如可以GetWindowInfo来确定一些东西，
比如当查找不到某个按钮的时候就说明游戏场景已经切换了，等等办法。有的游戏没有控件在里面，这是对图像做坐标变换的话，这种方法就要受到限制了。这就需
要我们用别的办法来辅助分析了。</p>
<p>&nbsp;&nbsp;&nbsp;
至于快捷键我们要用动态连接库实现了，里面要用到hook技术了，这个也非常简单。大家可能都会了，其实就是一个全局的hook对象然后
SetWindowHook就可以了，回调函数都是现成的，而且现在网上的例子多如牛毛。这个实现在外挂中已经很普遍了。如果还有谁不明白，那就去看看
MSDN查找SetWindowHook就可以了。</p>
<p>&nbsp;&nbsp;&nbsp; 不要低估了这个动态连接库的作用，它可以切入所有的进程空间，也就是可以加载到所有的游戏里面哦，只要用对，你会发现很有用途的。这个需要你复习一下Win32编程的基础知识了。呵呵，赶快去看书吧。</p>
<p>&nbsp;&nbsp;&nbsp; 2.2 截获消息</p>
<p>&nbsp;&nbsp;&nbsp; 有些游戏的响应机制比较简单，是基于消息的，或者用什么定时器的东西。这个时候你就可以用拦截消息来实现一些有趣的功能了。</p>
<p>&nbsp;&nbsp;&nbsp;
我们拦截消息使用的也是hook技术，里面包括了键盘消息，鼠标消息，系统消息，日志等，别的对我们没有什么大的用处，我们只用拦截消息的回调函数就可以
了，这个不会让我写例子吧。其实这个和上面的一样，都是用SetWindowHook来写的，看看就明白了很简单的。</p>
<p>&nbsp;&nbsp;&nbsp;
至于拦截了以后做什么就是你的事情了，比如在每个定时器消息里面处理一些我们的数据判断，或者在定时器里面在模拟一次定时器，那么有些数据就会处理两次，
呵呵。后果嘛，不一定是好事情哦，呵呵，不过如果数据计算放在客户端的游戏就可以真的改变数据了，呵呵，试试看吧。用途还有很多，自己想也可以想出来的，
呵呵。</p>
<p>&nbsp;&nbsp;&nbsp; 2.3 拦截Socket包</p>
<p>&nbsp;&nbsp;&nbsp; 这个技术难度要比原来的高很多。</p>
<p>&nbsp;&nbsp;&nbsp;
首先我们要替换WinSock.DLL或者WinSock32.DLL，我们写的替换函数要和原来的函数一致才行，就是说它的函数输出什么样的，我们也要
输出什么样子的函数，而且参数，参数顺序都要一样才行，然后在我们的函数里面调用真正的WinSock32.DLL里面的函数就可以了。</p>
<p>&nbsp;&nbsp;&nbsp; 首先：我们可以替换动态库到系统路径。</p>
<p>&nbsp;&nbsp;&nbsp; 其次：我们应用程序启动的时候可以加载原有的动态库，用这个函数LoadLibary然后定位函数入口用GetProcAddress函数获得每个真正Socket函数的入口地址。</p>
<p>&nbsp;&nbsp;&nbsp;
当游戏进行的时候它会调用我们的动态库，然后从我们的动态库中处理完毕后才跳转到真正动态库的函数地址，这样我们就可以在里面处理自己的数据了，应该是一
切数据。呵呵，兴奋吧，拦截了数据包我们还要分析之后才能进行正确的应答，不要以为这样工作就完成了，还早呢。等分析完毕以后我们还要仿真应答机制来和服
务器通信，一个不小心就会被封号。</p>
<p>&nbsp;&nbsp;&nbsp; 分析数据才是工作量的来源呢，游戏每次升级有可能加密方式会有所改变，因此我们写外挂的人都是亡命之徒啊，被人愚弄了还不知道。</p>
<p>&nbsp;&nbsp;&nbsp; 2.4 截获API</p>
<p>&nbsp;&nbsp;&nbsp; 上面的技术如果可以灵活运用的话我们就不用截获API函数了，其实这种技术是一种补充技术。比如我们需要截获Socket以外的函数作为我们的用途，我们就要用这个技术了，其实我们也可以用它直接拦截在Socket中的函数，这样更直接。</p>
<p>&nbsp;&nbsp;&nbsp;
现在拦截API的教程到处都是，我就不列举了，我用的比较习惯的方法是根据输入节进行拦截的，这个方法可以用到任何一种操作系统上，比如Windows
98/2000等，有些方法不是跨平台的，我不建议使用。这个技术大家可以参考《Windows核心编程》里面的545页开始的内容来学习，如果是
Win98系统可以用&#8220;Windows系统奥秘&#8221;那个最后一章来学习。</p>
<p>&nbsp;&nbsp;&nbsp; 网络游戏通讯模型初探</p>
<p>&nbsp;&nbsp;&nbsp; [文章导读]本文就将围绕三个主题来给大家讲述一下网络游戏的网络互连实现方法</p>
<p>&nbsp;&nbsp;&nbsp; 序言</p>
<p>&nbsp;&nbsp;&nbsp; 网络游戏，作为游戏与网络有机结合的产物，把玩家带入了新的娱乐领域。网络游戏在中国开始发展至今也仅有3，4年的历史，跟已经拥有几十年开发历史的单机游戏相比，网络游戏还是非常年轻的。当然，它的形成也是根据历史变化而产生的可以说没有<span class="channel_keylink">互联网</span>的
兴起，也就没有网络游戏的诞生。作为新兴产物，网络游戏的开发对广大开发者来说更加神秘，对于一个未知领域，开发者可能更需要了解的是网络游戏与普通单机
游戏有何区别，网络游戏如何将玩家们连接起来，以及如何为玩家提供一个互动的娱乐环境。本文就将围绕这三个主题来给大家讲述一下网络游戏的网络互连实现方
法。</p>
<p>&nbsp;&nbsp;&nbsp; 网络游戏与单机游戏</p>
<p>&nbsp;&nbsp;&nbsp;
说到网络游戏，不得不让人联想到单机游戏，实际上网络游戏的实质脱离不了单机游戏的制作思想，网络游戏和单机游戏的差别大家可以很直接的想到：不就是可以
多人连线吗？没错，但如何实现这些功能，如何把网络连线合理的融合进单机游戏，就是我们下面要讨论的内容。在了解网络互连具体实现之前，我们先来了解一下
单机与网络它们各自的运行流程，只有了解这些，你才能深入网络<span class="channel_keylink">游戏开发</span>的核心。</p>
<p>&nbsp;&nbsp;&nbsp; 现在先让我们来看一下普通单机游戏的简化执行流程：</p>
<p>Initialize() // 初始化模块<br>{<br>　初始化游戏数据;<br>}<br>Game() // 游戏循环部分<br>{<br>　绘制游戏场景、人物以及其它元素;<br>　获取用户操作输入;<br>　switch( 用户输入数据)<br>　{<br>　　case 移动:<br>　　{<br>　　　处理人物移动;<br>　　}<br>　　break;<br>　　case 攻击:<br>　　{<br>　　　处理攻击逻辑:<br>　　}<br>　　break;<br>　　...<br>　　其它处理响应;<br>　　...<br>　　default:<br>　　　break;<br>　}<br>　游戏的NPC等逻辑AI处理;<br>}<br>Exit() // 游戏结束<br>{<br>　释放游戏数据;<br>　离开游戏;<br>}<br><br>&nbsp;&nbsp;&nbsp;
我们来说明一下上面单机游戏的流程。首先，不管是游戏软件还是其他应用软件，初始化部分必不可少，这里需要对游戏的数据进行初始化，包括图像、声音以及一
些必备的数据。接下来，我们的游戏对场景、人物以及其他元素进行循环绘制，把游戏世界展现给玩家，同时接收玩家的输入操作，并根据操作来做出响应，此外，
游戏还需要对NPC以及一些逻辑AI进行处理。最后，游戏数据被释放，游戏结束。</p>
<p>&nbsp;&nbsp;&nbsp; 网络游戏与单机游戏有一个很显著的差别，就是网络游戏除了一个供操作游戏的用户界面平台（如单机游戏）外，还需要一个用于连接所有用户，并为所有用户提供数据服务的服务器，从某些角度来看，游戏服务器就像一个大型的数据库，</p>
<p>&nbsp;&nbsp;&nbsp; 提供数据以及数据逻辑交互的功能。让我们来看看一个简单的网络游戏模型执行流程：</p>
<p>&nbsp;&nbsp;&nbsp; 客户机：</p>
<p>Login()// 登入模块<br>{<br>　初始化游戏数据;<br>　获取用户输入的用户和密码;<br>　与服务器创建网络连接;<br>　发送至服务器进行用户验证;<br>　...<br>　等待服务器确认消息;<br>　...<br>　获得服务器反馈的登入消息;<br>　if( 成立 )<br>　　进入游戏;<br>　else<br>　　提示用户登入错误并重新接受用户登入;<br>}<br>Game()// 游戏循环部分<br>{<br>　绘制游戏场景、人物以及其它元素;<br>　获取用户操作输入;<br>　将用户的操作发送至服务器;<br>　...<br>　等待服务器的消息;<br>　...<br>　接收服务器的反馈信息;<br>　switch( 服务器反馈的消息数据 )<br>　{<br>　　case 本地玩家移动的消息:<br>　　{<br>　　　if( 允许本地玩家移动 )<br>　　　　客户机处理人物移动;<br>　　　else<br>　　　　客户机保持原有状态;<br>　　}<br>　　　break;<br>　　case 其他玩家/NPC的移动消息:<br>　　{<br>　　　根据服务器的反馈信息进行其他玩家或者NPC的移动处理;<br>　　}<br>　　break;<br>　　case 新玩家加入游戏:<br>　　{<br>　　　在客户机中添加显示此玩家;<br>　　}<br>　　　break;<br>　　case 玩家离开游戏:<br>　　{<br>　　　在客户机中销毁此玩家数据;<br>　　}<br>　　　break;<br>　　...<br>　　其它消息类型处理;<br>　　...　<br>　　default:<br>　　　break;<br>　}<br>}<br>Exit()// 游戏结束<br>{<br>　发送离开消息给服务器;<br>　...<br>　等待服务器确认;<br>　...<br>　得到服务器确认消息;<br>　与服务器断开连接;<br>　释放游戏数据;<br>　离开游戏;<br>}</p>
<p><br>　　服务器：</p>
<p>Listen()　　// 游戏服务器等待玩家连接模块<br>{<br>　...<br>　等待用户的登入信息;<br>　...<br>　接收到用户登入信息;<br>　分析用户名和密码是否符合;<br>　if( 符合 )<br>　{<br>　　发送确认允许进入游戏消息给客户机;　<br>　　把此玩家进入游戏的消息发布给场景中所有玩家;<br>　　把此玩家添加到服务器场景中;<br>　}<br>　else<br>　{<br>　　断开与客户机的连接;<br>　}<br>}<br>Game()　// 游戏服务器循环部分<br>{<br>　...<br>　等待场景中玩家的操作输入;<br>　...<br>　接收到某玩家的移动输入或NPC的移动逻辑输入;<br>　// 此处只以移动为例<br>　进行此玩家/NPC在地图场景是否可移动的逻辑判断;</p>
<p>　if( 可移动 )<br>　{<br>　　对此玩家/NPC进行服务器移动处理;<br>　　发送移动消息给客户机;<br>　　发送此玩家的移动消息给场景上所有玩家;<br>　}<br>　else<br>　　发送不可移动消息给客户机;<br>}<br>Exit()　　// 游戏服务＝器结束<br>{<br>　接收到玩家离开消息;<br>　将此消息发送给场景中所有玩家;<br>　发送允许离开的信息;<br>　将玩家数据存入数据库;<br>　注销此玩家在服务器内存中的数据;<br>}<br>}</p>
<p><br></p>
<p>&nbsp;&nbsp;&nbsp;
让我们来说明一下上面简单网络游戏模型的运行机制。先来讲讲服务器端，这里服务器端分为三个部分（实际上一个完整的网络游戏远不止这些）：登入模块、游戏
模块和登出模块。登入模块用于监听网络游戏客户端发送过来的网络连接消息，并且验证其合法性，然后在服务器中创建这个玩家并且把玩家带领到游戏模块中；
游戏模块则提供给玩家用户实际的应用服务，我们在后面会详细介绍这个部分；
在得到玩家要离开游戏的消息后，登出模块则会把玩家从服务器中删除，并且把玩家的属性数据保存到服务器数据库中，如： 经验值、等级、生命值等。</p>
<p>&nbsp;&nbsp;&nbsp;
接下来让我们看看网络游戏的客户端。这时候，客户端不再像单机游戏一样，初始化数据后直接进入游戏，而是在与服务器创建连接，并且获得许可的前提下才进入
游戏。除此之外，网络游戏的客户端游戏进程需要不断与服务器进行通讯，通过与服务器交换数据来确定当前游戏的状态，例如其他玩家的位置变化、物品掉落情
况。同样，在离开游戏时，客户端会向服务器告知此玩家用户离开，以便于服务器做出相应处理。</p>
<p>&nbsp;&nbsp;&nbsp;
以上用简单的伪代码给大家阐述了单机游戏与网络游戏的执行流程，大家应该可以清楚看出两者的差别，以及两者间相互的关系。我们可以换个角度考虑，网络游戏
就是把单机游戏的逻辑运算部分搬移到游戏服务器中进行处理，然后把处理结果（包括其他玩家数据）通过游戏服务器返回给连接的玩家。</p>
<p>&nbsp;&nbsp;&nbsp; 网络互连</p>
<p>&nbsp;&nbsp;&nbsp; 在了解了网络游戏基本形态之后，让我们进入真正的实际应用部分。首先，作为网络游戏，除了常规的单机游戏所必需的东西之外，我们还需要增加一个网络通讯模块，当然，这也是网络游戏较为主要的部分，我们来讨论一下如何实现网络的通讯模块。</p>
<p>&nbsp;&nbsp;&nbsp;
一个完善的网络通讯模块涉及面相当广，本文仅对较为基本的处理方式进行讨论。网络游戏是由客户端和服务器组成，相应也需要两种不同的网络通讯处理方式，不
过也有相同之处，我们先就它们的共同点来进行介绍。我们这里以Microsoft Windows 2000 [2000
Server]作为开发平台，并且使用Winsock作为网络接口（可能一些朋友会考虑使用DirectPlay来进行网络通讯，不过对于当前在线游
戏，DirectPlay并不适合，具体原因这里就不做讨论了）。</p>
<p>&nbsp;&nbsp;&nbsp; 确定好平台与接口后，我们开始进行网络连接创建之前的一些必要的初始化工作，这部分无论是客户端或者服务器都需要进行。让我们看看下面的代码片段：</p>
<p>WORD wVersionRequested;<br>WSADATAwsaData;<br>wVersionRequested MAKEWORD(1, 1);<br>if( WSAStartup( wVersionRequested, &amp;wsaData ) !0 )<br>{<br>　Failed( WinSock Version Error!" );<br>}<br>　　上面通过调用Windows的socket API函数来初始化<span class="channel_keylink">网络设备</span>，接下来进行网络Socket的创建，代码片段如下：</p>
<p>SOCKET sSocket socket( AF_INET, m_lProtocol, 0 );<br>if( sSocket == INVALID_SOCKET )<br>{<br>　Failed( "WinSocket Create Error!" );<br>}</p>
<p>&nbsp;&nbsp;&nbsp;
这里需要说明，客户端和服务端所需要的Socket连接数量是不同的，客户端只需要一个Socket连接足以满足游戏的需要，而服务端必须为每个玩家用户
创建一个用于通讯的Socket连接。当然，并不是说如果服务器上没有玩家那就不需要创建Socket连接，服务器端在启动之时会生成一个特殊的
Socket用来对玩家创建与服务器连接的请求进行响应，等介绍网络监听部分后会有更详细说明。</p>
<p>&nbsp;&nbsp;&nbsp; 有初始化与创建必然就有释放与删除，让我们看看下面的释放部分：</p>
<p>if( sSocket != INVALID_SOCKET )<br>{<br>　closesocket( sSocket );<br>}<br>if( WSACleanup() != 0 )<br>{<br>　Warning( "Can't release Winsocket" );<br>}</p>
<p>&nbsp;&nbsp;&nbsp; 这里两个步骤分别对前面所作的创建初始化进行了相应释放。</p>
<p>&nbsp;&nbsp;&nbsp; 接下来看看服务器端的一个网络执行处理，这里我们假设服务器端已经创建好一个Socket供使用，我们要做的就是让这个Socket变成监听网络连接请求的专用接口，看看下面代码片段：</p>
<p>SOCKADDR_IN addr;<br>memset( &amp;addr, 0, sizeof(addr) );<br>addr.sin_family = AF_INET;<br>addr.sin_addr.s_addr = htonl( INADDR_ANY );<br>addr.sin_port = htons( Port ); 　// Port为要监听的端口号<br>// 绑定socket<br>if( bind( sSocket, (SOCKADDR*)&amp;addr, sizeof(addr) ) == SOCKET_ERROR )<br>{<br>　Failed( "WinSocket Bind Error!");<br>}<br>// 进行监听<br>if( listen( sSocket, SOMAXCONN ) == SOCKET_ERROR )<br>{<br>　Failed( "WinSocket Listen Error!");<br>}<br><br>&nbsp;&nbsp;&nbsp; 这里使用的是阻塞式通讯处理，此时程序将处于等待玩家用户连接的状态，倘若这时候有客户端连接进来，则通过accept（）来创建针对此玩家用户的Socket连接，代码片段如下：<br><br>sockaddraddrServer;<br>int nLen sizeof( addrServer );<br>SOCKET sPlayerSocket accept( sSocket, &amp;addrServer, &amp;nLen );<br>if( sPlayerSocket == INVALID_SOCKET )<br>{<br>　Failed( WinSocket Accept Error!");<br>}<br><br>&nbsp;&nbsp;&nbsp; 这里我们创建了sPlayerSocket连接，此后游戏服务器与这个玩家用户的通讯全部通过此Socket进行，到这里为止，我们服务器已经有了接受玩家用户连接的功能，现在让我们来看看游戏客户端是如何连接到游戏服务器上，代码片段如下：<br><br>SOCKADDR_IN addr;<br>memset( &amp;addr, 0, sizeof(addr) );<br>addr.sin_family = AF_INET;// 要连接的游戏服务器端口号<br>addr.sin_addr.s_addr = inet_addr( IP );// 要连接的游戏服务器IP地址，<br>addr.sin_port = htons( Port );//到此，客户端和服务器已经有了通讯的桥梁，<br>//接下来就是进行数据的发送和接收：<br>connect( sSocket, (SOCKADDR*)&amp;addr, sizeof(addr) );<br>if( send( sSocket, pBuffer, lLength, 0 ) == SOCKET_ERROR )<br>{<br>　Failed( "WinSocket Send Error!");<br>}<br><br><br>&nbsp;&nbsp;&nbsp; 这里的pBuffer为要发送的数据缓冲指针，lLength为需要发送的数据长度，通过这支Socket API函数，我们无论在客户端或者服务端都可以进行数据的发送工作，同时，我们可以通过recv（）这支Socket API函数来进行数据接收：<br><br>lLength, 0 ) == SOCKET_ERROR )<br>{<br>　Failed( "WinSocket Recv Error!");<br>}<br><br>&nbsp;&nbsp;&nbsp; 其中pBuffer用来<span class="channel_keylink">存储</span>获取的网络数据缓冲，lLength则为需要获取的数据长度。</p>
<p><br></p>
<p>&nbsp;&nbsp;&nbsp; 现在，我们已经了解了一些网络互连的基本知识，但作为网络游戏，如此简单的连接方式是无法满足网络游戏中百人千人同时在线的，我们需要更合理容错性更强的网络通讯处理方式，当然，我们需要先了解一下网络游戏对网络通讯的需求是怎样的。</p>
<p>&nbsp;&nbsp;&nbsp;
大家知道，游戏需要不断循环处理游戏中的逻辑并进行游戏世界的绘制，上面所介绍的Winsock处理方式均是以阻塞方式进行，这样就违背了游戏的执行本
质，可以想象，在客户端连接到服务器的过程中，你的游戏不能得到控制，这时如果玩家想取消连接或者做其他处理，甚至显示一个最基本的动态连接提示都不行。</p>
<p>&nbsp;&nbsp;&nbsp; 所以我们需要用其他方式来处理网络通讯，使其不会与游戏主线相冲突，可能大家都会想到：
创建一个网络线程来处理不就可以了？没错，我们可以创建一个专门用于网络通讯的子线程来解决这个问题。当然，我们游戏中多了一个线程，我们就需要做更多的
考虑，让我们来看看如何创建网络通讯线程。</p>
<p>&nbsp;&nbsp;&nbsp; 在Windows系统中，我们可以通过CreateThread（）函数来进行线程的创建，看看下面的代码片段：<br><br>DWORD dwThreadID;<br>HANDLE hThread = CreateThread( NULL, 0, NetThread/*网络线程函式*/, sSocket, 0, &amp;dwThreadID );<br>if( hThread == NULL )<br>{<br>　Failed( "WinSocket Thread Create Error!");<br>}<br>　　这里我们创建了一个线程，同时将我们的Socket传入线程函数：</p>
<p>DWORD WINAPINetThread(LPVOID lParam)</p>
<p>{<br>　SOCKET sSocket (SOCKET)lParam;<br>　...<br>　return 0;<br>}</p>
<p>&nbsp;&nbsp;&nbsp; NetThread就是我们将来用于处理网络通讯的网络线程。那么，我们又如何把Socket的处理引入线程中？</p>
<p>&nbsp;&nbsp;&nbsp; 看看下面的代码片段：<br><br>HANDLE hEvent;<br>hEvent = CreateEvent(NULL,0,0,0);<br>// 设置异步通讯<br>if( WSAEventSelect( sSocket, hEvent,<br>FD_ACCEPT|FD_CONNECT|FD_READ|FD_WRITE|FD_CLOSE ) ==SOCKET_ERROR )<br>{<br>　Failed( "WinSocket EventSelect Error!");<br>}<br><br>&nbsp;&nbsp;&nbsp; 通过上面的设置之后，WinSock API函数均会以非阻塞方式运行，也就是函数执行后会立即返回，这时网络通讯会以事件方式存储于hEvent，而不会停顿整支程式。</p>
<p>&nbsp;&nbsp;&nbsp; 完成了上面的步骤之后，我们需要对事件进行响应与处理，让我们看看如何在网络线程中获得网络通讯所产生的事件消息：<br><br>WSAEnumNetworkEvents( sSocket, hEvent, &amp;SocketEvents );<br>if( SocketEvents.lNetworkEvents != 0 )<br>{<br>　switch( SocketEvents.lNetworkEvents )<br>　{<br>　　case FD_ACCEPT:<br>　　　WSANETWORKEVENTS SocketEvents;<br>　　　break;<br>　　case FD_CONNECT:<br>　　{<br>　　　if( SocketEvents.iErrorCode[FD_CONNECT_BIT] == 0)<br>　　　// 连接成功 　<br>　　　{<br>　　　// 连接成功后通知主线程（游戏线程）进行处理<br>　　　}<br>　　}<br>　　　break;<br>　　case FD_READ:<br>　　// 获取网络数据<br>　　{<br>　　　if( recv( sSocket, pBuffer, lLength, 0) == SOCKET_ERROR )<br>　　　{<br>　　　　Failed( "WinSocket Recv Error!");<br>　　　}<br>　　}<br>　　　break;<br>　　case FD_WRITE:<br>　　　break;<br>　　case FD_CLOSE:<br>　　　// 通知主线程（游戏线程）， 网络已经断开<br>　　　break;<br>　　default:<br>}<br>}</p>
<p><br></p>
<p>&nbsp;&nbsp;&nbsp; 这里仅对网络连接（FD_CONNECT） 和读取数据（FD_READ）
进行了简单模拟操作，但实际中网络线程接收到事件消息后，会对数据进行组织整理，然后再将数据回传给我们的游戏主线程使用，游戏主线程再将处理过的数据发
送出去，这样一个往返就构成了我们网络游戏中的数据通讯，是让网络游戏动起来的最基本要素。</p>
<p>&nbsp;&nbsp;&nbsp;
最后，我们来谈谈关于网络数据包（数据封包）的组织，网络游戏的数据包是游戏数据通讯的最基本单位，网络游戏一般不会用字节流的方式来进行数据传输，一个
数据封包也可以看作是一条消息指令，在游戏进行中，服务器和客户端会不停的发送和接收这些消息包，然后将消息包解析转换为真正所要表达的指令意义并执行。</p>
<p>&nbsp;&nbsp;&nbsp; 互动与管理</p>
<p>&nbsp;&nbsp;&nbsp;
说到互动，对于玩家来说是与其他玩家的交流，但对于计算机而言，实现互动也就是实现数据消息的相互传递。前面我们已经了解过网络通讯的基本概念，它构成了
互动的最基本条件，接下来我们需要在这个网络层面上进行数据的通讯。遗憾的是，计算机并不懂得如何表达玩家之间的交流，因此我们需要提供一套可让计算机了
解的指令组织和解析机制，也就是对我们上面简单提到的网络数据包（数据封包）的处理机制。</p>
<p>&nbsp;&nbsp;&nbsp; 为了能够更简单的给大家阐述网络数据包的组织形式，我们以一个聊天处理模块来进行讨论，看看下面的代码结构：<br><br>struct tagMessage{<br>　long lType;<br>　long lPlayerID;<br>};<br>// 消息指令<br>// 指令相关的玩家标识<br>char strTalk[256]; // 消息内容<br><br>&nbsp;&nbsp;&nbsp;
上面是抽象出来的一个极为简单的消息包结构，我们先来谈谈其各个数据域的用途：首先，lType
是消息指令的类型，这是最为基本的消息标识，这个标识用来告诉服务器或客户端这条指令的具体用途，以便于服务器或客户端做出相应处理。lPlayerID
被作为玩家的标识。大家知道，一个玩家在机器内部实际上也就是一堆数据，特别是在游戏服务器中，可能有成千上万个玩家，这时候我们需要一个标记来区分玩
家，这样就可以迅速找到特定玩家，并将通讯数据应用于其上。</p>
<p>&nbsp;&nbsp;&nbsp; strTalk 是我们要传递的聊天数据，这部分才是真正的数据实体，前面的参数只是数据实体应用范围的限定。</p>
<p>&nbsp;&nbsp;&nbsp; 在组织完数据之后，紧接着就是把这个结构体数据通过Socket
连接发送出去和接收进来。这里我们要了解，网络在进行数据传输过程中，它并不关心数据采用的数据结构，这就需要我们把数据结构转换为二进制数据码进行发
送，在接收方，我们再将这些二进制数据码转换回程序使用的相应数据结构。让我们来看看如何实现：<br><br>tagMessageMsg;<br>Msg.lTypeMSG_CHAT;<br>Msg.lPlayerID 1000;<br>strcpy( &amp;Msg.strTalk, "聊天信息" );<br><br>&nbsp;&nbsp;&nbsp; 首先，我们假设已经组织好一个数据包，这里MSG_CHAT 是我们自行定义的标识符，当然，这个标识符在服务器和客户端要统一。玩家的ID 则根据游戏需要来进行设置，这里1000 只作为假设，现在继续：<br><br>char* p = (char*)&amp;Msg;<br>long lLength = sizeof( tagMessage );<br>send( sSocket, p, lLength );<br>// 获取数据结构的长度<br><br>&nbsp;&nbsp;&nbsp; 我们通过强行转换把结构体转变为char 类型的数据指针，这样就可以通过这个指针来进行流式数据处理，这里通过</p>
<p>&nbsp;&nbsp;&nbsp; sizeof（） 获得结构体长度，然后用WinSock 的Send（） 函数将数据发送出去。</p>
<p>&nbsp;&nbsp;&nbsp; 接下来看看如何接收数据：<br><br>long lLength = sizeof( tagMessage );<br>char* Buffer = new char[lLength];<br>recv( sSocket, Buffer, lLength );<br>tagMessage* p = (tagMessage*)Buffer;<br>// 获取数据<br><br>&nbsp;&nbsp;&nbsp;
在通过WinSock 的recv（）
函数获取网络数据之后，我们同样通过强行转换把获取出来的缓冲数据转换为相应结构体，这样就可以方便地对数据进行访问。（注：强行转换仅仅作为数据转换的
一种手段，实际应用中有更多可选方式，这里只为简洁地说明逻辑）谈到此处，不得不提到服务器/
客户端如何去筛选处理各种消息以及如何对通讯数据包进行管理。无论是服务器还是客户端，在收到网络消息的时候，通过上面的数据解析之后，还必须对消息类型
进行一次筛选和派分，简单来说就是类似Windows 的消息循环，不同消息进行不同处理。这可以通过一个switch 语句（熟悉Windows
消息循环的朋友相信已经明白此意），基于消</p>
<p>&nbsp;&nbsp;&nbsp; 息封包里的lType 信息，对消息进行区分处理，考虑如下代码片段：<br><br>switch( p-&gt;lType ) // 这里的p-&gt;lType为我们解析出来的消息类型标识<br>{<br>　case MSG_CHAT: // 聊天消息<br>　　break;<br>　case MSG_MOVE: // 玩家移动消息<br>　　break;<br>　case MSG_EXIT: // 玩家离开消息<br>　　break;<br>　default:<br>　　break;<br>}<br><br>&nbsp;&nbsp;&nbsp;
面片段中的MSG_MOVE 和MSG_EXIT
都是我们虚拟的消息标识（一个真实游戏中的标识可能会有上百个，这就需要考虑优化和优先消息处理问题）。此外，一个网络游戏服务器面对的是成百上千的连接
用户，我们还需要一些合理的数据组织管理方式来进行相关处理。普通的单体游戏服务器，可能会因为当机或者用户过多而导致整个游戏网络瘫痪，而这也就引入分
组服务器机制，我们把服务器分开进行数据的分布式处理。</p>
<p>&nbsp;&nbsp;&nbsp;
我们把每个模块提取出来，做成专用的服务器系统，然后建立一个连接所有服务器的数据中心来进行数据交互，这里每个模块均与数据中心创建了连接，保证了每个
模块的相关性，同时玩家转变为与当前提供服务的服务器进行连接通讯，这样就可以缓解单独一台服务器所承受的负担，把压力分散到多台服务器上，同时保证了数
据的统一，而且就算某台服务因为异常而当机也不会影响其他模块的游戏玩家，从而提高了整体稳定性。分组式服务器缓解了服务器的压力，但也带来了服务器调度
问题，分组式服务器需要对服务器跳转进行处理，就以一个玩家进行游戏场景跳转作为讨论基础：假设有一玩家处于游戏场景A，他想从场景A
跳转到场景B，在游戏中，我们称之场景切换，这时玩家就会触发跳转需求，比如走到了场景中的切换点，这样服务器就把玩家数据从"游戏场景A
服务器"删除，同时在"游戏场景B 服务器"中把玩家建立起来。</p>
<p>&nbsp;&nbsp;&nbsp; 这里描述了场景切换的简单模型，当中处理还有很多步骤，不过通过这样的思考相信大家可以派生出很多应用技巧。</p>
<p>&nbsp;&nbsp;&nbsp; 不过需要注意的是，在场景切换或者说模块间切换的时候，需要切实考虑好数据的传输<span class="channel_keylink">安全</span>以及逻辑合理性，否则切换很可能会成为将来玩家复制物品的桥梁。</p>
<p>&nbsp;&nbsp;&nbsp; 总结</p>
<p>&nbsp;&nbsp;&nbsp; 本篇讲述的都是通过一些简单的过程来进行网络游戏通讯，提供了一个制作的思路，虽然具体实现起来还有许多要做
，但只要顺着这个思路去扩展、去完善，相信大家很快就能够编写出自己的网络通讯模块。由于时间仓促，本文在很多细节方面都有省略，文中若有错误之处也望大
家见谅</p>
<br><br><img src ="http://www.cppblog.com/keigoliye/aggbug/97065.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-23 23:39 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/23/97065.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>转：网络游戏中的数据包设计与定义（初论）</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/23/97064.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 23 Sep 2009 15:36:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/23/97064.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/97064.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/23/97064.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/97064.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/97064.html</trackback:ping><description><![CDATA[原文：http://hi.baidu.com/huangyunjun999/blog/item/7396b8c2378e4a3ce4dd3bda.html<br><br>接触了一段时间的网游封包设计，有了一些初步的思路，想借这篇文章总结一下，同时也作个记录，以利于以后更新自己的思路。<br>
网络游戏的技术研发，分为三个主要的方面：服务器设计，客户端设计，数据库设计。而在服务器和客户端之间实现游戏逻辑的中介则是游戏数据包，服务器和
客户端通过交换游戏数据包并根据分析得到的数据包来驱动游戏逻辑。网络游戏的实质是互动，而互动的控制则由服务器和客户端协同完成，协同就必然要依靠数据
来完成。<br>
当前网络游戏中的封包，其定义形式是各种各样的，但归纳起来，一般都具有如下要素：封包长度，封包类型，封包参数，校验码等。<br>
封包长度用于确定当前游戏数据包的长度，之所以提供这个数据，是因为在底层的TCP网络传输中，出于传输效率的考虑，传输有时会把若干个小的数据包合
并成一个大的数据包发送出去，而在合并的过程中，并不是把每一个逻辑上完整的数据包全部合并到一起，有时可能因为这种合并而将一个在逻辑上具有完整意义的
游戏数据包分在了两次发送过程中进行发送，这样，当前一次的数据发送到接受方之后，其尾部的数据包必然造成了&#8220;断尾&#8221;现象，为了判定这种断尾的情况以及断
尾的具体内容，游戏数据包在设计时一般都会提供封包长度这个信息，根据这个信息接受方就知道收到的包是否有断尾，如果有断尾，则把断尾的数据包与下次发过
来的数据包进行拼接生成原本在逻辑意义上完整的数据包。<br>
封包类型用于标识当前封包是何种类型的封包，表示的是什么含义。<br>
封包参数则是对封包类型更具体的描述，它里面指定了这种类型封包说明里所必须的参数。比如说话封包，它的封包类型，可以用一个数值进行表示，而具体的说话内容和发言的人则作为封包参数。<br>
校验码的作用是对前述封包内容进行校验，以确保封包在传递过程中内容不致被改变，同时根据校验码也可以确定这个封包在格式上是不是一个合法的封包。以
校验码作为提高封包安全性的方法，已经是目前网游普遍采用的方式。封包设计，一般是先确定封包的总体结构，然后再来具体细分有哪些封包，每个封包中应该含
有哪些内容，最后再详细写出封包中各部分内容具体占有的字节数及含义。<br>
数据包的具体设计，一般来说是根据游戏功能进行划分和圈定的。比如游戏中有聊天功能，那么就得设计客户端与服务器的聊天数据包，客户端要有一个描述发
言内容与发言人信息的数据包，而在服务器端要有一个包含用户发言内容及发言人信息的广播数据包，通过它，服务器端可以向其他附近玩家广播发送当前玩家的发
言内容。再比如游戏中要有交易功能，那么与这个功能相对应的就可能会有以下数据包：申请交易包，申请交易的信息包，允许或拒绝交易包，允许或拒绝交易的信
息包，提交交易物品包，提交交易物品的信息包，确认交易包，取消交易包，取消交易的信息包，交易成功或失败的信息包。需要注意的是，在这些封包中，有的是
一方使用而另一方不使用的，而有的则是双方都使用的包。比如申请交易包，只可能是一方使用，而另一方会得到一个申请交易的信息包；而确认交易包和提交交易
物品这样的数据包，都是双方在确定要进行交易时要同时使用的。封包的设计也遵从由上到下的设计原则，即先确定有哪些功能的封包，再确定封包中应该含有的信
息，最后确定这些信息应该占有的位置及长度。一层层的分析与定义，最终形成一个完善的封包定义方案。在实际的封包设计过程中，回溯的情况是经常出现的。由
于初期设计时的考虑不周或其它原因，可能造成封包设计方案的修改或增删，这时候一个重要的问题是要记得及时更新你的设计文档。在我的封包设计中，我采用的
是以下的封包描述表格进行描述：<br>
封包编号　　　功能描述　　对应的类或结构体名　　类型命令字　　命令参数结构体及含义　<br>
根据游戏的功能，我们可以大致圈定封包的大致结构及所含的大致内容。但是，封包设计还包含有其它更多的内容，如何在保证封包逻辑简洁的前提下缩短封包
的设计长度提高封包的传输速度和游戏的运行速度，这也是我们应该考虑的一个重要问题。一般情况下，设计封包时，应该尽量避免产生一百字节以上的封包，多数
封包的定义控制在100字节以内，甚至20－50字节以内。在我所定义的封包中，多数在20字节以内，对于诸如传递服务器列表和用户列表这样的封包可能会
大一点。总之一句话，应该用尽可能短的内容尽可能简洁清晰地描述封包功能和含义。<br>
在封包结构设计方面，我有另一种可扩展的思路：对封包中各元素的位置进行动态定义。这样，当换成其它游戏或想更换当前游戏的封包结构时，只要改变这些
元素的动态定义即可，而不需要完全重新设计封包结构。比如我们对封包编号，封包类型，封包参数，校验码这些信息的开始位置和长度进行定义，这样就可以形成
一个动态定义的封包结构，对以后的封包移植将会有很大帮助，一个可能动态改变封包结构的游戏数据包，在相当程度上增加了外挂分析封包结构的难度。<br>
在进行封包设计时，最好根据封包客户端和服务器端的不同来分类进行设计。比如大厅与游戏服务器的封包及游戏服务器与游戏客户端的封包分开来进行设计，在包的编号上表示出他们的不同（以不同的开头单词表示），这样在封包的总体结构上就会更清晰。<br><br><img src ="http://www.cppblog.com/keigoliye/aggbug/97064.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-23 23:36 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/23/97064.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>深入剖析MMORPG游戏服务器端的设计</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/14/96119.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Mon, 14 Sep 2009 05:29:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/14/96119.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/96119.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/14/96119.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/96119.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/96119.html</trackback:ping><description><![CDATA[　　MMORPG不同于其它的局域网的网络游戏，它是一个面向整个Internet的连接人数过万的网络游戏，因此他的服务器端设计则极为重要<br>　　<br>　　<strong>服务器的基本设置</strong><br>　　<br>　　在大型网络游戏里，通常设计为C/S结构，客户端不再对数据进行逻辑处理，而只是一个收发装置，从玩家那里接受到操作信息，然后反馈给服务器，再由服务器进行处理后发回客户端，经客户端通过图形化处理，给玩家呈现出一个缤纷的游戏世界。<br>　　　<img  src="http://game.chinaitlab.com/imgfiles/2005.7.8.11.29.5.mm01.gif"><br>　　登陆服务器<br>　　<br>　　在这里也可以称之为连接服务器，网络游戏的客户端一般是连接到这里，然后再由该连接服务器根据不同的需要，把游戏消息转发给其它相应的服务器（逻辑和地图服务器）也因为它是客户端直接连接的对象，它同时也负担了验证客户身份的工作。<br>　　<br>　　地图服务器<br>　　<br>　　在这里也可以称之为连续事件服务器。在这个服务器里要处理的对象（玩家）所做的动作都是一个连续事件。例如玩家从A点移动到B点，这样一个动作，需要一定的时间进行移动，因此说移动是一个连续事件。<br>　　<br>　　逻辑服务器<br>　　<br>
在这里可以称之为瞬时事件服务器，在这个服务器里，处理对象（玩家）所做的动作均可以在非常断时间内完成完成。例如玩家从商店购买一瓶药书，当玩家确认
购买后，服务器先扣除玩家的游戏币，然后再把相应的药水瓶加入玩家的背包里。这2个操作对于服务器来说，只是2个数字的加减，计算完这两个数字的加减，这
个事件就可以结束了。因此，我们可以说这个事件是一个瞬时事件<br>　　<br>　　<strong>服务器组的改进</strong><br>　　<br>　　不过在实际应用的过程中，游戏服务器的结构要比上面所说的3种服务结构要复杂些，不过也都是在这3种最基本的服务器架构下进行扩充，扩充的主要是其它辅助功能。在实际应用里可能增加的2种服务器，数据库服务器，计费服务器，由逻辑服务器独立出来的聊天服务器。<br>　　　<img  src="http://game.chinaitlab.com/imgfiles/2005.7.8.11.29.27.mm02.gif"><br>　　数据库服务器<br>　　<br>　　数据库服务器其实就是专门利用一台服务器进行数据库的读写操作。这点特别是在大型的网络游戏里尤为重要。因为在大型网络游戏里，要处理玩家的数据量非常大，如果不利用专门的服务器进行处理，很有可能会拖累这个服务器组。<br>　　<br>　　计费服务器<br>　　<br>　　通常在商业的网络游戏里出现，用于记录玩家在线的时间，给收费提供依据，同时也是整个服务器组里最重要的部分，一旦出现问题，运营商就不用赚钱了。<br>　　<br>　　聊天服务器<br>　　<br>　　在游戏里的聊天功能是属于一种瞬时动作，理论上是放在逻辑服务器里进行处理。不过在大型网络游戏里，因为这个部分功能与游戏里的其它部分联系并不紧密，因此可以独立出来做一个功能服务器。<br>　　<br>　　<strong>服务器的集群设置</strong><br>　　<br>　　在大型游戏的应用过程中，实际需要处理的玩家数量可能过万，一台普通的服务器是无法完成所要完成的工作，因此，在实际应用的时候，通常是由一组多台服务器共同完成一个功能。<br>　　例如地图服务器，可以根据需要，把游戏里所有的地域进行划分，划分为N个区域，然后让这一个区域里发生的事件都用一个特定的服务器进行处理。这样做的目的是减少一个服务器所承担的计算量，把整个系统组成一个分布式的网络。<br>
不过这样做的同时会造成一个麻烦：当一位玩家从区域1，移动到区域2。这个时候，就必须先在服务器1里把玩家删除，然后再在区域2里加入玩家。同时需要
由服务器1向服务器2转移玩家的数据信息（因为服务器组在工作的时候，玩家的信息只能保存在当前所在区域的服务器里），也就是说一旦玩家发生服务器间区域
移动，服务器端就不可避免的造成数据通讯。因为这种移动并不是有规律的，玩家所在的服务器都有可能到达其它服务器。这样，如果服务器组里有N台地图服务
器，那么，每个服务器都可能向其它N－1台服务器产生连接，总共就可能产生N&#215;N个连接。如此数量连接如果只是使用普通的socket设计，就很有可能会
给服务器通讯间的各种问题所困扰，为此，在商业网络游戏的服务器之间，通常都使用成熟的第三方的通讯中间件，如ACE，ICE等作为网络连接的传输层。<br>　　<img  src="http://game.chinaitlab.com/imgfiles/2005.7.8.11.29.43.mm03.gif"><img src ="http://www.cppblog.com/keigoliye/aggbug/96119.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-14 13:29 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/14/96119.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>多进程的游戏服务器设计</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/14/96120.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Mon, 14 Sep 2009 05:29:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/14/96120.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/96120.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/14/96120.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/96120.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/96120.html</trackback:ping><description><![CDATA[<h3 class="entry-header">转自云风BLOG http://blog.codingnow.com/2006/10/multi_process_design.html</h3>
<br>
<div class="entry-content">
<div class="entry-body">
<p>目前，我们的游戏服务器组是按多进程的方式设计的。强调多进程，是想提另外一点，我们每个进程上是单线程的。所以，我们在设计中，系统的复杂点在于进程间如何交换数据；而不需要考虑线程间的数据锁问题。</p>
<p>如果肆意的做进程间通讯，在进程数量不断增加后，会使系统混乱不可控。经过分析后，我决定做如下的限制：</p>
<ol>
    <li>
    <p>如果一个进程需要和多个服务器做双向通讯，那么这个进程不能处理复杂的逻辑，而只是过滤和转发数据用。即，这样的一个进程 S
    ，只会把进程 A 发过来的数据转发到 B ；或把进程 B 发过来的数据转发到 A
    。或者从一端发过来的数据，经过简单的协议分析后，可以分发到不同的地方。例如，把客户端发过来的数据包中的聊天信息分离处理，交到聊天进程处理。</p>
    </li>
    <li>
    <p>有逻辑处理的进程上的数据流一定是单向的，它可以从多个数据源读取数据，但是处理后一定反馈到另外的地方，而不需要和数据源做逻辑上的交互。</p>
    </li>
    <li>
    <p>每个进程尽可能的保持单个输入点，或是单个输出点。</p>
    </li>
    <li>
    <p>所有费时的操作均发到独立的进程，以队列方式处理。</p>
    </li>
    <li>
    <p>按功能和场景划分进程，单一服务和单一场景中不再分离出多个进程做负载均衡。</p>
    </li>
</ol>
<p>性能问题上，我是这样考虑的：</p>
<p>我们应该充分利用多核的优势，这会是日后的发展方向。让每个进程要么处理大流量小计算量的工作；要么处理小流量大计算量的工作。这样多个进程放在一台物理机器上可以更加充分的利用机器的资源。</p>
<p>单线程多进程的设计，个人认为更能发挥多核的优势。这是因为没有了锁，每个线程都可以以最大吞吐量工作。增加的负担只是进程间的数据复制，在网游这种复杂逻辑的系统中，一般不会比逻辑计算更早成为瓶颈。如果担心，单线程没有利用多核计算的优势，不妨考虑以下的例子：</p>
<p>计算 a/b+c/d+e/f ，如果我们在一个进程中开三条线程利用三个核同时计算 a/b c/d e/f
固然不错，但它增加了程序设计的复杂度。而换个思路，做成三个进程，第一个只算 a/b 把结果交给第二个进程去算 c/d
于之的和，再交个第三个进程算 e/f
。对于单次运算来算，虽然成本增加了。它需要做额外的进程间通讯复制中间结果。但，如果我们有大量连续的这样的计算要做，整体的吞吐量却增加了。因为在算
某次的 a/b 的时候，前一次的 c/d 可能在另一个核中并行计算着。</p>
<p>具体的设计中，我们只需要把处理数据包的任务切细，适当增加处理流水线的长度，就可以提高整个系统的吞吐量了。由于逻辑操作是单线程的，所以另需要注意的一点是，所有费时的操作都应该转发到独立的进程中异步完成。比如下面会提到的数据存取服务。</p>
<p>对于具体的场景管理是这样做的：</p>
</div>
<div id="more" class="entry-more">
<p>玩
家连接进来后，所有数据包会经过一个叫做位置服务的进程中。这个进程可以区分玩家所在的位置，然后把玩家数据分发到对应的场景服务进程中。这个位置服务同
时还管理玩家间消息的广播。即，单个的场景（逻辑）服务并不关心每个数据包为哪几个玩家所见，而由这个服务将其复制分发。</p>
<p>当玩家切换场景，场景服务器将玩家的数据发送给数据服务，数据服务进程 cache 玩家数据，并将数据写入数据库。然后把玩家的新的场景编号发回位置服务进程，这样位置服务器可以将后续的玩家数据包正确的转发到新的场景服务进程中。</p>
<p>掉落物品和资源生产同样可以统一管理，所以的场景（逻辑）进程都将生产新物件的请求发给物品分配服务，由物品分配服务生产出新物件后通知位置服务器产生新物品。</p>
<p>这样一系列的做法，最终保证了，每个场景服务器都有一个唯一的数据源——位置服务进程。它跟持久化在数据库中的数据无关，跟时钟也无关。由此带来的调试便利是很显著的。</p>
<p>最近，面临诸多进程的设计时，最先面临的一个复杂点在于启动阶段。显然，每个进程都配有一套配置文件指出其它进程的地址并不是一个好主意。而为每个
服务都分配一个子域名在开发期也不太合适。结果我们采取了一个简单的方案：单独开发了一个名字服务器。它的功能类似 DNS
，但是可以让每个进程自由的注册自己的位置，还可以定期汇报自己的当前状态。这样，我们可以方便的用程序查询到需要的服务。名字服务器的协议用的类似
POP3 的文本协议，这让我们可以人手工 telnet 上去查阅。我相信以后我们的维护人员会喜欢这样的设计的。:D</p>
<p>以上，国庆假期结束以来的工作。感谢项目组其他同事的辛勤编码。</p>
</div>
</div>
<br><br><img src ="http://www.cppblog.com/keigoliye/aggbug/96120.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-14 13:29 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/14/96120.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>总体设计和登陆服务器 [游戏服务器的设计思路 转]</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/14/96118.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Mon, 14 Sep 2009 05:26:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/14/96118.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/96118.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/14/96118.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/96118.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/96118.html</trackback:ping><description><![CDATA[作者博客：<br>http://blog.csdn.net/yahle<br>大纲：<br>项目的历史背景<br><a  href="http://www.server-development.cn/go.php/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/" target="_blank" title="http://www.server-development.cn/go.php/tags/服务器/" class="mykeyword">服务器</a>的设计思路<br>服务器的技术<br>服务器的设计<br>服务器的改进<br>图形引擎myhoho及UI库的设计<br><br>客户端与服务器的集成<br><a name="entrymore"></a><br><br>网
络游戏一般采用C\S模式，网络游戏的设计重点，我认为在于Server端,也就是我们说的服务器。在服务器端的设计，我把服务器按照功能分为2个部分，
一个负责游戏世界的处理，一个服务器服务器与客户端的通讯。在负责游戏世界的处理的服务器，我又按照功能分为地图服务器和逻辑服务器。这样划分的依据是他
们处理的内容不同进行。当初的设计还考虑到系统的集群功能，可以把游戏的地图移动处理和游戏的逻辑处理都分别分摊到其它服务器里面去。但是做到最后，发现
这样的设计也不是太好，主要是因为在处理一些游戏事件的时候需要两个服务器之间进行协同，这样势必要创建一定的网络游戏消息，在开始制作游戏的时候，因为
需要系统的东西不是很多，所以没有太注意，到项目的后期，想增加一个功能的时候，就发现在处理船只沉没的时候，服务器需要传递很多同步数据，而且服务器各
自在设置玩家数据的时候，也有很多重复的地方。如果今后还要再加点什么其它功能，那要同步的地方就实在是太多了，所以按照功能把服务器分为2个部分的设计
还是存在缺陷的，如果让我重新再来，我会选择单服务器的设计，当然这个服务器还是要和连接服务器进行分离，因为游戏的逻辑处理和与玩家的通讯还是很好分开
的，而且分开的话，也有利于逻辑服务器的设计。<br><br><br><br><br> <br><br><br>登陆（连接）服务器的设计：<br><br><br><br>
&nbsp;
&nbsp;在网络游戏里，其中一个很大的难点就是玩家与服务器的通讯，在Windos的服务器架构下，网络游戏服务器端采用的I/O模型，通常是完成端口。在项目
开始时研究完成端口，感觉很难，根本看不懂，因为它在很多地方与以前写网络通讯软件时用的方法不同。但是当我分析过3个完成端口的程序后，基本了解的它的
使用方法。而且在懂以后，回过头来看，其它完成端口的概念也不是很复杂，只要能清楚的了解几个函数的使用方法以及基本的处理框架流程，你就会发现它其实非
常的简单。<br><br><br><br> &nbsp; &nbsp;完成端口的一些需要理解的地方：<br><br><br><br>1。消息队列<br><br><br><br>2。工作线程<br><br><br><br>3。网络消息返回结构体<br><br><br><br><br> <br><br><br>
&nbsp;
&nbsp;一般我们在设计服务器端的时候，最关键的地方是如何分辩刚刚收到的网络数据是由那个玩家发送过来的，如果是采用消息事件驱动的话，是可以得到一个
socket的值，然后再用这个值与系统里存在的socket进行比对，这样就可以得到是那位玩家发送过来的游戏消息。我在还没有使用完成端口的时候，就
是使用这个方法。这样的设计有一个缺点就是每次收到数据的时候回浪费很多时间在于确定消息发送者身份上。但是在完成端口的设计里，我们可以采用一个取巧的
方法进行设计。所以，这个问题很轻易的就结局了，而且系统开销也不是很大，关于完成端口，可以参考一下的文章：<br><br><br><br>《关于Winsock异步I/O模型中的事件模型》<br><br><br><br>http://search.csdn.net/Expert/topic/166/166227.xml?temp=.4639093<br><br><br><br>《手把手教你玩转SOCKET模型之重叠I/O篇》<br><br><br><br>http://blog.csdn.net/piggyxp/archive/2004/09/23/114883.aspx<br><br><br><br>《学习日记]<a  href="http://www.server-development.cn/go.php/tags/IOCP/" target="_blank" title="http://www.server-development.cn/go.php/tags/IOCP/" class="mykeyword">IOCP</a>的学习－－初步理解》<br><br><br><br>http://www.gameres.com/bbs/showthread.asp?threadid=25898<br><br><br><br>《用完成端口开发大响应规模的Winsock应用程序》<br><br><br><br>http://www.xiaozhou.net/ReadNews.asp?NewsID=901<br><br><br><br>《理解I/O Completion Port》<br><br><br><br>http://dev.gameres.com/Program/Control/IOCP.htm<br><br><br><br>几个关键函数的说明：<br><br><br><br>http://msdn.microsoft.com/library/en-us/fileio/fs/postqueuedcompletionstatus.asp?frame=true<br><br><br><br>http://msdn.microsoft.com/library/en-us/fileio/fs/createiocompletionport.asp?frame=true<br><br><br><br>http://msdn.microsoft.com/library/en-us/fileio/fs/getqueuedcompletionstatus.asp?frame=true<br><br><br><br>http://msdn.microsoft.com/library/en-us/winsock/winsock/wsarecv_2.asp?frame=true<br><br><br><br><br> <br><br><br>如果你能认真的搞清楚上面的东西，我估计你离理解完成端口就只有一步了。剩下的这一步就是自己编码实现一个下了。有些时候，看得懂了不一定会实际应用，不实实在在的写一点程序，验证一下你的想法，是不会真正搞清楚原理的。<br><br><br><br><br> <br><br><br>不
过除非你想深入的研究网络技术，否则只要知道怎么用就可以了，剩下的就是寻找一个合适的别人封装好的类来使用。这样可以节省你很多的事件，当然拿来的东西
最好有源代码，这样如果发生什么问题，你也好确定是在那个地方出错，要改或者扩充功能都会方便很多。当然，还要注意人家的版权，最好在引用别人代码的地方
加一些小小的注解，这样用不了多少时间，而且对你，对原作者都有好处^_^。<br><br><br><br><br> <br><br><br>不过在
完成端口上我还是没有成为拿来主义者，还是自己封装了完成端口的操作，原因找到的源代码代码封装的接口函数我怎么看怎么觉得别扭，所以最后还是自己封装了
一个完成端口，有兴趣的可以去看我的源代码，里面有很详细的注解。而且就我看来，要拿我封装的完成端口类使用起来还是很简单的。使用的时候，只要继承我的
CIOCP，然后，根据需要覆盖3个虚函数（OnAccept，OnRead，OnClose）就可以了，最多是在连接函数里，需要用一个函数去设置一下
完成端口信息。当然，我封装的类稍微简单了一些，如果要拿来响应大规模连接，还是存在很多的问题，但是如果只是针对少量连接，还是可以应付的。<br><br><br><br><br> <br><br><br>对
于客户端的I/O模型，我就没有那么用心的去寻找什么好的解决方案，采用了一个最简单的，最原始的阻塞线程的方法做。原理很简单：创建一个sockt，把
socket设置为阻塞，连接服务器成功后，启动一个线程，在线程里面用recv()等待服务器发过来的消息。在我的代码里，也是把阻塞线程的方法封装成
一个类，在使用的时候，先继承TClientSocket，然后覆盖（重载）里面的OnRead()函数，并在里面写入一些处理收到数据后的操作代码。在
用的时候，只要connect成功，系统就会自动启动一个接收线程，一旦有数据就触发刚才覆盖的OnRead函数。这个类我也不是完全直接写的，在里面使
用了别人的一些代码，主要是让每个类都能把线程封装起来，这样在创建不同的类的实体的时候，每个类的实体自己都会有一个单独的数据接收线程。<br><br><br><br>当
然除了阻塞线程的方法，比较常用的还有就是用消息事件的方法收取数据了。我刚开始的时候，也是采用这个方法（以前用过^_^），但是后来发现不太好封装，
最后采用阻塞线程的方法，这样做还有一个好处可以让我的代码看起来更加舒服一些。不过就我分析《航海世纪》客户端采用的是消息事件的I/O模型。其它的网
络游戏就不太清楚了，我想也应该是采用消息事件方式的吧。。<br><br><br><br> &nbsp; &nbsp;我记得在gameres上看到过某人写的一篇关于完成端口的笔记，他在篇末结束的时候，提出一个思考题：我们在学习完成端口的时候，都知道它是用于server端的操作，而且很多文章也是这样写的，但是不知道有没有考虑过，用完成端口做客户端来使用？<br><br><br><br> &nbsp; &nbsp;其实这个问题很好回答，答案是OK。拿IOCP做客户端也是可行的，就以封装的IOCP为例，只要在继承原来的CIOCP类的基础上，再写一个Connect（char * ip, int port）的函数，就可以实现客户端的要求了。<br>
<div class="dp-highlighter">
<div class="bar">
<div class="tools"><a  href="http://doserver.net/#" onclick="dp.sh.Toolbar.Command('ViewSource',this);return false;">view plain</a><a  href="http://doserver.net/#" onclick="dp.sh.Toolbar.Command('PrintSource',this);return false;">print</a><a  href="http://doserver.net/#" onclick="dp.sh.Toolbar.Command('About',this);return false;">?</a></div>
</div>
<ol class="dp-cpp" start="1">
    <li class="alt"><span><span class="datatypes">bool</span><span>&nbsp;CIOCPClient::Connect(</span><span class="datatypes">char</span><span>&nbsp;*ip,&nbsp;</span><span class="datatypes">int</span><span>&nbsp;port)&nbsp;&nbsp;</span></span></li>
    <li class=""><span>{&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//&nbsp;&nbsp;连接服务器</span><span>&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span><span>&nbsp;(!bInit)&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span><span>&nbsp;(!Init())&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span><span>&nbsp;</span><span class="keyword">false</span><span>;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//&nbsp;&nbsp;初始化连接socket</span><span>&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;SOCKET&nbsp;m_socket&nbsp;=&nbsp;socket(AF_INET,&nbsp;SOCK_STREAM,&nbsp;IPPROTO_TCP);&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span><span>&nbsp;(m_socket&nbsp;==&nbsp;SOCKET_ERROR)&nbsp;&nbsp;</span></span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span><span>&nbsp;</span><span class="keyword">false</span><span>;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//&nbsp;填写服务器地址信息</span><span>&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;sockaddr_in&nbsp;ClientAddr;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;ClientAddr.sin_family&nbsp;=&nbsp;AF_INET;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;ClientAddr.sin_port&nbsp;=&nbsp;htons(port);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;ClientAddr.sin_addr.s_addr&nbsp;=&nbsp;inet_addr(ip);&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//&nbsp;绑定监听端口</span><span>&nbsp;&nbsp;</span></span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;bind(m_socket,&nbsp;(SOCKADDR&nbsp;*)&amp;ClientAddr,&nbsp;<span class="keyword">sizeof</span><span>(ClientAddr));&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span><span>&nbsp;(connect(m_socket,&nbsp;(SOCKADDR&nbsp;*)&amp;ClientAddr,&nbsp;</span><span class="keyword">sizeof</span><span>(ClientAddr))&nbsp;==&nbsp;SOCKET_ERROR)&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span><span>&nbsp;</span><span class="keyword">false</span><span>;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">this</span><span>-&gt;m_workThread&nbsp;=&nbsp;</span><span class="keyword">true</span><span>;&nbsp;&nbsp;</span></span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;g_hwThread&nbsp;=&nbsp;CreateThread(NULL,&nbsp;0,&nbsp;WorkThread,&nbsp;(<span class="datatypes">LPVOID</span><span>)</span><span class="keyword">this</span><span>,&nbsp;0,&nbsp;&amp;m_wthreadID);&nbsp;&nbsp;</span><span class="comment">//&nbsp;&nbsp;创建工作线程，用来处理完成端口消息的</span><span>&nbsp;&nbsp;</span></span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">this</span><span>-&gt;SetIoCompletionPort(m_socket,&nbsp;NULL);&nbsp;&nbsp;</span><span class="comment">//&nbsp;&nbsp;设置完成端口监听的socket</span><span>&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span><span>&nbsp;</span><span class="keyword">true</span><span>;&nbsp;&nbsp;</span></span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>}&nbsp;&nbsp;</span></li>
</ol>
</div>
<textarea style="display: none;" name="code" class="cpp" rows="15" cols="100">bool CIOCPClient::Connect(char *ip, int port)
{
&nbsp; &nbsp; &nbsp; &nbsp;// &nbsp;连接服务器
&nbsp; &nbsp;if (!bInit)
&nbsp; &nbsp; &nbsp; &nbsp;if (!Init())
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return false;
&nbsp; &nbsp;// &nbsp;初始化连接socket
&nbsp; &nbsp;SOCKET m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
&nbsp; &nbsp;if (m_socket == SOCKET_ERROR)
&nbsp; &nbsp; &nbsp; &nbsp;return false;
&nbsp; &nbsp;// 填写服务器地址信息
&nbsp; &nbsp;sockaddr_in ClientAddr;
&nbsp; &nbsp;ClientAddr.sin_family = AF_INET;
&nbsp; &nbsp;ClientAddr.sin_port = htons(port); &nbsp; &nbsp;
&nbsp; &nbsp;ClientAddr.sin_addr.s_addr = inet_addr(ip);
&nbsp; &nbsp;// 绑定监听端口
&nbsp; &nbsp;bind(m_socket, (SOCKADDR *)&amp;ClientAddr, sizeof(ClientAddr));
&nbsp; &nbsp;if (connect(m_socket, (SOCKADDR *)&amp;ClientAddr, sizeof(ClientAddr)) == SOCKET_ERROR)
&nbsp; &nbsp; &nbsp; &nbsp;return false;
&nbsp; &nbsp;this-&gt;m_workThread = true;
&nbsp; &nbsp;g_hwThread = CreateThread(NULL, 0, WorkThread, (LPVOID)this, 0, &amp;m_wthreadID); &nbsp;// &nbsp;创建工作线程，用来处理完成端口消息的
&nbsp; &nbsp;this-&gt;SetIoCompletionPort(m_socket, NULL); &nbsp;// &nbsp;设置完成端口监听的socket
&nbsp; &nbsp;return true;
}
</textarea><br>前面一段是用来连接服务器，所有的客户端程序都是要这样做的，当连接成功后，m_socket就是我们想要的用于与服务器
端通讯的socket，然后，我们启动工作线程，并使用SetIoCompletionPort来设置完成端口监听的socket。只要在原来的基础上增
加一个函数，就可以把用于服务器的ICOP变成用于客户端的IOCP。<br><br><br><br> &nbsp;
&nbsp;在收到网络数据以后，下一步就是根据需要，把收到的网络数据包转变为游戏消息数据包。在转换之前，首先是要从收到的网络数据里面提取出有效的消息。这里
为什么说是要提取有效部分？其主要原因是，我们创建的游戏消息数据，在进行网络传输的时候，不是以消息的长度来传的，而是根据系统在接收到发送数据请求的
时候，根据实际情况来发送的。例如我这里有一条很长的游戏消息，有3k，但是系统一次只能发送1k的数据，所以，我们的游戏消息，只能把我们的游戏消息分
为3个包，分3次发送，这样在我们接收消息的时候，就会触发3次OnRead，而这3次OnRead收到的数据都不是一次完整的游戏消息。所以，我们在收
到网络数据后，要先和上一次收到的网络数据进行合并，然后再在里面提取出有效的游戏消息，并在提取后，把已经提取的部分删除。我在这里把这一步操作封装到
一个类里CBuftoMsg。这里顺便说明一下：一条游戏消息的网络数据包是以0x00EEEE(16进制)为结束标记（《航海世纪》的做法）。<br>
<div class="dp-highlighter">
<div class="bar">
<div class="tools"><a  href="http://doserver.net/#" onclick="dp.sh.Toolbar.Command('ViewSource',this);return false;">view plain</a><a  href="http://doserver.net/#" onclick="dp.sh.Toolbar.Command('PrintSource',this);return false;">print</a><a  href="http://doserver.net/#" onclick="dp.sh.Toolbar.Command('About',this);return false;">?</a></div>
</div>
<ol class="dp-cpp" start="1">
    <li class="alt"><span><span class="keyword">struct</span><span>&nbsp;TMessage&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>{&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="datatypes">char</span><span>&nbsp;*&nbsp;p;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span class="comment">//&nbsp;&nbsp;消息头所在的位置</span><span>&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="datatypes">long</span><span>&nbsp;len;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span class="comment">//&nbsp;&nbsp;整个消息的长度</span><span>&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>};&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span><span class="keyword">class</span><span>&nbsp;CBuftoMsg&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>{&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span><span class="keyword">protected</span><span>:&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="datatypes">char</span><span>&nbsp;msgbuf[BUF_LEN];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="datatypes">char</span><span>&nbsp;*&nbsp;buf_end;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="datatypes">char</span><span>&nbsp;*&nbsp;buf_begin;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="datatypes">int</span><span>&nbsp;buf_len;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span><span class="keyword">public</span><span>:&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;CBuftoMsg(<span class="keyword">void</span><span>);&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;TMessage&nbsp;getMessage(<span class="keyword">void</span><span>);&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">void</span><span>&nbsp;cleanup_buf(</span><span class="keyword">void</span><span>);&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="datatypes">bool</span><span>&nbsp;AddMsgBuf(</span><span class="keyword">const</span><span>&nbsp;</span><span class="datatypes">char</span><span>&nbsp;*,&nbsp;</span><span class="datatypes">int</span><span>);&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="datatypes">int</span><span>&nbsp;tag;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>};&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>CBuftoMsg::CBuftoMsg(<span class="keyword">void</span><span>)&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>{&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;buf_begin&nbsp;=&nbsp;msgbuf;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;buf_end&nbsp;=&nbsp;msgbuf;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;buf_len&nbsp;=&nbsp;0;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>}&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>TMessage&nbsp;CBuftoMsg::getMessage()&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>{&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="datatypes">char</span><span>&nbsp;*&nbsp;p&nbsp;&nbsp;&nbsp;&nbsp;=&nbsp;buf_begin;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;TMessage&nbsp;result;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;result.len&nbsp;&nbsp;=&nbsp;0;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;result.p&nbsp;&nbsp;&nbsp;&nbsp;=&nbsp;NULL;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">while</span><span>(p&nbsp;&lt;=&nbsp;buf_begin&nbsp;+&nbsp;buf_len&nbsp;-&nbsp;2)&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span><span>&nbsp;(&nbsp;*p&nbsp;==&nbsp;0x00)&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">const</span><span>&nbsp;</span><span class="keyword">static</span><span>&nbsp;</span><span class="datatypes">char</span><span>&nbsp;ce&nbsp;=&nbsp;0xEE;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span><span>&nbsp;(*(p&nbsp;+&nbsp;1)&nbsp;==&nbsp;ce)&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span><span>(*(p&nbsp;+&nbsp;2)&nbsp;==&nbsp;ce)&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//&nbsp;&nbsp;每条消息都是以&nbsp;00&nbsp;EE&nbsp;EE&nbsp;为结束标志</span><span>&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.p&nbsp;&nbsp;&nbsp;&nbsp;=&nbsp;buf_begin;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.len&nbsp;&nbsp;=&nbsp;p&nbsp;-&nbsp;buf_begin&nbsp;+&nbsp;3;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;buf_begin&nbsp;&nbsp;&nbsp;=&nbsp;&nbsp;p&nbsp;+&nbsp;3;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;buf_end&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;=&nbsp;buf_begin&nbsp;+&nbsp;buf_len;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;buf_len&nbsp;-=&nbsp;result.len;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">break</span><span>;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;p++;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span><span>&nbsp;result;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>}&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span><span class="keyword">void</span><span>&nbsp;CBuftoMsg::cleanup_buf()&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>{&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span><span>&nbsp;(buf_len&nbsp;&lt;&nbsp;BUF_LEN)&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span><span>&nbsp;(buf_len&nbsp;==&nbsp;0)&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;buf_begin&nbsp;&nbsp;&nbsp;=&nbsp;msgbuf;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;buf_end&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;=&nbsp;msgbuf;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span><span>&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;memmove(msgbuf,&nbsp;buf_end&nbsp;-&nbsp;buf_len,&nbsp;buf_len);&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;buf_begin&nbsp;=&nbsp;msgbuf;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;buf_end&nbsp;=&nbsp;buf_end&nbsp;-&nbsp;buf_len;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span><span>&nbsp;&nbsp;</span></span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//&nbsp;&nbsp;加入缓冲区的数据过多，要抛弃原来的内容</span><span>&nbsp;&nbsp;</span></span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;buf_begin&nbsp;&nbsp;&nbsp;=&nbsp;msgbuf;&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;buf_end&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;=&nbsp;msgbuf;&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;buf_len&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;=&nbsp;0;&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>}&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span><span class="datatypes">bool</span><span>&nbsp;CBuftoMsg::AddMsgBuf(</span><span class="keyword">const</span><span>&nbsp;</span><span class="datatypes">char</span><span>&nbsp;*&nbsp;buf,&nbsp;</span><span class="datatypes">int</span><span>&nbsp;len)&nbsp;&nbsp;</span></span></li>
    <li class="alt"><span>{&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span><span>&nbsp;(len&nbsp;&lt;&nbsp;1)&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span><span>&nbsp;</span><span class="keyword">false</span><span>;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="datatypes">bool</span><span>&nbsp;result&nbsp;=&nbsp;</span><span class="keyword">true</span><span>;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;buf_len&nbsp;+=&nbsp;len;&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span><span>&nbsp;(buf_len&nbsp;&gt;=&nbsp;BUF_LEN)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span class="comment">//&nbsp;&nbsp;如果缓冲区装满了则直接把原来的缓冲区清空再重新复制数据</span><span>&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">this</span><span>-&gt;cleanup_buf();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result&nbsp;=&nbsp;<span class="keyword">false</span><span>;&nbsp;&nbsp;</span></span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;memcpy(buf_begin,&nbsp;buf,&nbsp;len);&nbsp;&nbsp;</span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span><span>&nbsp;result;&nbsp;&nbsp;</span></span></li>
    <li class="alt"><span>&nbsp;&nbsp;</span></li>
    <li class=""><span>}&nbsp;&nbsp;</span></li>
</ol>
</div>
<textarea style="display: none;" name="code" class="cpp" rows="15" cols="100">struct TMessage
{
&nbsp; &nbsp;char * p; &nbsp; &nbsp; &nbsp; // &nbsp;消息头所在的位置
&nbsp; &nbsp;long len; &nbsp; &nbsp; &nbsp; // &nbsp;整个消息的长度
};
class CBuftoMsg
{
protected:
&nbsp; &nbsp;char msgbuf[BUF_LEN]; &nbsp;
&nbsp; &nbsp;char * buf_end;
&nbsp; &nbsp;char * buf_begin;
&nbsp; &nbsp;int buf_len;
public:
&nbsp; &nbsp;CBuftoMsg(void);
&nbsp; &nbsp;TMessage getMessage(void);
&nbsp; &nbsp;void cleanup_buf(void);
&nbsp; &nbsp;bool AddMsgBuf(const char *, int);
&nbsp; &nbsp;int tag;
};
CBuftoMsg::CBuftoMsg(void)
{
&nbsp; &nbsp;buf_begin = msgbuf;
&nbsp; &nbsp;buf_end = msgbuf;
&nbsp; &nbsp;buf_len = 0;
}
TMessage CBuftoMsg::getMessage()
{
&nbsp; &nbsp;char * p &nbsp; &nbsp;= buf_begin;
&nbsp; &nbsp;TMessage result;
&nbsp; &nbsp;result.len &nbsp;= 0;
&nbsp; &nbsp;result.p &nbsp; &nbsp;= NULL;
&nbsp; &nbsp;while(p &lt;= buf_begin + buf_len - 2)
&nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;if ( *p == 0x00)
&nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;const static char ce = 0xEE;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (*(p + 1) == ce)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if(*(p + 2) == ce)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// &nbsp;每条消息都是以 00 EE EE 为结束标志
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;result.p &nbsp; &nbsp;= buf_begin;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;result.len &nbsp;= p - buf_begin + 3;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;buf_begin &nbsp; = &nbsp;p + 3;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;buf_end &nbsp; &nbsp; = buf_begin + buf_len;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;buf_len -= result.len;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp;p++;
&nbsp; &nbsp;}
&nbsp; &nbsp;return result;
}
void CBuftoMsg::cleanup_buf()
{
&nbsp; &nbsp;if (buf_len &lt; BUF_LEN)
&nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;if (buf_len == 0)
&nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;buf_begin &nbsp; = msgbuf;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;buf_end &nbsp; &nbsp; = msgbuf;
&nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp;else
&nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;memmove(msgbuf, buf_end - buf_len, buf_len);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;buf_begin = msgbuf;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;buf_end = buf_end - buf_len;
&nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp;}
&nbsp; &nbsp;else
&nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;// &nbsp;加入缓冲区的数据过多，要抛弃原来的内容
&nbsp; &nbsp; &nbsp; &nbsp;buf_begin &nbsp; = msgbuf;
&nbsp; &nbsp; &nbsp; &nbsp;buf_end &nbsp; &nbsp; = msgbuf;
&nbsp; &nbsp; &nbsp; &nbsp;buf_len &nbsp; &nbsp; = 0;
&nbsp; &nbsp;}
}
bool CBuftoMsg::AddMsgBuf(const char * buf, int len)
{
&nbsp; &nbsp;if (len &lt; 1)
&nbsp; &nbsp; &nbsp; &nbsp;return false;
&nbsp; &nbsp;bool result = true;
&nbsp; &nbsp;buf_len += len;
&nbsp; &nbsp;if (buf_len &gt;= BUF_LEN) &nbsp; &nbsp; // &nbsp;如果缓冲区装满了则直接把原来的缓冲区清空再重新复制数据
&nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;this-&gt;cleanup_buf(); &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp;result = false;
&nbsp; &nbsp;}
&nbsp; &nbsp;memcpy(buf_begin, buf, len);
&nbsp; &nbsp;return result;
}
</textarea><br><br>我在这里把 CBuftoMsg
的代码贴出来，主要是因为，我在写本文的时候，发现一个惊天动地的bug，有兴趣的读者可以自己去找一下。不过一开始写代码的时候，还不是这样的，当初的
代码bug比这个还要多，问题还要严重，严重到经常让服务器程序莫名其妙的崩溃，而且这个问题，一直到5月份，系统在进行集成测试的时候才发现并解决（还
没有彻底解决，至少目前我还发现了bug，），以前一直都没有怎么注意到这个问题，而且我们还把因为这个bug造成的问题，归结到线程的互斥上去^_^!<br><br><br><br><br> <br><br><br>我的登陆服务器，除了基本的处理网络数据包以外，还负责玩家系统的登陆验证，这部分东西不是很复杂，在我的程序里，只是简单的从ini文件里读取玩家的信息而已，有兴趣的自己去看我的代码（不过这部分远还没有真正的完善，存在很多问题）。<br><br><br><br><br> <br><br><br>除
了登陆验证以外，在登陆程序还负责进行消息转发，就是把客户端的消息分别发送到不同的服务器。如果当初设计的是一个逻辑服务器，这个功能就可以简单很多，
只要发送到一个服务器里就可以了。现在的要发到2个服务器，所以还需要对收到的游戏消息进行分类。为了方便，我对原来定义消息的ID进行了分类，所以，在
GameMessageID.h文件里定义的游戏消息对应的ID编号不是顺序编排的。不过也因为这样，在现在看来，这样的设计，有一些不太好。在整个系统
里，存在有4个主体，他们之间互相发送，就用了12组的数据，为了方便计算，我把一个变量的范围分为16个不同的区域，这样每个区域只有16个值可以用
（我这里是用char类型256/16=16）。在加上用另外一个变量表示逻辑上上的分类（目前按照功能分了12组，有登陆、贸易、银行、船厂等）这样对
于贸易这个类型的游戏消息，从客户端发送到逻辑服务器上，只能有16中可能性，如果要发送更多消息，可能要增加另外一个逻辑分类：贸易2^_^!当初这样
的设计只是想简化一下系统的处理过程，不过却造成了系统的扩充困难，要解决也不是没有办法，把类型分类的变量由char类型，改为int类型，这样对一个
变量分区，在范围上会款很多，而且不会造成逻辑分类上的困扰，但是，这样存在一个弊端就是就是每条网络消息数据包的长度增加了一点点。不要小看这一个字节
的变量，现在设计的一条游戏消息头的长度是10个字节，如果把char改为int，无形中就增加了3个字节，在和原来的比较，这样每条消息在消息头部分，
就多出23％，也就是我们100M的网络现在只能利用77％而已。<br><br><br><br> &nbsp; &nbsp;^_^呵呵看出什么问题没有？<br><br><br><br>
&nbsp;
&nbsp;没有，那我告诉你，有一个概念被偷换了，消息头的数据不等于整条游戏的消息数据，所以，消息头部分虽然多出了23％，但是整条游戏消息并不会增加这么
多，最多增加17％，最少应该不会操作5％。平均起来，应该在10％左右（游戏消息里，很多消息的实际部分可能就一个int变量而已）。不过，就算是
10％，也占用了带宽。<br><br><br><br> &nbsp; &nbsp;^_^呵呵还看出什么问题没有？<br><br><br><br> &nbsp; &nbsp;^_^先去读一下我的代码，再回头看看，上面的论述还有什么问题。<br><br><br><br> &nbsp; &nbsp;实际上，每条游戏消息由：消息头、消息实体、结束标记组成，其中固定的是消息头和结束标记，所以，实际上一条实际上游戏消息的数据包，最多比原来的多15％，平均起来，应该是8％～10％的增量而异。<br><br><br><br>
&nbsp;
&nbsp;好了，不在这个计算细节上扣太多精力了。要解决这个问题，要么是增加网络数据的发送量，要么，就是调整游戏结构，例如，把两个功能服务器合并为一个服务
器，这样服务器的对象实体就由原来的4个分为3个，两两间的通讯，就由原来的12路缩减为6路，只要分8个区域就ok了。这样每个逻辑分类就有32条游戏
消息可以使用。当然，如果进一步合并服务器，把服务器端都合并到一个程序，那就不用分类了^_^!<br><br> &nbsp;
&nbsp;在登陆服务器目录下，还有一组mynet.h/mynet.cpp的文件，是我当初为服务器端设计的函数，封装的是消息事件网络响应模型。只不过封装得
不是怎么好，被抛弃不用了，有兴趣的可以去看看，反正我是不推荐看的。只不过是在这里说明一下整个工程目录的结构而已。<img src ="http://www.cppblog.com/keigoliye/aggbug/96118.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-14 13:26 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/14/96118.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>网络游戏同步法则</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/12/95986.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Sat, 12 Sep 2009 02:39:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/12/95986.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/95986.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/12/95986.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/95986.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/95986.html</trackback:ping><description><![CDATA[<p>网路的硬件也有限，而人的创造也无限，在公网平均130ms的Latency下，是不存在&#8220;完全的&#8221;的同步情况。如何通过消除/隐藏延时，将用户带入快速的交互式实时游戏中，体验完美的互动娱乐呢？</p>
<p>以下六点，将助你分清楚哪些我们可以努力，哪些我们不值得努力，弄明白实时游戏中同步问题关键之所在，巧妙的化解与规避游戏，最终在适合普遍用户网络环境中(200ms)，实现实时快速互动游戏：</p>
<p>1. 基本情况：<br>
&nbsp;&nbsp;  (A) 网络性能指标一：带宽，限制了实时游戏的人数容量<br>
&nbsp;&nbsp;  (B) 网络性能指标二：延时，决定了实时游戏的最低反应时间</p>
<p>2. 两个基本原则：<br>
&nbsp;&nbsp;  (A) 让所有的用户屏幕上面表现出完全不同的表象是完全没有问题的。<br>
&nbsp;&nbsp;  (B) 把这些完全不同表象完全柔和在一个统一的逻辑中也是完全没有问题的。</p>
<p>3. 同步的十二条应对策略：<br>
&nbsp;&nbsp;  (A) 最大可能减少游戏中的数据传输<br>
&nbsp;&nbsp;  (B) 将阻塞通信放到线程池中实现<br>
&nbsp;&nbsp;  (C) 永远不要为了等待某个数据而不让游戏进行下去<br>
&nbsp;&nbsp;  (D) 利用预测和插值改进游戏的效果<br>
&nbsp;&nbsp;  (E) 当使用预测插值的时候传送的数据不仅包括坐标，还需要速度和加速度<br>
&nbsp;&nbsp;  (F) 将输入数据枷锁或者队列化(例如键盘消息队列)，直到下次发送数据的时刻，传统的方法是在固定的时间(发送数据前)检测键盘，在游戏的原理上隐藏延时<br>
&nbsp;&nbsp;  (G) 使用事件调度表，将需要在所有用户客户端同时发生的事件，提前广播到所有用户<br>
&nbsp;&nbsp;  (H) 使用多次攻击来杀死一个精灵，尽量减少一次性的、确定性的、延时敏感的事件<br>
&nbsp;&nbsp;  (I) 延长子弹或者火箭在空中飞行的时间(在其飞行的同时，在所有客户端进行预测插值)<br>
&nbsp;&nbsp;  (J) 所有物体从一个地方移动到另外一个地方都需要时间，避免诸如&#8220;瞬间移动&#8221;的设计<br>
&nbsp;&nbsp;  (K) 尽量使游戏中所有精灵，飞船或者其他物体，都按照可预测的轨迹运行，比如在移动中增加惯性<br>
&nbsp;&nbsp;  (L) 充分发挥创造力，尽最大可能的合并游戏中前后相关的事件，合并游戏中存在的延时此问题，需要在技术上改进的同时也需要策划有所重视，规避一些影响较大的设计，巧妙的隐藏"延时"</p>
<p>4. 同步问题现状：<br>
&nbsp;&nbsp;  (A) 重视程度不够：很多人尚未意识到此问题的存在，曾有公司花半年时间打算做一款&#8220;松鼠大战&#8221;的网络版。<br>
&nbsp;&nbsp;  (B) 技术上无彻底解决方案：对于多数程序员，单机游戏技术善未成熟就匆匆步入网络时代。<br>
&nbsp;&nbsp;  (C) 研究这个技术需要条件：需要有实力的公司才能提供，无此条件，即便有能力的程序员也无法成功。</p>
<p>5. 目前网游的三大技术难题：<br>
&nbsp;&nbsp;  (A) 服务器的响应问题：如何使服务器在支持越来越多的人数的情况下提供最高的响应。<br>
&nbsp;&nbsp;  (B) 同步问题：如何在有限的网络响应情况下，实现快速实时类游戏，提供最完美的交互。<br>
&nbsp;&nbsp;  (C) 服务器分布式问题：如何在统一用户数据的情况下，利用分部式将各个分散的&#8220;世界&#8221;统一到一个&#8220;世界&#8221;中。<br>
&nbsp;&nbsp;  谁能真正解决好以上三个问题，配合策划在设计上的突破，将使其他人在至少两年内无法超越。<br>
&nbsp;&nbsp;  <br>
6. 相关补充：<br>
&nbsp;&nbsp;  (A) 网格技术现在还是抄作，真正用到游戏中，还有很多技术难点需要突破(比如：目前网格的单位计算时间是以秒计算).<br>
&nbsp;&nbsp;  (B) 其实与很多人想法相反的是现在3D技术早已不是主要的矛盾。而现在国内外对于以上三个问题可以说处于同一个起跑线上，完全有机会取得先机。<br>
&nbsp;&nbsp;  (C) 现在解决同步问题已经很紧迫，而同时所需要的环境也已经成熟，只要有所关注，半年之内可以得出较成熟的结论</p>
<p><br>
那么具体怎么解决呢？再下一步怎么办？<br>
这就得自己去实践了，我只说这么多了，哈哈，不然又教懒了那些成天再网上搜方案的人。</p>
<p><br></p>
<p><br></p>
<p><span><span>MMO的同步策略目前已经没有什么秘密可言了，简单叙述下各要素： <br>
<br>
<br>
MMO人行走：MMO中如果是鼠标点击行走传目的地+中间寻路接点就行了，服务器简单验证一下。如果是键盘控制行走的话，需要做简单的预测插值。 <br>
<br>
MMO打怪：要看怎么个打法，客户端根本不必管服务端什么时候传来消息，直接演示动画就行了，最好把被砍的过程动画做长一点，可以在播放动画的过程中等待服务器传过来的延迟敏感事件，比如--该人已死。这样处理起来比较容易，最重要的是客户端看起来要流畅。 <br>
<br>
MMO插值：所谓&#8220;把不同表象柔和在一起&#8221;算法很多，简单的可以参考DR，位置不同的时候可以做一次线性插值，直接把人拉扯过去，或者二次线形插值，做一条平滑的曲线修正，看具体项目选择了。 <br>
<br>
时钟：就是时钟同步，所有时钟都是以&#8220;贞&#8221;为单位的，服务器主逻辑循环可以是5-10fps，多则无益，客户端同样维持一个和服务端频率相同的消息同步贞。 <br>
<br>
时间贞：把ping值加到时间计算里面是多余的，按贞计算的话，服务端告诉客户端在n贞发生xx事件的时候，客户端收到的时候如果客户端贞数大于n，说明是发生过了的，那么做插值，如果小于n，说明是未来发生的，那么加入时间表。 <br></span></span></p>
<p><br><span><span></span></span></p>
<p>1 游戏中的行走，则是一个需要同步的重头戏，当今游戏分成两种同步方式，一种是以服务器端为准，如果发现客户端的坐标和服务器的坐标不符合的话，则拉回。这点的好处是可以弥补瞬移外挂的出现，但在网络环境差的情况下，则出现游戏不流畅。</p>
<p>所
以，我比较推荐使用以客户端计算为准的方法（当然是我因为我这个游戏对游戏流畅性要求很高），客户端向服务器发送计算好的数据，服务器经过一定的预测判
断，广播给其他玩家，这个虽然是能引起和wow一样的问题，瞬移外挂，但完全可以从游戏设计方面进行根本避免。（这里就不谈策划方面的问题了）</p>
<p>2
游戏的战斗，战斗的同步性要求也相当高，这里我也引用楼主说的，在固定时间检测键盘，以一定频率发送攻击消息，这里有一个关键，就是服务器和客户端都进行
攻击判定，即使服务器中没有攻击判定成功，但在客户端判定成功，也要播放攻击效果。不过一切计算效果以服务器为准。这是一个欺骗玩家的手段。</p><img src ="http://www.cppblog.com/keigoliye/aggbug/95986.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-12 10:39 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/12/95986.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>网络游戏的对时以及同步问题</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/10/95830.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Thu, 10 Sep 2009 11:27:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/10/95830.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/95830.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/10/95830.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/95830.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/95830.html</trackback:ping><description><![CDATA[<div class=entry-content>
<div class=entry-body>
<p>大多数实时网络游戏，将 server 的时间和 client 的时间校对一致是可以带来许多其他系统设计上的便利的。这里说的对时，并非去调整 client 的 os 中的时钟，而是把 game client 内部的逻辑时间调整跟 server 一致即可。</p>
<p>一个粗略的对时方案可以是这样的，client 发一个数据包给 server，里面记录下发送时刻。server 收到后，立刻给这个数据包添加一个server 当前时刻信息，并发还给 client 。因为大部分情况下，game server 不会立刻处理这个包，所以，可以在处理时再加一个时刻。两者相减，client 可以算得包在 server 内部耽搁时间。</p>
<p>client 收到 server 发还的对时包时，因为他可以取出当初发送时自己附加的时刻信息，并知道当前时刻，也就可以算出这个数据包来回的行程时间。这里，我们假定数据包来回时间想同，那么把 server 通知的时间，加上行程时间的一半，则可以将 client 时间和 server 时间校对一致。</p>
</div>
<div class=entry-more id=more>
<p>这个过程用 udp 协议做比用 tcp 协议来的好。因为 tcp 协议可能因为丢包重发引起教大误差，而 udp 则是自己控制，这个误差要小的多。只是，现在网络游戏用 tcp 协议实现要比 udp 有优势的多，我们也不必为对时另起一套协议走 udp 。</p>
<p>一般的解决方法用多次校对就可以了。因为，如果双方时钟快慢一致的情况下，对时包在网络上行程时间越短，就一定表明误差越小。这个误差是不会超过包来回时间的一半的。我们一旦在对时过程中得到一个很小的行程时间，并在我们游戏逻辑的时间误差允许范围内，就不需要再校对了。</p>
<p>或者校对多次，发现网络比较稳定（虽然网速很慢），也可以认为校对准确。这种情况下，潜在的时间误差可能比较大。好在，一般，我们在时间敏感的包上都会携带时间戳。当双方时间校对误差很小的时候，client 发过来的时间戳是不应该早于 server 真实时刻的。（当时间校对准确后，server 收到的包上的时间戳加上数据包单行时间，应该等于 server 当前时刻）</p>
<p>一旦 server 发现 client 的包&#8220;提前&#8221;收到了，只有一种解释：当初校对时间时糟糕的网络状态带来了很多的时间误差，而现在的网络状态要明显优于那个时候。这时，server 应该勒令 client 重新对时。同理，client 发现 server 的数据包&#8220;提前&#8221;到达，也可以主动向 server 重新对时。</p>
<p>一个良好的对时协议的设定，在协议上避免 client 时间作弊（比如加速器，或者减速器）是可行的。这里不讨论也不分析更高级的利用游戏逻辑去时间作弊的方式，我们给数据包打上时间戳的主要目的也非防止时间作弊。</p>
<p>校对时间的一般通途是用来实现更流畅的战斗系统和位置同步。因为不依赖网络传输的统一时间参照标准可以使游戏看起来更为实时。</p>
<p>首先谈谈位置同步。</p>
<p>好的位置同步一定要考虑网络延迟的影响，所以，简单把 entity 的坐标广播到 clients 不是一个好的方案。我们应该同步的是一个运动矢量以及时间信息。既，无论是 client 还是 server ，发出和收到的信息都应该是每个 entity 在某个时刻的位置和运动方向。这样，接收方可以根据收到的时刻，估算出 entity 的真实位置。对于 server 一方的处理，只要要求 client 按一个频率(一般来说战斗时 10Hz 即可，而非战斗状态或 player 不改变运动状态时可以更低) 给它发送位置信息。server 可以在网络状态不好的情况下依据最近收到的包估算出现在 player 位置。而 client 发出的每次 player 位置信息，都应该被 server 信任，用来去修正上次的估算值。而 server 要做的只是抽查，或交给另一个模块去校验数据包的合法性（防止作弊）。</p>
<p>在 server 端，每个 entity 的位置按 10Hz 的频率做离散运动即可。</p>
<p>client 因为涉及显示问题，玩家希望看到的是 entity 的连续运动，所以处理起来麻烦一点。server 发过来的位置同步信息也可能因为网络延迟晚收到。client 同样根据最近收到的包做估算，但是再收到的包和之前已经收到的信息估算结果不同的时候，应该做的是运动方向和速度的修正，尽可能的让下次的估算更准确。</p>
<p>关于战斗指令同步，我希望是给所有战斗指令都加上冷却时间和引导时间，这正是 wow 的设计。这样，信任 client 的时间戳，就可以得到 client 准确的指令下达时间。引导时间（或者是公共冷却时间）可以充当网络延迟时间的缓冲。当然我们现在的设计会更复杂一些，这里不再列出。对于距离敏感的技能，例如远程攻击和范围魔法，我们的设计是有一个模糊的 miss 判定公式，解决距离边界的判定问题。</p>
<p>这里， server 对攻击目标的位置做估算的时候，可以不按上次发出包的运动方向去做位置估计，而选择用最有利于被攻击者的运动方向来做。这样，可以减少网络状况差的玩家的劣势。</p>
<p>对于 PVE 的战斗，甚至可以做更多的取舍，达到游戏流畅的效果。比如一个网络状态差的玩家去打 npc，他攻击 npc 的时刻，npc 是处于攻击范围之内的。但是由于网络延迟，数据包被 server 收到的时候，npc 已经离开。这个时候 server 可以以 client 的逻辑来将 npc 拉会原来的坐标。</p>
<p>虽然，这样做，可能会引起其他玩家（旁观者） client 上表现的不同。但是，网络游戏很多情况下是不需要严格同步的。在不影响主要游戏逻辑的情况下，player 的手感更为重要。</p>
</div>
</div>
<img src ="http://www.cppblog.com/keigoliye/aggbug/95830.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-10 19:27 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/10/95830.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>网络游戏同步原理荟萃</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/10/95829.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Thu, 10 Sep 2009 11:21:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/10/95829.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/95829.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/10/95829.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/95829.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/95829.html</trackback:ping><description><![CDATA[<p>看到这篇文章的时候，我觉得很惊讶，虽然我对这方面的了解并不多，但在自己的想像中，还是对网游这些东西稍有一点想法，因为曾经有朋友做过简单的外挂，比如，抓包发包然后尝试模拟包，来使网游达到你想实现的效果。<br>外挂这东西，在2003年左右应该是一个巅峰吧，那时候，奇迹外挂、传奇外挂，确实让一部分人先富起来，可是后来的零点行动，这些人都永远的消失在外挂长河中。<br>那时候我就在想，外挂是什么原理，为什么我这边的动作，可以让服务端产生那样的效果？其实，这就是一个同步的问题，我个人理解是服务器上有个触发器，这边发包后，然后那边判断包是否正常，然后就会有一个相应的动作。当然，动作程序还是在本机上，地图也在本机上，发出去的包，只是告诉服务器我是这样在动作的。于是就出现了瞬移，卡点这种情况，因为发出去的包，和坐标位置在服务器上都是正常的。（以上是我的猜测）<br><br>下面是文章：<br>不知道大家是否碰到过这种情况，当某个玩家发出一个火球，这个火球有自己的运动轨迹，那么如何来判断火球是否打中了人呢？大部分情况，当策划提出这个要求的时候，一般会被程序否认，原因是：太麻烦了，呵呵。复杂点的还有包括两个火球相撞之类的事情发生。</p>
<p>那么网络游戏中，是否真的无法模拟实现这种模拟呢？</p>
<p>首先我们来看看模拟此种操作会带来什么样的麻烦：</p>
<p>1，服务器必须trace火球的运行轨迹，乍一想，挺慢的。</p>
<p>2，网络延迟，传过来有延迟，传过去有延迟，延迟还不稳定，麻烦。</p>
<p>3，都有两点解决不了了，接下来不愿意再想了。</p>
<p>呵呵，实际上呢，对火球的模拟比对人物运动的模拟要轻松很多，原因很简单，火球的方向不会变。下面来看看具体用什么样的结构来实现：</p>
<p>不知道大家是否还记得我去年这个时候提到过的Dead Reckoning算法，我们要模拟火球运动的关键就在于一个叫Moving Objects Tracing Server的服务器程序，这个服务器是干什么的呢。这个服务器接收主游戏服务器发过来的注册事件的信息，比如有个玩家，开始移动了，那么主游戏服务器就 把该玩家的运动PDU，包括方向，速度，加速度，起点发给MOTS (Moving Objects Tracing Server)，然后MOTS自己开始对其运行进行模拟，当游戏服务器发来第二个PDU包的时候，则对各个物件的位置进行修正，并重新开始模拟。那么，我 们模拟的目的是什么呢？当然是发生某些事件，比如说碰撞，或者掉入地图的某个陷阱的时候，会将该事件回发给主逻辑服务器。然后逻辑服务器来处理该事件。</p>
<p>那么，对于火球的处理，也和处理其他玩家的同步一样，当接收到玩家的发火球的指令以后，产生一个火球，并指定其PDU信息，在MOTS上注册该个运 动物 体。当MOTS自行模拟到这个物体和其他玩家或者NPC物体产生碰撞，则通知主逻辑服务器，然后主逻辑服务器产生相应的动作。</p>
<p>那么关于延迟呢？有些人也许会说，比如说前面有个火球，我本地操纵的小人其实躲过去了，但是因为网络延迟，在服务器上我并没有躲过去，那么怎么算？ 呵呵， 不知道大家玩过星际没有，有没有发现在星际中玩多人连线模式的时候，有一点最特别的地方，就是控制一个小兵的时候，点了地图上的某个位置，但是小兵并不会 马上开始移动，而是有一定的延迟，但是这一小点延迟并不能掩盖星际的经典，同样的理论用到这里也成立。对于客户端的控制，当玩家操纵的主角改变PDU信息 的时候，确保信息发送到服务器之后，再开始处理本地的操作指令，这样就能保证本地的预测和服务器的预测几乎是没有什么误差的，即使有很小的误差产生，以服 务器为主，这样玩家也不会有太大的抱怨。</p>
<p>————————————————————————————————————————-</p>
<p><strong>网络游戏同步详解之一</strong></p>
<p>同步在网络游戏中是非常重要的，它保证了每个玩家在屏幕上看到的东西大体是一样的。其实呢，解决同步问题的最简单的方法就是把每个玩家的动作都向其 他玩家广播一遍，这里其实就存在两个问题：1，向哪些玩家广播，广播哪些消息。2，如果网络延迟怎么办。事实上呢，第一个问题是个非常简单的问题，不过之 所以我提出这个问题来，是提醒大家在设计自己的消息结构的时候，需要把这个因素考虑进去。而对于第二个问题，则是一个挺麻烦的问题，大家可以来看这么个例 子：<br>比如有一个玩家A向服务器发了条指令，说我现在在P1点，要去P2点。指令发出的时间是T0，服务器收到指令的时间是T1，然后向周围的玩家广播这条 消息，消息的内容是&#8220;玩家A从P1到P2&#8221;有一个在A附近的玩家B，收到服务器的这则广播的消息的时间是T2，然后开始在客户端上画图，A从P1到P2 点。这个时候就存在一个不同步的问题，玩家A和玩家B的屏幕上显示的画面相差了T2-T1的时间。这个时候怎么办呢？</p>
<p>有个解决方案，我给它取名叫预测拉扯，虽然有些怪异了点，不过基本上大家也能从字面上来理解它的意思。要解决这个问题，首先要定义一个值叫：预 测误差。然后需要在服务器端每个玩家连接的类里面加一项属性，叫TimeModified，然后在玩家登陆的时候，对客户端的时间和服务器的时间进行比 较，得出来的差值保存在TimeModified里面。还是上面的那个例子，服务器广播消息的时候，就根据要广播对象的TimeModified，计算出 一个客户端的CurrentTime，然后在消息头里面包含这个CurrentTime，然后再进行广播。并且同时在玩家A的客户端本地建立一个队列，保 存该条消息，只到获得服务器验证就从未被验证的消息队列里面将该消息删除，如果验证失败，则会被拉扯回P1点。然后当玩家B收到了服务器发过来的消息&#8220;玩 家A从P1到P2&#8221;这个时候就检查消息里面服务器发出的时间和本地时间做比较，如果大于定义的预测误差，就算出在T2这个时间，玩家A的屏幕上走到的地点 P3，然后把玩家B屏幕上的玩家A直接拉扯到P3，再继续走下去，这样就能保证同步。更进一步，为了保证客户端运行起来更加smooth，我并不推荐直接 把玩家拉扯过去，而是算出P3偏后的一点P4，然后用(P4-P1)/T(P4-P3)来算出一个很快的速度S，然后让玩家A用速度S快速移动到P4，这 样的处理方法是比较合理的，这种解决方案的原形在国际上被称为（Full plesiochronous），当然，该原形被我篡改了很多来适应网络游戏的同步，所以而变成所谓的：预测拉扯。</p>
<p>另外一个解决方案，我给它取名叫验证同步，听名字也知道，大体的意思就是每条指令在经过 服务器验证通过了以后再执行动作。具体的思路如下：首先 也需要在每个玩家连接类型里面定义一个 TimeModified，然后在客户端响应玩家鼠标行走的同时，客户端并不会先行走动，而是发一条走路的指令给服务器，然后等待服务器的验证。服务器接 受到这条消息以后，进行逻辑层的验证，然后计算出需要广播的范围，包括玩家A在内，根据各个客户端不同的TimeModified生成不同的消息头，开始 广播，这个时候这个玩家的走路信息就是完全同步的了。这个方法的优点是能保证各个客户端之间绝对的同步，缺点是当网络延迟比较大的时候，玩家的客户端的行 为会变得比较不流畅，给玩家带来很不爽的感觉。该种解决方案的原形在国际上被称为（Hierarchical master-slave synchronization），80年代以后被广泛应用于网络的各个领域。</p>
<p>最后一种解决方案是一种理想化的解决方案，在国际上被称为Mutual synchronization，是一种对未来网络的前景的良好预测出来的解决方案。这里之所以要提这个方案，并不是说我们已经完全的实现了这种方案，而 只是在网络游戏领域的某些方面应用到这种方案的某些思想。我对该种方案取名为：半服务器同步。大体的设计思路如下：</p>
<p>首先客户端需要在登陆世界的时候建立很多张广播列表，这些列表在客户端后台和服务器要进行不及时同步，之所以要建立多张列表，是因为要广播的类 型是不止一种的，比如说有local message,有remote message,还有global message 等等，这些列表都需要在客户端登陆的时候根据服务器发过来的消息建立好。在建立列表的同时，还需要获得每个列表中广播对象的TimeModified，并 且要维护一张完整的用户状态列表在后台，也是不及时的和服务器进行同步，根据本地的用户状态表，可以做到一部分决策由客户端自己来决定，当客户端发送这部 分决策的时候，则直接将最终决策发送到各个广播列表里面的客户端，并对其时间进行校对，保证每个客户端在收到的消息的时间是和根据本地时间进行校对过的。 那么再采用预测拉扯中提到过的计算提前量，提高速度行走过去的方法，将会使同步变得非常的smooth。该方案的优点是不通过服务器，客户端自己之间进行 同步，大大的降低了由于网络延迟而带来的误差，并且由于大部分决策都可以由客户端来做，也大大的降低了服务器的资源。由此带来的弊端就是由于消息和决策权 都放在客户端本地，所以给外挂提供了很大的可乘之机。</p>
<p>综合以上三种关于网络同步派系的优缺点，综合出一套关于网络游戏传输同步的较完整的解决方案，我称它为综合同步法（colligate synchronization）。大体设计思路如下：</p>
<p>首先将服务器需要同步的所有消息从划分一个优先等级，然后按照3/4的比例划分出重要消息和非重要消息，对于非重要消息，把决策权放在客户端，在客户端逻辑上建立相关的决策机构和各种消息缓存区，以及相关的消息缓存区管理机构，如下图所示：</p>
<p>上图简单说明了对于非重要消息，客户端的大体处理流程，其中有一个客户端被动行为值得大家注意，其中包括对服务器发过来的某些验证代码做返回， 来确保消息缓存中的消息和服务器端是一致的，从而有效的防止外挂来篡改本地消息缓存。其中的消息来源是包括本地的客户端响应玩家的消息以及远程服务器传递 过来的消息。</p>
<p>对于重要消息，比如说战斗或者是某些牵扯到玩家一些比较敏感数据的操作，则采用另外一套方案，该方案首先需要在服务器和客户端之间建立一套 Ping System，然后服务器保存和用户的及时的ping值，当ping比较小的时候，响应玩家消息的同时先不进行动作，而是先把该消息反馈给服务器，并且阻 塞，服务器收到该消息，进行逻辑验证之后向所有该详细广播的有效对象进行广播（包括消息发起者），然后客户端收到该消息的验证，才开始执行动作。而当 ping比较大的时候，客户端响应玩家消息的同时立刻进行动作，并且同时把该消息反馈给服务器，值得注意的是这个时候还需要在本地建立一个无验证消息的队 列，把该消息入队，执行动作的同时等待服务器的验证，还需要保存当前状态。服务器收到客户端的请求后，进行逻辑验证，并把消息反馈到各个客户端，带上各个 客户端校对过的本地时间。如果验证通过不过，则通知消息发起者，该消息验证失败，然后客户端自动把已经在进行中的动作取消，恢复原来状态。如果验证通过， 则广播到的各个客户端根据从服务器获得校对时间进行对其进行拉扯，保证在该行为完成之前完成同步。</p>
<p>至此，一个比较成熟的网络游戏的同步机制已经初步建立起来了，接下来的逻辑代码就根据各自不同的游戏风格以及侧重点来写了。</p>
<p>同步是网络游戏最重要的问题，如何同步也牵扯到各个方面的问题，比如说游戏的规模，游戏的类型以及各种各样的方面，对于规模比较大的游戏，在同 步方面可以下很多的工夫，把消息分得十分的细腻，对于不同的消息采用不同的同步机制，而对于规模比较小的游戏，则可以采用大体上一样的同步机制，究竟怎么 样同步，没有个定式，是需要根据自己的不同情况来做出不同的同步决策的网游同步算法之导航推测（Dead Reckoning）算法：</p>
<p>——————————————————————————————————————————</p>
<p><strong>网络游戏同步详解之二</strong></p>
<p>在了解该算法前，我们先来谈谈该算法的一些背景资料。大家都知道，在网络传输的时候，延迟现象是很普遍的，而在基于Server/Client结构 下的网络游戏的同步也就成了很头疼的问题，在保证客户端响应用户本地指令流畅的情况下，没法有效的保证的同步的及时性。同样，在军方也有类似的事情发生， 即使是同一LAN里面的机器，也会因为传输的延迟，导致一些运算的失误，介于此，美国国防部投入了大量的资金用于研究一种比较的好的方案来解决分布式系统 中的延迟问题，特别是一个叫分布式模拟运动（Distributed Interactive Simulation）的系统，这套系统呢，其中就提出了一套号称是Latency Hiding &amp; Bandwidth Reduction的方案，命名为Dead Reckoning。呵呵，来头很大吧，恩，那么我们下面就来看看这套系统的一些观点，以及我们如何把它运用到我们的网络游戏的同步中。</p>
<p>首先，这套同步方案是基于我那篇《网络游戏的同步》一文中的Mutual Synchronization同步方案的，也就是说，它并不是Server/Client结构的，而是基于客户端之间的同步的。下面我们先来说一些本文中将用到的名词概念：<br>网状网络：客户端之间构成的网络<br>节点：网状网络中的每个客户端<br>极限误差：进行同步的时候可能产生的误差的极值</p>
<p>恩，在探讨其原理的之前，我们先来看看我们需要一个什么样的环境。首先，需要一个网状网络，网状网络如何构成呢？当有新节点进入的时候，通知该 网络里面的所有节点，各节点为该客户端在本地创建一个副本，登出的时候，则通知所有节点销毁本地关于该节点的副本。然后每个节点该保存一些什么数据呢？首 先有一个很重要的包需要保存，叫做协议数据包（PDU Protocol Data Unit），PDU包含节点的一些相关的运动信息，比如当前位置，速度，运动方向，或者还有加速度等一些信息。除PDU之外，还有其他信息需要保存，比如 说节点客户端人物的HP，MP之类的。然后，保证每个节点在最少8秒之内要向其它节点广播一次PDU信息。最后，设置一个极限误差值。到此，其环境就算搭 建完成了。下面，我们就来看看相关的具体算法：</p>
<p>假设在节点A有一个小人（路人甲），开始跑路了，这个时候，就像所有的节点广播一次他的PDU信息，包括：速度（S），方向（O），加速度 （A）。那么所有的节点就开始模拟路人甲的运动轨迹和路线，包括节点A本身（这点很重要），同时，路人甲在某某玩家的控制下，会不时的改变一下方向，让其 跑路的路线变得不是那么正规。在跑路的过程中，节点A有一个值在不停的记录着其真实坐标和在后台模拟运动的坐标的差值，当差值大于极限误差的时候，则计算 出当前的速度S，方向O和速度A（算法将在后面介绍），并广播给网络中其他所有节点。其他节点在收到这条消息之后呢，就可以用一些很平滑的移动把路人甲拉 扯过去，然后重新调整模拟跑路的数据，让其继续在后台模拟跑路。</p>
<p>很显然，如果极限误差定义得大了，其他节点看到的偏差就会过大，如果极限偏差定义得小了，网络带宽就会增大。如果定义这个极限误差，就该根据各 种数据的重要性来设计了。如果是回合制的网络游戏，那么在走路上把极限误差定义得大些无所谓，可以减少带宽。但是如果是及时打斗的网络游戏，那么就得把极 限误差定义得小一些，否则会出现某人看到某人老远把自己给砍死的情况。</p>
<p>Dead Reckoning的主要算法有9种，但是只有两种是解决主要问题的，其他的基本上只是针对不同的坐标系的一些不同的算法，这里就不一一介绍了。好，那么我们下面来看传说中的最主要的两种算法：<br>第一：目标点 = 原点 + 速度 * 时间差<br>第二：目标点 = 原点 + 速度 * 时间差 + 1/2 * 加速度 * 时间差<br>呵呵，传说中的算法都是很经典的，虽然我们早在初中物理的时候就学过。</p>
<p>该算法的好处呢，正如它开始所说的，Latency Hiding &amp; Bandwidth Reduction，从原则上解决了网络延迟导致的不同步的问题，并且有效的减少了带宽，不好的地方就是该算法基本上只能使用于移动中的同步，当然，移动 的同步是网络游戏中同步的最大的问题。</p>
<p>该方法结合我在《网络游戏的同步》一文中提出的综合同步法的构架可以基本上解决掉网络游戏中走路同步的问题。相关问题欢迎大家一起讨论。</p>
<p><strong>有关导航推测算法（Dead Reckoning）中的平滑处理：</strong></p>
<p>根据我上篇文章所介绍的，在节点A收到节点B新的PDU包时，如果和A本地的关于B的模拟运动的坐标不一致时，怎么样在A的屏幕上把B拽到新的 PDU包所描叙的点上面去呢，上文中只提了用&#8220;很平滑的移动&#8221;把B&#8220;拉扯&#8221;过去，那么实际中应该怎么操作呢？这里介绍四种方法。</p>
<p>第一种方法，我取名叫直接拉扯法，大家听名字也知道，就是直接把B硬生生的拽到新的PDU包所描叙的坐标上去，该方法的好处是：简单。坏处是：看了以下三种方法之后你就不会用这种方法了。</p>
<p>第二种方法，叫直线行走（Linear），即让B从它的当前坐标走直线到新的PDU包所描叙的坐标，行走速度用上文中所介绍的经典算法：<br>目标点 = 原点 + 速度 * 时间差 + 1/2 * 加速度 * 时间差算出：<br>首先算出从当前坐标到PDU包中描叙的坐标所需要的时间：<br>T = Dest( TargetB &#8211; OriginB ) / Speed<br>然后根据新PDU包中所描叙的坐标信息模拟计算出在时间T之后，按照新的PDU包中的运动信息所应该达到的位置：<br>_TargetB = NewPDU.Speed * T<br>然后根据当前模拟行动中的B和_TargetB的距离配合时间T算出一个修正过的速度_S：<br>_S = Dest( _TargetB &#8211; OriginB ) / T<br>然后在画面上让B以速度_S走直线到Target_B，并且在走到之后调整其速度，方向，加速度等信息为新的PDU包中所描叙的。</p>
<p>这种方法呢，非常的土，会让物体在画面上移动起来变得非常的不现实，经常会出现很生硬的拐角，而且对于经常要修改的速度_S，在玩家A的画面上，玩家B的行动会变得非常的诡异。其好处是：比第一种方法要好。</p>
<p>第三种方法，叫二次方程行走（Quadratic），该方法的原理呢，就是在直线行走的过程中，加入二次方程来计算一条曲线路径，让Dest( _TargetB &#8211; OriginB )的过程是一条曲线，而不是一条直线，恩，具体的实现方法，就是在Linear方法的计算中，设定一个二次方程，在Dest函数计算距离的时候根据设定的 二次方程来计算，这样一来，可以使B在玩家A屏幕上的移动变得比较的有人性化一些。但是该方法的考虑也是不周全的，仅仅只考虑了TargetB到 _TargetB的方向，而没有考虑新的PDU包中的方向描叙，那么从_TargetB开始模拟行走的时候，仍然是会出现比较生硬的拐角，那么下面提出的 最终解决方案，将彻底解决这个问题。</p>
<p>——————————————————————————————————————————</p>
<p><strong>网络游戏同步详解之三</strong></p>
<p>最后一种方法叫：立方体抖动（Cubic Splines），这个东东比较复杂，它需要四个坐标信息作为它的参数来进行运算，第一个参数Pos1是OriginB，第二个参数Pos2是 OriginB在模拟运行一秒以后的位置，第三个参数Pos3是到达_TargetB前一秒的位置，第四个参数pos4是_TargetB的位置。</p>
<p>Struct pos {<br>Coordinate X;<br>Coordinate Y;<br>}<br>Pos1 = OriginB<br>Pos2 = OriginB + V<br>Pos3 = _TargetB &#8211; V<br>Pos4 = _TargetB<br>运动轨迹中(x, y)的坐标。<br>x = At^3 + Bt^2 + Ct + D<br>y = Et^3 + Ft^2 + Gt + H<br>（其中时间t的取值范围为0-1，在Pos1的时候为0，在Pos4的时候为1）<br>x(0-3)代表Pos1-Pos4中x的值，y(0-3)代表Pos1-Pos4中y的值<br>A = x3 &#8211; 3 * x2 +3 * x1 &#8211; x0<br>B = 3 * x2 &#8211; 6 * x1 + 3 * x0<br>C = 3 * x1 &#8211; 3 * x0<br>D = x0<br>E = y3 &#8211; 3 * y2 +3 * y1 &#8211; y0<br>F = 3 * y2 &#8211; 6 * y1 + 3 * y0<br>G = 3 * y1 &#8211; 3 * y0<br>H = y0</p>
<p>上面是公式，那么下面我们来看看如何获得Pos1-Pos4：首先，Pos1和 Pos2的取值会比较容易获得，根据OriginB配合当前的速度和方向可以获得，然而Pos3和Pos4呢，怎么获得呢？如果在从Pos1到Pos4的 过程中有新的PDU到达，那么我们定义它为NewPackage。</p>
<p>Pos3 = NewPackage.X + NewPackage.Y * t + 1/2 * NewPackage.a * t^2<br>Pos4 = Pos3 &#8211; (NewPackage.V + NewPackage.a * t)</p>
<p>如果没有NewPackage的情况下,则Pos3和Pos4按照开始所规定的方法获得。</p>
<p>至此，关于导航推测的算法大致介绍完毕。</p>
<p>原文来自：http://xinsync.xju.edu.cn/index.php/archives/4079</p>
<img src ="http://www.cppblog.com/keigoliye/aggbug/95829.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-10 19:21 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/10/95829.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>无缝世界网游服务器架构的设计思路</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/09/95668.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 09 Sep 2009 02:53:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/09/95668.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/95668.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/09/95668.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/95668.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/95668.html</trackback:ping><description><![CDATA[<p><a href="http://canremember.com/?p=8">http://canremember.com/?p=8</a></p>
<p><a href="http://canremember.com/?p=10">http://canremember.com/?p=10</a></p>
<p>过去一年中，花了很多时间在考虑服务器架构设计方面的问题。看了大量文章、也研究了不少开源项目，眼界倒是开阔了不少，不过回过头来看，对网游架构设计方面的帮助却是不多。老外还是玩儿console game的多，MMO Games方面涉及的还是不如国内广泛。看看 Massively Multiplayer Games Development 1 &amp; 2 这两本书吧，质量说实话很一般，帮助自然也很有限。当然这也是好事，对国内的研发公司/团队来说，在网游服务器技术方面当然就存在超越老外的可能性，而且在这方面技术超越的机会更大，当然前提是要有积累、要舍得投入，研发人员更要耐得住寂寞、经得起诱惑，在平均每天收到超过3个猎头电话的时候——依然不动心。</p>
<p>上面有点儿扯远了，下面聊聊无缝世界架构（Seamless world server architecture）设计方面的一点儿看法。</p>
<p>先说架构设计的目标——我的看法，服务器组架构设计的目标就是确定各服务器拓补关系和主要的业务逻辑处理方法。主要要解决的问题就是在满足游戏内容设计需要的前提下，如何提高带负载能力的问题。</p>
<p>最简单的架构就是基本的C/S架构，一台Server直接构成一个Cluster，所有Client直接连接这个Server，这个Server完成所有逻辑和数据处理。这架构其实很好，最大的好处就是它架构上的 Simplicity ，Cluster内部的跨进程交互完全被排除，复杂度立刻就降下来了，而且——完全可以实现一个无缝（Seamless world）的游戏世界。但是即使我不说，大家也知道这种单Server架构会有什么问题。不过我们不妨以另外一个角度来看这个Server——一个黑盒子。从系统外部的角度来看，什么样的系统都可以看成一个整体、一个黑盒，而不管系统内部的拓补关系和实现复杂度方面的问题。在不考虑这个系统的实现的前提下，理论上Cluster的处理能力就是由硬件的数量和能力决定的，也就是说一个Server Cluster内包含越多的服务器、服务器越&#8216;快&#8217;，那么这个Cluster的处理能力越好、带负载能力越好。那么我们要面对的带负载能力的问题，就是如何高效的利用这些Server的问题，基本上也可以理解为如何提高玩家请求的并发处理能力的问题。</p>
<p>CPU厂商在很久以前就在考虑这方面的问题了，CPU其实也可以看成个黑盒。看看他们用过的技术——流水线（pipeline）技术、多CPU/多核（multicore）技术，以及这些技术的衍生技术。我想了很久让 Server Cluster 内部处理并行的方法、并且有了比较清晰的思路之后，才发现其实早就可以参照CPU厂商的方法。流水线的方法就是把一个指令处理拆分成很多个步骤，这样指令的处理被分解之后就可以部分重叠（相当于变成并发的了）执行。我们的Server Cluster一样可以用这种方法来拆分，我想了个名字——</p>
<p>Services-based Architecture——基于服务的架构。在这种架构内部，我们根据处理数据、逻辑的相关性来划分组内各个服务器的工作任务。例如：位置服务提供物体可见性信息、物品服务处理所有物品相关的逻辑、社会关系服务提供行会家族等等方面的逻辑、战斗服务器只处理战斗相关的逻辑，等等。这样划分的话、逻辑处理的并发就有了可能性。举例来说：A砍B一刀这件事情与C从奸商手里买到一件武器这个事情是完全不相干的，而且这2个请求本来就在不同的服务器上被处理，他们是被不同的Service Server并发处理的。这就是 Services-based Architecture 的并发方法。</p>
<p>基本上，把游戏逻辑的处理拆分成一个个的service，就和设计cpu的时候把机器指令的具体处理拆分，然后设计出一个个流水线单元是一个道理。</p>
<p>Cells-based Architecture——基于cell的架构。每个cell都在不同的物理 server上面运行着完全一样的应用程序服务器，但是他们负责承载不同的游戏场景区域的游戏逻辑。和 services-based arch. 明显不同的就是，每个cell都是个&#8216;在逻辑上完整的&#8217;服务器。它得处理物品操作、人物移动、战斗计算等等几乎所有的游戏逻辑。尽管这么做会带来一些（可能是很复杂）的问题，但是它完全是可行的。举例来说：在吴国A砍B一刀显然地和千里之外在越国的C砍D一刀不搭界，他们完全可以被不同的Cell并发地处理。</p>
<p>基本上，这就相当于一个主板上面插多个CPU或者一个CPU但是有多个内核，每个CPU能做的事情都是一样的，而且能一起做。</p>
<p>从一组服务器的角度来看，一般来说，我们的服务器组（Cluster）内都会有登陆验证服务器（Login Server）、持久性数据服务器（DB及DB Proxy）、连接代理服务器（Gate Server、FEP Server、Client Proxy等）以及Auto Patch Server、还有用于集中管理及控制组的服务器等等，由于这些服务器基本上什么样的架构设计都会用到，所以——现在不考虑以上这些服务器，只考虑具体处理游戏逻辑、游戏规则的各个服务器。以此为前提来分析一下 Services-based Architecture 和 Cells-based Architecture 的优缺点。</p>
<p>对Services-based Architecture 的分析<br>&nbsp;</p>
<p>基于服务的架构，顾名思义这种架构的实现（程序）会是和服务的具体内容（策划）相关的，这是因为——各种【服务】内容的确定是建立于项目的【需求分析】基础上的，【需求分析】的前提是基本确定了【策划设计】，至少是项目的概要设计。</p>
<p>我想多数做过游戏项目的人都应该对需求变更有很深的感触，每个人都说&#8220;开始想做的那个和最后实际做出来的那个不一样&#8221;。特别是在项目的早期阶段，团队的不同成员对项目做完之后的样子有相当不同的看法（很可能大家互相都不知道对方怎么看的），这很容易理解，谁也不可能从几页纸几张图就确切地知道这个游戏做完了什么样子，即使不考虑需求变更。涉及到项目开发方法方面的东西这里就不多说了，总之我的看法就是——尽管我们不大可能设计出一个架构能够适应任何的游戏设计，但是不同开发任务间的耦合度显然还是越低越好，基于服务的架构适应需求变更的能力较差。</p>
<p>关于服务耦合<br>不管如何划分service，不同 service之间都一定存在不同程度的耦合（coupling）关系，不同的 service 之间会有相互依赖关系。而你们的策划设计可能会让这种关系复杂到程序在运行时的状态很难以琢磨的程度。</p>
<p>假设：<br>服务器组内的战斗处理和物品处理分别由两个不同的服务（器）提供<br>游戏规则：<br>人物被攻击后自己携带的物品可能掉落到地上<br>某些物品掉落后会爆炸<br>物品在地上爆炸可能伤及周围（半径10米内）人物<br>人物之间的&#8216;仇恨度&#8217;影响战斗数值计算<br>被攻击时掉落的物品爆炸后伤及的人物，会增加对&#8216;被攻击人&#8217;的&#8216;仇恨度&#8217;</p>
<p>我想我还能想出很多很多&#8220;看上去不算过分&#8221;的规则来让这个事情变得复杂无比，很可能你们的策划也在无意中，已经拥有我这种能力 :) 而且他们在写文档时候的表达还多半不如我上面写的清楚，另外，他们还会把这些规则分到很多不同的文档里面去写。好吧，你肯定会想&#8220;把这两个服务合二为一好了 &#8221;，实际上不管你想把哪两个（或多个）服务合并为一个服务的时候，都应该先考虑一下当时是为什么把他们独立为不同服务的？</p>
<p>实际上很多这样&#8220;看上去不算过分&#8221;的规则都会导致service间的频繁交互，所以每个service最好都是stateless service，这样的话情况会好很多，但是对于游戏来说这很难做到。</p>
<p>请求处理的时序问题<br>服务耦合的问题在不考虑开发复杂度比较高的情况下，还是可以被搞定的，只要脑袋够清醒，愿意花够多的时间，那么还有更难以搞定的么？我看确实还有，如果你对将要面对的问题，了解得足够多的话:)</p>
<p>&nbsp;</p>
<p>&nbsp;<img height=295 alt="" src="http://canremember.com/wp-content/uploads/2008/02/problem_of_services-based_arch.gif" width=997 border=0></p>
<p>&nbsp;</p>
<p>上面两个序列图描述的是某个玩家做了连续做了两次同样的操作但是很可能得到了不同的结果，当然这些请求都是异步地被处理。问题的关键在于——尽管两次玩家执行的命令一样、顺序一样，甚至时间间隔都一样，但是结果却很不同——因为图（1）里面C2CS::Request_to_attack请求被处理的时候，C2IS::Request_equip_item 这个请求还没有被处理完，但是图（2）显示的情况就不一样了。因为C2IS::Request_equip_item这个操作很可能会改变游戏人物的属性，这个属性又很可能影响attack的结果。这两幅图实际上省略了 Combat Server 与 Item Server 之间的交互过程。但是已经足以说明问题了，每个Service处理每个Request时具体会消耗的时间，是无法在设计时确定的!</p>
<p>谁喜欢这类结果上的不确定性？举个例子：玩家很可能已经装备上了&#8220;只能使用1次的魔兽必杀刀&#8221;然后攻击了一下魔兽，但是它却没死！这会导致什么样的结果？请自行想象。另外，这种不确定性还会表现为&#8220;在项目开发期和运营期的行为差异&#8221;，或者&#8220;出现某些偶然的奇怪现象&#8221;。</p>
<p>那还有解决方案么？有的，其实只要序列化玩家请求的处理，使处理有序进行就可以了。但是又一次的，这会带来新的复杂度——在某个范围（整个服务器组？一个行会？一个队伍？）内，以每个玩家为单位，序列化他（们）的（可能是所有）操作，但是也显而易见，这在某种程度上降低了请求处理的并发性，尽管它对并发性的影响可能只局限于不大（最少是一个玩家）的范围。</p>
<p><br>&nbsp;</p>
<p><br>对Cells-based Architecture 的分析<br>&nbsp;</p>
<p>基于Cell的架构有个明显的优势就是Cell如何划分和你的策划没有关系J这是真的。而且Cell间如何交互可以被放到系统的底层，具体有多底层、多隐蔽（实际上可以隐蔽到对开发上层游戏逻辑的程序员都不可见的程度）要看你的实现如何了。如果做到了某个系统的程序设计与游戏设计完全无关的话，显然，这个系统受到游戏设计变更（需求变更）的影响就会很小很小，甚至会到完全不受影响的程度，当然这是理想情况。</p>
<p>关于跨边界对象交互<br>在基于Cell的服务器架构里面，实现无缝世界（Seamless World）的主要难点在于实现跨边界对象的交互时会出现的一些问题，因为这些对象在不同的Cell进程里面，这些Cell一般来说是在不同的物理服务器上运行。</p>
<p>无缝世界的特点自然就是无缝，并且因为无缝给玩家带来更好的游戏体验，所以显然我们希望&#8220;跨边界对象交互&#8221;问题不把事情搞砸，那么这种交互的表现就必须满足稳定、高效的前提。一般来说，高于300ms的延迟对玩家操作来说就属于&#8220;明显可见&#8221;的程度了，不能让玩家骑着500块RMB买来的虚拟马在一片大草原上面畅快的奔跑的时候，在某个地方突然就被&#8220;看不见的墙&#8221;给&#8220;挡&#8221;了一下，因为这&#8220;墙&#8221;根本看不见，所以会很影响&#8220;上帝&#8221;的游戏心情。</p>
<p>关于组成整个虚拟世界的Cell之间的关系，下面来分析两种情况：</p>
<p><br>&lt;!--[if !supportLists]--&gt;一， &lt;!--[endif]--&gt;Cell 承载的场景不重叠</p>
<p><img height=375 alt="" src="http://canremember.com/wp-content/uploads/2008/02/cell_1.gif" width=349 border=0></p>
<p>&nbsp;</p>
<p>如图（1），一个连续的虚拟世界场景被分成左右两块，分别在不同的Cell Server上面运行。A、B、C分别是3个不同的游戏角色。在这种情况下B与C的交互并不存在任何障碍，因为B和C只不过是同一个物理服务器上同一个进程内的两块不同的内存数据而已。但是A与B/C的交互就不那么直接了，尽管他们所在的场景看上去是&#8220;连续的、一体的&#8221;但是事情不会像表面上那么简单。A与B发生交互时候会发生什么事情？例如A攻击了B、A与B交易物品等等，因为在这种结构下做数据同步会带来很多问题，例如对象状态不确定性、开发复杂度等等、相对来说两个Cell Server之间做网络通讯而带来的延迟可能反而是最小的问题，这些问题不需要很复杂的分析就可以得出结论，在此不再多说了。</p>
<p>&lt;!--[if !supportLists]--&gt;二，Cell 承载的场景（部分地）重叠</p>
<p><img height=375 alt="" src="http://canremember.com/wp-content/uploads/2008/02/cell_2.gif" width=514 border=0></p>
<p>&nbsp;</p>
<p>如图（2），一个连续的虚拟世界场景被分成左右两块，分别在不用的Cell Server上面运行。A、B、C、D分别是4个不同的游戏角色。这个情况下，中间的区域为2个Cell所共同维护，中间区域的对象同属于2个Cell所&#8216;拥有&#8217;。这有什么好处？现在，任意两个对象之间，除了A与C之间的交互，都变得更&#8216;直接&#8217;了。变得直接肯定是一件好事儿，那么A与C之间呢？他们之间其实也没有任何问题J 因为双方都已经超出了对方的Area of Interest（AoI）区域，游戏规则可以限制他们不能直接交互。</p>
<p>上面提到的第二种方案算不上什么魔法，但是肯定是比第一种方案更有效。接下来怎么办？假设B是个玩家，他站在中间这块区域上面时，并不会产生&#8220;我到底是在哪里&#8221;这样的疑问J 问题的关键在于对于Cell Server来说，怎么样同步那些处于重叠区域对象的状态。游戏世界内的对象可能同时处于1个、2个、3个或者4个不同的Cell Server。如果你的Cell分隔方法不限于水平线和垂直线、或者有人故意捣乱的话，还可能会更多。需要被同步的对象也不只是玩家本身，还包括怪物、NPC、一颗会走的树、某玩家在地上吐的痰等等。</p>
<p>由于我们的基于无缝世界的游戏规则不大会直接去限制游戏世界某处玩家的行为，也就是说玩家如果能相互交易物品的话，他们肯定希望在任何地方都能交易，&#8220;为什么其他地方都行，但是在某个墙角做交易就会导致物品丢失？&#8221;所以比较可靠的方法是建立一套的用于同步的底层机制，来同步这些跨边界对象。</p>
<p>怎么实现？这个话题很大，恐怕再写几篇Blog我也讲不完，但是有一些东西可以作为参考，例如：DCOM和CORBA规范，Java的RMI，基于Python的 PYRO，TAO(The ACE ORB)等等。好在分布式处理的问题不止是网络游戏会涉及到，可以借鉴的东西还是很多的。</p>
<p>总结<br>很显然，这篇文章在两种架构的评价上面存在某些倾向性，但是倾向性本身只是副产品。另外一个副产品就是关于一些技术分析方法。</p>
<p>在考虑采用何种技术的时候，我们往往很容易地就会忽略对程序之外那些事情的影响。上面我提到的关于Services-based架构实现的时候，提到划分service及数据设计对程序设计能力的挑战、对策划设计的制约，对适应需求变更能力的影响，都不会只是空谈。这些问题也不是只在实现这种架构的时候才出现。</p>
<p>不要高估自己的智商，Keep It Simple and Stupid :) 应该可以让我们离成功更近一点儿。</p>
<p>&nbsp;</p>
<p>本文来自CSDN博客，转载请标明出处：<a href="http://blog.csdn.net/romandion/archive/2009/04/02/4044368.aspx">http://blog.csdn.net/romandion/archive/2009/04/02/4044368.aspx</a></p>
<img src ="http://www.cppblog.com/keigoliye/aggbug/95668.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-09 10:53 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/09/95668.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>网游服务器通信架构的设计 </title><link>http://www.cppblog.com/keigoliye/archive/2009/09/09/95666.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 09 Sep 2009 02:43:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/09/95666.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/95666.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/09/95666.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/95666.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/95666.html</trackback:ping><description><![CDATA[由于网游服务器的设计牵涉到太多内容，比如：网络通信方面、人工智能、数据库设计等等，所以本文将重点从网络通信方面的内容展开论述。谈到网络通信，就不能不涉及如下五个问题：<br>1、 常见的网游服务通信器架构概述<br>2、 网游服务器设计的基本原则<br>3、 网游服务器通信架构设计所需的基本技术<br>4、 网游服务器通信架构的测试<br>5、 网游服务器通信架构设计的常见问题<br><br>下面我们就从第一个问题说起：<br><br>常见的网游服务器通信架构概述<br>　　目前，国内的网游市场中大体存在两种类型的网游游戏：MMORPG（如：魔兽世界）和休闲网游（如：QQ休闲游戏和联众游戏，而如泡泡堂一类的游戏与QQ休闲游戏有很多相同点，因此也归为此类）。由于二者在游戏风格上的截然不同，导致了他们在通信架构设计思路上的较大差别。下面笔者将分别描述这两种网游的通信架构。<br><br>1．MMORPG类网游的通信架构<br>　　网游的通信架构，通常是根据几个方面来确定的：游戏的功能组成、游戏的预计上线人数以及游戏的可扩展性。<br>　　目前比较通用的MMORPG游戏流程是这样的：<br><br>a. 玩家到游戏官方网站注册用户名和密码。<br>b. 注册完成后，玩家选择在某一个区激活游戏账号。<br>c. 玩家在游戏客户端中登录进入已经被激活的游戏分区，建立游戏角色进行游戏。<br><br>　　通常，在这样的模式下，玩家的角色数据是不能跨区使用的，即：在A区建立的游戏角色在B区是无法使用的，各区之间的数据保持各自独立性。我们将这样独立的A区或B区称为一个独立的服务器组，一个独立的服务器组就是一个相对完整的游戏世界。而网游服务器的通信架构设计，则包括了基于服务器组之上的整个游戏世界的通信架构，以及在一个服务器组之内的服务器通信架构。<br><br>　　我们先来看看单独的服务器组内部的通信是如何设计的。<br>　　一个服务器组内的各服务器组成，要依据游戏功能进行划分。不同的游戏内容策划会对服务器的组成造成不同的影响。一般地，我们可以将一个组内的服务器简单地分成两类：场景相关的（如：行走、战斗等）以及场景不相关的（如：公会聊天、不受区域限制的贸易等）。为了保证游戏的流畅性，可以将这两类不同的功能分别交由不同的服务器去各自完成。另外，对于那些在服务器运行中进行的比较耗时的计算，一般也会将其单独提炼出来，交由单独的线程或单独的进程去完成。<br><br>　　各个网游项目会根据游戏特点的不同，而灵活选择自己的服务器组成方案。经常可以见到的一种方案是：场景服务器、非场景服务器、服务器管理器、AI服务器以及数据库代理服务器。<br>　　以上各服务器的主要功能是：<br><br>　　场景服务器：它负责完成主要的游戏逻辑，这些逻辑包括：角色在游戏场景中的进入与退出、角色的行走与跑动、角色战斗（包括打怪）、任务的认领等。场景服务器设计的好坏是整个游戏世界服务器性能差异的主要体现，它的设计难度不仅仅在于通信模型方面，更主要的是整个服务器的体系架构和同步机制的设计。<br><br>　　非场景服务器：它主要负责完成与游戏场景不相关的游戏逻辑，这些逻辑不依靠游戏的地图系统也能正常进行，比如公会聊天或世界聊天，之所以把它从场景服务器中独立出来，是为了节省场景服务器的CPU和带宽资源，让场景服务器能够尽可能快地处理那些对游戏流畅性影响较大的游戏逻辑。<br><br>　　服务器管理器：为了实现众多的场景服务器之间以及场景服务器与非场景服务器之间的数据同步，我们必须建立一个统一的管理者，这个管理者就是服务器组中的服务器管理器。它的任务主要是在各服务器之间作数据同步，比如玩家上下线信息的同步。其最主要的功能还是完成场景切换时的数据同步。当玩家需要从一个场景A切换到另一个场景B时，服务器管理器负责将玩家的数据从场景A转移到场景B，并通过协议通知这两个场景数据同步的开始与结束。所以，为了实现这些内容繁杂的数据同步任务，服务器管理器通常会与所有的场景服务器和非场景服务器保持socket连接。<br><br>　　AI（人工智能）服务器：由于怪物的人工智能计算非常消耗系统资源，所以我们把它独立成单独的服务器。AI服务器的主要作用是负责计算怪物的AI，并将计算结果返回给场景服务器，也就是说，AI服务器是单独为场景服务器服务的，它完成从场景服务器交过来的计算任务，并将计算结果返回给场景服务器。所以，从网络通信方面来说，AI服务器只与众多场景服务器保持socket连接。<br><br>　　数据库代理服务器：在网游的数据库读写方面，通常有两种作法，一种是在应用服务器中直接加进数据库访问的代码进行数据库访问，还有一种方式是将数据库读写独立出来，单独作成数据库代理，由它统一进行数据库访问并返回访问结果。<br><br>　　其中，非场景服务器在不同的游戏项目中可能会被设计成不同的功能，比如以组队、公会或全频道聊天为特色的游戏，很可能为了满足玩家的聊天需求而设立单独的聊天服务器；而如果是以物品贸易（如拍卖等）为特色的游戏，很可能为了满足拍卖的需求而单独设立拍卖服务器。到底是不是有必要将某一项游戏功能独立处理成一个服务器，要视该功能对游戏的主场景逻辑（指行走、战斗等玩家日常游戏行为）的影响程度而定。如果该功能对主场景逻辑的影响比较大，可能对主场景逻辑的运行造成比较严重的性能和效率损失，那么应考虑将其从主场景逻辑中剥离，但能否剥离还有另一个前提：此功能是否与游戏场景（即地图坐标系统）相关。如果此功能与场景相关又确实影响到了主场景逻辑的执行效率，则可能需要在场景服务器上设立专门的线程来处理而不是将它独立成一个单独的服务器。<br><br>　　以上是一个服务器组内的各服务器组成情况介绍，那么，各服务器之间是如何通信的呢？它的基本通信构架有哪些呢？<br>　　MMORPG的单组服务器架构通常可以分为两种：第一种是带网关的服务器架构；第二种是不带网关的服务器架构。两种方案各有利弊。<br><br>　　就带网关的服务器架构而言，由于它对外只向玩家提供唯一的一个通信端口，所以在玩家一侧会有比较流畅的游戏体验，这通常也是那些超大规模无缝地图网游所采用的方案，但这种方案的缺点是服务器组内的通信架构设计相对复杂、调试不方便、网关的通信压力过大、对网关的通信模型设计要求较高等。第二种方案会同时向玩家开放多个游戏服务器端口，除了游戏场景服务器的通信端口外，同时还可能提供诸如聊天服务器等的通信端口。这种方案的主要缺点是在进行场景服务器的切换时，玩家客户端的表现中通常会有一个诸如场景调入的界面出现，影响了游戏的流畅感。基于这种方案的游戏在客户端的界面处理方面，比较典型的表现是：当要进行场景切换时，只能通过相应的&#8220;传送功能&#8221;传送到另外的场景去，或者需要进入新的场景时，客户端会有比较长时间的等待进入新场景的等待界面(Loading界面)。<br><br>　　从技术角度而言，笔者更倾向于将独立的服务器组设计成带网关的模型，虽然这加大了服务器的设计难度，但却增强了游戏的流畅感和安全性，这种花费还是值得的。<br>　　笔者在下面附上了带网关的MMORPG通信架构图，希望能给业内的朋友们一点有益的启迪。 
<img src ="http://www.cppblog.com/keigoliye/aggbug/95666.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-09 10:43 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/09/95666.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>目前我们的游戏服务器逻辑层设计草案（转帖于：云风的BLOG) </title><link>http://www.cppblog.com/keigoliye/archive/2009/09/09/95665.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 09 Sep 2009 02:42:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/09/95665.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/95665.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/09/95665.html#Feedback</comments><slash:comments>1</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/95665.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/95665.html</trackback:ping><description><![CDATA[<div class=entry-body style="FONT-SIZE: 10pt">
<p style="FONT-SIZE: 10pt">我们一开始的游戏逻辑层是基于网络包驱动的，也就是将 client 消息定义好结构打包发送出去，然后再 server 解析这些数据包，做相应的处理。</p>
<p style="FONT-SIZE: 10pt">写了一段时间后，觉得这种方案杂乱不利于复杂的项目。跟同事商量以后，改成了非阻塞的 RPC 模式。</p>
<p style="FONT-SIZE: 10pt">首先由处理逻辑的 server 调用 client 的远程方法在 client 创建出只用于显示表现的影子对象；然后 server 对逻辑对象的需要client 做出相应表现的操作，变成调用 client 端影子对象的远程方法来实现。</p>
<p style="FONT-SIZE: 10pt">这使得游戏逻辑编写变的清晰了很多，基本可以无视网络层的存在，和单机游戏的编写一样简单。</p>
</div>
<div class=entry-more id=more style="FONT-SIZE: 10pt">
<p style="FONT-SIZE: 10pt">本质上，这样一个系统跟网络包驱动的方式没有区别；但是从编码表现形式上要自然很多。正如 C 语言也可以实现面向对象，但却没有 C++ 实现的自然一样。在这个系统中，引擎封装了对象管理的部分，使得逻辑编写的时候不再需要处理讨厌的对象数字 id ；还隐藏了消息发送或广播的问题。</p>
<p style="FONT-SIZE: 10pt">我把玩家控制的角色，和服务器上你的角色分做两个东西。即，你控制的你，和服务器认为的你就分开了。服务器认为的你，你看见的服务器上的其他人是一类东西。操作自己的角色行动时，你通过 client 上的控制器的远程方法向服务器发送指令；而服务器通过远程调用每个角色的远程方法让 client 可以收到感兴趣的所有角色的行为。</p>
<p style="FONT-SIZE: 10pt">这样，client 永远都是通过一个控制器调用其远程方法来告诉服务器"我要干什么"，而服务器的逻辑层则通过调用其上所有逻辑对象的远程方法来改变每个对象的状态。而引擎就根据每个链接的需要，广播这些消息，使得每个 client 上对应的影子对象可以收到状态改变的消息。</p>
<p style="FONT-SIZE: 10pt">这些，就是半个月来我跟同事一起做的工作。当然，由于我们用脚本编写逻辑层，这样，脚本接口可以比 C 接口实现的漂亮的多。</p>
<p style="FONT-SIZE: 10pt">首先是自定义格式的接口描述文件，用自编写的工具自动编译成对应脚本代码。我们只需要在脚本中编写对应的类，就可以自动响应远端调用的方法了。而调用远程方法，也跟本地方法保持同样的形式，写起来跟本地函数调用没有区别。这在以前用 C/C++ 编写逻辑的时候是很难做到的。</p>
<p style="FONT-SIZE: 10pt">其次，引擎内部做好对象的管理工作，负责把通讯协议上的 id 转换成逻辑层中的对象传递给逻辑层使用。</p>
<p style="FONT-SIZE: 10pt">再次，enum 这样的类型再也不需要用一些数字的常数了，也不需要在脚本额外的定义出来。可以在接口文件中定义好，经过引擎的处理后，逻辑层可以直接用更为友好的字符串代替，而不失去效率。</p>
<p style="FONT-SIZE: 10pt">编写逻辑的程序员不再需要关心网络的问题后，就可以把心思放在细节上。</p>
<p style="FONT-SIZE: 10pt">最后，对于实现行为预测来补偿网络延迟的特性上。在先前的版本中，我们为了实现这个，花了不少的气力。主要是将时间戳信息放在基础通讯协议中来辅助实现。具体的消息包收到后，再计算延迟时间来推算当前的状态。现在，可以把时间信息封装到 RPC 中，让每个远程方法自动带有延迟时间，方便计算。按模拟程序的实际效果上看，单单位置同步的预测策略，可以让延迟在 8 秒之内的玩家可以忍受；而延迟小于 1 秒的时候，几乎不会受到滞后的影响了。</p>
<p style="FONT-SIZE: 10pt">关于每个链接感兴趣的信息的问题，决定了每个逻辑对象的状态改变要通知哪些人。目前的想法是独立到单独进程去处理，我们在处理连接的服务器和处理逻辑的服务器之间设置单独的服务器来管理每个链接感兴趣的对象，这个任务相对单一且责任重大，独立出来可以大大减轻逻辑服务器的复杂度。</p>
</div>
<img src ="http://www.cppblog.com/keigoliye/aggbug/95665.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-09 10:42 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/09/95665.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>MMORPG开发入门</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/09/95664.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 09 Sep 2009 02:41:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/09/95664.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/95664.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/09/95664.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/95664.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/95664.html</trackback:ping><description><![CDATA[<table class=t_table cellSpacing=0 width="98%" align=center>
    <tbody>
        <tr>
            <td width="80%"><font size=3>作者： Radu Privantu <br>译者：<a href="http://blog.vckbase.com/panic" target=_blank><font color=#0000ff><u>pAnic</u></font></a> 2005年5月11日<br>原文链接：<a href="http://www.devmaster.net/articles/building-mmorpg/" target=_blank><font color=#0000ff><u>http://www.devmaster.net/articles/building-mmorpg/</u></font></a><br>译者序：这是一篇讲解如何开发一款MMORPG的入门文章，作者本人也是一款游戏的开发者，文中的内容源于实践，有很高的参考价值。很多人都想拥有自己的游戏，这篇文章对那些想自己开发游戏的人来说可能是一纸福音，也可能是一盆冷水。无论如何，开发游戏都不是一件简单的事情。<br>以下是翻译正文：<br></font></td>
            <td width="2%">[/td]
            <tr>
                <td>文章的中心是如何起步开发你自己的 大型多人在线角色扮演游戏( 原文：Massive Multiplayer Online Role Playing Games) (MMORPG)(译者注：俗称：网络游戏，网游)。 针对的读者是经验和资源有限的开发者。 读完文章之后，你应该懂得如何起步， 还有一些关于什么是应该做的和不应该做的忠告。第一步是评估你的能力和资源。你必须对自己诚实，因为做你力不从心的事情会浪费你的时间并让你心灰意冷。<br><br>第一步：评估你的能力必须的技能:
                <ul type=1>
                    <li>懂至少一种编程语言。 迄今为止， C++因为性能和效率的优越性成为游戏开发者的首选。 Visual Basic, Java 或者 C# 可能也是不错的选择。<br>
                    <li>熟悉一种图形库。通常的选择是SDL, OpenGL, 或者DX/D3D。(<font color=#ff0000>译者注：网上也有很多免费/付费引擎下载和出售</font>)
                    <li>选择一种网络通讯库。 你可以从WinSock, SDL_net, 或DirectPlay中选择。(<font color=#ff0000>译者注：很多人喜欢开发自己独特的网络库，这并不复杂，似乎ACE也是一种选择</font>)<br>
                    <li>对游戏开发有大体的经验。 例如，事件循环， 多线程， GUI 设计，等等。 </li>
                </ul>
                强烈推荐的技能:
                <ul type=1>
                    <li>C/S结构通讯。
                    <li>多平台开发。 你可能希望设计一个MMORPG, 尤其是服务器能运行在多种操作系统。为此，我推荐使用SDL, OpenGL 和SDL_net。<br>
                    <li>网站开发。如果你想让用户通过网站查看玩家统计，服务器信息和其他信息，这是必须的。(<font color=#ff0000>译者注：其实网站可以交给其他人开发，如果有必要的话</font>)。
                    <li>安全管理。 你当然不想因为有人攻击你的服务器而浪费时间！
                    <li>团队组织能力。 你需要一个你能成功领导和管理的团队。<br></li>
                </ul>
                第二步：初步规划我注意到很多人在不同的论坛发帖子寻找团队开发MMORPG。他们中的大部分是这样：&#8220;我们成立了一个公司/游戏工作室，需要3个美工，两个程序，1个音乐制作，等等。为了创新，不要参考过去的MMORPG，你有全部的自由用来创造你想要的世界，等等。 我们会在项目完成并赚到钱的时候付给你酬劳，等等&#8221;。 不幸的是，以现有的技术和带宽，你无法拥有一个动态的世界。 朝向无法到达的目标前进只会导致失败。正确的做法是拿出一些小规模的，功能性强的，可扩展的设计和构架。<br>基本软件构架首先，尝试创建一个简单的C/S模型，有如下功能：<br>
                <ul type=1>
                    <li>创建一个新角色
                    <li>保存那个角色(服务器端)
                    <li>用那个角色登陆
                    <li>能够和其他人交谈
                    <li>能在3D空间游览 </li>
                </ul>
                保存角色看起来简单，其实不然。 例如，有两种方式保存角色：使用数据库服务或者使用文件。两者有各自的优缺点： <br><br>
                <table class=t_table cellSpacing=0 width="98%" align=center>
                    <tbody>
                        <tr>
                            <td width="10%"></td>
                            <td width="45%"><strong>数据库</strong></td>
                            <td width="45%"><strong>文件</strong></td>
                        </tr>
                        <tr>
                            <td><strong>优点</strong></td>
                            <td>
                            <ul>
                                <li>添加新域或者修改现有的都很简单。
                                <li>更新玩家统计数据非常简单(从游戏外)。
                                <li>你可以通过SQL查询方便的获取不同种类的统计结果。
                                <li>无需自行完成I/O操作，数据库会替你做好。 <br>
                                <li>易于更新/恢复。 </li>
                            </ul>
                            </td>
                            <td>
                            <ul>
                                <li>高速操作(读/写)。
                                <li>实现简单。
                                <li>无需额外的库。
                                <li>不依赖数据库服务器。因此你不必担心数据库升级或安全问题。 <br></li>
                            </ul>
                            </td>
                        </tr>
                        <tr>
                            <td><strong>缺点</strong></td>
                            <td>
                            <ul>
                                <li>容易出错。 例如，做一个更新查询的时候遗漏了'where'子句。会导致惨痛的损失，尤其是你没有备份的时候。
                                <li>数据库会比打开/写入一个玩家档案文件慢。你查询一些数据的时候会耗费几个毫秒，尤其是大量玩家同时登入/登出的时候。
                                <li>需要额外的代码进行游戏和数据库间的数据转换。
                                <li>需要操作数据库和SQL的经验。并且需要一个程序和数据库之间的接口库。
                                <li>如果因为某些原因数据库文件损坏，那算你倒霉，你可能会丢失所有的玩家数据(尤其是短期内没有备份的时候)。 </li>
                            </ul>
                            </td>
                            <td>
                            <ul>
                                <li>很难添加新的域，除非一开始就很小心的设计了文件的格式/结构。
                                <li>没法做全体玩家的查询。(这可以通过每天晚上用程序把重要字段添加进一个数据库间接实现)。
                                <li>如果你想更新/检查玩家状态，你必须额外写代码。 <br>
                                <li>更新和还原比较复杂。 </li>
                            </ul>
                            </td>
                        </tr>
                    </tbody>
                </table>
                现在你决定了如何存储角色，你还得选择C/S通讯的网络协议：TCP 还是 UDP？,我们都知道TCP速度慢，但是更准确，并且需要额外带宽。我实际使用TCP并没有遇到什么问题。 如果你有充足的带宽，TCP是个好选择，至少对初学者是这样。&nbsp;&nbsp;UDP 会很麻烦，尤其是对新手。 记住，游戏或引擎的初步测试会在你的局域网进行，所有的包都会按顺序依次抵达。在Internet上无法保证这一点。虽然包会按顺序到达，但是有时候会丢包，这通常是个麻烦事。 当然，你可以设计你的协议使得C/S能够从丢包中恢复。但这对初学者来说很痛苦，不值得推荐。 <br>第三步：选择数据传输协议又是看起来很简单，其实不然。你不能只是发送'\0'结尾的串。因为你需要一个通用的协议，能同时适用字符串和二进制数据。用0(或其他字符)做结束符是不明智的，因为那个结束符可能是你要发送的数据的一部分。此外，如果你发送20字节，然后再20字节，服务器极有可能收不到两个20字节的包。取而代之的是，它会一次性收到40字节，这是为了避免浪费带宽在不必要的头上。 而且，你可以发送1KB的包，但服务器会以两个小包的形式收到它。所以你必须知道哪里是一个包的开始，哪里是结束。在 &#8220;永恒大陆&#8221;(<font color=#ff0000>译者注：原文： Eternal Lands，本文的作者正在开发的一款MMORPG</font>)中，我们用如下的方法：<br>
                <ul>
                    <li><strong>Offset 0</strong>: 1 字节 表示传输的命令。
                    <li><strong>Offset 1</strong>: 2 字节，传输的数据长度。
                    <li><strong>Offset 3</strong>: 变长，消息内容。 </li>
                </ul>
                这种方法有一致的优点：所有的数据传输有统一的标准。缺点是有些命令有固定已知的长度，浪费了一些带宽。以后我们会改成混合的方法。<br>下一件事是决定服务器模型： &#8220;非阻塞soket,不使用线程&#8221;，或者&#8220;阻塞soket，使用线程&#8221;。两种方法(使用线程 vs 不使用线程)各有优缺点。<br><strong>线程:</strong><br>
                <ul type=1>
                    <li>服务器响应会更加平滑，因为如果一个玩家需要大量时间(例如从数据库中读取数据)，这会在它自己的线程中完成，不会影响其他人。(<font color=#ff0000>译者注：也许作者的意思是每个玩家都有独立的线程，但这对MMORPG不太现实</font>)
                    <li>难以恰当的实现和调试：你可能需要大量同步，并且一个小疏忽就会导致灾难性的后果( 服务器瘫痪，物品复制，等等)。
                    <li>可以利用多处理器。 </li>
                </ul>
                <strong>无线程:</strong>
                <ul type=1>
                    <li>实现和调试更简单。
                    <li>响应速度慢。 </li>
                </ul>
                在我的公司，我们使用无线程的方法，因为我没有足够的资源和人力处理线程模式。<br>第四步：客户端你打算做2D还是3D游戏？有些人认为2D游戏做起来简单。我两者都做过，并且我倾向于3D游戏更简单。容我解释。<br>2D下，你通常有一个帧缓冲，也就是一个巨大的象素点数组。象素点的格式会因显卡的不同而不同。 有些是RGB模式，另一些是BGR模式，等等。每种颜色的bit数也会不同。只有在16bpp模式才有这个问题。8-bit和24-bit模式简单一些，但有他们各自的问题(8-bit颜色数太少(256)，而24-bit速度更慢)。同时，你需要制作你的精灵动画程序，不得不自己排序所有对象，以便他们以正确的顺序绘制。 当然，你可以用OpenGL或者D3D制作2D游戏，但通常这并不值得。并不是所有人都有3D加速卡，所以使用3D库开发2D游戏一般会带给你两者的缺点：不是所有人都能玩，你也不能旋转摄像机，拥有漂亮的阴影，和3D游戏炫目的效果。(<font color=#ff0000>译者注，目前绝大部分显卡都支持565的16bpp格式，这个也成为目前16位色的业界通用格式，有不少文章和代码都是讲述这一格式下图像处理的，尤其是使用MMX技术</font>) <br>3D的途径，正如我所说，更简单。但是需要一些数学(尤其是三角)的知识。现代的图形库很强大，免费提供了基本的操作(你不需要从后到前排列对象，改变物体的色彩和/或帖图都十分简单，对象的光照会按照光源和它的位置计算(只要你为它们计算了法向量)，还有更多)。并且。3D给了你的创作和运动更多的自由度，缺点就是不是所有人都能玩你的游戏(没有3D卡的人数可能会让你大吃一惊的)，并且，预渲染的图片总是比实时渲染的更漂亮。(<font color=#ff0000>译者注：市面上想买不支持3D的显卡目前很困难，只是高性能的3D卡价格也不低</font>)<br>第五步：安全显然，不能相信用户。任何时候都不能假设用户无法破解你精巧的加密算法(如果你使用了的话)或者协议，用户发送的任何信息都要通过验证。极有可能，在你的服务器上，你有固定的缓冲区。例如，通常有一个小(可能是4k)缓冲区用来接收数据(从soket)。恶意用户会发送超长数据。如果不检查，这会导致缓冲区溢出，引起服务器瘫痪，或者更坏的，这个用户可以hack你的服务器，执行非法代码。每个单独的消息都必须检查：缓冲区是否溢出，数据是否合法(例如用户发送&#8220;进入那扇门&#8221;，即使门在地图的另一端，或者&#8220;使用治疗药水&#8221;尽管用户没有那种药水，等等)。 我再次强调，验证所有数据非常重要。一旦有非法数据，把它和用户名，IP，时间和日期，和非法的原因记录下来。偶尔检查一下那个记录。如果你发现少量的非法数据，并且来自于大量用户，这通常是客户端的bug或者网络问题。然而，如果你发现从一个用户或者IP发现大量非法数据，这是明显的迹象表明有人正在欺骗服务器，试图hack服务器，或者运行宏/脚本。同时，决不要在客户端存储数据。客户端应该从服务器接收数据。换句话说，不能发送这样的消息&#8220;OK，这是我得物品列表&#8221;或者&#8220;我的力量是10，魔法是200，生命值是2000/2000&#8221;。 而且，客户端不应收到它不需要的数据。例如：客户端不应该知道其他玩家的位置，除非他们在附近。 这是常识，给每个人发送所有玩家会占用大量带宽，并且有些玩家会破解客户端从中获取不公平的利益(像在地图上显示特定玩家的位置)(<font color=#ff0000>译者注：就像传奇的免蜡烛外挂</font>)。所有这些似乎都是常识，但，再次，你会惊奇的发现有多少人不知道这些我们认为的常识。 <br>另一个要考虑的问题，当涉及到安全：玩家走动的速度必须在服务器计算，而不是客户端。(<font color=#ff0000>译者注：这是重要的原则，但是会耗费大量服务器资源。魔兽世界没有这样做，它采用类似其他玩家揭发的形式掩盖这个事实，导致加速外挂可以用，但是在有其他玩家的时候会暴露</font>)。服务器应该跟踪时间(以ms为单位)当客户最后一次移动的时候，并且，移动的请求如果比通常的极限更快到来，这个请求应该被抛弃。不要记录这类虚假请求，因为这可能是因为网络延迟(也就是玩家延迟，过去的10秒内发送的数据同时到达了)。<br>检查距离。如果一个玩家试图和100亿公里以外的玩家交易(或者甚至在另一张地图上)，记录下来。如果一个玩家试图查看，或者使用一个遥远的地图对象，记录它。小心假的ID。例如，正常情况下每个玩家都会分配一个ID(ID在登陆的时候分配，可以是持久的(唯一ID)。 如果ID在玩家登陆的时候赋予9或怪物被创建的时候)，显然可以用玩家数组(保存玩家)的位置(索引)作为ID。<br>所以第一个登陆的玩家ID是0，第二个是1，依此类推。现在，通常你会有一个限制，比如说2000个索引在玩家列表里。所以如果一个客户端发送一条命令类似：&#8220;查看ID200000的角色&#8221;，这会使服务器当机，如果没有防备的话，因为服务器会访问非法的内存区域。所以，一定要检查，就像这样： "if actor id&lt;0 or if actor id&gt; max players 然后记录非法操作并且断开玩家。如果你使用C或者C++，注意或者定义索引为'unsigned int' 并且检查上限，或因为某些原因定义为int(int,默认是有符号的)，记得检查 &lt;0 and &gt;max 。没有做这些会严重挫伤你和其他用户。类似的，要检查超出地图坐标。如果你的服务器有某种寻路算法，并且客户端通过点击地面来移动，确保他们不要点击在地图外部。 <br>第六步：获得一个团队制作游戏需要大量的工作(除非是个Pong and Tetris游戏)。尤其是MMORPG。你无法单靠自己。理论上，一个完整的团队组成是这样：<br>
                <ul>
                    <li>至少<strong>3 个程序员：</strong> 1 个做服务器，两个客户端(或者一个客户端，一个负责工具，例如美术插件，世界编辑器，等等)。有6个程序员是最好的，更多就没必要了。这取决于你的领导能力。最少一个美工，2到3个更合适。如果这是个3D游戏，你需要一个3D美工，一个2D美工(制作帖图，界面，等等)，一个动画师，和一个美术部负责人。美术部应该由有经验的人组织和安排，除非你就是个艺术家。,
                    <li><strong>少数世界构建者:</strong>创建所有地图是个漫长的过程， 并且直接关系到游戏的成败。再次，你需要一个世界构建部的负责人。你的世界需要协调一致，所以不能只有一个意气用事的人。
                    <li>一个 <strong>网站管理员</strong>是必须的，除非你精通网站设计，并且愿意花时间做网站。音效和音乐不是必须的，但是有音效和音乐的游戏比没有的会更吸引人。
                    <li>一个游戏经济系统 <strong>设计师</strong>.。你也许觉得那很简单，可以自己来做，但事实上那是最复杂的工作之一。如果经济系统设计不良(比如物品没有平衡，资源在地图上随意放置，等等。)玩家会觉得无聊并且退出游戏。我们早期的进展存在很大的问题，尤其是因为经济系统主要是由我(一个程序员)设计的，它没有被恰当的计划。 于是，我们花费了两个月来重新思考和建立一整个新的经济系统。这需要一次完全的物品清除。我告诉你，玩家会很不乐意你删除他们的物品。幸运的是，大部分玩家赞同这个想法，但是这么多小时的争论，妥协，解释和时间的浪费还是让我们丧气。以后会更多。 <br></li>
                </ul>
                如前所说，你需要一个10~15人的团队，不包括协调员和管理者。这10~15人必须是有经验的。如果都是新手就不值得，因为你需要花大量时间解释要做什么，怎样做，为什么他现在的做法不好，等等。<br>一开始就凑齐10~15人几乎是不可能的。不管你在不同的论坛发多少帖，你也无法找到合适的团队成员。毕竟，如果一个人在他/她的领域得心应手，为什么在你无法拿出任何东西的时候他/她要加入你的团队？很多人有远大的想法，但是实现它们需要大量时间和努力，所以他们宁可从事自己的工作也不会加入你。那如果你需要10~15人，但是无法让他们加入你的团队，你如何才能制作一款MMORPG呢？ 好，事实上，你一开始不需要所有人都到位。你真正需要的是一个程序员和一个美工。如果你是个程序员，只要找个美工就可以了。请求懂美术的朋友帮忙，花钱请大学生/朋友做一些美术或者其他工作。<br>现在你有了一个美工，你期待的游戏的样子，现在可以开始实现了。一旦你有了可以运行的C/S引擎，一些用来展示的截图(或者更好，玩家可以登陆你的世界，四处走动，聊天)，更多的人会愿意加入你的团队。更恰当的是，除非你使用独有的技术，否则你的客户端可以开源。许多程序员会加入(作为志愿者)一个开源工程而不是非开源项目。而服务器不应该开源(除非你打算做一款完全开源的MMORPG)。<br>其他一些忠告：在有东西可展示之前，不要夸大你的游戏。最惹人烦的事情之一就是一个新手发一个&#8220;需要帮助&#8221;的请求，并且解释这个游戏到底有多酷，最后要求一个巨大的团队加入他的游戏制作。一旦你拥有了网站广告(通常是在一个免费主机)，你会看到一个吸引人的导航条，包含&#8220;下载&#8221;，&#8220;截图&#8221;，&#8220; 原画&#8221;(译者注，原文：Concept art，概念艺术，在游戏应该指美工的原始设计)，&#8220;论坛&#8221;。你点击下载链接，然后看到美妙的&#8220;建设中&#8221;页面(或者更糟糕，一个404错误)。然后你点击截图，得到同样的结果。如果你没有东西给人下载，就不要放下载链接。如果没有截图展示，不要放截图链接。然而更好的是，在工程进展10%(程序和美工)之前，不要浪费时间在网站上。 <br>第七步：打破某些神话
                <ul type=1>
                    <li><strong>你无法制作MMORPG, 只有大公司才可以。</strong><br>我不同意。虽然制作一款像魔兽世界(World of Warcraft)，无尽任务2(Ever Quest 2)，亚瑟王的召唤2(Asheron's Call 2)，血统2(Lineage 2)，和其他一些游戏对一个小型的自发团队是不可能的，但是做一款像样的游戏还是可以的，只要你有经验，动机，和时间。,你需要1000小时的编程来制作一个可运行的测试版，大概10~15k小时完成几乎完整的客户端和服务器。但是作为团队领导者，你不能只编程。保持团队团结，解决争执，维护公共关系(PR)，技术支持，架设服务器，惩罚捣乱分子，自由讨论，等等都是你的职责。你可能会被非编程的任务淹没。你很可能需要上班/上学，这减少了你花费在项目上的时间。我们很幸运，没有成员离开团队，但是如果这种事情发生，那的确是大问题。假设你的美工半途离开。或者更糟糕，他/她没有给你使用他/她作品的许可。当然这可以通过和他们签订合同来解决，但找另外一个美工仍然很麻烦。一个工程中有两种不同的美术风格也是问题。
                    <li><strong>需要大笔金钱(通常 4-6 位数) 用来架设一个 MMORPG 服务器.<br></strong>当然，这不是真的。我见过专业服务器，1000GB/月，不到100美元/月(2~300美元的初装费)。除非你的数据传输协议设计非常不合理，1000GB/月对一个1000玩家在线(平均)的服务器来说足够了。当然，你还需要另一个服务器做网站和客户端下载(客户端下载会占用大量流量，当游戏变得流行的时候)。我们的客户端有22MB，有时候会有400GB/月的传输量。而我们还没有很流行(仍然)。另一件事，我们不需要另一台专用服务器开启这个工程。ADSL/cable服务器可以胜任，直到你的同时在线人数达到20~30。然后要么找一个友好的主机公司，用广告交换免费主机，要么就只能自己掏腰包了。&nbsp;&nbsp;<br>
                    <li><strong>制作一个MMORPG很有趣。<br></strong>这不是真的。你可能认为每个人都会赏识你，玩家会支持你，你标新立异，并且，当然，很多玩家都玩你的游戏。玩家可能让人讨厌。即使是完全免费的游戏，他们也能找到理由抱怨。更糟糕的是人们经常会抱怨矛盾的事。战士会抱怨升级太难，商人会对战士掠夺大量钱财很失望。如果你减少怪物掉落物品，有些玩家就会威胁说要退出游戏。如果你增加，同样的一群人会不满新手能更简单赚钱的事实。 真是左右为难。改革和改进是必须的。如果你决定改变某些东西，例如给加工物品增加挑战性，有些人会说太难了。如果你不做，他们又会说太简单无味。你会发现满意的玩家通常不会说什么并且感到满意，同时破坏者会怨声载道。<br></li>
                </ul>
                MMORPG的经济比单机版难以平衡的多。在单机游戏，你可以逐渐改良武器，只要玩家进展，他/她可以使用更好的装备，丢弃(或者卖掉)旧的。另一方面，在多人游戏里，这种观点不成立，因为每个人都试图得到最好的武器，而跳过低等级武器。大部分玩家宁可空手省钱，直到他们能买游戏中最好的武器。经济系统设计要参考相关的文章。<br>迄今为止我列举的所有事情，加上额外的工作和挑战，足以让你在决定涉足这个工程之前三思而行。你必须知道你的决定意味着什么。<br>总结希望这篇文章能给你足够的知识。我的下一篇文章将介绍如何建立一个经济系统(更明确的，要避免哪些错误)，还有一些调试服务器和客户端的信息。<br>关于作者这篇文章作者是 Radu Privantu, 永恒大陆(Eternal Lands) <a href="http://www.eternal-lands.com/" target=_blank><font color=#0000ff><u>www.eternal-lands.com</u></font></a>的主程序和项目规划, 永恒大陆是一款免费，客户端开源的MMORPG。作者可以通过 chaos_rift at yahoo dot com 联系。</td>
            </tr>
        </tbody>
    </table>
<img src ="http://www.cppblog.com/keigoliye/aggbug/95664.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-09 10:41 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/09/95664.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>服务器宕机(转)</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/09/95663.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 09 Sep 2009 02:40:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/09/95663.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/95663.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/09/95663.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/95663.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/95663.html</trackback:ping><description><![CDATA[<p>我不得不承认，我的能力不足以写出一个100%不会宕机的游戏服务器程序，这也不能全怪我的能力太弱，谁让咱国内网游玩家数量庞大，哪个游戏刚上线时没有挤的爆满过？还有些或是猎奇，或是谋私的个人和组织，在制造着千奇百怪，匪夷所思的数据包及操作流程来试探你的服务器。这些都曾是我在服务器宕机后向老板开脱的理由。</p>
<p>&nbsp;</p>
<p>当WOW终于来到中国时，我一边欣喜着终于可以在艾泽拉斯的大陆上自由翱翔，一边却咒骂着9C的破服务器，动不动就宕机。当然，身为游戏程序设计师的我明知道，这大部分的错误都不应归罪于代理商9C，但是，谁让blizzard是我心目中的神，谁又让WOW成为我游戏制作的教科书呢。好吧，我知道上面这段极力追捧blizzard跟WOW的话可能早已让你恶心连连，不堪入目了，对不起，忘了这一节，让我们继续。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>服务器宕机后都发生了些什么？</strong></p>
<p>&nbsp;</p>
<p>显然的，宕机后玩家会骂，就像我在玩WOW时那样，骂游戏公司，骂老板，骂GM。非常抱歉，我们可爱的玩家们似乎并不清楚，这个时候最该骂的其实是我们这些程序员们。长久的遗忘被我们当成了包容，以至于游戏程序员在公司里都养成了趾高气扬，不可一世的坏毛病：看吧，策划们，你们做的太烂了，数值不平衡，玩法没新意，只会照抄WOW跟大菠萝，能怪玩家骂你们吗？运营不得力，买服务器的钱不知道去了哪里，游戏里卡的要死，偶尔办个活动还没半点吸引力，能不被玩家骂你是无良运营商吗？GM们能不天天被骂家指着骂吗？&#8230;&#8230;呃，又扯远了。</p>
<p>&nbsp;</p>
<p>赶紧先把服务器重启吧。老板正站在你的身后，一脸愁容，虽然暂时还没有发作，但看得出来：老板很生气，后果很严重！</p>
<p>玩家们很快又回来了，不得不为玩家们的毅力和执着精神而感动，更为自己的错误而愧疚，凌晨时分，服务器启了又宕，宕了又启，如此反复，可热情的玩家们依然陪着我在折腾。哦，当年安其拉开门的时候，我也曾这样折腾过。</p>
<p>这个时候不是你一个人在战斗。GM们在忙碌地处理着玩家不断打来的投诉电话：刚买的装备在宕机后消失了；花光了身上所有材料合成的武器回档了，但材料却没有还给我&#8230;&#8230;数据库维护组的同事们也在紧张的恢复着数据，尽可能的将玩家的损失减到最少。</p>
<p>&nbsp;</p>
<p>真是一件令人沮丧的事。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>真的该试着做点什么了吧！</strong></p>
<p>&nbsp;</p>
<p>既然我们非常不愿意看到宕机的情况发生，但又无法100%保证写出来的服务器程序一定不会出错，那我们就在当机发生后的抢救措施上花点功夫，让玩家的损失不至于太大，也让我们的维护人员少些压力吧。</p>
<p>&nbsp;</p>
<p>一个最简单也最有效的做法是为每一台服务器都配备物理冗余，同步更新冗余服务器上的状态，当宕机发生时，立即将处理切换到后备服务器上。只是，物理冗余的代价太大，从成本方面考虑，老板可能不大愿意点头。</p>
<p>&nbsp;</p>
<p>既然不能做硬冗余，那就再来考虑软的吧。</p>
<p>如果只是简单的启动冗余进程，其实是换汤不换药的做法。原来能跑1000人的服务器，由于同时运行了两个相同的进程，使得CPU和内存开销都翻了倍，结果是只能跑500人了。还是要加服务器。</p>
<p>&nbsp;</p>
<p>看来只能更深一层，从架构设计上来动手了。</p>
<p>假设我们的游戏世界是由多个独立场景构成的，那么在实现上我们可以让这些场景在进程上也独立，这样做的好处是可以使得一个场景的宕机不会影响到其他场景的正常运行。如果我们的游戏世界物理上没有分隔，是一个无缝的大世界，我们也可以人为的将其分成多个独立区域，所需要做的额外工作是处理好那些站在区域边界附近的对象。事实上，现在的无缝大世界也都是这样实现的。</p>
<p>&nbsp;</p>
<p>有了这样一个前提，我们再来看这个已宕掉的场景该如何处理。</p>
<p>还是老办法，赶紧先把它拉起来吧。一个具体可行的方案是，由场景管理器，或者你也有可能叫它世界服务器，来监视各个场景进程的运行状态，当某个场景异常失去联系时，由管理器来将其重新启动。这里需要再花点心思的是，如何让玩家数据正常地发送到新启动的场景进程中，而且这个过程对于客户端来说是透明的。</p>
<p>&nbsp;</p>
<p>这个方案听起来似乎不错，只是，如果宕掉的是场景管理器进程，那该怎么办呢？</p>
<p>按照前面的描述，场景管理器可以看作是整个游戏世界的中心，它以一个指挥者的身份维护着游戏世界的有序运行，所以它的宕机对整个游戏世界的影响也将会是巨大的。</p>
<p>有没有什么办法能够使得场景管理器进程再次启动后能够恢复先前的状态呢？</p>
<p>我们可以为管理器和场景进程定义一套协议，使得管理器不仅能够创建并恢复一个已有场景，而且场景管理器还能通过现有的场景进程数据恢复出自己。</p>
<p>一个理论上可行的方案是，场景管理器与场景进程间保持TCP长连接，并以一定频率进行心跳联系，任意一方发现联系中断或长时间未收到心跳包后都会立即做出处理。</p>
<p>如果是管理器发现场景进程失去联系，那就启动新的场景，如前面所描述的那样。如果是场景进程发现管理器失去联系，那就立即启动重连过程，直接再次连接上管理器，然后立即将自己当前的状态和负责的场景ID报告给管理器。管理器通过这些上报的数据就能恢复出游戏世界内的场景对应关系表，也就是恢复出了自己原来的状态。</p>
<p>&nbsp;</p>
<p>进程是恢复出来了，可我们忘了最重要的内容：数据。当场景进程宕机后，上面保存的玩家属性数据也随之丢失了，虽然我们能够再次将这个场景创建出来，并把原来在这个场景内的客户端数据重新定向过来，但这些客户端对应的玩家对象的数据却没有了，游戏仍然无法继续。</p>
<p>&nbsp;</p>
<p>也许我们可以再做一点修改，把场景内的玩家数据分离出来，保存到一个独立的进程上，比如，我们可以把这个进程叫做数据服务器，或者数据中心之类的。一个隐含的要求是，数据服务器的逻辑实现非常简单，简单到你可以认为它是绝对安全的，不会宕机。所以，保存在这里的玩家数据也就是绝对安全的。</p>
<p>&nbsp;</p>
<p>让我们在这个问题上稍微再深入一点。</p>
<p>场景进程上每次执行玩家的游戏逻辑时都要异步地到数据服务器上来存取数据，这个开销可能太大，而且会使得一些游戏逻辑的实现变的很复杂，那么，把一些会频繁使用到的数据直接保存在场景进程中，当数据发生改变时同步更新到数据服务器上，这样可能会比较容易接受。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>老板全都满意了吗？</strong></p>
<p>&nbsp;</p>
<p>从理论上来说，我们已经解决了场景进程宕机和管理器宕机后的状态恢复问题，并且在场景恢复后也不会因为丢失了玩家数据而无法继续进行游戏，而且，只要处理得当，这个过程对客户端来说可以是完全透明的，也就是玩家根本不知道服务器上有个进程意外结束，我们做了这么多的工作来将它恢复了。</p>
<p>&nbsp;</p>
<p>事实上，这个过程的透明也是必须的，我们并不需要嚷嚷着告诉我们的用户，也就是玩家，我们做了多少多少事情来让你玩的更顺畅，又花了多少多少精力来解决因为服务器宕机而引起的麻烦，对于最终的用户来说，他只需要享受最好的服务。闲话少说，让我们继续。</p>
<p>&nbsp;</p>
<p><strong>真的已经完全解决了所有问题吗？</strong></p>
<p>想象这样一个场景：我带着几个刚刚降临到艾泽拉斯大陆的伙伴冲向了艾尔文森林，去开荒霍格！正在霍格只剩下一丝血的时候，服务器稍稍卡了一下，等我缓过神来，面前的霍格骤然消失，地上也不见尸体。找了一圈，它正在出生点摇头晃脑，也在四处张望，但头顶上的血条分明是，满血！</p>
<p>怎么回事？</p>
<p>处理这张地图的场景进程意外结束了，服务器的宕机处理机制很快地恢复了这个场景进程，并且把我的客户端数据重新定向到了新场景。只是，事情并不是一切都完美。因为这个场景是完全重新创建的，这意味着所有的怪物也是重新创建并被摆放到了初始位置，所以，只剩下一丝血的霍格碰上了好运气&#8230;&#8230;</p>
<p>&nbsp;</p>
<p>类似的还有，正在护送NPC返回营地，在稍微停顿了一会儿之后，NPC又重新回到了原来的地方，等等。</p>
<p>&nbsp;</p>
<p>虽然这比起最初的&#8220;客户端被迫断开连接，服务器端数据丢失&#8221;要进步了许多，但会给我工资的老板仍然可能不太满意，他希望，霍格应该还在我的面前，而且只有一丝血，那个跟着我的NPC也应该还在我旁边&#8230;&#8230;</p>
<p>&nbsp;</p>
<p>我要是不能说服老板，这是&#8220;根本不可能完成的任务！&#8221;，那也就只能坐下来再试一试。</p>
<p>也许，可以考虑将所有对象的数据都保存到数据服务器上，当然，这要求每个怪物都跟玩家一样，有一个唯一ID，这一点实现起来可能会有些麻烦。</p>
<p>再不然，为对象提供一个从已有的内存数据构造的方法，这样便可以使用共享内存来保存现场数据，再从共享内存中恢复出原来的对象。理论上来说，这个方法是可行的，只是，这三十多个字的文字描述要用C++来实现也可能将会是一项极大的挑战，所以，这也仅只是可供参考的一个尝试方案。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>我想，我们走的够远了</strong></p>
<p>&nbsp;</p>
<p>让我们先暂停一会儿，回过头来看一看最初的目的。其实我们想要的只是尽可能的让服务器进程不要宕机，如果实在是没有办法，就尽可能的让宕机后的玩家损失比较小，不需要我们做大量的工作去做善后处理。</p>
<p>很简单的需求，似乎我们纠缠的有些过头了。</p>
<p>&nbsp;</p>
<p>写出能够稳定运行的程序是对程序员的最基本要求，如果一个程序连稳定性都不具备，那根本都不用再去考虑功能啊、扩展啊等其他标准了。但是，正如我最开始所说的，没有一个人能够100%保证他写出来的服务器程序是绝对不会崩溃的。我们所能要求的只是尽可能的仔细，尽可能的多一些必要的测试，离安全尽可能的更近一步。</p>
<p>&nbsp;</p>
<p>剩下的就是在宕机后如何降低损失的问题了。</p>
<p>对于一般的MMOG来说，玩家在进入游戏时会从数据库中将该玩家的所有相关数据读到内存，以便快速的进行游戏逻辑的处理，而在玩家下线时再将数据的改动存回数据库。</p>
<p>显然的，当服务器进程出现意外宕机时，内存中所有的数据都丢失了，这也就造成了玩家数据的回档，而且玩家在游戏中呆的时间越长，回档的损失就越大。所以，一个被广泛采用的做法是为玩家数据实现一种定时存盘的机制，就像现在大多数的单机游戏一样，AutoSave。比如，每5分钟自动为玩家存一次盘，这样就可以使得回档的最大损失控制在5分钟以内。</p>
<p>另外，对于一些重要数据的变动，比如玩家花大量游戏货币购买了一件贵重的武器装备，这时可将玩家数据立即做一次存盘操作，这也将有效的减少玩家的重大损失。</p>
<p>&nbsp;</p>
<p>听起来这是一项不错的技术，在意外宕机的时候最多只回档5分钟，而且还没有贵重物品的损失，玩家应该是可以接受的吧。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>我已经听到了数据库维护员的咆哮</strong></p>
<p>&nbsp;</p>
<p>&#8220;数据库已经快要崩溃了，你就不能让每秒需要执行的SQL语句少一点吗？&#8221;</p>
<p>&#8220;呃&#8230;&#8230;&#8230;&#8221;</p>
<p>&nbsp;</p>
<p>我一直以为我们的数据库非常强大，可以处理任何的数据，唯一的缺点就是查询速度比直接内存读取要慢很多。所以我使用了异步数据存取的方法，并且开启了多个数据库操作线程来并行的执行我的请求，运行的效果看起来还不错。</p>
<p>&nbsp;</p>
<p>也许，我应该来算一算，每秒种究竟丢了多少条操作请求给数据库。</p>
<p>&nbsp;</p>
<p>请允许我再自私一回，我已经很久没有提到WOW了&#8230;&#8230;</p>
<p>大概可信的数字是，WOW一组服务器的玩家数量在3000到5000之间，去掉最大的数，再去掉最小的数，最后的平均值是，4000吧，就算4000。</p>
<p>4000人在线，假设也是每5分钟定时存盘一次，再假设所有玩家的存盘时间是平均分布的。这样算下来，每秒种就会有67个玩家向数据库发出存盘请求操作。</p>
<p>&nbsp;</p>
<p>才67个，数据库维护组的同事就跟我说不堪重负了？笑话，这数据库服务器是谁买的？</p>
<p>先别急，67是玩家数，但是每个玩家的存盘请求不会只有一条SQL语句。</p>
<p>&nbsp;</p>
<p>虽然每个游戏的内容都各有差别，但是一款MMOG需要存入数据库的数据少不了会有技能、物品、任务、宠物、好友、公会这些东西。取决于游戏的类型差异，每个游戏都会有自己的存盘方式，比如我可能会把所有的技能ID作为一条数据来存储，但是我也有可能把每个技能作为一条单独的记录来存储，这样可以方便对技能附加数据的扩展，等等。</p>
<p>&nbsp;</p>
<p>但是，游戏中的物品存储大概都是相同的，只能是一件(组)物品作为一条记录来存储。</p>
<p>而且，可以说游戏中存储量最大的就是物品数据。算一算你的角色背包有多大，50格？&nbsp;100格？还是200格？不要忘了银行、摆摊位、装备拦、宠物背包和邮箱这些地方也能放物品。并且，在游戏进行过程中，玩家背包中物品数据的变动也是相当的频繁，不断的有药品被用掉，不断地又有些小玩意儿被捡起来，不久后，它们又被卖给了NPC。</p>
<p>&nbsp;</p>
<p>虽然你可以使用一些巧妙的比较算法来过滤掉那些实际上没有发生变动的物品更新，另外也不是所有的玩家物品数据变动都很频繁，但在实际运营中，尤其是当玩家的背包格数都很多的时候，物品数据的存盘的确会成为一个很大的问题。</p>
<p>&nbsp;</p>
<p>除了物品，还有玩家的基本属性存盘，社会关系存盘等等，再加上全局公共数据的存盘，如公会数据，拍卖行物品数据，如果老板也要我在游戏中开上一家拍卖行的话。</p>
<p>&nbsp;</p>
<p>这么一算下来，似乎是有些多了。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>再一次的挑战</strong></p>
<p>&nbsp;</p>
<p>具体的数字将取决于游戏的类型和设计的数据表结构。</p>
<p>而数据库服务器能承担的每秒查询数则取决于数据库服务器的软硬件配置情况。</p>
<p>但是一般来说，数据库维护人员可能会告诉我，当每秒执行的SQL语句数达到1000条时，数据库服务器将会感受到明显的压力，我可能也会看到数据库执行队列中的请求数一直在增长，也可能会看到数据库服务器间歇性地拒绝响应，等等。</p>
<p>&nbsp;</p>
<p>看起来我们又一次的面对到了巨大的打冷战。</p>
<p>这个问题的起因是什么？我们不希望服务器进程宕机时回档太久，所以我们增加了一个玩家数据定时存盘的机制，结果却导致了数据库请求的骤然增多。</p>
<p>那再退回到这个起点处，将定时存盘的时间间隔延长点，比如10分钟才存一次？数据库的压力会有缓解，但最初的问题却又会有所暴露。真是个两难的问题。</p>
<p>&nbsp;</p>
<p>既想要玩家数据存盘间隔时间短一点，又不想给数据库造成的压力太大。</p>
<p>同样的需求似乎出现过很多回了：在中间加一层代理做缓冲。我们姑且称这一层代理为数据库代理服务器，它所要完成的工作是从场景进程收集玩家的定时存盘请求数据，再以一个低一点的频率写入到数据库。</p>
<p>&nbsp;</p>
<p>听起来这又像是一个换汤不换药的做法，写入数据库的时间间隔还是变长了。但实际上在前面我们就曾经描述过，如果服务器进程不会出现意外的宕机，玩家数据只需要在他上线时读取，在他下线时写入即可，中间添加的这些定时存盘过程完全只是为了防范宕机回档所造成的巨大损失。</p>
<p>&nbsp;</p>
<p>因为这个中间代理层的加入，我们把场景进程宕机的隐患与数据丢失的后果隔离开来了，现在即使场景进程宕机，数据还在数据库代理服务器上，当然这里又隐含了一个条件：数据库代理服务器需要足够稳定，不会在场景进程之前先宕掉。事实上，因为这个代理进程的工作是，我们完全有理由相信，这个进程是非常稳定的，那样的话，多久时间才把缓存的数据真正写入数据库，就看你自己的喜好了。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>该结束了吧</strong></p>
<p>&nbsp;</p>
<p>是否有些似曾相识的感觉？</p>
<p>没错，前面我们曾经描述过一个数据服务器，也是这样说的。</p>
<p>&nbsp;</p>
<p>所以，数据服务器，数据库代理服务器可以合并到一起，来共同保证数据的安全。</p>
<p>再加上场景进程与管理器进程的恢复协议，让服务器的重启对玩家保持透明。</p>
<p>看起来这个晚上可以睡个安稳觉。</p>
<img src ="http://www.cppblog.com/keigoliye/aggbug/95663.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-09 10:40 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/09/95663.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>MMORPG中游戏世界的构建 </title><link>http://www.cppblog.com/keigoliye/archive/2009/09/09/95662.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 09 Sep 2009 02:38:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/09/95662.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/95662.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/09/95662.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/95662.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/95662.html</trackback:ping><description><![CDATA[<p>Author: Fox<br>原文地址：<a href="http://www.cppblog.com/Fox/archive/2007/12/16/game_world_architecture.html"><u><font color=#0000ff>http://www.cppblog.com/Fox/archive/2007/12/16/game_world_architecture.html</font></u></a><br><span><br>一个</span><span>MMORPG</span><span>（</span><span>Massively Multiplayer Online Role Playing Game</span><span>）的架构包含客户端和服务器两部分。客户端主要涉及计算机图形学、物理学、多媒体技术等，服务器主要涉及网络通信技术、数据库技术，而人工智能、操作系统等计算机基础学科知识的应用体现在</span><span>MMORPG</span><span>开发过程中的方方面面。</span></p>
<p><span>一、游戏世界的划分</span> </p>
<p><span>理想状态的游戏世界仅由一个完整的场景组成，在《魔兽争霸</span> <span>III</span> <span>》、《</span> <span>CS</span> <span>》这样的单机游戏中，所有玩家位于该场景中，在理论上，位于该场景中的任意玩家都可以看到游戏中所有玩家并与之交互，出于公平性和游戏性（而不是技术上）的考虑，游戏中并不会这样做。</span> </p>
<p><span>然而，目前的</span> <span>MMORPG</span> <span>中，几乎没有任何一款可以做到整个游戏世界只包含一个场景，因为在一款</span> <span>MMORPG</span> <span>中，同时在线的玩家数量成百上千，甚至是数万人同时在一个游戏世界中交互。以现在的网络技术和计算机系统，还无法为这么多玩家的交互提供即时处理。因此，</span> <span>MMORPG</span> <span>的游戏世界被划分为大小不等、数量众多的场景，游戏服务器对于这些场景的处理分为分区和无缝两种。</span> </p>
<p><span>在分区式服务器中，一个场景中的玩家无法看到另一个场景中的玩家，当玩家从一个场景到另外一个场景跨越时，都有一个数据转移和加载的过程（尤其是从一个分区服务器跨越到另外一个服务器时），玩家都有一个等待的时间，在这段时间内，服务器的主要工作是实现跨越玩家数据的转移和加载以及后一个场景中玩家、</span> <span>NPC</span> <span>等数据的传输，客户端的主要工作是实现新场景资源的加载和服务器通信。主要时间的长短主要取决于后一个场景中资源数据的大小。分区式服务器的优点主要是各分区服务器保持相对独立，缺点是游戏空间不够大，而且，一旦某个分区服务器中止服务，位于该服务器上的所有玩家将失去连接。</span> </p>
<p><span>所谓无缝服务器，玩家几乎察觉不到场景之间的这种切换，在场景间没有物理上的屏障，对于玩家而言，众多场景构成了一个巨大的游戏世界。场景之间，甚至服务器之间&#8220;没有了&#8221;明确的界线。因此，无缝服务器为玩家提供了更大的游戏空间和更友好的交互，实现了动态边界的无缝服务器甚至可以在某个服务器中止服务时，按一定策略将负载动态分散到其他服务器。因此，无缝服务器在技术上要比分区服务器更加复杂。</span> </p>
<p><span>目前国内上市的</span> <span>MMORPG</span> <span>，大多采用分区式服务器，做到无缝世界的主要有《完美世界》和《天下贰》等，国外的</span> <span>MMORPG</span> <span>中，像《魔兽世界》、《</span> <span>EVE</span> <span>》等，都实现了无缝世界。</span> </p>
<p><span>无缝服务器与分区式服务器在技术上的主要区别是，当位于场景</span> <span>S1</span> <span>中的玩家</span> <span>P1</span> <span>处于两个（甚至更多）场景</span> <span>S1</span> <span>、</span> <span>S2</span> <span>的边界区域内时，要保证</span> <span>P1</span> <span>能够看到场景</span> <span>S2</span> <span>中建筑、玩家、</span> <span>NPC</span> <span>等可感知对象。而且边界区域的大小要大于等于</span> <span>P1</span> <span>可感知的范围，否则就可能发生</span> <span>S2</span> <span>中的可感知对象突然闪现在</span> <span>P1</span> <span>视野中的异常。</span> </p>
<p><span>无疑，无缝世界为玩家提供了更人性化和更具魅力的用户体验。</span> </p>
<p><span>二、无缝世界游戏服务器的整体架构</span> </p>
<p><span>MMORPG</span> <span>的服务器架构从功能上主要划分为三种：</span> </p>
<p><span><span>1、</span> </span><span>登录服务器（</span> <span>Login Server</span> <span>）</span> </p>
<p><span>登录服务器用于玩家验证登录，并根据系统记录玩家信息得到其所在节点服务器，并通过世界服务器为登录玩家和对应节点服务器建立连接。</span> </p>
<p><span><span>2、</span> </span><span>世界服务器（</span> <span>World Server</span> <span>）</span> </p>
<p><span>世界服务器将整个游戏世界划分成不同场景，将所有场景按一定策略分配给节点服务器，并对节点服务器进行管理。世界服务器的另一功能是与登录服务器交互。因此，世界服务器是登录服务器、节点服务器的沟通桥梁，当然，一旦玩家登录成功，世界服务器将主要处理节点服务器间的通信。因此，世界服务器对于玩家是透明的。</span> </p>
<p><span><span>3、</span> </span><span>节点服务器（</span> <span>Node Server</span> <span>）</span> </p>
<p><span>节点服务器负责管理位于该节点的所有玩家、</span> <span>NPC</span> <span>的所有交互，在无缝世界游戏中，由于边界区域的存在，一个节点服务器甚至要处理相邻节点上位于边界区域的玩家和</span> <span>NPC</span> <span>的信息。</span> </p>
<p><span>在具体实现上，不同的</span> <span>MMORPG</span> <span>为了便于管理，可能还会具有</span> <span>AI</span> <span>服务器、日志服务器、数据库缓存服务器、代理服务器等。</span> </p>
<p><span><span>三、</span> </span><span>无缝世界游戏服务器的主要技术需求</span> </p>
<p><span><span>1、</span> </span><span>编程语言（</span> <span>C/C++</span> <span>、</span> <span>SQL</span> <span>、</span> <span>Lua</span> <span>、</span> <span>Python</span> <span>）</span> </p>
<p><span><span>2、</span> </span><span>图形库（</span> <span>Direct 3D</span> <span>、</span> <span>OpenGL</span> <span>）</span> </p>
<p><span><span>3、</span> </span><span>网络通信（</span> <span>WinSock</span> <span>、</span> <span>BSD Socket</span> <span>，或者</span> <span>ACE</span> <span>）</span> </p>
<p><span><span>4、</span> </span><span>消息、事件、多线程、</span> <span>GUI</span> </p>
<p><span><span>5、</span> </span><span>OS</span> </p>
<p><span>三、无缝世界游戏服务器需要解决的主要问题</span> </p>
<p><span><span>1、</span> </span><span>资源管理</span> </p>
<p><span>无论是服务器还是客户端，都涉及到大量资源：玩家数据、</span> <span>NPC</span> <span>数据、战斗公式、模型资源、通信资源等。当这些资源达到一定规模，其管理的难度不可忽视。而且，资源管理的好坏，直接关系到游戏的安全和生命。</span> </p>
<p><span><span>2、</span> </span><span>网络安全</span> </p>
<p><span>安全永远是第一位的，我们无法指望所有的玩家及其所持的客户端永远是友好的。事实上，威胁到游戏的公平性和安全性的大多数问题，归根结底，都是由于网络通信中存在的欺骗和攻击造成的，这些问题包含但不限于交易欺骗、物品复制。</span> </p>
<p><span><span>3、</span> </span><span>逻辑安全</span> </p>
<p><span>逻辑安全按理说应该是游戏中最基本的考虑，覆盖的范围也最广最杂。随机数系统是一个非常值得重视的问题，随机数不仅仅用于玩家可见的一些任务系统、战斗公式、人工智能、物品得失等，还可用于网络报文加密等。因此，随机数系统本身的安全不容忽视。另外一个常见的逻辑安全是玩家的移动，最主要的就是防止加速齿轮这样的变态操作。</span> </p>
<p><span><span>4、</span> </span><span>负载均衡</span> </p>
<p><span>MMORPG</span> <span>中的负载均衡包括客户端及服务器资源管理和逻辑处理的负载均衡，其中最难预知的是网络通信的负载均衡，正常情况下的网络通信数量是可以在游戏设计时做出评估的，但因恶意攻击造成的网络负载是无法预测的。因此，负载均衡所要处理的主要是实时动态负载均衡和灾难恢复。负载均衡需要解决的问题包括负载监控、负载分析、负载分发和灾难恢复。</span> </p>
<p><span><span>5、</span> </span><span>录像系统</span> </p>
<p><span>录像系统的构建，主要用于重现关键数据的输入输出，如玩家交易、玩家充值，或者当</span> <span>bug</span> <span>出现后，为逻辑服务器（泛指上文提到的所有类型服务器，主要是节点服务器）相应部分启动录像系统。待收集到足够数据后，通过录像系统重现</span> <span>bug</span> <span>。为了使逻辑服务器不受自身时间（如中断调试等）的影响，还可以专门设计心跳服务器来控制数据传输。</span> </p>
<p><span>四、总结</span> </p>
<p><span>在</span> <span>MMORPG</span> <span>中，真正的</span> <span>bug</span> <span>永远存在于将来。从这一点出发，关于</span> <span>MMORPG</span> <span>中游戏世界的构建，怎样苛刻的思考都不为过。<br><br>参考资料：</span> </p>
<p><span>1、 [美] Kim Pallister编, 孟宪武 等译. 游戏编程精粹5, P467-474, P516. 人民邮电出版社, 2007年9月. 北京.<br>2、 [美] Thor Alexander编, 史晓明 译. 大型多人在线游戏开发, P174-185. 人民邮电出版社, 2006年12月. 北京.<br>3、 [美] Dante Treglia编, 张磊 译. 游戏编程精粹3, P117-122. 人民邮电出版社, 2003年7月. 北京.<br>4、 [美] Mark DeLoura编, 王淑礼 等译. 游戏编程精粹1, P90-93. 人民邮电出版社, 2004年10月. 北京.<br>5、 [美] Douglas 等著, 於春景 译. C++网络编程 卷1. 中国电力出版社, 2004年11月. 北京.<br>6、 [美] Stephen D. Huston 等著, 马维达 译. ACE程序员指南. 中国电力出版社, 2004年11月. 北京.<br>7、 [美] Erich Gamma等著, 李英军 等译. 设计模式. 机械工业出版社, 2000年6月. 北京.<br>8、 游戏引擎全剖析. <a href="http://bbs.gameres.com/showthread.asp?threadid=101293"><u><font color=#0000ff>http://bbs.gameres.com/showthread.asp?threadid=101293</font></u></a>.<br>9、 服务器结构探讨：登录服的负载均衡. <a href="http://gamedev.csdn.net/page/351491d0-05ad-48a4-85e1-77870bc1eef3"><u><font color=#0000ff>http://gamedev.csdn.net/page/351491d0-05ad-48a4-85e1-77870bc1eef3</font></u></a>.<br>10、服务器结构探讨：最终的结构. <a href="http://gamedev.csdn.net/page/28695655-974c-4291-8ac4-2589c4e770d3"><u><font color=#0000ff>http://gamedev.csdn.net/page/28695655-974c-4291-8ac4-2589c4e770d3</font></u></a>.<br></span></p>
<img src ="http://www.cppblog.com/keigoliye/aggbug/95662.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-09 10:38 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/09/95662.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>超大地图MMORPG的场景管理(转)</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/09/95661.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 09 Sep 2009 02:36:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/09/95661.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/95661.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/09/95661.html#Feedback</comments><slash:comments>1</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/95661.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/95661.html</trackback:ping><description><![CDATA[<p>目前在做一个超大地图MMORPG的场景管理部分，客户端通过动态预读解决了超大图量的动态加载，但是在做人物行走的时候遇到了一些问题：<br>　　一张地图上的PLAYER和NPC等是存放在一个list中的，地图超大那么上面的PLAYER就可能超多（预计大于２００），这样的话每个行走动作都要发送２００条以上的消息，这对于服务器是一种很大的负担，而且这种负担是呈级数增长（10个玩家都走一步服务器将发送10*10=100条消息，而200个的话就是200*200=40000条消息！），可能任何服务器都无法负担。<br>　　肯定有很多朋友都遇到了类似的问题，很想知道大家是怎么解决的？<br>　　</p>
<p>方案一：<br>　&#183;服务器上每个场景用一个list来保存上面的player和NPC<br>　&#183;玩家行走、进入和离开等事件发给list中的所有player<br>　&#183;客户端的list保有该场景上的所有player和npc<br>　优点：处理起来简单直接<br>　缺点：发送的消息会随玩家数量的增加而暴增、客户端负担很重<br><br>方案二：<br>　&#183;服务器上每个场景用一个list来保存上面的player和NPC<br>　&#183;玩家行走、进入和离开等事件只发给该玩家一定范围内的Player<br>　&#183;客户端的list只保有本玩家附近的player和npc<br>　&#183;在服务器可以考虑使用hash表来优化查询速度<br>　优点：减少了服务器发送消息的数量、减轻了客户端的负担<br>　缺点：实现相对复杂，服务器负担大大加重（因为要不断判断玩家间的位置关系）<br><br>方案三：<br>　&#183;在服务器上把场景划分为小区域（大于屏幕大小）。每个区域对应一个list，场景中的所有对象按他们的位置加入到对应区域的list中，那么每次行走只需要把消息发送给最多４个相临区域的Player<br>　&#183;客户端的list只保有本玩家附近的player和npc<br>　优点：大大减轻了服务器遍历list的负担、减少了发送消息的数量、减轻了客户端的负担<br>　缺点：实现非常复杂、而且在服务器需要不断判断玩家是否跨越区域<br><br>方案四：<br>　&#183;服务器上场景的每个TILE保存一个Object指针用来绑定该格子上的player或NPC<br>　&#183;玩家行走、进入和离开等事件发给玩家周围一定范围内的player<br>　&#183;客户端保有该player周围一定范围内的player和npc<br>　优点：处理起来极为直接、避免了耗时链表遍历（典型的以空间换时间）<br>　缺点：地图每个TILE都要加入一个指针变量（管理不善容易出错）、每次发送场景广播要遍历所有TILE<br><br>方案五：<br>　&#183;服务器上每个场景用一个list来保存上面的player和NPC<br>　&#183;不使用事件通知，而使用状态位置通知的方式通过定时发送状态来更新客户端的player和npc状态<br>　&#183;客户端保有该player周围一定范围内的player和npc<br>　优点：处理比较简单<br>　缺点：实时性太低，对于要求同步比较精确的ＡＲＰＧ不太适合</p>
<img src ="http://www.cppblog.com/keigoliye/aggbug/95661.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-09 10:36 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/09/95661.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>高性能的网络游戏服务器的设计。。。作者谈了QQGAME SERVER。。。 （转）</title><link>http://www.cppblog.com/keigoliye/archive/2009/09/09/95660.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Wed, 09 Sep 2009 02:35:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/09/95660.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/95660.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/09/95660.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/95660.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/95660.html</trackback:ping><description><![CDATA[<font size=2>&nbsp;&nbsp;&nbsp; 说起高性能的网络游戏，有2个典范，1个是暴雪的WOW，另外一个要数腾讯的QQGame了，因为对于MMPRPG的体系接触不深，几乎属于文盲，没有太多的发言权，而自己又是搞休闲游戏开发的所以本文就主要谈谈QQGame了。 <br minmax_bound="true"><br minmax_bound="true">&nbsp; &nbsp; 前些天通过朋友得到了QQGame的一个系统分析的文档，看完后很是震惊，彻底被QQ的设计所折服了，到底是千万人在线系统经验的拥有者，牛！ <br minmax_bound="true"><br minmax_bound="true">&nbsp; &nbsp; 通过资料了解到QQGame目前有以下让我欣赏的特性： <br minmax_bound="true"><br minmax_bound="true">1.单机最高容纳35,000人同时在线，对没有看错是这么多，由于它适用了Linux下高性能的网络处理模型ePoll技术，并且一系列高超的优化技术轻松破万人，当然为了稳定性考虑单机保持了2万人的容量，此时的带宽消耗为近30M； <br minmax_bound="true">2.采用共享内存方式高速完成进程间高速通讯； <br minmax_bound="true">3.服务器的扩充方式不是平面的方式，而是裂变式的扩充方式，形成负载均衡阵列树状结构； <br minmax_bound="true">4.所有的游戏服务器不是直接和数据库联系，而是和数据proxy(qq管叫数据交换机和路由器)进行联系，由此带来的就是游戏用户数据的分布存储，我分析着应该是proxy上记录着这个用户数据所在的实际的dbserver的信息，然后定时的将最新的用户信息写回到db中去，这样就大大缓解了数据库服务器的压力，而且可以非常平滑的将数据分裂开来，数据库服务器也就可以无限的扩充，当然我觉得肯定有个数据库信息索引了用户的id和对应的存储地点的关联关系，这点就类似于google的原理了，所以对于数据库的硬件要求也就不是那么高了，qqgame的一组服务器通常是7台服务器，可以容纳5万人，其中就包含了数据库服务器，这点就不是棋牌游戏所常使用的数据集中存储了； <br minmax_bound="true">5.游戏服务器的网络和逻辑分开，不仅仅是层次上的分开，而是在进程上分开，然后中间通过共享通道进行管理和协调，并且增加了辅助线程，在主线程处理大压力的异步的操作的时候直接交给辅助线程处理，保障了游戏服务器的高效性运转。 <br minmax_bound="true">&nbsp; &nbsp; 作为游戏服务器的结构方面(以下只讨论休闲游戏部分,MMORPG服务器不属于讨论范围)的设计已经相对于成熟并且统一,结构方面和3年前我所接触的中游系的那套平台没有太大的差异,无非是服务器群采用星状的结构,以1个中心节点作为核心,然后向四周扩散出一些应用服务器,如负责登陆的LoginServer,负责具体游戏逻辑的GameServer等等,当然最精简的结构是这样的,这样的结构可以满足50万以下同时在线的容量,如果为了满足更大的容量,如QQGAME这样的目前已经有200万以上同时在线的超大容量的应用则需要额外的优化,从这个结构中分离出一些子应用独立开发出一些服务器端来处理,一方面降低偶合度,另外一方面作为高可用性系统为负载均衡提供条件. <br minmax_bound="true"><br minmax_bound="true">关于负载均衡,作为整个游戏平台的所有服务器.我觉得除了具体的游戏逻辑服务器以外都是可以采用负载均衡,多点分担的方式来处理,惟独逻辑服务器不可以,因为休闲游戏,都是分层次的,不管泡泡堂也好,QQGAME也好这些游戏其实在客户端的表现形式都是分层次的,如QQGAME就是LOBBY-HALL-ROOM这样的结构,LOBBY这层就是游戏广场了,可以看到所有的游戏类别,游戏服务器和具体的游戏大厅,比如:牌类&#8211;斗地主&#8211;新手场&#8211;新手场1 这样的顺序,传统的设计中新手场1就是属于一个独立的游戏逻辑服务器拥有一个独立的IP以及侦听端口,在服务器端通常也是一个独立的进程.一般的游戏服务器允许的连接数通常都是300-600人之间超过的就提示服务器已满了，这样做的原因并不是因为进程的限制因为一个进程完全可以做到同时让3000以上的玩家同时游戏，而是人为设计的考虑，因为在游戏逻辑服务器中有很多需要广播的消息，如游戏玩家的聊天信息，某个房间的开始信息，结束信息，某人进出的信息等，而对于广播来说，给300个人广播和给3000个人广播所消耗的资源是绝对没有可比性的。但是通过从进程上独立来处理这个传统的方式也有个缺陷，比如通过开10个进程来达到3000人和1个进程达到3000人，如果不考虑广播的因素在内的话前者的资源是要高与后者的资源的，并且进程间的通讯也要比进程内的通讯要耗费资源和复杂度方面要高很多，比如说如果要实现一个需求让玩家可以在同一类游戏中可以使用小喇叭类或者跨游戏服务器找人之类的功能的话，同一个进程的优势就显示出来了，为此QQGAME所使用的是Channel(频道)的概念，即一个游戏逻辑服务器的进程可以容纳5000人左右，然后服务器端通过设置分割出很多的Channel如新手1，新手2，新手3之类的传统意义上的游戏大厅，将消息的分发范围进行隔离，节约了资源。这一点可以通过查看连接属性看到，连接QQGAME的同类型靠近的几个游戏大厅其实端口和IP都是一致的。 <br minmax_bound="true"><br minmax_bound="true">我们目前的游戏服务器类型主要有3类：CenterServer，管理着所有的服务器连接，LoginServer 负责处理用户的登陆、注册，并且用来给用户传递游戏逻辑服务器列表等功能， GameServer具体逻辑服务器，根据不同的逻辑来实现不同的游戏需求。 <br minmax_bound="true"><br minmax_bound="true">当然，根据业务的发展需要，我们正准备把用户的状态在服务器端保存，并且增加一个类似IM服务器的功能来满足玩家跨服务器聊天和查询其他玩家状态的需要。以及GM服务器实现多功能的网管室的需求，这些将在以后慢慢写。 <br minmax_bound="true"><br minmax_bound="true">今天开始写些技术的题材,一方面记录一些自己的本分工作的东西,另外一方面也是充实一下BLOG,工作太忙也没有什么太多的思绪来一直写其他的题材,所以就拿工作来填充了.同时如果有人有幸看到了这些文章,并且也有兴趣的话也希望多多探讨. <br minmax_bound="true"><br minmax_bound="true">先简单介绍一下,由于本人的工作就是游戏开发公司的,一直与游戏开发打交道,主要做休闲类的游戏,目前又以棋牌游戏为主,在这个行业中摸爬滚打了整3年了,从运营开始做起,运营过当时国内一流的游戏平台中游系列的产品,然后由于合作方面的原因又自主开发过一套游戏平台,然后由于发展方向上的分歧出来单独做,目前在开发并运维一套全新的产品. <br minmax_bound="true"><br minmax_bound="true">这篇主要是对于游戏服务器的一些想法,结合目前自身的产品的一些问题延伸开来的. <br minmax_bound="true"><br minmax_bound="true">目前我们的服务器还是属于Windows平台的架构,暂时还没有考虑到跨平台,主要原因有2:<br minmax_bound="true">1.成本:作为公司来说首先得考虑的就是成本了,虽然说Linux类的平台存在着操作系统成本低廉,性能优异,稳定性高这几个特色,但是作为全局考虑来说,一个Linux的系统管理员,以及开发人员的成本要比Windows平台要高很多,并且作为操作系统方面是免费的,但是支持几乎是没有的只有一些不适合商业应用的开源社区作为支撑,要得到更加好的服务或者额外的支持还是得通过大厂商的高额的产品或服务来实现,而Windows平台则不同,虽然操作系统貌似价格不扉,但是本身就带了很多的模块,比如组件服务,日志服务等等,加上平台上各种软件的数量也和Linux下的不是同一量级的,可选择性要大的多,而让Linuxer所诟病的Win32的安全性其实完全可以通过另外独立的安全模块,如硬件防火墙等来完善,毕竟操作系统不是专门做安全防护的Linux也存在很多的漏洞,并且随着Linux发行版的日益增多,用户的逐渐了解,漏洞也日益的增多.<br minmax_bound="true">2.开发效率.无疑Visual Studio系列开发套件是目前开发领域最方便最强大的IDE环境,这已是不争的事实,而如果用VI + GCC的模式去开发LINUX下面的服务器其效率和质量至少从我目前的水平(可以部分代表国内目前中小游戏软件开发商实际现状)要彻底的领悟并转换需要一定的时间,并且还没有成熟的开发平台作为保证, JAVA好象有 但是C++还没有听说有超过VC++的. </font><!-- google_ad_section_end -->
<img src ="http://www.cppblog.com/keigoliye/aggbug/95660.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-09 10:35 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/09/95660.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>小谈网络游戏同步 </title><link>http://www.cppblog.com/keigoliye/archive/2009/09/07/95464.html</link><dc:creator>暗夜教父</dc:creator><author>暗夜教父</author><pubDate>Mon, 07 Sep 2009 08:22:00 GMT</pubDate><guid>http://www.cppblog.com/keigoliye/archive/2009/09/07/95464.html</guid><wfw:comment>http://www.cppblog.com/keigoliye/comments/95464.html</wfw:comment><comments>http://www.cppblog.com/keigoliye/archive/2009/09/07/95464.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/keigoliye/comments/commentRss/95464.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/keigoliye/services/trackbacks/95464.html</trackback:ping><description><![CDATA[<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>同步在网络游戏中是非常重要的，它保证了每个玩家在屏幕上看到的东西大体是一样的。其实呢，解决同步问题的最简单的方法就是把每个玩家的动作都向其他玩家广播一遍，这里其实就存在两个问题：1，向哪些玩家广播，广播哪些消息。2，如果网络延迟怎么办。事实上呢，第一个问题是个非常简单的问题，不过之所以我提出这个问题来，是提醒大家在设计自己的消息结构的时候，需要把这个因素考虑进去。而对于第二个问题，则是一个挺麻烦的问题，大家可以来看这么个例子： <br><br>　　比如有一个玩家A向服务器发了条指令，说我现在在P1点，要去P2点。指令发出的时间是T0，服务器收到指令的时间是T1，然后向周围的玩家广播这条消息，消息的内容是&#8220;玩家A从P1到P2&#8221;有一个在A附近的玩家B，收到服务器的这则广播的消息的时间是T2，然后开始在客户端上画图，A从P1到P2点。这个时候就存在一个不同步的问题，玩家A和玩家B的屏幕上显示的画面相差了T2-T1的时间。这个时候怎么办呢？</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>　　有个解决方案，我给它取名叫 预测拉扯，虽然有些怪异了点，不过基本上大家也能从字面上来理解它的意思。要解决这个问题，首先要定义一个值叫：预测误差。然后需要在服务器端每个玩家连接的类里面加一项属性，叫TimeModified，然后在玩家登陆的时候，对客户端的时间和服务器的时间进行比较，得出来的差值保存在TimeModified里面。还是上面的那个例子，服务器广播消息的时候，就根据要广播对象的TimeModified，计算出一个客户端的CurrentTime，然后在消息头里面包含这个CurrentTime，然后再进行广播。并且同时在玩家A的客户端本地建立一个队列，保存该条消息，只到获得服务器验证就从未被验证的消息队列里面将该消息删除，如果验证失败，则会被拉扯回P1点。然后当玩家B收到了服务器发过来的消息&#8220;玩家A从P1到P2&#8221;这个时候就检查消息里面服务器发出的时间和本地时间做比较，如果大于定义的预测误差，就算出在T2这个时间，玩家A的屏幕上走到的地点P3，然后把玩家B屏幕上的玩家A直接拉扯到P3，再继续走下去，这样就能保证同步。更进一步，为了保证客户端运行起来更加smooth，我并不推荐直接把玩家拉扯过去，而是算出P3偏后的一点P4，然后用(P4-P1)/T(P4-P3)来算出一个很快的速度S，然后让玩家A用速度S快速移动到P4，这样的处理方法是比较合理的，这种解决方案的原形在国际上被称为（Full plesiochronous），当然，该原形被我篡改了很多来适应网络游戏的同步，所以而变成所谓的：预测拉扯。</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>　　</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>另外一个解决方案，我给它取名叫 验证同步，听名字也知道，大体的意思就是每条指令在经过服务器验证通过了以后再执行动作。具体的思路如下：首先也需要在每个玩家连接类型里面定义一个TimeModified，然后在客户端响应玩家鼠标行走的同时，客户端并不会先行走动，而是发一条走路的指令给服务器，然后等待服务器的验证。服务器接受到这条消息以后，进行逻辑层的验证，然后计算出需要广播的范围，包括玩家A在内，根据各个客户端不同的TimeModified生成不同的消息头，开始广播，这个时候这个玩家的走路信息就是完全同步的了。这个方法的优点是能保证各个客户端之间绝对的同步，缺点是当网络延迟比较大的时候，玩家的客户端的行为会变得比较不流畅，给玩家带来很不爽的感觉。该种解决方案的原形在国际上被称为（Hierarchical master-slave synchronization），80年代以后被广泛应用于网络的各个领域。</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>　</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>　最后一种解决方案是一种理想化的解决方案，在国际上被称为Mutual synchronization，是一种对未来网络的前景的良好预测出来的解决方案。这里之所以要提这个方案，并不是说我们已经完全的实现了这种方案，而只是在网络游戏领域的某些方面应用到这种方案的某些思想。我对该种方案取名为：半服务器同步。大体的设计思路如下：</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>　　</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>首先客户端需要在登陆世界的时候建立很多张广播列表，这些列表在客户端后台和服务器要进行不及时同步，之所以要建立多张列表，是因为要广播的类型是不止一种的，比如说有local message,有remote message,还有global message 等等，这些列表都需要在客户端登陆的时候根据服务器发过来的消息建立好。在建立列表的同时，还需要获得每个列表中广播对象的TimeModified，并且要维护一张完整的用户状态列表在后台，也是不及时的和服务器进行同步，根据本地的用户状态表，可以做到一部分决策由客户端自己来决定，当客户端发送这部分决策的时候，则直接将最终决策发送到各个广播列表里面的客户端，并对其时间进行校对，保证每个客户端在收到的消息的时间是和根据本地时间进行校对过的。那么再采用预测拉扯中提到过的计算提前量，提高速度行走过去的方法，将会使同步变得非常的smooth。该方案的优点是不通过服务器，客户端自己之间进行同步，大大的降低了由于网络延迟而带来的误差，并且由于大部分决策都可以由客户端来做，也大大的降低了服务器的资源。由此带来的弊端就是由于消息和决策权都放在客户端本地，所以给外挂提供了很大的可乘之机。</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>　</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>　综合以上三种关于网络同步派系的优缺点，综合出一套关于网络游戏传输同步的较完整的解决方案，我称它为综合同步法（colligate synchronization）。大体设计思路如下：</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>　　首先将服务器需要同步的所有消息从划分一个优先等级，然后按照3/4的比例划分出重要消息和非重要消息，对于非重要消息，把决策权放在客户端，在客户端逻辑上建立相关的决策机构和各种消息缓存区，以及相关的消息缓存区管理机构，如下图所示：</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1></font></span>&nbsp;</p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>　　上图简单说明了对于非重要消息，客户端的大体处理流程，其中有一个客户端被动行为值得大家注意，其中包括对服务器发过来的某些验证代码做返回，来确保消息缓存中的消息和服务器端是一致的，从而有效的防止外挂来篡改本地消息缓存。其中的消息来源是包括本地的客户端响应玩家的消息以及远程服务器传递过来的消息。</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>　　对于重要消息，比如说战斗或者是某些牵扯到玩家一些比较敏感数据的操作，则采用另外一套方案，该方案首先需要在服务器和客户端之间建立一套Ping System，然后服务器保存和用户的及时的ping值，当ping比较小的时候，响应玩家消息的同时先不进行动作，而是先把该消息反馈给服务器，并且阻塞，服务器收到该消息，进行逻辑验证之后向所有该详细广播的有效对象进行广播（包括消息发起者），然后客户端收到该消息的验证，才开始执行动作。而当ping比较大的时候，客户端响应玩家消息的同时立刻进行动作，并且同时把该消息反馈给服务器，值得注意的是这个时候还需要在本地建立一个无验证消息的队列，把该消息入队，执行动作的同时等待服务器的验证，还需要保存当前状态。服务器收到客户端的请求后，进行逻辑验证，并把消息反馈到各个客户端，带上各个客户端校对过的本地时间。如果验证通过不过，则通知消息发起者，该消息验证失败，然后客户端自动把已经在进行中的动作取消，恢复原来状态。如果验证通过，则广播到的各个客户端根据从服务器获得校对时间进行对其进行拉扯，保证在该行为完成之前完成同步。</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1></font></span>&nbsp;</p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>　　至此，一个比较成熟的网络游戏的同步机制已经初步建立起来了，接下来的逻辑代码就根据各自不同的游戏风格以及侧重点来写了。</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>　　同步是网络游戏最重要的问题，如何同步也牵扯到各个方面的问题，比如说游戏的规模，游戏的类型以及各种各样的方面，对于规模比较大的游戏，在同步方面可以下很多的工夫，把消息分得十分的细腻，对于不同的消息采用不同的同步机制，而对于规模比较小的游戏，则可以采用大体上一样的同步机制，究竟怎么样同步，没有个定式，是需要根据自己的不同情况来做出不同的同步决策的</font></span></p>
<p><span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1></font></span>&nbsp;</p>
<span style="FONT-SIZE: x-small"><font style="FONT-SIZE: 10pt" size=1>
<p><br>网游同步算法之导航推测（Dead Reckoning）算法：</p>
<p>&nbsp;</p>
<p>　　在了解该算法前，我们先来谈谈该算法的一些背景资料。大家都知道，在网络传输的时候，延迟现象是很普遍的，而在基于Server/Client结构下的网络游戏的同步也就成了很头疼的问题，在保证客户端响应用户本地指令流畅的情况下，没法有效的保证的同步的及时性。同样，在军方也有类似的事情发生，即使是同一LAN里面的机器，也会因为传输的延迟，导致一些运算的失误，介于此，美国国防部投入了大量的资金用于研究一种比较的好的方案来解决分布式系统中的延迟问题，特别是一个叫分布式模拟运动（Distributed Interactive Simulation）的系统，这套系统呢，其中就提出了一套号称是Latency Hiding &amp; Bandwidth Reduction的方案，命名为Dead Reckoning。呵呵，来头很大吧，恩，那么我们下面就来看看这套系统的一些观点，以及我们如何把它运用到我们的网络游戏的同步中。</p>
<p>　　首先，这套同步方案是基于我那篇《网络游戏的同步》一文中的Mutual Synchronization同步方案的，也就是说，它并不是Server/Client结构的，而是基于客户端之间的同步的。下面我们先来说一些本文中将用到的名词概念：<br>　　网状网络：客户端之间构成的网络<br>　　节点：网状网络中的每个客户端<br>　　极限误差：进行同步的时候可能产生的误差的极值</p>
<p>　　恩，在探讨其原理的之前，我们先来看看我们需要一个什么样的环境。首先，需要一个网状网络，网状网络如何构成呢？当有新节点进入的时候，通知该网络里面的所有节点，各节点为该客户端在本地创建一个副本，登出的时候，则通知所有节点销毁本地关于该节点的副本。然后每个节点该保存一些什么数据呢？首先有一个很重要的包需要保存，叫做协议数据包（PDU Protocol Data Unit），PDU包含节点的一些相关的运动信息，比如当前位置，速度，运动方向，或者还有加速度等一些信息。除PDU之外，还有其他信息需要保存，比如说节点客户端人物的HP，MP之类的。然后，保证每个节点在最少8秒之内要向其它节点广播一次PDU信息。最后，设置一个极限误差值。到此，其环境就算搭建完成了。下面，我们就来看看相关的具体算法：</p>
<p>　　假设在节点A有一个小人（路人甲），开始跑路了，这个时候，就像所有的节点广播一次他的PDU信息，包括：速度（S），方向（O），加速度（A）。那么所有的节点就开始模拟路人甲的运动轨迹和路线，包括节点A本身（这点很重要），同时，路人甲在某某玩家的控制下，会不时的改变一下方向，让其跑路的路线变得不是那么正规。在跑路的过程中，节点A有一个值在不停的记录着其真实坐标和在后台模拟运动的坐标的差值，当差值大于极限误差的时候，则计算出当前的速度S，方向O和速度A（算法将在后面介绍），并广播给网络中其他所有节点。其他节点在收到这条消息之后呢，就可以用一些很平滑的移动把路人甲拉扯过去，然后重新调整模拟跑路的数据，让其继续在后台模拟跑路。</p>
<p>　　很显然，如果极限误差定义得大了，其他节点看到的偏差就会过大，如果极限偏差定义得小了，网络带宽就会增大。如果定义这个极限误差，就该根据各种数据的重要性来设计了。如果是回合制的网络游戏，那么在走路上把极限误差定义得大些无所谓，可以减少带宽。但是如果是及时打斗的网络游戏，那么就得把极限误差定义得小一些，否则会出现某人看到某人老远把自己给砍死的情况。</p>
<p>　　Dead Reckoning的主要算法有9种，但是只有两种是解决主要问题的，其他的基本上只是针对不同的坐标系的一些不同的算法，这里就不一一介绍了。好，那么我们下面来看传说中的最主要的两种算法：<br>　　　　第一：目标点 = 原点 + 速度 * 时间差<br>　　　　第二：目标点 = 原点 + 速度 * 时间差 + 1/2 * 加速度 * 时间差<br>呵呵，传说中的算法都是很经典的，虽然我们早在初中物理的时候就学过。</p>
<p>　　该算法的好处呢，正如它开始所说的，Latency Hiding &amp; Bandwidth Reduction，从原则上解决了网络延迟导致的不同步的问题，并且有效的减少了带宽，不好的地方就是该算法基本上只能使用于移动中的同步，当然，移动的同步是网络游戏中同步的最大的问题。</p>
<p>　　该方法结合我在《网络游戏的同步》一文中提出的综合同步法的构架可以基本上解决掉网络游戏中走路同步的问题。相关问题欢迎大家一起讨论。</p>
<p><br>有关导航推测算法（Dead Reckoning）中的平滑处理：</p>
<p>　　根据我上篇文章所介绍的，在节点A收到节点B新的PDU包时，如果和A本地的关于B的模拟运动的坐标不一致时，怎么样在A的屏幕上把B拽到新的PDU包所描叙的点上面去呢，上文中只提了用&#8220;很平滑的移动&#8221;把B&#8220;拉扯&#8221;过去，那么实际中应该怎么操作呢？这里介绍四种方法。 </p>
<p>　　第一种方法，我取名叫直接拉扯法，大家听名字也知道，就是直接把B硬生生的拽到新的PDU包所描叙的坐标上去，该方法的好处是：简单。坏处是：看了以下三种方法之后你就不会用这种方法了。</p>
<p>　　第二种方法，叫直线行走（Linear），即让B从它的当前坐标走直线到新的PDU包所描叙的坐标，行走速度用上文中所介绍的经典算法：<br>　　　　目标点 = 原点 + 速度 * 时间差 + 1/2 * 加速度 * 时间差算出：<br>　　首先算出从当前坐标到PDU包中描叙的坐标所需要的时间：<br>　　　　T = Dest( TargetB &#8211; OriginB ) / Speed<br>　　然后根据新PDU包中所描叙的坐标信息模拟计算出在时间T之后，按照新的PDU包中的运动信息所应该达到的位置：<br>　　　　_TargetB = NewPDU.Speed * T<br>　　然后根据当前模拟行动中的B和_TargetB的距离配合时间T算出一个修正过的速度_S：<br>　　　　_S = Dest( _TargetB &#8211; OriginB ) / T<br>　　然后在画面上让B以速度_S走直线到Target_B，并且在走到之后调整其速度，方向，加速度等信息为新的PDU包中所描叙的。</p>
<p>　　这种方法呢，非常的土，会让物体在画面上移动起来变得非常的不现实，经常会出现很生硬的拐角，而且对于经常要修改的速度_S，在玩家A的画面上，玩家B的行动会变得非常的诡异。其好处是：比第一种方法要好。</p>
<p>　　第三种方法，叫二次方程行走（Quadratic），该方法的原理呢，就是在直线行走的过程中，加入二次方程来计算一条曲线路径，让Dest( _TargetB &#8211; OriginB )的过程是一条曲线，而不是一条直线，恩，具体的实现方法，就是在Linear方法的计算中，设定一个二次方程，在Dest函数计算距离的时候根据设定的二次方程来计算，这样一来，可以使B在玩家A屏幕上的移动变得比较的有人性化一些。但是该方法的考虑也是不周全的，仅仅只考虑了TargetB到_TargetB的方向，而没有考虑新的PDU包中的方向描叙，那么从_TargetB开始模拟行走的时候，仍然是会出现比较生硬的拐角，那么下面提出的最终解决方案，将彻底解决这个问题。</p>
<p>　　最后一种方法叫：立方体抖动（Cubic Splines），这个东东比较复杂，它需要四个坐标信息作为它的参数来进行运算，第一个参数Pos1是OriginB，第二个参数Pos2是OriginB在模拟运行一秒以后的位置，第三个参数Pos3是到达_TargetB前一秒的位置，第四个参数pos4是_TargetB的位置。</p>
<p><br>Struct pos {<br>&nbsp;&nbsp;&nbsp; Coordinate X;<br>&nbsp;&nbsp;&nbsp; Coordinate Y;<br>}</p>
<p>　　　Pos1 = OriginB<br>　　　Pos2 = OriginB + V<br>　　　Pos3 = _TargetB &#8211; V<br>　　　Pos4 = _TargetB<br>运动轨迹中(x, y)的坐标。<br>　　　x = At^3 + Bt^2 + Ct + D<br>　　　y = Et^3 + Ft^2 + Gt + H<br>（其中时间t的取值范围为0-1，在Pos1的时候为0，在Pos4的时候为1）</p>
<p>x(0-3)代表Pos1-Pos4中x的值，y(0-3)代表Pos1-Pos4中y的值<br>　　　A = x3 &#8211; 3 * x2 +3 * x1 &#8211; x0<br>　　　B = 3 * x2 &#8211; 6 * x1 + 3 * x0<br>　　　C = 3 * x1 &#8211; 3 * x0<br>　　　D = x0</p>
<p>　　　E = y3 &#8211; 3 * y2 +3 * y1 &#8211; y0<br>　　　F = 3 * y2 &#8211; 6 * y1 + 3 * y0<br>　　　G = 3 * y1 &#8211; 3 * y0<br>　　　H = y0</p>
<p>　　上面是公式，那么下面我们来看看如何获得Pos1-Pos4：首先，Pos1和 Pos2的取值会比较容易获得，根据OriginB配合当前的速度和方向可以获得，然而Pos3和Pos4呢，怎么获得呢？如果在从Pos1到Pos4的过程中有新的PDU到达，那么我们定义它为NewPackage。</p>
<p>　　　Pos3 = NewPackage.X + NewPackage.Y * t + 1/2 * NewPackage.a * t^2<br>　　　Pos4 = Pos3 &#8211; (NewPackage.V + NewPackage.a * t)</p>
<p>如果没有NewPackage的情况下,则Pos3和Pos4按照开始所规定的方法获得。</p>
<p>至此，关于导航推测的算法大致介绍完毕。</p>
<p>欢迎讨论，联系作者：QQ 181194&nbsp;&nbsp; MSN: <a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#120;&#105;&#97;&#116;&#97;&#105;&#121;&#105;&#64;&#104;&#111;&#116;&#109;&#97;&#105;&#108;&#46;&#99;&#111;&#109;"><u><font color=#0000ff>xiataiyi@hotmail.com</font></u></a><br>参考文献《Defeating Lag with Cubic Splines》</p>
<p>&nbsp;</p>
<p>本文来自CSDN博客，转载请标明出处：<a href="http://blog.csdn.net/wind520/archive/2009/02/17/3898948.aspx"><u><font color=#810081>http://blog.csdn.net/wind520/archive/2009/02/17/3898948.aspx</font></u></a></font></span></p>
<img src ="http://www.cppblog.com/keigoliye/aggbug/95464.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/keigoliye/" target="_blank">暗夜教父</a> 2009-09-07 16:22 <a href="http://www.cppblog.com/keigoliye/archive/2009/09/07/95464.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>