.NET 4.0网络开发入门之旅——
“麻烦”的数据缓冲区
注:
这是一个针对 网络开发领域初学者 的系列文章,可作为《.NET 4.0 面向对象编程漫谈 》一书的扩充阅读,写作过程中我假设读者可以对照阅读此书的相关章节,不再浪费笔墨重复介绍相关的内容。
对于其他类型的读者,除非您已经有相应的.NET 技术背景与一定的开发经验,否则,阅读中可能会遇到困难。
我希望这系列文章能让读者领略到网络开发的魅力!
另外,这些文章均为本人原创,请读者尊重作者的劳动,我允许大家出于知识共享的目的自由转载这些文章及相关示例,但未经本人许可,请不要用于商业盈利目的。
本文如有错误,敬请回贴指正。
谢谢大家!
金旭亮
=================================================
点击以下链接阅读本系列前面的文章:
1 《 开篇语—— 无网不胜》
2 《 IP知多少》
3 《我在“网” 中央 》
4 《与Socket的第一次“约会” 》
5 《与Socket的“再次见面” 》
=================================================
1 引子 在前面的两讲中,大家已经掌握了不少使用Socket在两台计算机间传送字符串信息的方法,并且开发了“一问一答”的网络应用程序。 不知道大家注意到没有,到目前为止,我们所开发的程序要正确地工作,都有以下两条假设: (1)要发送的数据不超过应用程序所给出的数据缓冲区大小(前面的例子大多采用1024字节的byte[]数组作为数据缓冲区) (2)客户端调用的Socket.Send()方法和服务端调用的Socket.Receive()方法必须严格配对。 如果上述两点要求得不到满足,很不幸,我们就陷入到了“沼泽地”中去了。这两个问题都与数据缓冲区有着直接或间接的联系。 这篇文章,我们就来聊聊这个“沼泽地”--数据缓冲区大小问题。
2 数据缓冲区试验
请打开示例解决方案TCPBufferDemo。 文本文件TextFile1.txt中准备了一首古诗,客户端TCPClientApp需要将其发送给服务端。 围炉夜话 天地无穷期,生命则有穷期,去一日,便少一日; 富贵有定数,学问则无定数,求一分,便得一分。 exit 注意上述数据中的最后一个是“exit”命令,通知服务端“数据传输完毕”断开连接。以下是客户端的代码: //按行发送文本文件中的内容到服务端 foreach (String UserInput in File.ReadLines("TextFile1.txt")) { byte[] SentBytes = Encoding.UTF8.GetBytes(UserInput); server.Send(SentBytes); //发送到服务端 Thread.Sleep(1000); } 请注意上面的“Thread.Sleep”一句会很有趣的,决不是整个试验中的一个无关紧要的“小角色”。 服务端TcpServerApp在一个无限循环中接收客户端传来的数据,以下是代码片断。 byte[] data = new byte[BufferSize ]; // BufferSize=1024 //…… while (true) { //Thread.Sleep(4000); recv = client.Receive(data); //接收数据 StringSentByClient = Encoding.UTF8.GetString(data, 0, recv); Console.WriteLine("客户端传来:{0}", StringSentByClient); if (StringSentByClient == "exit" ) client.Close(); break; } 注意上述代码中也有一个“Thread.Sleep”,目前先注释掉它。另外,BufferSize是一个常量,目前定义为1024字节。 以下是“一切正常”时的屏幕截图:
图 1
请读者先仔细阅读客户端和服务端的代码,弄明白其中的每句代码的含义。 现在,好玩的实验开始了。
实验1: 将BufferSize由1024改为10。
图 2
哟,出现了一堆的“?”,服务端汉字无法正确解码!
实验2 :将服务端用于接收数据的缓冲区设置改回1024字节,然后注释掉客户端中的“Thread.Sleep(1000)”那条语句。
图 3
完了,服务端“死掉”了!
实验4:取消客户端的“Thread.Sleep(1000)”语句的注释,取消服务端个“Thread.Sleep(4000)”语句的注释,其目的是延迟接收客户端发来的数据:
图 4
通过这4个实验,我们可以看到这样的网络应用程序是多么地“脆弱”,就象是温室中的花朵,经不起任何的风吹雨打。其中的要点可以总结如下:
当使用TCP在两台计算机之间相互传送数据时,应该保证双方“Send”和“Receive”的配对,并且双方设置的数据缓冲区大小应该能放下一个完整的消息。否则,将会给数据的发送和接收带来很大的麻烦。 另外,数据发送方和接收方的数据处理速度应该“匹配”,否则,同样会带来麻烦。我们可以将由于各种原因导致的数据发送与接收缓冲区大小不匹配问题形象地用“黏包”与“丢包”两个词来表示。
3 “黏包”与“丢包”的直观展示
我们再深入形象地看一下“黏包”与“丢包”问题。 Socket的Send和Receive方法都有一个特殊的重载形式,一次可以发送多个缓冲区的数据: public int Send( IList<ArraySegment<byte>> buffers); public int Receive( IList<ArraySegment<byte>> buffers); 上述代码中的ArraySegment<byte>代表一个一维字节数组的“片断”,通俗地说,就是一个字节数组的“一部分”。 Send方法的上述重载形式,会把放在一个集合(必须实现IList接口)中的所有ArraySegment对象所包容的数据依次地发送到“远方”。对应地,Receive方法则负责接收这些数据,将收到数据依次“填入”到各个原先为“空”的ArraySegment对象中。
请看示例解决方案TestArraySegment。 客户端ClientApp准备了3个ArraySegment对象,每个ArraySegment对象包容ArraySegmentSize个字节,分别为“1”,“2”,“3”,然后调用Send方法发送过去。 类似地,服务端ServerApp也提前准备好了3个ArraySegment对象,由Receive方法负责填充它们。 实验1: 如果客户端与服务端的字节数组列表大小一致,ArraySegmentSize = 10
图 5
实验2:客户端与服务端的字节数组列表大小不同,服务端ArraySegmentSize = 10,客户端ArraySegmentSize=5
图 6
实验3: 客户端与服务端的字节数组列表大小不同,服务端ArraySegmentSize = 5,客户端ArraySegmentSize=10。
图 7
4 缓冲区问题小结:
经过两轮实验,虽然这些示例都是高度简化了的,但读者一定对TCP网络应用程序中的数据缓冲区有了直观的体验,并且一定会认识到网络应用程序开发远比桌面应用程序要复杂和困难。 事实上,基于Socket开发的TCP网络应用有两种缓冲区:
一种是由应用程序设置的数据缓冲区,可称之为“应用程序TCP/IP数据缓冲区 ”,多使用字节数组,我们在本篇文章中涉及到的都是这种类型的缓冲区。 另一种则是由操作系统为TCP/IP所提供的专用数据缓冲区,可称之为“TCP/IP系统缓冲区 ”。以阻塞方式调用Socket的Send方法,它只是保证这些数据已经被成功地送到了操作系统为TCP/IP设置的缓冲区中,但是否马上就被操作系统送到网络上,并且马上对方就能收到这些数据,还是个未知数。 默认情况下,操作系统为TCP所提供的数据缓冲区大小为8K。我们可以通过Socket对象的以下属性对其进行调整。 public int ReceiveBufferSize { get; set; } public int SendBufferSize { get; set; } 系统缓冲区的大小会影响到网络应用程序的性能。比如如果需要在两台计算机间传送一个较大的文件,那么,较大的缓冲区(比如8K)就比较小的缓冲区(比如1K)数据传送效率高。
总而言之,TCP协议在本质上仅仅保证“要发送的所有数据都按顺序地提交给接收者”,但它不维持要发送消息的边界,完全可能出现发送方连接调用多次Send方法,而接收方只调用一次Receive就全部接收完的情形。再加上网络可能拥塞,发送端与接收端处理数据的速度不同,……,情况就更复杂了。
因此,我们必须仔细地考虑各种情况,采取相应的策略来应对,通常采用的方式有以下3种: (1)只传播固定大小的消息。 (2)在消息开头附上一个消息的尺寸信息。 (3)采用一问一答的方式
我们将在下一讲介绍这三种策略的具体示例。
感兴趣读者不妨先试一试,能不能自己实现这三种策略?
=========================================
点击下载本文示例源码
(博客园的下载链接:http://files.cnblogs.com/bitfan/TCPBufferSourceCode.rar)