【问题标题】:What is the correct pattern to implement TCP framing? Is it a filter stack?实现 TCP 成帧的正确模式是什么?它是一个过滤器堆栈吗?
【发布时间】:2012-07-05 22:28:04
【问题描述】:

我正在尝试实现一个强大的 TCP 库,它允许用户选择应用程序协议或实现他们自己的应用程序协议,然后简单地将它们“插入”到客户端/服务器中。

我所说的协议是指定义流应如何构建为消息的能力。

我正在为堆栈的其余部分使用内置的异步 TCP 库,并开发了一个客户端,该客户端在建立连接、读取或写入数据或引发异常时引发事件。

我有两个选项来实现成帧协议。第一个已经在工作,是扩展客户端类并覆盖接收到的数据事件,以便仅在接收到完整消息时才引发此事件。 (即,在后台,我缓冲来自套接字的原始数据,并根据协议决定何时收到完整消息,然后才引发数据接收事件。)这类似于 Nito.Asynch 库的工作方式。

这种方法的问题在于,它意味着每个新协议都需要新的客户端实现。我希望客户端维护一个可以添加或删除的内部过滤器堆栈。

当在套接字上接收到数据时,它会被传递给第一个缓冲的过滤器,直到它决定传递一个完整的消息,并且删除了标头或元数据。然后将其传递给堆栈中的下一个过滤器等。

这样过滤器可以独立于库定义/开发,并根据配置(在运行时)注入客户端。

为了实现这一点,我考虑将过滤器定义为由客户端内部保存的 System.IO.Stream(传入和传出)的成对实现。

从套接字读取的数据将被写入堆栈的底部传入流。然后从该流读取的数据将被写入下一个流等,直到最后一个流(堆栈顶部)返回数据,然后由客户端返回。 (我的计划是使用 Stream 的 CopyTo() 函数)。

写入客户端的数据将被写入顶部传出流并向下复制堆栈,直到底部传出流写入底层套接字。

显然有很多事情需要考虑,我正在努力思考如何以正确的方式作为 Stream 对象。 示例:当有人调用 Flush()...时我该怎么办?

这是实现这一目标的好方法,还是我在这里重新发明轮子?

Nito.Asynch 库

【问题讨论】:

    标签: c# .net tcp


    【解决方案1】:

    我正在回答我自己的问题,希望我的解决方案能得到一些好评,并可能对其他人有所帮助。

    我为协议过滤器和数据帧定义了两个接口。 (为了明确术语,我避免使用数据包这个词,以避免与低级协议中定义的数据包混淆。)

    虽然不是我自己的意图,但我想这可以在任何传输协议(即命名管道、TCP、串行)之上使用。

    首先是数据框的定义。这包括“数据”(有效负载)以及将传输数据作为原子“消息”的任何字节。

    /// <summary>
    /// A packet of data with some form of meta data which frames the payload for transport in via a stream.
    /// </summary>
    public interface IFramedData
    {
        /// <summary>
        /// Get the data payload from the framed data (excluding any bytes that are used to frame the data)
        /// i.e. The received data minus protocl specific framing
        /// </summary>
        public readonly byte[] Data { get; }
    
        /// <summary>
        /// Get the framed data (payload including framing bytes) ready to send
        /// </summary>
        /// <returns>Framed data</returns>
        public byte[] ToBytes();
    }
    

    然后是协议过滤器,它从某个源(例如 TCP 套接字,如果它们在堆栈中使用,甚至是另一个过滤器)读取数据并将数据写回。

    过滤器应读入数据(包括帧)并为每个完整的帧读取引发 DataReceived 事件。通过 IFramedData 实例的“Data”属性访问有效负载。

    当数据写入过滤器时,它应该适当地“构建”它,然后在每次准备好发送完整的数据帧时引发 DataToSend 事件。 (在我的情况下,这将是立竿见影的,但我试图允许一个协议在返回准备发送的完整帧之前可能会发送固定长度的消息或缓冲输入。

    /// <summary>
    /// A protocol filter can be used to read and write data from/to a Stream and frame/deframe the messages.
    /// </summary>
    /// <typeparam name="TFramedData">The data frame that is handled by this filter</typeparam>
    public interface IProtocolFilter<TFramedData> where TFramedData : IFramedData
    {
        /// <summary>
        /// Should be raised whenever a complete data frame is ready to send.
        /// </summary>
        /// <remarks>
        /// May be raised after a call to <see cref="FlushSend()"/>
        /// </remarks>
        public event Action<TFramedData> DataToSend;
    
        /// <summary>
        /// Should be raised whenever a complete data frame has been received.
        /// </summary>
        /// <remarks>
        /// May be raised after a call to <see cref="FlushReceive()"/>
        /// </remarks>
        public event Action<TFramedData> DataReceived;
    
        /// <summary>
        /// Should be raised if any data written or read breaks the protocol.
        /// This could be due to any asynchronous operation that cannot be raised by the calling function.
        /// </summary>
        /// <remarks>
        /// Behaviour may be protocol specific such as flushing the read or write cache or even resetting the connection.
        /// </remarks>
        public event Action<Exception> ProtocolException;
    
        /// <summary>
        /// Read data into the recieve buffer
        /// </summary>
        /// <remarks>
        /// This may raise the DataReceived event (possibly more than once if multiple complete frames are read)
        /// </remarks>
        /// <param name="buffer">Data buffer</param>
        /// <param name="offset">Position within the buffer where data must start being read.</param>
        /// <param name="count">Number of bytes to read.</param>
        /// <returns></returns>
        public int Read(byte[] buffer, int offset, int count);
    
        /// <summary>
        /// Write data to the send buffer.
        /// </summary>
        /// <remarks>
        /// This may raise the DataToSend event (possibly more than once if the protocl requires the data is broken into multiple frames)
        /// </remarks>
        /// <param name="buffer">Data buffer</param>
        /// <param name="offset">Position within the buffer where data must start being read.</param>
        /// <param name="count">Number of bytes to read from the buffer</param>
        public void Write(byte[] buffer, int offset, int count);
    
        /// <summary>
        /// Flush any data from the receive buffer and if appropriate, raise a DataReceived event.
        /// </summary>
        public void FlushReceive();
    
        /// <summary>
        /// Flush any data from the send buffer and if appropriate, raise a DataToSend event.
        /// </summary>
        public void FlushSend();
    }
    

    然后,我围绕 TcpClient 编写了一个非常简单的包装器,它执行异步读取和写入,并在协议堆栈顶部的过滤器引发 DataReceived 事件或底部的过滤器引发 DataToSend 事件时引发事件(我还写了数据到套接字,但这允许应用程序监视它写入客户端的数据何时实际发送)。

    【讨论】:

    • 我在这里唯一能指出的是“IProtocolFilter”不遵循事件的.net标准,通常你使用EventHandler并创建一个继承EventArgs的类..
    猜你喜欢
    • 2015-11-15
    • 2011-09-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-21
    • 2013-09-06
    • 1970-01-01
    相关资源
    最近更新 更多