C#Socket服务端框架之SuperSocket教程(三)

一.内置的命令行协议

什么是协议?

什么是协议? 很多人会回答 "TCP" 或者 "UDP"。 但是构建一个网络应用程序, 仅仅知道是 TCP 还是 UDP 是远远不够的。 TCP 和 UDP 是传输层协议。仅仅定义了传输层协议是不能让网络的两端进行通信的。你需要定义你的应用层通信协议把你接收到的二进制数据转化成你程序能理解的请求。

内置的命令行协议

命令行协议是一种被广泛应用的协议。一些成熟的协议如 Telnet, SMTP, POP3 和 FTP 都是基于命令行协议的。 在SuperSocket 中, 如果你没有定义自己的协议,SuperSocket 将会使用命令行协议, 这会使这样的协议的开发变得很简单。

命令行协议定义了每个请求必须以回车换行结尾 "\r\n"。

如果你在 SuperSocket 中使用命令行协议,所有接收到的数据将会翻译成 StringRequestInfo 实例。

StringRequestInfo 是这样定义的:

public class StringRequestInfo
{
    public string Key { get; }

    public string Body { get; }

    public string[] Parameters { get; }

    /*
    Other properties and methods
    */
}

由于 SuperSocket 中内置的命令行协议用空格来分割请求的Key和参,因此当客户端发送如下数据到服务器端时:

"LOGIN kerry 123456" + NewLine

SuperSocket 服务器将会收到一个 StringRequestInfo 实例,这个实例的属性为:

Key: "LOGIN"
Body: "kerry 123456";
Parameters: ["kerry", "123456"]

如果你定义了名为 "LOGIN" 的命令, 这个命令的 ExecuteCommand 方法将会被执行,服务器所接收到的StringRequestInfo实例也将作为参数传给这个方法:

public class LOGIN : CommandBase<AppSession, StringRequestInfo>
{
    public override void ExecuteCommand(AppSession session, StringRequestInfo requestInfo)
    {
        //Implement your business logic
    }
}

自定义你的命令行协议

有些用户可能会有不同的请求格式, 比如:

"LOGIN:kerry,12345" + NewLine

请求的 key 和 body 通过字符 ':' 分隔, 而且多个参数被字符 ',' 分隔。 支持这种类型的请求非常简单, 你只需要用下面的代码扩展命令行协议:

public class YourServer : AppServer<YourSession>
{
    public YourServer()
        : base(new CommandLineReceiveFilterFactory(Encoding.Default, new BasicRequestInfoParser(":", ",")))
    {

    }
}

如果你想更深度的定义请求的格式, 你可以基于接口 IRequestInfoParser 来实现一个 RequestInfoParser 类, 然后当实例化 CommandLineReceiveFilterFactory 时传入拟定一个 RequestInfoParser 实例:

public class YourServer : AppServer<YourSession>
{
    public YourServer()
        : base(new CommandLineReceiveFilterFactory(Encoding.Default, new YourRequestInfoParser()))
    {

    }
}

文本编码

命令行协议的默认编码是 Ascii,但是你也可以通过修改配置中的服务器节点的"textEncoding"属性来改变编码:

<server name="TelnetServer"
      textEncoding="UTF-8"
      serverType="YourAppServer, YourAssembly"
      ip="Any" port="2020">
</server>

二.内置的常用协议实现模版 

阅读了前面一篇文档之后, 你可能会觉得用 SuperSocket 来实现你的自定义协议并不简单。 为了让这件事变得更容易一些, SuperSocket 提供了一些通用的协议解析工具, 你可以用他们简单而且快速的实现你自己的通信协议:

  • TerminatorReceiveFilter (SuperSocket.SocketBase.Protocol.TerminatorReceiveFilter, SuperSocket.SocketBase)
  • CountSpliterReceiveFilter (SuperSocket.Facility.Protocol.CountSpliterReceiveFilter, SuperSocket.Facility)
  • FixedSizeReceiveFilter (SuperSocket.Facility.Protocol.FixedSizeReceiveFilter, SuperSocket.Facility)
  • BeginEndMarkReceiveFilter (SuperSocket.Facility.Protocol.BeginEndMarkReceiveFilter, SuperSocket.Facility)
  • FixedHeaderReceiveFilter (SuperSocket.Facility.Protocol.FixedHeaderReceiveFilter, SuperSocket.Facility)

