教父的告白
一切都是纸老虎
posts - 82,  comments - 7,  trackbacks - 0
使用下列JSON库:
http://www.lshift.net/blog/2007/02/17/json-and-json-rpc-for-erlang

该JSON库采用Joe Armstrong prefered Data type mapping
即:
JSON Obj    = type obj()   = {obj, [{key(), val()}]}   
JSON Array  
= type array() = [val()]   
JSON Number 
= type num()   = int() | float()    
JSON String 
= type str()   = bin()   
JSON 
true false null       = truefalse null (atoms)   
With Type val() 
= obj() | array() | num() | str() | true | false | null  
and key() being a str(). (Or a binary or atom, during JSON encoding.)  

测试如下:
Eshell V5.6.3  (abort with ^G)   
1> O = rfc4627:encode({obj, [{name, hideto}{age, 23}]}).   
"{\"name\":\"hideto\",\"age\":23}"  
2> rfc4627:decode(O).   
{ok,{obj,[{"name",<<"hideto">>},{"age",23}]},[]}   
3> A = rfc4627:encode([1,2,3,4,5]).   
"[1,2,3,4,5]"  
4> rfc4627:decode(A).   
{ok,[1,2,3,4,5],[]}   
5> N = rfc4627:encode(12345).   
"12345"  
6> rfc4627:decode(N).   
{ok,12345,[]}   
7> S = rfc4627:encode("12345").   
"[49,50,51,52,53]"  
8> rfc4627:decode(S).   
{ok,"12345",[]}   
9> T = rfc4627:encode(true).   
"true"  
10> rfc4627:decode(T).   
{ok,true,[]}   
11> F = rfc4627:encode(false).   
"false"  
12> rfc4627:decode(F).   
{ok,false,[]}   
13> Null = rfc4627:encode(null).   
"null"  
14> rfc4627:decode(Null).   
{ok,null,[]}  

posted @ 2009-09-18 15:38 暗夜教父 阅读(3016) | 评论 (0)编辑 收藏

转自http://www.javaeye.com/topic/401041


学erlang有一段时间了,现在在维护一套webim系统
并打算扩展成 webgame 的服务程序

在没有使用包协议的时候,遇到好多粘包问题,实在恼火

查阅了相关资料:

Flash Socket 的 writeUTF() 会自动增加包头长度的协议,刚好对应了
Erlang的Socket选项 {packet,2}

这使得两者的通信非常完美,再也不用担心粘包什么的问题了

 

下面是我写的一个Flash Socket 接口:SocketBridge.as

package {   
    
import flash.display.Sprite;   
    
import flash.events.*;   
    
import flash.net.Socket;   
    
import flash.utils.*;   
    
import flash.external.ExternalInterface;   
    
import flash.system.*;   
    
public class SocketBridge extends Sprite {   
        Socket.prototype.timeout    
=3000;   
        
private var socket:Socket;   
        
public function SocketBridge()   
        
{   
            socket 
= new Socket();   
            socket.addEventListener( Event.CONNECT, onConnect );   
            socket.addEventListener( ProgressEvent.SOCKET_DATA, onDataRecevice);   
            socket.addEventListener( Event.CLOSE, onClose);   
            socket.addEventListener( IOErrorEvent.IO_ERROR, onError);    
               
            
if(ExternalInterface.available)   
            
{   
                   
                ExternalInterface.addCallback(
"socket_connect",socket_connect);   
                ExternalInterface.addCallback(
"socket_send",socket_send);   
                ExternalInterface.addCallback(
"load_policy",load_policy);   
            }
   
        }
   
        
public function onError(e):void  
        
{   
            ExternalInterface.call(
"sb_onerror",e.text);   
            socket.close();   
        }
   
        
public function load_policy(host:String,port):void  
        
{   
            Security.loadPolicyFile(
"xmlsocket://"+host+":"+port);     
        }
   
           
        
public function socket_connect(host:String,port):void  
        
{   
            
try{   
                socket.connect(host,port);   
            }
catch(e){   
                ExternalInterface.call(
"sb_onerror",e.text);   
            }
   
        }
   
           
        
public function socket_send(msg:String)   
        
{      
            socket.writeUTF(msg);   
            socket.flush();   
        }
   
           
        
private function onConnect(event:Event):void    
        
{   
            ExternalInterface.call(
"sb_onconnect",true);   
        }
   
           
        
private function onClose(event:Event):void    
        
{   
            socket.close();   
            ExternalInterface.call(
"sb_onclose",true);   
        }
   
  
        
private function onDataRecevice( eventrogressEvent ):void  
        
{   
            var sdata:String;   
            
while(socket.bytesAvailable){   
                sdata 
= socket.readUTF();   
                ExternalInterface.call(
"sb_ondata",sdata);   
            }
   
        }
   
  
    }
   
}
  
posted @ 2009-09-18 12:40 暗夜教父 阅读(885) | 评论 (0)编辑 收藏

转自云风BLOG http://blog.codingnow.com/2006/10/multi_process_design.html


目前,我们的游戏服务器组是按多进程的方式设计的。强调多进程,是想提另外一点,我们每个进程上是单线程的。所以,我们在设计中,系统的复杂点在于进程间如何交换数据;而不需要考虑线程间的数据锁问题。

如果肆意的做进程间通讯,在进程数量不断增加后,会使系统混乱不可控。经过分析后,我决定做如下的限制:

  1. 如果一个进程需要和多个服务器做双向通讯,那么这个进程不能处理复杂的逻辑,而只是过滤和转发数据用。即,这样的一个进程 S ,只会把进程 A 发过来的数据转发到 B ;或把进程 B 发过来的数据转发到 A 。或者从一端发过来的数据,经过简单的协议分析后,可以分发到不同的地方。例如,把客户端发过来的数据包中的聊天信息分离处理,交到聊天进程处理。

  2. 有逻辑处理的进程上的数据流一定是单向的,它可以从多个数据源读取数据,但是处理后一定反馈到另外的地方,而不需要和数据源做逻辑上的交互。

  3. 每个进程尽可能的保持单个输入点,或是单个输出点。

  4. 所有费时的操作均发到独立的进程,以队列方式处理。

  5. 按功能和场景划分进程,单一服务和单一场景中不再分离出多个进程做负载均衡。

性能问题上,我是这样考虑的:

我们应该充分利用多核的优势,这会是日后的发展方向。让每个进程要么处理大流量小计算量的工作;要么处理小流量大计算量的工作。这样多个进程放在一台物理机器上可以更加充分的利用机器的资源。

单线程多进程的设计,个人认为更能发挥多核的优势。这是因为没有了锁,每个线程都可以以最大吞吐量工作。增加的负担只是进程间的数据复制,在网游这种复杂逻辑的系统中,一般不会比逻辑计算更早成为瓶颈。如果担心,单线程没有利用多核计算的优势,不妨考虑以下的例子:

计算 a/b+c/d+e/f ,如果我们在一个进程中开三条线程利用三个核同时计算 a/b c/d e/f 固然不错,但它增加了程序设计的复杂度。而换个思路,做成三个进程,第一个只算 a/b 把结果交给第二个进程去算 c/d 于之的和,再交个第三个进程算 e/f 。对于单次运算来算,虽然成本增加了。它需要做额外的进程间通讯复制中间结果。但,如果我们有大量连续的这样的计算要做,整体的吞吐量却增加了。因为在算 某次的 a/b 的时候,前一次的 c/d 可能在另一个核中并行计算着。

