[翻译]Genesis UDP 服务端 和 客户端

时间:2022-10-12 22:16:19

下载源代码(104KB):http://www.codeproject.com/cs/internet/Genesis/genesis.zip 

原文地址:http://www.codeproject.com/cs/internet/Genesis.asp

简介

[翻译]Genesis UDP 服务端 和 客户端

Genesis UDP 项目是一个使用.NET Sockets来实现的轻量级UDP服务端和客户端的类库,它使用UDP以使得在网络上传输的数据量较低,并且提供了简单加密、有序传输和可靠链路的特色。

Genesis通过“命令包”来相互通讯,命令包是一个或多个UDP包,它包括2字节的操作码和多个的字符串数据段。不可靠包可以最多达到512字节大小,而不限长度的可靠包会被Genesis分割来按序传输。Genesis中自定义了一些内置的操作码,除此之外,包、操作码和数据段如何来处理完全由开发者自己来决定。

Genesis还提供了可选的加密系统,并不是很高级,但是可以对每一个客户端提供一个随机的320位长的key,并在一个异或加密算法中使用这个密钥。如果初始化连接的包没有被别人监测到,这样还是相对安全的,因为每个客户端的密钥是不一样的。在类库中也可以加入一些公钥私钥加密,还可以选择哪些连接需要加密或者不需要加密。

服务端和客户端

Genesis工作的基础是每一个节点都可以作为一个客户端或者一个服务端。这两者之间的界限是模糊的,任何使用Genesis的程序都可以连接一个服务端或者接受来自客户端的连接。当然,仅使用默认连接是没有任何功能的,在程序中必须为连接加入相应的事件处理。服务端是一个可以接受连接的远程主机,客户端则是一个可以发起连接的远程主机。因为一个Genesis实例即可以发起也可以接受连接,所以它即可以做服务端也可以做客户端。服务端和客户端统称为节点。

[翻译]Genesis UDP 服务端 和 客户端

上图表示了Genesis中的服务端和客户端是如何工作的。每一个方框表示一个Genesis类库的实例,没有一个特定的客户端或者服务端,每个节点都可以发起连接或接受连接。只有根据Genesis实例的内涵来定义服务端和客户端。我们看“Genesis1”,它连接到了服务端2和服务端3,并且4作为一个客户端和它连接,这是由箭头的方向判断的(箭头表明是谁发起的连接)。如果我们看“Genesis2”,它有两个客户端:1和3。如果我们看“Genesis4”,它有一个服务端“Genesis1”。注意,根据环境不同,“Genesis1”即可以做为服务端也可以作为客户端。

背景

Genesis的概念是源自联机第一人称射击游戏的网络协议上,比如Quake和Half-Life,它们使用UDP在服务器和连接的客户端上进行快速通讯。这些实现都可以发送可靠的有序的数据,Genesis也实现了这一点。Genesis的原本意图是作为一个用.NET开发的游戏引擎的网络通讯API,现在作为一个独立项目提供给大家使用。

使用代码

这里有三个肯定要用到的接口,它们是:

  • IConnection
  • ICommand
  • IGenesisUDP

IConnection对象包含有一个远程连接的信息,不论是对服务器还是客户端的。ICommand包含一个命令包的信息,包括操作码和数据段。IGenesisUDP是一个实际通讯的类来实现具体的功能。

在源代码中包含有两个项目“GenesisChatServer”和“GenesisChatClient”,这两个项目实现了一个网络聊天系统。用这两个项目可以帮助我们更好的来理解Genesis类库。

建立服务端

让我们来看GenesisChatServer,这个项目说明了Genesis如何来作为一个服务端来给其他客户端提供服务。

首先,我们需要在程序中声明并建立一个Genesis对象。

[翻译]Genesis UDP 服务端 和 客户端private  GenesisCore.IGenesisUDP m_UDP;
[翻译]Genesis UDP 服务端 和 客户端
[翻译]Genesis UDP 服务端 和 客户端... 
[翻译]Genesis UDP 服务端 和 客户端
[翻译]Genesis UDP 服务端 和 客户端m_UDP 
=  GenesisCore.InterfaceFactory.CreateGenesisUDP( " ChatServer " );
[翻译]Genesis UDP 服务端 和 客户端