TerminatorReceiveFilter - 结束符协议

与命令行协议类似,一些协议用结束符来确定一个请求.

例如, 一个协议使用两个字符 "##" 作为结束符, 于是你可以使用类 "TerminatorReceiveFilterFactory":

/// <summary>
/// TerminatorProtocolServer
/// Each request end with the terminator "##"
/// ECHO Your message##
/// </summary>
public class TerminatorProtocolServer : AppServer
{
    public TerminatorProtocolServer()
        : base(new TerminatorReceiveFilterFactory("##"))
    {

    }
}

默认的请求类型是 StringRequestInfo, 你也可以创建自己的请求类型, 不过这样需要你做一点额外的工作:

基于TerminatorReceiveFilter实现你的接收过滤器(ReceiveFilter):

public class YourReceiveFilter : TerminatorReceiveFilter<YourRequestInfo>
{
    //More code
}

实现你的接收过滤器工厂(ReceiveFilterFactory)用于创建接受过滤器实例:

public class YourReceiveFilterFactory : IReceiveFilterFactory<YourRequestInfo>
{
    //More code
}

然后在你的 AppServer 中使用这个接收过滤器工厂(ReceiveFilterFactory).

CountSpliterReceiveFilter - 固定数量分隔符协议

有些协议定义了像这样格式的请求 "#part1#part2#part3#part4#part5#part6#part7#". 每个请求有7个由 '#' 分隔的部分. 这种协议的实现非常简单:

/// <summary>
/// Your protocol likes like the format below:
/// #part1#part2#part3#part4#part5#part6#part7#
/// </summary>
public class CountSpliterAppServer : AppServer
{
    public CountSpliterAppServer()
        : base(new CountSpliterReceiveFilterFactory((byte)'#', 8)) // 7 parts but 8 separators
    {

    }
}

你也可以使用下面的类更深入的定制这种协议:

CountSpliterReceiveFilter<TRequestInfo>
CountSpliterReceiveFilterFactory<TReceiveFilter>
CountSpliterReceiveFilterFactory<TReceiveFilter, TRequestInfo>

FixedSizeReceiveFilter - 固定请求大小的协议

在这种协议之中, 所有请求的大小都是相同的。如果你的每个请求都是有9个字符组成的字符串,如"KILL BILL", 你应该做的事就是想如下代码这样实现一个接收过滤器(ReceiveFilter):

class MyReceiveFilter : FixedSizeReceiveFilter<StringRequestInfo>
{
    public MyReceiveFilter()
        : base(9) //传入固定的请求大小
    {

    }

    protected override StringRequestInfo ProcessMatchedRequest(byte[] buffer, int offset, int length, bool toBeCopied)
    {
        //TODO: 通过解析到的数据来构造请求实例,并返回
    }
}

然后在你的 AppServer 类中使用这个接受过滤器 (ReceiveFilter):

public class MyAppServer : AppServer
{
    public MyAppServer()
        : base(new DefaultReceiveFilterFactory<MyReceiveFilter, StringRequestInfo>()) //使用默认的接受过滤器工厂 (DefaultReceiveFilterFactory)
    {

    }
}

BeginEndMarkReceiveFilter - 带起止符的协议

在这类协议的每个请求之中 都有固定的开始和结束标记。例如, 我有个协议,它的所有消息都遵循这种格式 "!xxxxxxxxxxxxxx$"。因此,在这种情况下, "!" 是开始标记, "$" 是结束标记,于是你的接受过滤器可以定义成这样:

class MyReceiveFilter : BeginEndMarkReceiveFilter<StringRequestInfo>
{
    //开始和结束标记也可以是两个或两个以上的字节
    private readonly static byte[] BeginMark = new byte[] { (byte)'!' };
    private readonly static byte[] EndMark = new byte[] { (byte)'$' };

    public MyReceiveFilter()
        : base(BeginMark, EndMark) //传入开始标记和结束标记
    {

    }

    protected override StringRequestInfo ProcessMatchedRequest(byte[] readBuffer, int offset, int length)
    {
        //TODO: 通过解析到的数据来构造请求实例,并返回
    }
}

然后在你的 AppServer 类中使用这个接受过滤器 (ReceiveFilter):

public class MyAppServer : AppServer
{
    public MyAppServer()
        : base(new DefaultReceiveFilterFactory<MyReceiveFilter, StringRequestInfo>()) //使用默认的接受过滤器工厂 (DefaultReceiveFilterFactory)
    {

    }
}