具体的设计中,我们只需要把处理数据包的任务切细,适当增加处理流水线的长度,就可以提高整个系统的吞吐量了。由于逻辑操作是单线程的,所以另需要注意的一点是,所有费时的操作都应该转发到独立的进程中异步完成。比如下面会提到的数据存取服务。

对于具体的场景管理是这样做的:

玩 家连接进来后,所有数据包会经过一个叫做位置服务的进程中。这个进程可以区分玩家所在的位置,然后把玩家数据分发到对应的场景服务进程中。这个位置服务同 时还管理玩家间消息的广播。即,单个的场景(逻辑)服务并不关心每个数据包为哪几个玩家所见,而由这个服务将其复制分发。

当玩家切换场景,场景服务器将玩家的数据发送给数据服务,数据服务进程 cache 玩家数据,并将数据写入数据库。然后把玩家的新的场景编号发回位置服务进程,这样位置服务器可以将后续的玩家数据包正确的转发到新的场景服务进程中。

掉落物品和资源生产同样可以统一管理,所以的场景(逻辑)进程都将生产新物件的请求发给物品分配服务,由物品分配服务生产出新物件后通知位置服务器产生新物品。

这样一系列的做法,最终保证了,每个场景服务器都有一个唯一的数据源——位置服务进程。它跟持久化在数据库中的数据无关,跟时钟也无关。由此带来的调试便利是很显著的。

最近,面临诸多进程的设计时,最先面临的一个复杂点在于启动阶段。显然,每个进程都配有一套配置文件指出其它进程的地址并不是一个好主意。而为每个 服务都分配一个子域名在开发期也不太合适。结果我们采取了一个简单的方案:单独开发了一个名字服务器。它的功能类似 DNS ,但是可以让每个进程自由的注册自己的位置,还可以定期汇报自己的当前状态。这样,我们可以方便的用程序查询到需要的服务。名字服务器的协议用的类似 POP3 的文本协议,这让我们可以人手工 telnet 上去查阅。我相信以后我们的维护人员会喜欢这样的设计的。:D

以上,国庆假期结束以来的工作。感谢项目组其他同事的辛勤编码。



posted @ 2009-09-14 13:29 暗夜教父 阅读(531) | 评论 (0)编辑 收藏
  MMORPG不同于其它的局域网的网络游戏,它是一个面向整个Internet的连接人数过万的网络游戏,因此他的服务器端设计则极为重要
  
  服务器的基本设置
  
  在大型网络游戏里,通常设计为C/S结构,客户端不再对数据进行逻辑处理,而只是一个收发装置,从玩家那里接受到操作信息,然后反馈给服务器,再由服务器进行处理后发回客户端,经客户端通过图形化处理,给玩家呈现出一个缤纷的游戏世界。
   
  登陆服务器
  
  在这里也可以称之为连接服务器,网络游戏的客户端一般是连接到这里,然后再由该连接服务器根据不同的需要,把游戏消息转发给其它相应的服务器(逻辑和地图服务器)也因为它是客户端直接连接的对象,它同时也负担了验证客户身份的工作。
  
  地图服务器
  
  在这里也可以称之为连续事件服务器。在这个服务器里要处理的对象(玩家)所做的动作都是一个连续事件。例如玩家从A点移动到B点,这样一个动作,需要一定的时间进行移动,因此说移动是一个连续事件。
  
  逻辑服务器
  
在这里可以称之为瞬时事件服务器,在这个服务器里,处理对象(玩家)所做的动作均可以在非常断时间内完成完成。例如玩家从商店购买一瓶药书,当玩家确认 购买后,服务器先扣除玩家的游戏币,然后再把相应的药水瓶加入玩家的背包里。这2个操作对于服务器来说,只是2个数字的加减,计算完这两个数字的加减,这 个事件就可以结束了。因此,我们可以说这个事件是一个瞬时事件
  
  服务器组的改进
  
  不过在实际应用的过程中,游戏服务器的结构要比上面所说的3种服务结构要复杂些,不过也都是在这3种最基本的服务器架构下进行扩充,扩充的主要是其它辅助功能。在实际应用里可能增加的2种服务器,数据库服务器,计费服务器,由逻辑服务器独立出来的聊天服务器。
   
  数据库服务器
  
  数据库服务器其实就是专门利用一台服务器进行数据库的读写操作。这点特别是在大型的网络游戏里尤为重要。因为在大型网络游戏里,要处理玩家的数据量非常大,如果不利用专门的服务器进行处理,很有可能会拖累这个服务器组。
  
  计费服务器
  
  通常在商业的网络游戏里出现,用于记录玩家在线的时间,给收费提供依据,同时也是整个服务器组里最重要的部分,一旦出现问题,运营商就不用赚钱了。
  
  聊天服务器
  
  在游戏里的聊天功能是属于一种瞬时动作,理论上是放在逻辑服务器里进行处理。不过在大型网络游戏里,因为这个部分功能与游戏里的其它部分联系并不紧密,因此可以独立出来做一个功能服务器。
  
  服务器的集群设置
  
  在大型游戏的应用过程中,实际需要处理的玩家数量可能过万,一台普通的服务器是无法完成所要完成的工作,因此,在实际应用的时候,通常是由一组多台服务器共同完成一个功能。
  例如地图服务器,可以根据需要,把游戏里所有的地域进行划分,划分为N个区域,然后让这一个区域里发生的事件都用一个特定的服务器进行处理。这样做的目的是减少一个服务器所承担的计算量,把整个系统组成一个分布式的网络。
不过这样做的同时会造成一个麻烦:当一位玩家从区域1,移动到区域2。这个时候,就必须先在服务器1里把玩家删除,然后再在区域2里加入玩家。同时需要 由服务器1向服务器2转移玩家的数据信息(因为服务器组在工作的时候,玩家的信息只能保存在当前所在区域的服务器里),也就是说一旦玩家发生服务器间区域 移动,服务器端就不可避免的造成数据通讯。因为这种移动并不是有规律的,玩家所在的服务器都有可能到达其它服务器。这样,如果服务器组里有N台地图服务 器,那么,每个服务器都可能向其它N-1台服务器产生连接,总共就可能产生N×N个连接。如此数量连接如果只是使用普通的socket设计,就很有可能会 给服务器通讯间的各种问题所困扰,为此,在商业网络游戏的服务器之间,通常都使用成熟的第三方的通讯中间件,如ACE,ICE等作为网络连接的传输层。
  
posted @ 2009-09-14 13:29 暗夜教父 阅读(1575) | 评论 (1)编辑 收藏
作者博客:
http://blog.csdn.net/yahle
大纲:
项目的历史背景
服务器的设计思路
服务器的技术
服务器的设计
服务器的改进
图形引擎myhoho及UI库的设计

客户端与服务器的集成


网 络游戏一般采用C\S模式,网络游戏的设计重点,我认为在于Server端,也就是我们说的服务器。在服务器端的设计,我把服务器按照功能分为2个部分, 一个负责游戏世界的处理,一个服务器服务器与客户端的通讯。在负责游戏世界的处理的服务器,我又按照功能分为地图服务器和逻辑服务器。这样划分的依据是他 们处理的内容不同进行。当初的设计还考虑到系统的集群功能,可以把游戏的地图移动处理和游戏的逻辑处理都分别分摊到其它服务器里面去。但是做到最后,发现 这样的设计也不是太好,主要是因为在处理一些游戏事件的时候需要两个服务器之间进行协同,这样势必要创建一定的网络游戏消息,在开始制作游戏的时候,因为 需要系统的东西不是很多,所以没有太注意,到项目的后期,想增加一个功能的时候,就发现在处理船只沉没的时候,服务器需要传递很多同步数据,而且服务器各 自在设置玩家数据的时候,也有很多重复的地方。如果今后还要再加点什么其它功能,那要同步的地方就实在是太多了,所以按照功能把服务器分为2个部分的设计 还是存在缺陷的,如果让我重新再来,我会选择单服务器的设计,当然这个服务器还是要和连接服务器进行分离,因为游戏的逻辑处理和与玩家的通讯还是很好分开 的,而且分开的话,也有利于逻辑服务器的设计。







