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

当以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字节数据包上被填充两次,并假设有更多数据。 然后阻止,因为没有什么可以阅读。 问题是接收function不知道数据包的结尾是什么时候。


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

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

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

我该怎么办?

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

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

荡。 鉴于已经权衡过的要人,我甚至对回复这个问题犹豫不决,但现在就去了。 温柔,O 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 PacketList = new List(); 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 packets; while (true) { PacketReceived.WaitOne(); PacketReceived.Reset(); lock (SyncRoot) { packets = PacketList; PacketList = new List(); } foreach (Packet packet in packets) { // Process the packet } } } ); t.IsBackground = true; t.Name = "Data Processing Thread"; t.Start(); } 

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

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

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

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

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

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

对应用程序数据划分的需求

TCP将传入数据视为流的另一个影响是,使用TCP的应用程序接收的数据是非结构化的。 为了传输,数据流在一个设备上进入TCP,并且在接收时,数据流返回到接收设备上的应用程序。 即使流被分成段以供TCP传输,这些段也是从应用程序隐藏的TCP级细节。 因此,当设备想要发送多个数据时,TCP没有提供用于指示各个部分之间“分界线”的位置的机制,因为TCP根本不检查数据的含义。 应用程序必须提供执行此操作的方法。

例如,考虑发送数据库记录的应用程序。 它需要从Employees数据库表传输记录#579,然后记录#581和记录#611。 它将这些记录发送到TCP,TCP将它们统称为字节流。 TCP会将这些字节打包成段,但应用程序无法预测。 有可能每个都会在不同的段中结束,但更有可能它们都在一个段中,或者每个段的一部分最终会在不同的段中,具体取决于它们的长度。 记录本身必须具有某种显式标记,以便接收设备可以判断一条记录的结束位置和下一条记录的开始位置。

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

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

我也困扰同样的问题。

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

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

首先,我定义了足够的数据包大小,只做一次BeginReceive() ~ EndReceive();

第二,当我收到大量数据时,我使用了NetworkStream.Read()而不是BeginReceive() - EndReceive()

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

有关信息(一般开始/结束用法),您可能希望看到此博客文章 ; 这种方法对我来说很好,并且节省了很多痛苦……