注意如何使用InterfaceFactory类来创建一个Genesis的实例。

使用GetLocalAddresses方法来返回一个本地IP地址的列表,把这个列表填到聊天服务器程序的一个ComboBox中。

[翻译]Genesis UDP 服务端 和 客户端string [] addresses  =  m_UDP.GetLocalAddresses( );
[翻译]Genesis UDP 服务端 和 客户端...

要处理Genesis通讯事件,必须按下面各行注册事件处理:

[翻译]Genesis UDP 服务端 和 客户端// Hook genesis core events
[翻译]Genesis UDP 服务端 和 客户端
m_UDP.OnListenStateChanged  +=   new  ListenHandler(m_UDP_OnListenStateChanged);
[翻译]Genesis UDP 服务端 和 客户端m_UDP.OnConnectionAuth 
+=   new  ConnectionAuthHandler(m_UDP_OnConnectionAuth);
[翻译]Genesis UDP 服务端 和 客户端m_UDP.OnCommandReceived 
+=   new  IncomingCommandHandler(m_UDP_OnCommandReceived);
[翻译]Genesis UDP 服务端 和 客户端m_UDP.OnConnectionStateChanged 
+=   new  
[翻译]Genesis UDP 服务端 和 客户端   ConnectionStateChangeHandler(m_UDP_OnConnectionStateChanged);

OnListenStateChanged:当Genesis通讯状态改变时会被调用,它有两种状态:“Listen”和“Closed”,表示开启和关闭通讯。如果Closed,Genesis会断开所有的远程连接并关闭Socket。

OnConnectionAuth:当一个客户端发来认证信息时会被调用,如果登陆信息不被接受,那么可以拒绝该客户端。在这个聊天服务器例子中,如果发来的客户端昵称太短或者服务器密码不匹配,那么就会拒绝客户端。注意如何给客户端发回一个拒绝原因。通过修改客户端发来的ConnectionAuthEventArgs对象来控制,通过它的AuthCommand属性来得到认证信息。

 

[翻译]Genesis UDP 服务端 和 客户端private   void  OnConnectionAuth( object  o, ConnectionAuthEventArgs e)
[翻译]Genesis UDP 服务端 和 客户端[翻译]Genesis UDP 服务端 和 客户端
... {
[翻译]Genesis UDP 服务端 和 客户端    ... 
[翻译]Genesis UDP 服务端 和 客户端 
[翻译]Genesis UDP 服务端 和 客户端    
if(e.AuthCommand.Fields[1].Length < 3)
[翻译]Genesis UDP 服务端 和 客户端[翻译]Genesis UDP 服务端 和 客户端      
...{
[翻译]Genesis UDP 服务端 和 客户端        e.AllowConnection 
= false;
[翻译]Genesis UDP 服务端 和 客户端        e.DisallowReason 
= "Nickname too short.";
[翻译]Genesis UDP 服务端 和 客户端        
return;
[翻译]Genesis UDP 服务端 和 客户端      }

[翻译]Genesis UDP 服务端 和 客户端      
else if(e.AuthCommand.Fields[1].Length > 15)
[翻译]Genesis UDP 服务端 和 客户端[翻译]Genesis UDP 服务端 和 客户端      
...{
[翻译]Genesis UDP 服务端 和 客户端        e.AllowConnection 
= false;
[翻译]Genesis UDP 服务端 和 客户端        e.DisallowReason 
= "Nickname too long.";
[翻译]Genesis UDP 服务端 和 客户端        
return;
[翻译]Genesis UDP 服务端 和 客户端        }

[翻译]Genesis UDP 服务端 和 客户端
[翻译]Genesis UDP 服务端 和 客户端    ...
[翻译]Genesis UDP 服务端 和 客户端}