登陆(连接)服务器的设计:



   在网络游戏里,其中一个很大的难点就是玩家与服务器的通讯,在Windos的服务器架构下,网络游戏服务器端采用的I/O模型,通常是完成端口。在项目 开始时研究完成端口,感觉很难,根本看不懂,因为它在很多地方与以前写网络通讯软件时用的方法不同。但是当我分析过3个完成端口的程序后,基本了解的它的 使用方法。而且在懂以后,回过头来看,其它完成端口的概念也不是很复杂,只要能清楚的了解几个函数的使用方法以及基本的处理框架流程,你就会发现它其实非 常的简单。



   完成端口的一些需要理解的地方:



1。消息队列



2。工作线程



3。网络消息返回结构体







   一般我们在设计服务器端的时候,最关键的地方是如何分辩刚刚收到的网络数据是由那个玩家发送过来的,如果是采用消息事件驱动的话,是可以得到一个 socket的值,然后再用这个值与系统里存在的socket进行比对,这样就可以得到是那位玩家发送过来的游戏消息。我在还没有使用完成端口的时候,就 是使用这个方法。这样的设计有一个缺点就是每次收到数据的时候回浪费很多时间在于确定消息发送者身份上。但是在完成端口的设计里,我们可以采用一个取巧的 方法进行设计。所以,这个问题很轻易的就结局了,而且系统开销也不是很大,关于完成端口,可以参考一下的文章:



《关于Winsock异步I/O模型中的事件模型》



http://search.csdn.net/Expert/topic/166/166227.xml?temp=.4639093



《手把手教你玩转SOCKET模型之重叠I/O篇》



http://blog.csdn.net/piggyxp/archive/2004/09/23/114883.aspx



《学习日记]IOCP的学习--初步理解》



http://www.gameres.com/bbs/showthread.asp?threadid=25898



《用完成端口开发大响应规模的Winsock应用程序》



http://www.xiaozhou.net/ReadNews.asp?NewsID=901



《理解I/O Completion Port》



http://dev.gameres.com/Program/Control/IOCP.htm



几个关键函数的说明:



http://msdn.microsoft.com/library/en-us/fileio/fs/postqueuedcompletionstatus.asp?frame=true



http://msdn.microsoft.com/library/en-us/fileio/fs/createiocompletionport.asp?frame=true



http://msdn.microsoft.com/library/en-us/fileio/fs/getqueuedcompletionstatus.asp?frame=true



http://msdn.microsoft.com/library/en-us/winsock/winsock/wsarecv_2.asp?frame=true







如果你能认真的搞清楚上面的东西,我估计你离理解完成端口就只有一步了。剩下的这一步就是自己编码实现一个下了。有些时候,看得懂了不一定会实际应用,不实实在在的写一点程序,验证一下你的想法,是不会真正搞清楚原理的。







不 过除非你想深入的研究网络技术,否则只要知道怎么用就可以了,剩下的就是寻找一个合适的别人封装好的类来使用。这样可以节省你很多的事件,当然拿来的东西 最好有源代码,这样如果发生什么问题,你也好确定是在那个地方出错,要改或者扩充功能都会方便很多。当然,还要注意人家的版权,最好在引用别人代码的地方 加一些小小的注解,这样用不了多少时间,而且对你,对原作者都有好处^_^。







不过在 完成端口上我还是没有成为拿来主义者,还是自己封装了完成端口的操作,原因找到的源代码代码封装的接口函数我怎么看怎么觉得别扭,所以最后还是自己封装了 一个完成端口,有兴趣的可以去看我的源代码,里面有很详细的注解。而且就我看来,要拿我封装的完成端口类使用起来还是很简单的。使用的时候,只要继承我的 CIOCP,然后,根据需要覆盖3个虚函数(OnAccept,OnRead,OnClose)就可以了,最多是在连接函数里,需要用一个函数去设置一下 完成端口信息。当然,我封装的类稍微简单了一些,如果要拿来响应大规模连接,还是存在很多的问题,但是如果只是针对少量连接,还是可以应付的。







对 于客户端的I/O模型,我就没有那么用心的去寻找什么好的解决方案,采用了一个最简单的,最原始的阻塞线程的方法做。原理很简单:创建一个sockt,把 socket设置为阻塞,连接服务器成功后,启动一个线程,在线程里面用recv()等待服务器发过来的消息。在我的代码里,也是把阻塞线程的方法封装成 一个类,在使用的时候,先继承TClientSocket,然后覆盖(重载)里面的OnRead()函数,并在里面写入一些处理收到数据后的操作代码。在 用的时候,只要connect成功,系统就会自动启动一个接收线程,一旦有数据就触发刚才覆盖的OnRead函数。这个类我也不是完全直接写的,在里面使 用了别人的一些代码,主要是让每个类都能把线程封装起来,这样在创建不同的类的实体的时候,每个类的实体自己都会有一个单独的数据接收线程。



当 然除了阻塞线程的方法,比较常用的还有就是用消息事件的方法收取数据了。我刚开始的时候,也是采用这个方法(以前用过^_^),但是后来发现不太好封装, 最后采用阻塞线程的方法,这样做还有一个好处可以让我的代码看起来更加舒服一些。不过就我分析《航海世纪》客户端采用的是消息事件的I/O模型。其它的网 络游戏就不太清楚了,我想也应该是采用消息事件方式的吧。。



   我记得在gameres上看到过某人写的一篇关于完成端口的笔记,他在篇末结束的时候,提出一个思考题:我们在学习完成端口的时候,都知道它是用于server端的操作,而且很多文章也是这样写的,但是不知道有没有考虑过,用完成端口做客户端来使用?



   其实这个问题很好回答,答案是OK。拿IOCP做客户端也是可行的,就以封装的IOCP为例,只要在继承原来的CIOCP类的基础上,再写一个Connect(char * ip, int port)的函数,就可以实现客户端的要求了。
  1. bool CIOCPClient::Connect(char *ip, int port)  
  2. {  
  3.         //  连接服务器  
  4.   
  5.     if (!bInit)  
  6.   
  7.         if (!Init())  
  8.   
  9.             return false;  
  10.   
  11.     //  初始化连接socket  
  12.     SOCKET m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  
  13.   
  14.     if (m_socket == SOCKET_ERROR)  
  15.         return false;  
  16.   
  17.     // 填写服务器地址信息  
  18.   
  19.     sockaddr_in ClientAddr;  
  20.   
  21.     ClientAddr.sin_family = AF_INET;  
  22.   
  23.     ClientAddr.sin_port = htons(port);      
  24.   
  25.     ClientAddr.sin_addr.s_addr = inet_addr(ip);  
  26.     // 绑定监听端口  
  27.     bind(m_socket, (SOCKADDR *)&ClientAddr, sizeof(ClientAddr));  
  28.   
  29.     if (connect(m_socket, (SOCKADDR *)&ClientAddr, sizeof(ClientAddr)) == SOCKET_ERROR)  
  30.   
  31.         return false;  
  32.     this->m_workThread = true;  
  33.   
  34.   
  35.   
  36.     g_hwThread = CreateThread(NULL, 0, WorkThread, (LPVOID)this, 0, &m_wthreadID);  //  创建工作线程,用来处理完成端口消息的  
  37.     this->SetIoCompletionPort(m_socket, NULL);  //  设置完成端口监听的socket  
  38.     return true;  
  39.   
  40. }  

