c/s客户端用C#的Socket进行通信。开单独的接收线程使用networkstream进行数据接收 |
|
使用上面的read方法数据读不全,网上有看到说法是“这是典型的读比发快,既发送端还没发完接收端已经读完了。
http://q.cnblogs.com/q/31971/ 说“TcpClient NetworkStream 是基于流的 ” 要想接全要用BeginRead,可是BeginRead该怎么用? |
|
30分 |
1. 数据包长度应该放在发送数据的头上,
接收数据后,首先进行数据校验,读取长度, 根据长度进行后续接收处理。 lz 提到接收中出现多余数据,就是发送的数据发送过来的数据长度不定,而接收按定长处理导致。 再差的网络,包头几个字节还是能收到的。 如果包头校验失败,那继续接收也无意义, 可以断开该socket,让客户端重新连接。 2.接收数据后,不要直接在该线程中进行数据解析处理。 需要考虑将数据放入队列或者其他容器,让接收动作立即返回。 否则一旦数据量较大,就会出现接收被阻塞的情况。 |
20分 |
必须要用异步委托递归获取,否则你想一次性获取那是不可能的,要么你把缓冲区设计无限大,代价是会大量占用资源而导致异常。但如果一个包仍然非常大,比如传送文件,你想一次性获取数据还是不可取。
|
1分 |
楼主我发现你根本没有理解socket是什么意思,数据包是怎么回事。别怪我说的太难听。socket接收数据没有用while去循环获取的,这种同步获取数据会导致线程阻塞和大量丢包。其他客户端数据会无法接受,并且存在时间同步问题,势必造成由于发送和接受时间和速度不一致导致丢包。你这种设计是个严重的逻辑错误。
为什么socket数据传送必须要用异步委托?因为首先不可能保证同一个时间只会有一个人向你发送数据,所以要使用异步,要解决异步消息队列问题,保证消息间不冲突。 第二,不光要用异步还要用递归,因为你不一定知道发送来的消息的大小,还有一个原因是用递归不用考虑时间问题,什么时候数据接收完成了,什么时候才停止接收。如果你不用递归,你必须有足够的自信你的接收端不会漏掉每一个字节的数据。 从windows消息循环的机制来探讨,但是由于各种原因会使得你服务器的消息循环速度不可能与每个客户端消息循环速度一致,如果你用同步方法,你不可能保证不会漏掉至少一个字节的数据。 |
20分 |
msdn 上的关于通讯的例子,大多都是有 bug 的。那些例子在单机或者局域网(而且网络很好没有什么干扰),传送内容极短,并且并发数太低时不容易测试出来。
uffer的大小,设置成100也可以(其实设置成1也可以),或者设置成 819200 也可以,而且大的buffer会比小的快。不论把buffer大小设置成100还是100万,都可以接收1万多个字节。 但是都不能保证一次Read接收1万字节数据。不管你设置多大,你接收的数据都可能是对方的底层机制分包、粘包的结果,因此绝不能胡乱执行你的“//do sth”这种代码,因为一次 Read 完毕之后,可能你读取到119个字节,而其实对方发送的是200个字节的一个消息,你需要再执行一次Read(这一次readSize值为1)之后才能把两次收到的消息进行“do”。 我在以前写过一个简单的示例:http://bbs.csdn.net/topics/390930620 虽然那只是针对“串口”的例子,但是重点是在于读取消息的预处理操作,你可以看看其流程。绝不是读取到一些字节就以为可以“do sth”了,你需要等到消息结束。 也就是说,你要知道对方(或者你)制定的信令协议标准。例如,当我们以tcp方式来接收http协议的消息时,http协议规定了连续两个换行回车(四个字节)是消息结束符号。 至于说“可是一样接不全,中间好像掺杂了一些没用的数据”,那就是你的客户端程序的bug问题了,它(如果有并发情况)没有能够一次性send数据,而是多次send且次序搞乱,以错误的顺序掺杂着你所谓“没用的数据”,这是你们自己搞的。 |
当然你也可以规定“Read超时”作为对方发送消息结束标志,或者捕获到对方关闭了连接时判断对方消息结束。总之不能在执行一次 Read 只后就断然地去“//do sth”,一定要连续Read()。
你看看一个代码能不能连续 Read,就知道这个代码是用来糊弄小孩子进行单机“玩儿”的,还是可能是真实的拿到互联网也能用的产品代码了。 |
|
协议规定了连续两个换行回车(四个字节)是消息结束 –> 协议规定了连续两个换行回车(四个字节)是消息头部信息结束
然后在消息头中,再标记出消息的长度数值。 比如说我们第一次Read收到了2100个字节,第二次Read操作收到 1580个字节,总共收到3680个字节。从第80个字节出找到了连续的两个换行回车,然后首先取出前79个字节转换为消息头。然后从中找到标记消息体长度的部分,发现是2108,于是再从第84个字节开始取出连续2108个字节,得到消息体。剩下的多出来的字节,就不是当前这个消息的东西了。 |
|
private static void RecvServerMsgThread() { while (m_ConnectSocket.Connected) { if (!m_Reader.CanRead)//DataAvailable { continue; } else { try { if (m_Reader.DataAvailable)//首次读,读包头和Md5 { byte[] bytes = new byte[m_ConnectSocket.ReceiveBufferSize]; byte[] totalBytes = new byte[0];//C#神器,byte[]会自动延长 int readSize = m_Reader.Read(bytes, 0, (int)m_ConnectSocket.ReceiveBufferSize); if (readSize > 0)//说明是有读到数据的 { totalBytes = totalBytes.Concat(bytes).ToArray();//C#神器,byte[]会自动延长 byte[] md5 = new byte[32]; Array.Copy(totalBytes, 0, md5, 0, 32); if (Global.BytesCompare(md5, GlobalDefine.KEY))//是我的包,开始循环读 { byte[] packSizeBuffer = new byte[4];//先读整个包的大小(不包含MD5) Array.Copy(totalBytes, 32, packSizeBuffer, 0, 4);//取出整个包的大小 int packSize = Global.BytesToInt(packSizeBuffer); int currentSize = readSize - 32 - packSizeBuffer.Length;//标记整个包有多少,默认减掉包头Md5+PackSize while (m_Reader.DataAvailable || currentSize < packSize) { readSize = m_Reader.Read(bytes, 0, (int)m_ConnectSocket.ReceiveBufferSize); if (readSize > 0) { currentSize += readSize;//读取成功,进行叠加 totalBytes = totalBytes.Concat(bytes).ToArray();//将读取的内容添加到容器后面 } } //读取完了,开始获取数据 try { byte[] data = new byte[packSize]; Array.Copy(totalBytes, 36, data, 0, packSize);//获取数据 DataThread.Star(data); } catch (Exception e) { } } } } } catch (Exception e) { Global.ShowMessage(e.Message); } } } } /pre> 上面是我客户端读消息的代码。Socket包的组成为:Md5标记(32,标记我的消息) + 数据长度 (4,我的数据) + 数据 (长度未知,包含在前面数据长度内) 接收的时候先预读缓冲区的内容,如果读取成功,取出前32位判断是否为我的包,是的话在读取数据长度,然后在循环中依次读取数据。 strong>最后,将读取的数据开启数据处理线程,交由数据处理线程处理。 |
|
public static ManualResetEvent allDone = new ManualResetEvent(false); private static byte[] bytes = new byte[m_ConnectSocket.ReceiveBufferSize]; private static byte[] totalBytes = new byte[0];//C#神器,byte[]会自动延长 public class State { public NetworkStream ns; public byte[] buffer; public byte[] result; } public static void myReadCallBack(IAsyncResult iar) { State state = (State)iar.AsyncState; NetworkStream myNetworkStream = state.ns; int numberOfBytesRead; numberOfBytesRead = myNetworkStream.EndRead(iar); if (numberOfBytesRead > 0) { totalBytes = totalBytes.Concat(state.buffer).ToArray(); //接收到的消息长度可能大于缓冲区总大小,反复循环直到读完为止 while (myNetworkStream.DataAvailable) { myNetworkStream.BeginRead(state.buffer, 0, state.buffer.Length, new AsyncCallback(myReadCallBack), state); } } else { //执行这里我认为数据已经全部接收完毕 //开始执行如11楼的处理数据代码 //也就是从 state.buffer 中取出包头判断是否我的包 //然后提取数据长度,根据数据长度提取数据,将数据交由数据处理线程进行处理 allDone.Set(); } } //线程接收代码如下 while (m_Reader.DataAvailable) { State state = new State(); state.ns = m_Reader; state.buffer = new byte[m_ConnectSocket.ReceiveBufferSize]; ; state.result = new byte[0]; totalBytes = new byte[0]; m_Reader.BeginRead(state.buffer, 0, state.buffer.Length, myReadCallBack, state); allDone.WaitOne(); } /pre> 使用异步的BeginRead,但是代码没有实现我想要的结果。其实对于BeginRead很混乱,希望各位大牛能帮我理清思路。 因为网上关于BeginRead的代码真是惨不忍睹,全是代码片段,全是无脑复制,好一点的在网络环境好的情况下还能接收成功。 但更多的根本没用 |
|
问题已自行解决。
在11楼我读取的代码中 pre class=”brush: csharp”> while (m_Reader.DataAvailable || currentSize < packSize) { readSize = m_Reader.Read(bytes, 0, (int)m_ConnectSocket.ReceiveBufferSize); if (readSize > 0) { currentSize += readSize;//读取成功,进行叠加 totalBytes = totalBytes.Concat(bytes).ToArray();//将读取的内容添加到容器后面 } } 其中这一段是有问题的,我将数据串起来采用的是 byte.Concat方法,而该方法是将bytes添加到totalBytes后面。 ytes长度为1024。这代码在单机或网络环境好的情况是可以使用的,也是网上搜索给出的代码标准答案。 但是如果在网络没那么好的情况下,是有问题的。 Networkstream的Read方法是尽可能的往stream里面读取数据。如果网络不好的情况下,真实有用的数据可能只有512,但是Read中强行读取1024,导致bytes前512个数据是有用数据,而后面的512~1024数据则是之前的数据。 而我的代码中直接把1024的bytes添加到结果后面,这样是不正确的。 解决办法是类似下面的代码一样 pre class=”brush: csharp”> if (readSize > 0) { LogRecord.WriteLog(readSize); currentSize += readSize; byte[] temp = new byte[readSize]; Array.Copy(bytes, 0, temp, 0, readSize); totalBytes = totalBytes.Concat(temp).ToArray();//C#神器,byte[]会自动延长 } /pre> 多谢xian_wwq指出应该打印到日志内。项目中引用Log4Net将结果打到文件里面比对一下就发现问题了。 至于zanfeng指出Networkstream并不可靠,但我并不知道改用什么其他方法接收。 以及chengbin0602指出的要异步接收。 |