FixedHeaderReceiveFilter - 头部格式固定并且包含内容长度的协议

这种协议将一个请求定义为两大部分, 第一部分定义了包含第二部分长度等等基础信息. 我们通常称第一部分为头部.

例如, 我们有一个这样的协议: 头部包含 6 个字节, 前 4 个字节用于存储请求的名字, 后两个字节用于代表请求体的长度:

/// +-------+---+-------------------------------+
/// |request| l |                               |
/// | name  | e |    request body               |
/// |  (4)  | n |                               |
/// |       |(2)|                               |
/// +-------+---+-------------------------------+

使用 SuperSocket, 你可以非常方便的实现这种协议:

class MyReceiveFilter : FixedHeaderReceiveFilter<BinaryRequestInfo>
{
    public MyReceiveFilter()
        : base(6)
    {

    }

    protected override int GetBodyLengthFromHeader(byte[] header, int offset, int length)
    {
        return (int)header[offset + 4] * 256 + (int)header[offset + 5];
    }

    protected override BinaryRequestInfo ResolveRequestInfo(ArraySegment<byte> header, byte[] bodyBuffer, int offset, int length)
    {
        return new BinaryRequestInfo(Encoding.UTF8.GetString(header.Array, header.Offset, 4), bodyBuffer.CloneRange(offset, length));
    }
}

你需要基于类FixedHeaderReceiveFilter实现你自己的接收过滤器.

  • 传入父类构造函数的 6 表示头部的长度;
  • 方法"GetBodyLengthFromHeader(...)" 应该根据接收到的头部返回请求体的长度;
  • 方法 ResolveRequestInfo(....)" 应该根据你接收到的请求头部和请求体返回你的请求类型的实例.

然后你就可以使用接收或者自己定义的接收过滤器工厂来在 SuperSocket 中启用该协议.

三.使用 IRequestInfo 和 IReceiveFilter 等等其他对象来实现自定义协议

为什么你要使用自定义协议?

通信协议用于将接收到的二进制数据转化成您的应用程序可以理解的请求。 SuperSocket提供了一个内置的通信协议“命令行协议”定义每个请求都必须以回车换行"\r\n"结尾。

但是一些应用程序无法使用命令行协议由于不同的原因。 这种情况下,你需要使用下面的工具来实现你的自定义协议:

* RequestInfo
* ReceiveFilter
* ReceiveFilterFactory
* AppServer and AppSession

请求(RequestInfo)

RequestInfo 是表示来自客户端请求的实体类。 每个来自客户端的请求都能应该被实例化为 RequestInfo 类型。 RequestInfo 类必须实现接口 IRequestInfo,该接口只有一个名为"Key"的字符串类型的属性:

public interface IRequestInfo
{
    string Key { get; }
}

上面文档提到了, 请求类型 StringRequestInfo 用在 SuperSocket 命令行协议中。

你也可以根据你的应用程序的需要来定义你自己的请求类型。 例如, 如果所有请求都包含 DeviceID 信息,你可以在RequestInfo类里为它定义一个属性:

public class MyRequestInfo : IRequestInfo
{
     public string Key { get; set; }

     public int DeviceId { get; set; }

     /*
     // Other properties
     */
}

SuperSocket 还提供了另外一个请求类 "BinaryRequestInfo" 用于二进制协议:

public class BinaryRequestInfo
{
    public string Key { get; }

    public byte[] Body { get; }
}

你可以直接使用此类型 BinaryRequestInfo, 如果他能满足你的需求的话。

接收过滤器(ReceiveFilter)

接收过滤器(ReceiveFilter)用于将接收到的二进制数据转化成请求实例(RequestInfo)。

实现一个接收过滤器(ReceiveFilter), 你需要实现接口 IReceiveFilter:

public interface IReceiveFilter<TRequestInfo>
    where TRequestInfo : IRequestInfo
{
    /// <summary>
    /// Filters received data of the specific session into request info.
    /// </summary>
    /// <param name="readBuffer">The read buffer.</param>
    /// <param name="offset">The offset of the current received data in this read buffer.</param>
    /// <param name="length">The length of the current received data.</param>
    /// <param name="toBeCopied">if set to <c>true</c> [to be copied].</param>
    /// <param name="rest">The rest, the length of the data which hasn't been parsed.</param>
    /// <returns></returns>
    TRequestInfo Filter(byte[] readBuffer, int offset, int length, bool toBeCopied, out int rest);

    /// <summary>
    /// Gets the size of the left buffer.
    /// </summary>
    /// <value>
    /// The size of the left buffer.
    /// </value>
    int LeftBufferSize { get; }

    /// <summary>
    /// Gets the next receive filter.
    /// </summary>
    IReceiveFilter<TRequestInfo> NextReceiveFilter { get; }

    /// <summary>
    /// Resets this instance to initial state.
    /// </summary>
    void Reset();
}
  • TRequestInfo: 类型参数 "TRequestInfo" 是你要在程序中使用的请求类型(RequestInfo);
  • LeftBufferSize: 该接收过滤器已缓存数据的长度;
  • NextReceiveFilter: 当下一块数据收到时,用于处理数据的接收过滤器实例;
  • Reset(): 重设接收过滤器实例到初始状态;
  • Filter(....): 该方法将会在 SuperSocket 收到一块二进制数据时被执行,接收到的数据在 readBuffer 中从 offset 开始, 长度为 length 的部分。

    TRequestInfo Filter(byte[] readBuffer, int offset, int length, bool toBeCopied, out int rest);
    
    • readBuffer: 接收缓冲区, 接收到的数据存放在此数组里
    • offset: 接收到的数据在接收缓冲区的起始位置
    • length: 本轮接收到的数据的长度
    • toBeCopied: 表示当你想缓存接收到的数据时,是否需要为接收到的数据重新创建一个备份而不是直接使用接收缓冲区
    • rest: 这是一个输出参数, 它应该被设置为当解析到一个为政的请求后,接收缓冲区还剩余多少数据未被解析

这儿有很多种情况需要你处理:

  • 当你在接收缓冲区中找到一条完整的请求时,你必须返回一个你的请求类型的实例.
  • 当你在接收缓冲区中没有找到一个完整的请求时, 你需要返回 NULL.
  • 当你在接收缓冲区中找到一条完整的请求, 但接收到的数据并不仅仅包含一个请求时,设置剩余数据的长度到输出变量 "rest". SuperSocket 将会检查这个输出参数 "rest", 如果它大于 0, 此 Filter 方法 将会被再次执行, 参数 "offset" 和 "length" 会被调整为合适的值.

接收过滤器工厂(ReceiveFilterFactory)

接收过滤器工厂(ReceiveFilterFactory)用于为每个会话创建接收过滤器. 定义一个过滤器工厂(ReceiveFilterFactory)类型, 你必须实现接口 IReceiveFilterFactory. 类型参数 "TRequestInfo" 是你要在整个程序中使用的请求类型

/// <summary>
/// Receive filter factory interface
/// </summary>
/// <typeparam name="TRequestInfo">The type of the request info.</typeparam>
public interface IReceiveFilterFactory<TRequestInfo> : IReceiveFilterFactory
    where TRequestInfo : IRequestInfo
{
    /// <summary>
    /// Creates the receive filter.
    /// </summary>
    /// <param name="appServer">The app server.</param>
    /// <param name="appSession">The app session.</param>
    /// <param name="remoteEndPoint">The remote end point.</param>
    /// <returns>
    /// the new created request filer assosiated with this socketSession
    /// </returns>
    IReceiveFilter<TRequestInfo> CreateFilter(IAppServer appServer, IAppSession appSession, IPEndPoint remoteEndPoint);
}

你也可以直接使用默认的过滤器工厂(ReceiveFilterFactory)

DefaultReceiveFilterFactory<TReceiveFilter, TRequestInfo>

, 当工厂的CreateFilter方法被调用时,它将会调用TReceiveFilter类型的无参构造方法来创建并返回TReceiveFilter.

和 AppSession,AppServer 配合工作

现在, 你已经有了 RequestInfo, ReceiveFilter 和 ReceiveFilterFactory, 但是你还没有正式使用它们. 如果你想让他们在你的程序里面可用, 你需要定义你们的 AppSession 和 AppServer 来使用他们.

  • 为 AppSession 设置 RequestInfo

    public class YourSession : AppSession<YourSession, YourRequestInfo>
    {
         //More code...
    }
    
  • 为 AppServer 设置 RequestInfo 和 ReceiveFilterFactory

    public class YourAppServer : AppServer<YourSession, YourRequestInfo>
    {
        public YourAppServer()
            : base(new YourReceiveFilterFactory())
        {
    
        }
    }
    