前面一段是用来连接服务器,所有的客户端程序都是要这样做的,当连接成功后,m_socket就是我们想要的用于与服务器 端通讯的socket,然后,我们启动工作线程,并使用SetIoCompletionPort来设置完成端口监听的socket。只要在原来的基础上增 加一个函数,就可以把用于服务器的ICOP变成用于客户端的IOCP。



   在收到网络数据以后,下一步就是根据需要,把收到的网络数据包转变为游戏消息数据包。在转换之前,首先是要从收到的网络数据里面提取出有效的消息。这里 为什么说是要提取有效部分?其主要原因是,我们创建的游戏消息数据,在进行网络传输的时候,不是以消息的长度来传的,而是根据系统在接收到发送数据请求的 时候,根据实际情况来发送的。例如我这里有一条很长的游戏消息,有3k,但是系统一次只能发送1k的数据,所以,我们的游戏消息,只能把我们的游戏消息分 为3个包,分3次发送,这样在我们接收消息的时候,就会触发3次OnRead,而这3次OnRead收到的数据都不是一次完整的游戏消息。所以,我们在收 到网络数据后,要先和上一次收到的网络数据进行合并,然后再在里面提取出有效的游戏消息,并在提取后,把已经提取的部分删除。我在这里把这一步操作封装到 一个类里CBuftoMsg。这里顺便说明一下:一条游戏消息的网络数据包是以0x00EEEE(16进制)为结束标记(《航海世纪》的做法)。
  1. struct TMessage  
  2.   
  3. {  
  4.   
  5.     char * p;       //  消息头所在的位置  
  6.   
  7.   
  8.   
  9.     long len;       //  整个消息的长度  
  10.   
  11.   
  12.   
  13. };  
  14.   
  15.   
  16.   
  17.   
  18.    
  19.   
  20.   
  21. class CBuftoMsg  
  22.   
  23.   
  24.   
  25. {  
  26.   
  27.   
  28.   
  29. protected:  
  30.   
  31.   
  32.   
  33.     char msgbuf[BUF_LEN];     
  34.   
  35.   
  36.   
  37.     char * buf_end;  
  38.   
  39.   
  40.   
  41.     char * buf_begin;  
  42.   
  43.   
  44.   
  45.     int buf_len;  
  46.   
  47.   
  48.   
  49. public:  
  50.   
  51.   
  52.   
  53.     CBuftoMsg(void);  
  54.   
  55.   
  56.   
  57.     TMessage getMessage(void);  
  58.   
  59.   
  60.   
  61.     void cleanup_buf(void);  
  62.   
  63.   
  64.   
  65.     bool AddMsgBuf(const char *, int);  
  66.   
  67.   
  68.   
  69.     int tag;  
  70.   
  71.   
  72.   
  73. };  
  74.   
  75.   
  76.   
  77.   
  78.    
  79.   
  80.   
  81. CBuftoMsg::CBuftoMsg(void)  
  82.   
  83.   
  84.   
  85. {  
  86.   
  87.   
  88.   
  89.     buf_begin = msgbuf;  
  90.   
  91.   
  92.   
  93.     buf_end = msgbuf;  
  94.   
  95.   
  96.   
  97.     buf_len = 0;  
  98.   
  99.   
  100.   
  101. }  
  102.   
  103.   
  104.   
  105.   
  106.    
  107.   
  108.   
  109. TMessage CBuftoMsg::getMessage()  
  110.   
  111.   
  112.   
  113. {  
  114.   
  115.   
  116.   
  117.     char * p    = buf_begin;  
  118.   
  119.   
  120.   
  121.     TMessage result;  
  122.   
  123.   
  124.   
  125.     result.len  = 0;  
  126.   
  127.   
  128.   
  129.     result.p    = NULL;  
  130.   
  131.   
  132.   
  133.     while(p <= buf_begin + buf_len - 2)  
  134.   
  135.   
  136.   
  137.     {  
  138.   
  139.   
  140.   
  141.         if ( *p == 0x00)  
  142.   
  143.   
  144.   
  145.         {  
  146.   
  147.   
  148.   
  149.             const static char ce = 0xEE;  
  150.   
  151.   
  152.   
  153.             if (*(p + 1) == ce)  
  154.   
  155.   
  156.   
  157.                 if(*(p + 2) == ce)  
  158.   
  159.   
  160.   
  161.                 {  
  162.   
  163.   
  164.   
  165.                     //  每条消息都是以 00 EE EE 为结束标志  
  166.   
  167.   
  168.   
  169.                     result.p    = buf_begin;  
  170.   
  171.   
  172.   
  173.                     result.len  = p - buf_begin + 3;  
  174.   
  175.   
  176.   
  177.                     buf_begin   =  p + 3;  
  178.   
  179.   
  180.   
  181.                     buf_end     = buf_begin + buf_len;  
  182.   
  183.   
  184.   
  185.                     buf_len -= result.len;  
  186.   
  187.   
  188.   
  189.                     break;  
  190.   
  191.   
  192.   
  193.                 }  
  194.   
  195.   
  196.   
  197.         }  
  198.   
  199.   
  200.   
  201.         p++;  
  202.   
  203.   
  204.   
  205.     }  
  206.   
  207.   
  208.   
  209.     return result;  
  210.   
  211.   
  212.   
  213. }  
  214.   
  215.   
  216.   
  217.   
  218.    
  219.   
  220.   
  221. void CBuftoMsg::cleanup_buf()  
  222.   
  223.   
  224.   
  225. {  
  226.   
  227.   
  228.   
  229.     if (buf_len < BUF_LEN)  
  230.   
  231.   
  232.   
  233.     {  
  234.   
  235.   
  236.   
  237.         if (buf_len == 0)  
  238.   
  239.   
  240.   
  241.         {  
  242.   
  243.   
  244.   
  245.             buf_begin   = msgbuf;  
  246.   
  247.   
  248.   
  249.             buf_end     = msgbuf;  
  250.   
  251.   
  252.   
  253.         }  
  254.   
  255.   
  256.   
  257.         else  
  258.   
  259.   
  260.   
  261.         {  
  262.   
  263.   
  264.   
  265.             memmove(msgbuf, buf_end - buf_len, buf_len);  
  266.   
  267.   
  268.   
  269.             buf_begin = msgbuf;  
  270.   
  271.   
  272.   
  273.             buf_end = buf_end - buf_len;  
  274.   
  275.         }  
  276.   
  277.   
  278.     }  
  279.   
  280.     else  
  281.   
  282.     {  
  283.   
  284.         //  加入缓冲区的数据过多,要抛弃原来的内容  
  285.   
  286.         buf_begin   = msgbuf;  
  287.   
  288.         buf_end     = msgbuf;  
  289.   
  290.         buf_len     = 0;  
  291.   
  292.     }  
  293.   
  294. }  
  295.   
  296. bool CBuftoMsg::AddMsgBuf(const char * buf, int len)  
  297. {  
  298.   
  299.     if (len < 1)  
  300.   
  301.         return false;  
  302.   
  303.     bool result = true;  
  304.   
  305.     buf_len += len;  
  306.   
  307.     if (buf_len >= BUF_LEN)     //  如果缓冲区装满了则直接把原来的缓冲区清空再重新复制数据  
  308.     {  
  309.         this->cleanup_buf();      
  310.         result = false;  
  311.   
  312.     }  
  313.   
  314.     memcpy(buf_begin, buf, len);  
  315.   
  316.     return result;  
  317.   
  318. }  