OnCommandReceived:当收到一个远程节点发来的命令时会被调用。这个事件的CommandEventArgs可以来获得ICommand对象(通过它的SentCommand属性),包含有发来命令的相关信息以及操作码和数据段。也可以获得发送命令的远程主机的相关信息(通过它的Sender属性)。这个事件只会在认证的节点上产生,聊天服务器通过这个事件来处理接受的聊天消息和请求用户列表。

OnConnectionStateChanged:当与一个远程节点连接或断开连接时会被调用。EventArgs包含有远程节点的信息、连接建立还是断开以及一个断开的原因。聊天服务器使用这个事件来发送用户列表给新连接的客户端。

建立客户端

现在让我们来看这个聊天系统的客户端“GenesisChatClient”,它和服务端程序很相似,先建立一个Genesis实例并注册事件,不过有些新的事件需要注册:

[翻译]Genesis UDP 服务端 和 客户端m_UDP.OnListenStateChanged  +=   new  ListenHandler(m_UDP_OnListenStateChanged);
[翻译]Genesis UDP 服务端 和 客户端m_UDP.OnLoginRequested 
+=   new  SendLoginHandler(m_UDP_OnLoginRequested);
[翻译]Genesis UDP 服务端 和 客户端m_UDP.OnAuthFeedback 
+=   new  AuthenticatedHandler(m_UDP_OnAuthFeedback);
[翻译]Genesis UDP 服务端 和 客户端m_UDP.OnConnectionStateChanged 
+=   new  
[翻译]Genesis UDP 服务端 和 客户端  ConnectionStateChangeHandler(m_UDP_OnConnectionStateChanged);
[翻译]Genesis UDP 服务端 和 客户端m_UDP.OnCommandReceived 
+=   new  IncomingCommandHandler(m_UDP_OnCommandReceived);
[翻译]Genesis UDP 服务端 和 客户端m_UDP.OnConnectionAuth 
+=   new  ConnectionAuthHandler(m_UDP_OnConnectionAuth);
[翻译]Genesis UDP 服务端 和 客户端m_UDP.OnSocketError 
+=   new  SocketErrorHandler(m_UDP_OnSocketError);
[翻译]Genesis UDP 服务端 和 客户端m_UDP.OnConnectionRequestTimedOut 
+=   new  
[翻译]Genesis UDP 服务端 和 客户端  RequestTimedOutHandler(m_UDP_OnConnectionRequestTimedOut);

OnLoginRequested:当服务端请求发送登陆信息时被调用。客户端必须发回一个操作码是OPCODE_LOGINDETAILS和相应数据段的命令包。在这个例子中,数据段包含着昵称和服务器密码。

[翻译]Genesis UDP 服务端 和 客户端private   void  OnLoginRequested( object  o, LoginSendEventArgs e)
[翻译]Genesis UDP 服务端 和 客户端[翻译]Genesis UDP 服务端 和 客户端
... {
[翻译]Genesis UDP 服务端 和 客户端    
if(e.Connected)
[翻译]Genesis UDP 服务端 和 客户端[翻译]Genesis UDP 服务端 和 客户端    
...{
[翻译]Genesis UDP 服务端 和 客户端        e.ServerConnection.SendUnreliableCommand(
0
[翻译]Genesis UDP 服务端 和 客户端               GenesisConsts.OPCODE_LOGINDETAILS, 
[翻译]Genesis UDP 服务端 和 客户端[翻译]Genesis UDP 服务端 和 客户端               
new string[] ...{txtServerPW.Text, txtNickName.Text} );
[翻译]Genesis UDP 服务端 和 客户端        spState.Text 
= "Sending login details...";
[翻译]Genesis UDP 服务端 和 客户端    }

[翻译]Genesis UDP 服务端 和 客户端    
else
[翻译]Genesis UDP 服务端 和 客户端[翻译]Genesis UDP 服务端 和 客户端    
...{
[翻译]Genesis UDP 服务端 和 客户端        spState.Text 
= "Connection rejected - " + e.Reason;
[翻译]Genesis UDP 服务端 和 客户端    }

[翻译]Genesis UDP 服务端 和 客户端}

