C#Begin / EndReceive - 如何读取大数据?

时间:2009-02-24 16:56:22

标签: c# networking sockets asynchronous

当以1024块的数据块读取数据时,如何继续从接收大于1024字节的消息的套接字读取,直到没有数据为止?我应该只使用BeginReceive来读取数据包的长度前缀,然后一旦检索到它,使用Receive()(在异步线程中)读取数据包的其余部分?或者还有另一种方式吗?

编辑:

我认为Jon Skeet的链接有解决方案,但是有一些关于该代码的speedbump。我使用的代码是:

public class StateObject
{
    public Socket workSocket = null;
    public const int BUFFER_SIZE = 1024;
    public byte[] buffer = new byte[BUFFER_SIZE];
    public StringBuilder sb = new StringBuilder();
}

public static void Read_Callback(IAsyncResult ar)
{
    StateObject so = (StateObject) ar.AsyncState;
    Socket s = so.workSocket;

    int read = s.EndReceive(ar);

    if (read > 0) 
    {
        so.sb.Append(Encoding.ASCII.GetString(so.buffer, 0, read));

        if (read == StateObject.BUFFER_SIZE)
        {
            s.BeginReceive(so.buffer, 0, StateObject.BUFFER_SIZE, 0, 
                    new AyncCallback(Async_Send_Receive.Read_Callback), so);
            return;
        }
    }

    if (so.sb.Length > 0)
    {
        //All of the data has been read, so displays it to the console
        string strContent;
        strContent = so.sb.ToString();
        Console.WriteLine(String.Format("Read {0} byte from socket" + 
        "data = {1} ", strContent.Length, strContent));
    }
    s.Close();
}

现在这个纠正在大多数情况下工作正常,但是当数据包的大小是缓冲区的倍数时它会失败。原因是如果缓冲区在读取时被填充,则假设有更多数据;但同样的问题和以前一样。例如,2字节缓冲区在4字节数据包上填充两次,并假设有更多数据。然后阻止,因为没有什么可以阅读。 问题是接收功能不知道数据包的结尾是什么时候。


这让我想到了两个可能的解决方案:我可以有一个数据包结束分隔符,或者我可以读取数据包标题来查找长度,然后确切地接收该数量(正如我最初建议的那样)。

但是,每个都存在问题。我不喜欢使用分隔符的想法,因为用户可以某种方式将其用于应用程序的输入字符串中的数据包并将其搞砸。它对我来说也似乎有点草率。

长度标题听起来不错,但我打算使用协议缓冲区 - 我不知道数据的格式。有长度标题吗?这是多少字节?这会是我自己实现的吗?等。

我该怎么办?

7 个答案:

答案 0 :(得分:14)

否 - 再次从回调处理程序调用{​​{1}},直到BeginReceive返回0.基本上,您应该异步接收,假设您希望获得异步IO的最大好处。

如果查看Socket.BeginReceive的MSDN页面,您会看到一个示例。 (不可否认,这并不像现在这么容易。)

答案 1 :(得分:6)

荡。鉴于已经权衡过的要人,我甚至对回复这个问题犹豫不决,但现在就去了。温柔,O Great Ones!

没有阅读Marc博客的好处(由于公司的互联网政策,它已被封锁),我将提供“另一种方式。”

我认为,诀窍是 将数据的接收与该数据的处理分开

我使用像这样定义的StateObject类。它与MSDN StateObject实现的不同之处在于它不包含StringBuilder对象,BUFFER_SIZE常量是私有的,并且为了方便起见它包含一个构造函数。

public class StateObject
{
    private const int BUFFER_SIZE = 65535;
    public byte[] Buffer = new byte[BUFFER_SIZE];
    public readonly Socket WorkSocket = null;

    public StateObject(Socket workSocket)
    {
        WorkSocket = workSocket;
    }
}

我还有一个Packet类,它只是一个缓冲区和时间戳的包装器。

public class Packet
{
    public readonly byte[] Buffer;
    public readonly DateTime Timestamp;

    public Packet(DateTime timestamp, byte[] buffer, int size)
    {
        Timestamp = timestamp;
        Buffer = new byte[size];
        System.Buffer.BlockCopy(buffer, 0, Buffer, 0, size);
    }
}

我的ReceiveCallback()函数看起来像这样。

public static ManualResetEvent PacketReceived = new ManualResetEvent(false);
public static List<Packet> PacketList = new List<Packet>();
public static object SyncRoot = new object();
public static void ReceiveCallback(IAsyncResult ar)
{
    try {
        StateObject so = (StateObject)ar.AsyncState;
        int read = so.WorkSocket.EndReceive(ar);

        if (read > 0) {
            Packet packet = new Packet(DateTime.Now, so.Buffer, read);
            lock (SyncRoot) {
                PacketList.Add(packet);
            }
            PacketReceived.Set();
        }

        so.WorkSocket.BeginReceive(so.Buffer, 0, so.Buffer.Length, 0, ReceiveCallback, so);
    } catch (ObjectDisposedException) {
        // Handle the socket being closed with an async receive pending
    } catch (Exception e) {
        // Handle all other exceptions
    }
}

请注意,此实现绝对不会处理接收到的数据,也不会对应该接收到多少字节进行任何预测。它只是接收套接字上发生的任何数据(最多65535个字节)并将该数据存储在数据包列表中,然后立即将另一个异步接收排队。