我在这里把 CBuftoMsg 的代码贴出来,主要是因为,我在写本文的时候,发现一个惊天动地的bug,有兴趣的读者可以自己去找一下。不过一开始写代码的时候,还不是这样的,当初的 代码bug比这个还要多,问题还要严重,严重到经常让服务器程序莫名其妙的崩溃,而且这个问题,一直到5月份,系统在进行集成测试的时候才发现并解决(还 没有彻底解决,至少目前我还发现了bug,),以前一直都没有怎么注意到这个问题,而且我们还把因为这个bug造成的问题,归结到线程的互斥上去^_^!







我的登陆服务器,除了基本的处理网络数据包以外,还负责玩家系统的登陆验证,这部分东西不是很复杂,在我的程序里,只是简单的从ini文件里读取玩家的信息而已,有兴趣的自己去看我的代码(不过这部分远还没有真正的完善,存在很多问题)。







除 了登陆验证以外,在登陆程序还负责进行消息转发,就是把客户端的消息分别发送到不同的服务器。如果当初设计的是一个逻辑服务器,这个功能就可以简单很多, 只要发送到一个服务器里就可以了。现在的要发到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%而已。



   ^_^呵呵看出什么问题没有?



   没有,那我告诉你,有一个概念被偷换了,消息头的数据不等于整条游戏的消息数据,所以,消息头部分虽然多出了23%,但是整条游戏消息并不会增加这么 多,最多增加17%,最少应该不会操作5%。平均起来,应该在10%左右(游戏消息里,很多消息的实际部分可能就一个int变量而已)。不过,就算是 10%,也占用了带宽。



   ^_^呵呵还看出什么问题没有?



   ^_^先去读一下我的代码,再回头看看,上面的论述还有什么问题。



   实际上,每条游戏消息由:消息头、消息实体、结束标记组成,其中固定的是消息头和结束标记,所以,实际上一条实际上游戏消息的数据包,最多比原来的多15%,平均起来,应该是8%~10%的增量而异。



   好了,不在这个计算细节上扣太多精力了。要解决这个问题,要么是增加网络数据的发送量,要么,就是调整游戏结构,例如,把两个功能服务器合并为一个服务 器,这样服务器的对象实体就由原来的4个分为3个,两两间的通讯,就由原来的12路缩减为6路,只要分8个区域就ok了。这样每个逻辑分类就有32条游戏 消息可以使用。当然,如果进一步合并服务器,把服务器端都合并到一个程序,那就不用分类了^_^!

   在登陆服务器目录下,还有一组mynet.h/mynet.cpp的文件,是我当初为服务器端设计的函数,封装的是消息事件网络响应模型。只不过封装得 不是怎么好,被抛弃不用了,有兴趣的可以去看看,反正我是不推荐看的。只不过是在这里说明一下整个工程目录的结构而已。
posted @ 2009-09-14 13:26 暗夜教父 阅读(348) | 评论 (0)编辑 收藏
转自http://coderplay.javaeye.com/blog/94209

前些天给echo_server写了个非常简单的连接压力测试程序,

   1.  -module(stress_test).  
   
2.   
   
3-export([start/0, tests/1]).  
   
4.   
   
5. start() ->  
   
6.     tests(12345).  
   
7.   
   
8. tests(Port) ->  
   
9.     io:format("starting~n"),  
  
10.     spawn(fun() -> test(Port) end),  
  
11.     spawn(fun() -> test(Port) end),  
  
12.     spawn(fun() -> test(Port) end),  
  
13.     spawn(fun() -> test(Port) end).  
  
14.   
  
15. test(Port) ->  
  
16.      case gen_tcp:connect("192.168.0.217", Port, [binary,{packet, 0}]) of  
  
17.     {ok, _} ->  
  
18.             test(Port);  
  
19.     _ ->  
  
20.         test(Port)  
  
21.     end.  
一开始我的这个stress_test客户端运行在windows上面, echo_server服务器端运行在linux上面。 结果接受了1016个连接就停止了. 于是我用ulimit -n 改了服务器端的文件描述符数量为10240. 接着还是如此,折腾了几天,最终还是没有搞明白。

于是就求助于公司的linux编程牛人,结果让我一倒...  客户端没有修改文件描述符个数. windows上得在注册表里面改.
牛人开始对这东西的性能感兴趣了,刚好我摸了一阵子erlang的文档,于是我俩就走向了erlang网络连接的性能调优之旅啦~~过程真是让人兴奋。 我们很快通过了1024这一关~~到了4999个连接,很兴奋.

但为什么4999个连接呢, 检查一下代码终于发现echo_server.erl定义了一个宏, 最大连接数为5000. 我又倒~~
修改编译之后, 连接数跑到101xx多了, 太哈皮了!
再测102400个连接时,到32767个连接数erl挂了~说是进程开得太多了. 好在记得这个erl的参数+P,可以定义erlang能生成的进程数. 默认是32768. 改了!

后面不知怎么着,在81231个连接停止了. 新的性能瓶颈又卡了我们.  好在牛人对linux熟, 用strace(这东西会莫名地退出), stap查出一些苗头.   我也想到在otp文档好像提过另一个limit,那就是端口数...在此同时我们发现erlang在linux上是用的传统poll模型. 但查erlang的源代码发现是支持epoll的. 在网上搜了半天,终于搜到了个maillist的帖子.

代码
  1. $./configure --enable-kernel-poll  

由于我们的测试服务器是双核的,我们在配置的时候也打开了smp支持.  欢快的make  & make install之后....
把 /proc/sys/net/ipv4/ip_local_port_range 的内容改成了1024到65535.  最多也也能改成65535 :)

代码
  1. $echo 1024 65535 > ip_local_port_range  

另外再添加一个erl的环境变量
代码
  1. $export ERL_MAX_PORTS=102400  

于是开始跑了,不过这次跑不一样了
echo_server
 
  1. $erl -noshell  +P 102400 +K true +S 2 -smp -s echo_server start  
   
stress_test
 
  1. $erl -noshell  +P 102400 +K true +S 2 -smp -s stress_test start  
这里的+K true,表示使用内核poll,+S 2 表示两个核. 这样可欢快啦~~~ 10w大关过咯! 而且比刚才没用epoll的速度快暴多~~
于是我们又开始了204800个连接发测试了~~~

用top一看cpu占用率极低,服务器只在5%左右。 内存也不是很大~~

posted @ 2009-09-14 12:25 暗夜教父 阅读(572) | 评论 (0)编辑 收藏
转自http://coderplay.javaeye.com/blog/93403
   1.  -module(echo_server).  
   
2-export([start/0,stop/0]).  
   
3.   
   
4-define(LISTEN_PORT,12345).     % 开放端口  
   
5-define(MAX_CONN, 5000).        % 最大连接数  
   
6.   
   
7. start() ->  
   
8.     process_flag(trap_exit, true), % 设置退出陷阱  
   