如果服务器没有接受连接,LoginSendEventArgs对象的Connected属性是false,比如服务器已经到了最大服务能力。在Reason属性中可以得到拒绝的原因。

OnAuthFeedback:当服务端决定接受或拒绝登陆时会被调用。EventArgs包含一个表示是否成功登陆的值,和一个登陆失败的原因。

OnConnectionAuth:当收到一个远程节点发来的连接请求时被调用,还记得在聊天服务器中我们用它来认证客户端么。聊天客户端不能接受连接请求,所以这里用一小段程序来拒绝连接并发回一个原因。如果客户端没有注册这个事件,连接仍然会被拒绝,但是就不会返回一个拒绝原因。

 

[翻译]Genesis UDP 服务端 和 客户端private   void  m_UDP_OnConnectionAuth( object  o, ConnectionAuthEventArgs e)
[翻译]Genesis UDP 服务端 和 客户端[翻译]Genesis UDP 服务端 和 客户端
... {
[翻译]Genesis UDP 服务端 和 客户端    
//Clients don't accept connections.
[翻译]Genesis UDP 服务端 和 客户端
    e.AllowConnection = false;
[翻译]Genesis UDP 服务端 和 客户端    e.DisallowReason 
= "Can't connect directly to a chat client";
[翻译]Genesis UDP 服务端 和 客户端}

 

一个关键是从客户端如何建立连接,在聊天客户端中它是按如下代码开始的:

[翻译]Genesis UDP 服务端 和 客户端[翻译]Genesis UDP 服务端 和 客户端/**/ /// <summary>
[翻译]Genesis UDP 服务端 和 客户端
/// Connect to server button was clicked
[翻译]Genesis UDP 服务端 和 客户端
/// </summary>

[翻译]Genesis UDP 服务端 和 客户端private   void  btnConnect_Click( object  sender, System.EventArgs e)
[翻译]Genesis UDP 服务端 和 客户端[翻译]Genesis UDP 服务端 和 客户端
... {
[翻译]Genesis UDP 服务端 和 客户端    server_ip 
= txtServerIP.Text;
[翻译]Genesis UDP 服务端 和 客户端    m_UDP.RequestConnect(
ref server_ip, 
[翻译]Genesis UDP 服务端 和 客户端       Convert.ToInt32(txtServerPort.Text), 
[翻译]Genesis UDP 服务端 和 客户端       
out server_req_id);
[翻译]Genesis UDP 服务端 和 客户端    spState.Text 
= "Connecting...";
[翻译]Genesis UDP 服务端 和 客户端    btnConnect.Enabled 
= true;
[翻译]Genesis UDP 服务端 和 客户端}

方法RequestConnect初始化连接。注意服务器IP是ref传入的,这是因为有可能传入的字符串是一个主机名,然后这个字符串会被改为实际解析出来的IP地址。这样可以使程序方便的使用主机名解析,并且Genesis的其它部分可以使用IP地址。还要注意的是最后一个参数,这是一个out参数,返回的是这个连接的请求ID。这个请求ID是唯一的,来标示不同的连接请求。连接可以通过CancelConnect方法被断开。

注意建立连接是一个异步操作,要得知连接是否被成功建立需要使用两个事件,OnConnectionRequestTimedOutOnConnectionStateChanged。如果连接请求超时,前者会被调用。如果试图连接成功,后者会被调用,参数中会表明一个连接被建立。这两个事件对可以获得远程节点的地址,以及在RequestConnect方法中生成的请求ID,这样可以根据不同的连接来不同处理。这在GenesisChatClient项目中可以看到。


向远程节点发送数据

Genesis把这个功能包装的非常容易使用。IConnection对象有两个方法,如下所示:

[翻译]Genesis UDP 服务端 和 客户端int  SendUnreliableCommand( byte  flags,  string  opcode,  string [] fields);
[翻译]Genesis UDP 服务端 和 客户端
int  SendReliableCommand( byte  flags,  string  opcode,  string [] fields);

这两个方法可以向IConnection对象相应的远程节点发送一个命令包。操作码和数据段可以是任何数据,flags的有效值如下所示(在Constants.cs中)