完成上面两件事情,你的自定义协议就应该可以工作了。

 四.命令和命令加载器

命令 (Command)

SuperSocket 中的命令设计出来是为了处理来自客户端的请求的, 它在业务逻辑处理之中起到了很重要的作用。

命令类必须实现下面的基本命令接口:

public interface ICommand<TAppSession, TRequestInfo> : ICommand
    where TRequestInfo : IRequestInfo
    where TAppSession : IAppSession
{

    void ExecuteCommand(TAppSession session, TRequestInfo requestInfo);
}

public interface ICommand
{
    string Name { get; }
}

请求处理代码必须被放置于方法 "ExecuteCommand(TAppSession session, TRequestInfo requestInfo)" 之中,并且属性 "Name" 的值用于匹配接收到请求实例(requestInfo)的Key。当一个请求实例(requestInfo) 被收到时,SuperSocket 将会通过匹配请求实例(requestInfo)的Key和命令的Name的方法来查找用于处理该请求的命令。

举个例子, 如果你收到如下请求(requestInfo):

Key: "ADD"
Body: "1 2"

于是 SuperSocket 将会寻找Name属性为"ADD"的命令。如果有个命令定义如下:

public class ADD : StringCommandBase
{
    public override void ExecuteCommand(AppSession session, StringRequestInfo requestInfo)
    {
          session.Send((int.Parse(requestInfo[0] + int.Parse(requestInfo[1])).ToString());
    }
}

因为基类 StringCommandBase 会设置Name属性的值为此类的名称(ADD),因此个命令将会被找到.

但是在有些情况, 请求实例(requestInfo)的Key 无法当做类的名称。 比如说:

Key: "01"
Body: "1 2"

为了让让你的 ADD 命令起作用,你需要为命令类重写Name属性:

public class ADD : StringCommandBase
{
    public override string Name
    {
        get { return "01"; }
    }

    public override void ExecuteCommand(AppSession session, StringRequestInfo requestInfo)
    {
          session.Send((int.Parse(requestInfo[0] + int.Parse(requestInfo[1])).ToString());
    }
}

命令程序集定义

是的,SuperSocket是用反射来查找哪些公开的类实现了基本的命令接口,但是它只在你的AppServer类定义的程序集中查找。

举例来说, 你的 AppServer 定义在程序集 GameServer.dll 中, 但是你的 ADD 命令是定义在程序集 BasicModules.dll 中:

GameServer.dll
    + MyGameServer.cs
 
BasicModules.dll
    + ADD.cs

默认的, 命令 "ADD" 将不会被加载到游戏服务器实例。 如果你想要加载该命令, 你如要在配置中添加程序集 BasicModules.dll 到命令程序集列表之中:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <section name="superSocket" type="SuperSocket.SocketEngine.Configuration.SocketServiceConfig, SuperSocket.SocketEngine"/>
    </configSections>
    <appSettings>
        <add key="ServiceName" value="BroardcastService"/>
    </appSettings>
    <superSocket>
        <servers>
            <server name="SampleServer"
                    serverType="GameServer.MyGameServer, GameServer"
                    ip="Any" port="2012">
              <commandAssemblies>
                <add assembly="BasicModules"></add>
              </commandAssemblies>
            </server>
        </servers>
    </superSocket>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" />
    </startup>
</configuration>

当然你也可以在配置中添加多个命令程序集。

命令加载器 (Command Loader)

在某些情况下,你可能希望通过直接的方式来加载命令,而不是通过自动的反射。 如果是这样,你可以实现你自己的命令加载器 (Command Loader):

public interface ICommandLoader<TCommand>

然后配置你的服务器来使用你新建的命令加载器 (Command Loader):

<superSocket>
    <servers>
      <server name="SampleServer"
              serverType="GameServer.MyGameServer, GameServer"
              ip="Any" port="2012"
              commandLoader="MyCommandLoader">
      </server>
    </servers>
    <commandLoaders>
        <add name="MyCommandLoader"
           type="GameServer.MyCommandLoader, GameServer" />
    </commandLoaders>
</superSocket>

ps:以上内容均转自http://www.supersocket.net/ 

猜你喜欢

转载自blog.csdn.net/a462575515/article/details/93463039
今日推荐