9.     tcp_server:start_raw_server(?LISTEN_PORT,  
  
10.                 fun(Socket) -> socket_handler(Socket,self()) end,  
  
11.                 ?MAX_CONN,   
  
12.                 0).  
  
13.   
  
14%% 处理数据  
  
15. socket_handler(Socket,Controller) ->  
  
16.     receive  
  
17.         {tcp, Socket, Bin} ->  
  
18.             gen_tcp:send(Socket, Bin); % echo  
  
19.         {tcp_closed, Socket} ->  
  
20.             ok;  
  
21.         _ ->  
  
22.             socket_handler(Socket,Controller)  
  
23.     end.  
  
24.   
  
25. stop() ->  
  
26.     tcp_server:stop(?LISTEN_PORT).  

基于Joe Armstrong 的tcp_server模块来做的, 试试先 :)

编译
    erl -noshell -s make all -s init stop
运行
    erl -noshell -sname coderplay -s echo_server start
posted @ 2009-09-14 12:24 暗夜教父 阅读(399) | 评论 (0)编辑 收藏
原文网址:erlang网络编程的几个性能调优和注意点
原文作者:coderplay
前些天给echo_server写了个非常简单的连接压力测试程序,
下载: stress_test.erl
  • -module(stress_test).  

  • -export([start/0, tests/1]).  

  • start() ->  

  • tests(12345).  

  • tests(Port) ->  

  • io:format("starting~n"),  

  • spawn(fun() -> test(Port)
    end),  

  • spawn(fun() -> test(Port)
    end),  

  • spawn(fun() -> test(Port)
    end),  

  • spawn(fun() -> test(Port)
    end).  

  • test(Port) ->  

  • case
    gen_tcp:connect("192.168.0.217", Port, [binary,{packet, 0}])
    of
  •     {ok, _} ->  

  • test(Port);  

  • _ ->  

  • test(Port)

  • end.

一开始我的这个stress_test客户端运行在windows上面,echo_server服务器端运行在linux上面。结果接受了1016个连接就停止了. 于是我用ulimit -n改了服务器端的文件描述符数量为10240. 接着还是如此,折腾了几天,最终还是没有搞明白。
于是就求助于公司的linux编程牛人,结果让我一倒…  客户端没有修改文件描述符个数. windows上得在注册表里面改.

牛人开始对这东西的性能感兴趣了,刚好我摸了一阵子erlang的文档,于是我俩就走向了erlang网络连接的性能调优之旅啦~~过程真是让人兴奋。 我们很快通过了1024这一关~~到了4999个连接,很兴奋.
但为什么4999个连接呢, 检查一下代码终于发现echo_server.erl定义了一个宏, 最大连接数为5000. 我又倒~~
修改编译之后, 连接数跑到101xx多了, 太哈皮了!
再测102400个连接时,到32767个连接数erl挂了~说是进程开得太多了. 好在记得这个erl的参数+P,可以定义erlang能生成的进程数. 默认是32768. 改了!
后面不知怎么着,在81231个连接停止了. 新的性能瓶颈又卡了我们. 好在牛人对linux熟, 用strace(这东西会莫名地退出),stap查出一些苗头.我也想到在otp文档好像提过另一个limit,那就是端口数…在此同时我们发现 erlang在linux上是用的传统poll模型.但查erlang的源代码发现是支持epoll的. 在网上搜了半天,终于搜到了个maillist的帖子.
  • $./configure --enable-kernel-poll

由于我们的测试服务器是双核的,我们在配置的时候也打开了smp支持. 欢快的make  & make install之后….
把 /proc/sys/net/ipv4/ip_local_port_range 的内容改成了1024到65535. 最多也也能改成65535
  • $echo 1024 65535 > ip_local_port_range

另外再添加一个erl的环境变量
  • $export ERL_MAX_PORTS=102400

于是开始跑了,不过这次跑不一样了
echo_server
  • $erl -noshell  +P 102400 +K true +S 2 -smp -s echo_server start

stress_test
  • $erl -noshell  +P 102400 +K true +S 2 -smp -s stress_test start

这里的+K true,表示使用内核poll,+S 2 表示两个核. 这样可欢快啦~~~ 10w大关过咯! 而且比刚才没用epoll的速度快暴多~~
于是我们又开始了204800个连接发测试了~~~
用top一看cpu占用率极低,服务器只在5%左右。内存也不是很大~~
posted @ 2009-09-14 11:20 暗夜教父 阅读(491) | 评论 (0)编辑 收藏

网路的硬件也有限,而人的创造也无限,在公网平均130ms的Latency下,是不存在“完全的”的同步情况。如何通过消除/隐藏延时,将用户带入快速的交互式实时游戏中,体验完美的互动娱乐呢?

以下六点,将助你分清楚哪些我们可以努力,哪些我们不值得努力,弄明白实时游戏中同步问题关键之所在,巧妙的化解与规避游戏,最终在适合普遍用户网络环境中(200ms),实现实时快速互动游戏:

1. 基本情况:
   (A) 网络性能指标一:带宽,限制了实时游戏的人数容量
   (B) 网络性能指标二:延时,决定了实时游戏的最低反应时间

2. 两个基本原则:
   (A) 让所有的用户屏幕上面表现出完全不同的表象是完全没有问题的。
   (B) 把这些完全不同表象完全柔和在一个统一的逻辑中也是完全没有问题的。

3. 同步的十二条应对策略:
   (A) 最大可能减少游戏中的数据传输
   (B) 将阻塞通信放到线程池中实现
   (C) 永远不要为了等待某个数据而不让游戏进行下去
   (D) 利用预测和插值改进游戏的效果
   (E) 当使用预测插值的时候传送的数据不仅包括坐标,还需要速度和加速度
   (F) 将输入数据枷锁或者队列化(例如键盘消息队列),直到下次发送数据的时刻,传统的方法是在固定的时间(发送数据前)检测键盘,在游戏的原理上隐藏延时
   (G) 使用事件调度表,将需要在所有用户客户端同时发生的事件,提前广播到所有用户
   (H) 使用多次攻击来杀死一个精灵,尽量减少一次性的、确定性的、延时敏感的事件
   (I) 延长子弹或者火箭在空中飞行的时间(在其飞行的同时,在所有客户端进行预测插值)
   (J) 所有物体从一个地方移动到另外一个地方都需要时间,避免诸如“瞬间移动”的设计
   (K) 尽量使游戏中所有精灵,飞船或者其他物体,都按照可预测的轨迹运行,比如在移动中增加惯性
   (L) 充分发挥创造力,尽最大可能的合并游戏中前后相关的事件,合并游戏中存在的延时此问题,需要在技术上改进的同时也需要策划有所重视,规避一些影响较大的设计,巧妙的隐藏"延时"

4. 同步问题现状:
   (A) 重视程度不够:很多人尚未意识到此问题的存在,曾有公司花半年时间打算做一款“松鼠大战”的网络版。
   (B) 技术上无彻底解决方案:对于多数程序员,单机游戏技术善未成熟就匆匆步入网络时代。
   (C) 研究这个技术需要条件:需要有实力的公司才能提供,无此条件,即便有能力的程序员也无法成功。

5. 目前网游的三大技术难题:
   (A) 服务器的响应问题:如何使服务器在支持越来越多的人数的情况下提供最高的响应。
   (B) 同步问题:如何在有限的网络响应情况下,实现快速实时类游戏,提供最完美的交互。
   (C) 服务器分布式问题:如何在统一用户数据的情况下,利用分部式将各个分散的“世界”统一到一个“世界”中。
   谁能真正解决好以上三个问题,配合策划在设计上的突破,将使其他人在至少两年内无法超越。
  