[翻译]Genesis UDP 服务端 和 客户端// Command packet flags
[翻译]Genesis UDP 服务端 和 客户端
public   static   byte  FLAGS_NONE  =   0 ;
[翻译]Genesis UDP 服务端 和 客户端
public   static   byte  FLAGS_CONNECTIONLESS  =   1 ;
[翻译]Genesis UDP 服务端 和 客户端
public   static   byte  FLAGS_ENCRYPTED  =   2 ;
[翻译]Genesis UDP 服务端 和 客户端
public   static   byte  FLAGS_COMPOUNDPIECE  =   4 ;
[翻译]Genesis UDP 服务端 和 客户端
public   static   byte  FLAGS_COMPOUNDEND  =   8 ;
[翻译]Genesis UDP 服务端 和 客户端
public   static   byte  FLAGS_RELIABLE  =   16 ;
[翻译]Genesis UDP 服务端 和 客户端
public   static   byte  FLAGS_SEQUENCED  =   32 ;

在Constants.cs记录的有效flags中,只有一个需要在调用方法时被手工使用,即FLAGS_SEQUENCED,它表示在不可靠包中的有序性。其他的flags都是被Genesis自动加入的,并不需要在程序中改变。

也可以向多个远程节点广播一个命令包,使用下面列出的IGenesisUDP接口的方法:

[翻译]Genesis UDP 服务端 和 客户端int  SendUnreliableCommandToAll(BroadcastFilter filter,  byte  flags,  string  opcode,  string [] fields);
[翻译]Genesis UDP 服务端 和 客户端
int  SendReliableCommandToAll(BroadcastFilter filter,  byte  flags,  string  opcode,  string [] fields);

这两个方法和上面的两个工作的一模一样,只不过他们增加了一个广播过滤标志,在Constants.cs中定义如下:

[翻译]Genesis UDP 服务端 和 客户端[Flags]
[翻译]Genesis UDP 服务端 和 客户端
public   enum  BroadcastFilter :  int
[翻译]Genesis UDP 服务端 和 客户端[翻译]Genesis UDP 服务端 和 客户端
... {
[翻译]Genesis UDP 服务端 和 客户端    None     
= 0,  //Filter out everything
[翻译]Genesis UDP 服务端 和 客户端
    Servers  = 1,  //Send to servers we are connected to.
[翻译]Genesis UDP 服务端 和 客户端
    Clients  = 2,  //Send to clients connected to us.
[翻译]Genesis UDP 服务端 和 客户端
    All      = Servers | Clients,
[翻译]Genesis UDP 服务端 和 客户端                  
//Send to both servers
[翻译]Genesis UDP 服务端 和 客户端                  
//and clients (every connection).
[翻译]Genesis UDP 服务端 和 客户端
    AuthedOnly = 4,//Only send to authed clients or servers we are authed with.
[翻译]Genesis UDP 服务端 和 客户端
}

这样可以限定广播给服务端、客户端或者那些成功认证的节点。

兴趣点

需要非常注意的是Genesis中的事件都是在一个单独的线程中调用的,而并不是UI线程。这表示如果要改变UI元素的话,需要使用Invoke方法来传送给UI线程。这个聊天程序已经使用此技术来改变UI。

当调用StopListen方法时,Genesis自动地发送一个断开连接的命令包给所有连接的远程节点,通知它们自己已经关闭。

在整个Genesis中唯一一个可以接受主机名作参数的是RequestConnect方法。其他的方法都需要显式的IP地址。

历史

v1.00 - 初次修订

关于原作者

Rob Harwood 在这里查看他的档案 http://www.codeproject.com/script/profile/whos_who.asp?vt=arts&id=1770761

关于我 

这是小弟第一次发表技术文章,虽然是翻译的,而且自己水平有限,不论是技术还是英文都需要努力,希望大家多多指正,有什么错误还请留言说明,或发邮箱联系:flankerfc at gmail dot com 。

第一次发表忘了附带源代码了,还有怎么上传文件啊