由于处理每个异步接收的线程中不再发生处理,因此数据显然将由不同的线程处理,这就是通过lock语句同步Add()操作的原因。此外,处理线程(无论是主线程还是其他专用线程)需要知道 何时 才能处理数据。为此,我通常使用ManualResetEvent,这就是我在上面显示的内容。

以下是处理的工作原理。

static void Main(string[] args)
{
    Thread t = new Thread(
        delegate() {
            List<Packet> packets;
            while (true) {
                PacketReceived.WaitOne();
                PacketReceived.Reset();
                lock (SyncRoot) {
                    packets = PacketList;
                    PacketList = new List<Packet>();
                }

                foreach (Packet packet in packets) {
                    // Process the packet
                }
            }
        }
    );
    t.IsBackground = true;
    t.Name = "Data Processing Thread";
    t.Start();
}

这是我用于所有套接字通信的基本基础结构。它在接收数据和处理数据之间提供了很好的分离。

至于你遇到的另一个问题,重要的是要记住这种方法,每个Packet实例不一定代表应用程序上下文中的完整消息。数据包实例可能包含部分消息,单个消息或多个消息,并且您的消息可能跨越多个数据包实例。我已经解决了如何知道您在发布的here相关问题中收到完整消息的时间。

答案 2 :(得分:3)

您将首先阅读长度前缀。一旦你有了这个,你就会继续读取块中的字节(并且你可以像你猜测的那样执行异步),直到你已经耗尽了你知道的线路数量。

请注意,在某些时候,在读取最后一个块时,您不希望读取完整的1024个字节,具体取决于长度前缀的总和,以及您已读取的字节数。

答案 3 :(得分:1)

围绕这一点似乎存在很多混乱。 MSDN网站上使用TCP进行异步套接字通信的示例具有误导性,并且没有得到很好的解释。如果消息大小是接收缓冲区的精确倍数,则EndReceive调用确实会阻塞。这将导致您永远不会收到您的消息和应用程序挂起。

只是为了清理 - 如果您使用TCP,您必须为数据提供自己的分隔符。阅读以下内容(这是非常可靠的来源)。

  

对应用数据的需求   界定

     

TCP处理的其他影响   作为流的传入数据就是该数据   由使用TCP的应用程序接收   是非结构化的。对于传输,a   数据流在一个上进入TCP   设备,接收,流   数据返回到应用程序   接收设备。即便如此   流被分成几个部分   通过TCP传输这些段   是隐藏的TCP级别详细信息   从申请。所以,当一个   设备想要发送多个部分   对于数据,TCP没有提供任何机制   指示“分界线”的位置   因为TCP,所以是件之间的   不检查的意思   数据。申请必须   提供这样做的手段。

     

考虑一个应用程序   即发送数据库记录。它   需要传输记录#579   员工数据库表,其次是   记录#581并记录#611。它发送   这些记录到TCP,对待   他们都集体作为一个流   字节。 TCP将打包这些字节   分段,但在某种程度上   应用程序无法预测。它是   可能每个人最终都会进入   不同的细分,但更有可能   他们都将在一个部分,或   每个部分都会以不同的方式结束   细分,取决于它们的长度。   记录本身必须有一些   一种明确的标记,所以   接收设备可以分辨出哪一个   记录结束,下一次开始。

     

来源:http://www.tcpipguide.com/free/t_TCPDataHandlingandProcessingStreamsSegmentsandSequ-3.htm

我在网上看到的大多数使用EndReceive的例子都是错误的或误导性的。它通常不会在示例中引起任何问题,因为只发送一条预定义的消息,然后关闭连接。

答案 4 :(得分:1)

我也困扰同样的问题。

当我多次测试时,我发现有时多个BeginReceive - EndReceive会导致数据包丢失。 (此循环未正确结束)

就我而言,我使用了两个解决方案。

首先,我定义了足够的数据包大小,只能进行1次BeginReceive() ~ EndReceive();

其次,当我收到大量数据时,我使用的是NetworkStream.Read()而不是BeginReceive() - EndReceive()

异步套接字不易使用,需要对socket有很多了解。

答案 5 :(得分:0)

有关信息(一般的开始/结束用法),您可能希望看到this blog post;这种方法对我来说很好,并且可以节省很多痛苦...

答案 6 :(得分:0)

这是一个非常古老的话题,但是我来到这里寻找其他东西,发现了这一点:

  

现在,此纠正的大部分时间都可以正常工作,,但是当数据包的大小是缓冲区的倍数时,它会失败。原因是如果假定缓冲区在读取时被填充,则假定有更多数据;但还是发生了同样的问题例如,一个2字节的缓冲区在一个4字节的数据包中填充两次,并假定有更多数据。然后它阻塞了,因为没有什么可读取的了。问题在于接收功能不知道数据包的结束时间。

我遇到了同样的问题,并且由于没有任何答复似乎可以解决此问题,因此我的处理方式是使用Socket.Available

public static void Read_Callback(IAsyncResult ar)
{
    StateObject so = (StateObject) ar.AsyncState;
    Socket s = so.workSocket;

    int read = s.EndReceive(ar);    
    if (read > 0) 
    {
        so.sb.Append(Encoding.ASCII.GetString(so.buffer, 0, read));

        if (s.Available == 0)
        {
            // All data received, process it as you wish
        }
    }
    // Listen for more data
    s.BeginReceive(so.buffer, 0, StateObject.BUFFER_SIZE, 0, 
                new AyncCallback(Async_Send_Receive.Read_Callback), so);
}

希望这对其他人有帮助,所以帮助了我很多次,谢谢大家!