6. 相关补充:
   (A) 网格技术现在还是抄作,真正用到游戏中,还有很多技术难点需要突破(比如:目前网格的单位计算时间是以秒计算).
   (B) 其实与很多人想法相反的是现在3D技术早已不是主要的矛盾。而现在国内外对于以上三个问题可以说处于同一个起跑线上,完全有机会取得先机。
   (C) 现在解决同步问题已经很紧迫,而同时所需要的环境也已经成熟,只要有所关注,半年之内可以得出较成熟的结论


那么具体怎么解决呢?再下一步怎么办?
这就得自己去实践了,我只说这么多了,哈哈,不然又教懒了那些成天再网上搜方案的人。



MMO的同步策略目前已经没有什么秘密可言了,简单叙述下各要素:


MMO人行走:MMO中如果是鼠标点击行走传目的地+中间寻路接点就行了,服务器简单验证一下。如果是键盘控制行走的话,需要做简单的预测插值。

MMO打怪:要看怎么个打法,客户端根本不必管服务端什么时候传来消息,直接演示动画就行了,最好把被砍的过程动画做长一点,可以在播放动画的过程中等待服务器传过来的延迟敏感事件,比如--该人已死。这样处理起来比较容易,最重要的是客户端看起来要流畅。

MMO插值:所谓“把不同表象柔和在一起”算法很多,简单的可以参考DR,位置不同的时候可以做一次线性插值,直接把人拉扯过去,或者二次线形插值,做一条平滑的曲线修正,看具体项目选择了。

时钟:就是时钟同步,所有时钟都是以“贞”为单位的,服务器主逻辑循环可以是5-10fps,多则无益,客户端同样维持一个和服务端频率相同的消息同步贞。

时间贞:把ping值加到时间计算里面是多余的,按贞计算的话,服务端告诉客户端在n贞发生xx事件的时候,客户端收到的时候如果客户端贞数大于n,说明是发生过了的,那么做插值,如果小于n,说明是未来发生的,那么加入时间表。


1 游戏中的行走,则是一个需要同步的重头戏,当今游戏分成两种同步方式,一种是以服务器端为准,如果发现客户端的坐标和服务器的坐标不符合的话,则拉回。这点的好处是可以弥补瞬移外挂的出现,但在网络环境差的情况下,则出现游戏不流畅。

所 以,我比较推荐使用以客户端计算为准的方法(当然是我因为我这个游戏对游戏流畅性要求很高),客户端向服务器发送计算好的数据,服务器经过一定的预测判 断,广播给其他玩家,这个虽然是能引起和wow一样的问题,瞬移外挂,但完全可以从游戏设计方面进行根本避免。(这里就不谈策划方面的问题了)

2 游戏的战斗,战斗的同步性要求也相当高,这里我也引用楼主说的,在固定时间检测键盘,以一定频率发送攻击消息,这里有一个关键,就是服务器和客户端都进行 攻击判定,即使服务器中没有攻击判定成功,但在客户端判定成功,也要播放攻击效果。不过一切计算效果以服务器为准。这是一个欺骗玩家的手段。

posted @ 2009-09-12 10:39 暗夜教父 阅读(4408) | 评论 (2)编辑 收藏
ErLang语法中充满了一些约定。大写字母开头的名字(比如Address),表示一个变量,包括参数、局部变量等;小写字母开头的单词(比如ok),表示一个常量,叫做atom(原子的意思),包括常量名、函数名、模块名等。

ErLang的注释用%开头。ErLang用下划线“_”表示任意变量,类似于Java的switch语法里面的default选项。

ErLang脱胎于Prolog,不过,我觉得,ErLang语法和Haskell语法比较象,都是采用 -> 定义函数。

ErLang语句中的标点符号用法很象文章的标点符号。

整个函数定义结束用一个句号“.”;同一个函数中,并列的逻辑分支之间,用分号“;”分界;顺序语句之间,用逗号“,”分隔。

ErLang中,{ }不是表示程序块的开头和结尾,而是表示一种特殊的数据结构类型——Tuple(元组),比如,{12, 3, ok}。我们可以把Tuple理解为定长数组。

[ ] 则表示最基本的函数式编程的数据结构类型——List。List数据结构很基本,写法和用法也有一定的复杂度,不是表面上看起来那么简单,后面讲解Closure的章节会详细介绍List的最基本的构造原理。

下面我们来看一个简单的例子。

我们首先定义一个最简单的函数,把一个参数乘以10,然后加1。
times10( Number ) –>
Temp = 10 * Number,
Temp + 1.

为了说明问题,上面的代码把乘法操作和加法操作分成两个步骤。Temp = 10 * Number语句后面是逗号,因为这是两条顺序执行的语句。Temp + 1语句后面是句号,表示整个函数定义结束。而且,可以看出,ErLang没有return语句,最后执行的那条语句的执行结果就是返回值。

下面,我们把这个函数优化一下。当参数等于0的时候,直接返1;否则,就乘以10,然后加1,然后返回。这时候,我们就要用到case of逻辑分支语句,相当于java的switch语句。

times10( Number ) –>
case Number of
0 -> 1;
_ ->
Temp = 10 * Number,
Temp + 1
end.

我们来仔细观察这段ErLang程序。

当Number等于0的时候,直接返回1。由于这是一条分支语句,和后面的分支是并列的关系,所以,1的后面的标点符号是分号。后面这个分支,下划线“_”表示任何其它值,这里就表示除了1之外的任何其它数值。

需要注意的一点是,case of语句需要用end结尾,end之前不需要有标点符号。

上述代码中的case of 语句,其实就是Pattern Match的一种。ErLang的Pattern Match很强大,能够大幅度简化程序逻辑,后面进行专门介绍。
Pattern Match
Pattern Match主要有两个功能——比较分派和变量赋值。
其中,比较分派是最主要的功能。比较分派的意思是,根据参数值进行条件分支的分派。可以把比较分派功能看作是一种类似于if, else等条件分支语句的简洁强大写法。
上面的例子中,case Number of 就是根据Number的值进行比较分派。更常见的写法是,可以把Pattern Match部分提到函数定义分支的高度。于是,上述代码可以写成下面的形式:
times10( 0 ) –> 1;
times10( Number ) –>
Temp = 10 * Number,
Temp + 1.

这段代码由两个函数定义分支构成,由于两个函数分支的函数名相同,而且参数个数相同,而且两个函数定义分支之间采用分号“;”分隔,说明这是同一个函数的定义。函数式编程语言中,这种定义方式很常见,看起来形式很整齐,宛如数学公式。

这段代码的含义是,当参数值等于0的时候,那么,程序走第一个函数定义分支(即分号“;”结尾的“times10( 0 ) –> 1;”),否则,走下面的函数定义分支(即“times10( Number ) –>…”)。

第二个分支中的参数不是一个常数,而是一个变量Number,表示这个分支可以接受任何除了0之外的参数值,比如,1、2、12等等,这些值将赋给变量Number。
因此,这个地方也体现了Pattern Match的第二个功能——变量赋值。

Pattern Match的形式可以很复杂,下面举几个典型的例子。
(1)数据结构拆解赋值
前面将到了ErLang语言有一种相当于定长数组的Tuple类型,我们可以很方便地根据元素的位置进行并行赋值。比如,
{First, Second} = {1, 2}
我们还可以对复合Tuple数据结构进行赋值,比如
{A, {B, C}, D} = { 1, {2, 3}, 4 }
List数据结构的赋值也是类似。由于List的写法和用法不是那么简单,三言两语也说不清楚,还徒增困扰,这里不再赘述。
(2)assertEquals语句
在Java等语言中,我们写单元测试的时候,会写一些assert语句,验证程序运行结果。这些assert语句通常是以API的方式提供,比如,assertTrue()、assertEquals()等。
在ErLang中,可以用简单的语句达到类似于assertTrue()、assertEquals()等API的效果。
比如,ErLang中,true = testA() 这样的语句表示testA的返回结果必须是true,否则就会抛出异常。这个用法很巧妙。这里解释一下。
前面讲过,ErLang语法约定,小写字母开头的名字,都是常量名。这里的true自然也是一个常量,既然是常量,我们不可能对它赋值,那么true = testA()的意思就不是赋值,而是进行匹配比较。
(3)匹配和赋值同时进行
我们来看这样一段代码。
case Result of
{ok, Message} -> save(Message);
{error, ErrorMessage} -> log(ErrorMessage)
end.

这段代码中,Result是一个Tuple类型,包含两个元素,第一个元素表示成功(ok)或者失败(error),第二个元素表示具体的信息。
可以看到,这两个条件分支中,同时出现了常量和变量。第一个条件分支中的ok是常量,Message是变量;第二个条件分支中的error是常量,ErrorMessage是变量。
这两个条件分支都既有比较判断,也有变量赋值。首先,判断ResultTuple中的第一个元素和哪一个分支的第一个元素匹配,如果相配,那么把ResultTuple中的第二个元素赋给这个分支的第二个变量元素。即,如果Result的第一个元素是ok,那么走第一个条件分支,并且把Result的第二个元素赋给Message变量;如果Result的第二个元素是error,那么走第二个条件分支,并且把Result的第二个元素赋给ErrorMessage变量。

在Java等语言中,实现上述的条件分支逻辑,则需要多写几条语句ErLang语法可以从形式上美化和简化逻辑分支分派复杂的程序。
除了支持数相等比较,Pattern Match还可以进行范围比较、大小比较等,需要用到关键字when,不过用到when的情况,就比if else简洁不了多少,这里不再赘述。
匿名函数
ErLang允许在一个函数体内部定义另一个匿名函数,这是函数式编程的最基本的功能。这样,函数式语言才可以支持Closure。我们来看一个ErLang的匿名函数的例子。
outer( C ) –>
Inner = fun(A, B) -> A + B + C end,
Inner(2, 3).

这段代码首先定义了一个命名函数outer,然后在outer函数内部定义了一个匿名函数。可以看到,这个匿名函数采用关键字fun来定义。前面讲过,函数式编程的函数就相当于面向对象编程的类实例对象,匿名函数自然也是这样,也相当于类实例,我们可以把这个匿名函数赋给一个变量Inner,然后我们还可以把这个变量当作函数来调用,比如,Inner(2, 3)。
fun是ErLang用来定义匿名函数的关键字。这个关键字很重要。fun定义匿名函数的用法不是很复杂,和命名函数定义类似。
函数分支的定义也是类似,只是需要用end结尾,而不是用句号“.”结尾,而且fun只需要写一次,不需要向命名函数那样,每个分支都要写。比如,
MyFunction = fun(0) -> 0;
(Number) -> Number * 10 + 1 end,
MyFunction(3),
函数作为变量
匿名函数可以当作对象赋给变量,命名函数同样也可以赋给变量。具体用法还是需要借助重要的fun关键字。比如,
MyFunction = fun outer / 1

就可以把上述定义的outer函数赋给MyFunction变量。后面的 / 0表示这个outer函数只有一个参数。因为ErLang允许有多个同名函数的定义,只要参数个数不同,就是不同的函数。
我们可以看到,任何函数都可以作为变量,也可以作为参数和返回值传来传去,这些变量也可以随时作为函数进行调用,于是就具有了一定的动态性。
函数的动态调用
ErLang有一个apply函数,可以动态调用某一个函数变量。
基本用法是 apply( 函数变量,函数参数列表 )。比如,上面的MyFunciton函数变量,就可以这么调用,apply( MyFunction, [ 5 ])。
那么我们能否根据一个字符串作为函数名获取一个函数变量呢?这样我们就可以根据一个字符串来动态调用某个函数了。
ErLang中,做到这一点很简单。前面讲过,函数名一旦定义了,自然就固定了,这也类似于常量名,属于不可变的atom(原子)。所有的atom都可以转换成字符串,也可以从字符串转换过来。ErLang中的字符串实质上都是List。字符串和atom之间的转换通过list_to_atom和atom_to_list来转换。
于是我们可以这样获取MyFunciton:MyFunction = list_to_atom(“outer”)
如果outer函数已经定义,那么MyFucntion就等于outer函数,如果outer函数没有定义,那么list_to_atom(“outer”)会产生一个新的叫做outer的atom,MyFucntion就等于这个新产生的atom。
如果需要强制产生一个已经存在的atom,那么我们需要调用list_to_existing_atom转换函数,这个函数不会产生新的atom,而是返回一个已经存在了的atom。
Tuple作为数据成员集合
前面讲解函数式编程特性的时候,提到了函数式编程没有面向对象编程的成员变量,这是一个限制。
ErLang的Tuple类型可以一定程度克服这个限制。Tuple可以一定程度上担当容纳成员变量的职责。
面向对象的类定义,其实就是一群数据和函数的集合,只是集合的成员之间都有一个this指针相关联,可以相互找到。
ErLang的Tuple类型就是数据的集合,可以很自然地发挥成员变量的作用,比如,{Member1, Member2}。
读者可能会说,ErLang的函数也可以作为变量,也可以放到Tuple里面,比如, { Memer1, Member2, Funtion1, Function2}。这不就和面向对象编程一样了吗?
遗憾的是,这样做是得不偿失的。因为函数式编程没有面向对象的那种内在的this指针支持,自然也没有内在的多态和继承支持,硬把数据和函数糅合在一个Tuple里面,一点好处都没有,而且还丧失了函数作为实例对象的灵活性。
所以,函数式编程的最佳实践(Best Practice)应该是:Tuple用来容纳成员数据,函数操作Tuple。Tuple定义和函数定义加在一起,就构成了松散的数据结构,功能上类似于面向对象的类定义。Tuple + 函数的数据结构,具有多态的特性,因为函数本身能够作为变量替换;但是不具有继承的特性,因为没有this指针的内在支持。
正是因为Tuple在数据类型构造方面的重大作用,所以,ErLang专门引入了一种叫做Record的宏定义,可以对Tuple的数组下标位置命名。比如,把第一个元素叫做Address,第二个元素叫做Zipcode,这样程序员就可以这些名字访问Tuple里面的元素,而不需要按照数组下标位置来访问。
Tuple和Record的具体用法还是有一定复杂度,限于篇幅,本章没有展开说明,只提了一些原理方面的要点。
其它
ErLang还有其它语法特性和细节,不再一一赘述。有兴趣的读者,可以自行去ErLang网站(www.erlang.org)进行研究。
posted @ 2009-09-11 11:04 暗夜教父 阅读(655) | 评论 (0)编辑 收藏
仅列出标题
共9页: 1 2 3 4 5 6 7 8 9 

<2024年11月>
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

常用链接

留言簿(2)

随笔分类

随笔档案

文章分类

文章档案

搜索

  •  

最新评论

阅读排行榜

评论排行榜