《.NET 4.0网络开发入门之旅》——
填平缓冲区陷阱
注:
这是一个针对
网络开发领域初学者
的系列文章,可作为《.NET 4.0 面向对象编程漫谈
》一书的扩充阅读,写作过程中我假设读者可以对照阅读此书的相关章节,不再浪费笔墨重复介绍相关的内容。
对于其他类型的读者,除非您已经有相应的.NET 技术背景与一定的开发经验,否则,阅读中可能会遇到困难。
我希望这系列文章能让读者领略到网络开发的魅力!
另外,这些文章均为本人原创,请读者尊重作者的劳动,我允许大家出于知识共享的目的自由转载这些文章及相关示例,但未经本人许可,请不要用于商业盈利目的。
本文如有错误,敬请回贴指正。
谢谢大家!
金旭亮
=================================================
点击以下链接阅读本系列前面的文章:
1 《
开篇语——
无网不胜》
2 《
IP知多少》
3 《我在“网” 中央
》
4 《与Socket的第一次“约会”
》
5 《与Socket的“再次见面”
》
6 《“麻烦“的数据缓冲区
》
=========================================
前一篇文章《“引发麻烦”的缓冲区
》,介绍了TCP Socket编程数据缓冲区必须要注意的两个问题:
(1)TCP不保存消息的边界,因此,服务端必须能有一种方法从收到的数据中正确地“切分”出一条完整的消息
(2)客户端与服务端的数据发送和接收速率应该匹配,否则,有可能出现“黏包”和“丢包”现象。
那么,我们怎么样来解决这两个问题?
1 为要传输的多条消息规定统一的长度
这是最直观的方法,我们可以事先制定一个消息代码表,每个消息代码都代表不同的含义,比如“000”代表“初始化”,“999”表示“结束”之类,这种思想在HTTP中我们也可以看到,比如HTTP就定义了一些状态码,200代表“OK”,500代表“服务端内部错误”。还可以参考CPU指令的设计方法,自行制定一些定长的“消息代码表”。
由于所有消息长度都一致,服务端的处理将变得非常简单,它将收到的数据按约定的长度“切块”即可。
请看示例解决方案FixedSizeMessageDemo。客户端需要将一个int数组发给服务端,服务端使用一个MemoryStream保存这些数据,然后按照4个字节一块一块地读取它们,正确地还原数据。
以下是服务端的代码框架:
int recv = 0;
//用于暂存数据的内存流
MemoryStream mem = new MemoryStream();
while (true) //接收客户端发来的所有数据
{
//将接收到的数据保存到内存流中
recv = client.Receive(data);
mem.Write(data, 0, recv);
if (recv == 0) //数据接收完毕,断开客户端 {0} 连接
{
client.Close();
break;
}
}
mem.Seek(0, SeekOrigin.Begin);
long datalength = mem.Length;
BinaryReader reader = new BinaryReader(mem);
Console.WriteLine("接收到数据为:");
while (reader.BaseStream.Position < datalength)
{
//切分数据
Console.Write("{0},", reader.ReadInt32()
);
}
reader.Close();
2 给消息附加长度信息
使用定长的消息虽然可以简化服务端的代码,但却受到很大的限制,而且如何设计一整套消息代码也是件比较麻烦的事。
一种比较好的方式是将两者结合起来,在每个消息开头附加一个固定长度的“消息长度”信息,这样,服务端就知道本消息到底有多长。
HTTP协议就是这么干的,在HTTP响应消息的头部(Headers)中有一个Content-length项,通知浏览器HTTP消息的主体(Body)部分占多少个字节。
提示:
HTTP是应用层协议,它在底层依赖TCP协议完成HTTP消息的传输。
首先,我们设计一个发送数据的静态方法:
// 发送变长的数据,将数据长度附加于数据开头
public static int SendVarData(Socket s, byte[] data)
{
int total = 0;
int size = data.Length; //要发送的消息长度
int dataleft = size; //剩余的消息
int sent;
//将消息长度(int类型)的,转为字节数组
byte[] datasize = new byte[4];
datasize = BitConverter.GetBytes(size);
//将消息长度发送出去
sent = s.Send(datasize);
//发送消息剩余的部分
while (total < size)
{
sent = s.Send(data, total, dataleft, SocketFlags.None);
total += sent;
dataleft -= sent;
}
return total;
}
仔细看一下注释,上述代码完成的工作“一目了然”,无需废话。
以下静态方法则完成接收并切分消息的工作:
// 接收变长的数据,要求其打头的4个字节代表有效数据的长度
public static byte[] ReceiveVarData(Socket s)
{
if (s == null)
throw new ArgumentNullException("s");
int total = 0; //已接收的字节数
int recv;
//接收4个字节,得到“消息长度”
byte[] datasize = new byte[4];
recv = s.Receive(datasize, 0, 4, 0);
int size = BitConverter.ToInt32(datasize, 0);
//按消息长度接收数据
int dataleft = size;
byte[] data = new byte[size];
while (total < size)
{
recv = s.Receive(data, total, dataleft, 0);
if (recv == 0)
{
break;
}
total += recv;
dataleft -= recv;
}
return data;
}
可以看到,由于“事先”知道消息长度,接收消息变得非常直观。
为了方便重用,我们可以把上述两个静态方法放到一个静态类SocketHelper中,并且将此类添加到MyNetworkLibrary类库中。以后的例子,还会用到这两个方法。
示例解决方案VariableLengthMessageDemo展示了使用上述方法发送变长数据。
3 “一问一答”的数据传送
仔细分析一下TCP协议,会发现它其实是通过“一问一答”的“握手”方式实现数据的可靠传输。
我们可以依葫芦画瓢,在更高的层次实现“一问一答”的通讯,简单地说:
数据发送方发送完一条消息之后,就停下来等待接收方发来一个确认消息,收到之后,再发送第二条消息。
数据接收方由于确切地知道发送方一次只发送一条消息,所以,它可以“放心大胆”地不断接收数据,直到receive方法返回0为止,然后,再向发送方发送一条“消息已收到”的“通知”,然后,准备接收下一条消息。
对于这种方式的数据通讯,每条消息可以不必附加上长度信息。
请看示例解决方案SendAndWaitDemo。客户端发送数据完毕之后,发送一条“SendFinished”消息。 服务端接收完数据之后,发送一条“ReceiveFinished”消息。 客户端没收到“ReceiveFinished”消息,就不会发送新的消息。
就请读者自行阅读源码,不再赘述。
4 开发一个“网络计算器”
前面介绍的许多示例程序都是出于学习目的而设计的,几乎没有什么实际用途,在学习了这么多的Socket编程知识之后,我们终于具备了开发一个“有点用”的网络应用程序的前提。
我在《.NET 4.0面向对象编程漫谈》一书的第24章,介绍了一个支持加减乘除和多级括号的“四则运算计算器”,并且将相关的前序、中序表达式解析算法封装成了一个程序集MathFuncLib.dll。我们就通过重用这个程序集,加上新学的Socket编程技术,实现一个“网络版四则运算计算器”(示例程序NetworkCalculator)。
图 1
上述示例程序客户端使用前面介绍的SendVarData方法发送表达式,使用ReceiveVarData方法接收服务端发回的计算结果。服务端使用MathFuncLib程序集封装的中序算法解析表达式,它的表达式接收和发回计算结果也是用的ReceiveVarData和SendVarData方法。
请读者自行阅读源码。
最后留几个作业:
请读者应用《.NET 4.0面向对象编程漫谈
》中介绍的多线程技术,改造NetworkCalculator示例程序:
(1)让服务端可以同时响应多个客户端的表达式计算请求
(2)将客户端由Console程序改为Windows Forms或WPF程序,在后台启动线程发送和接收表达式及计算结果。
再来点难度大的:
为了提升处理效率,允许客户端将“多条要计算的表达式”打包在一起,一起发送给服务端,服务端计算完毕之后,再把所有结果也“打包”一次性地发回给客户端。
应用本文所介绍的技术,现在读者您能开发出这样的程序吗?
下一讲,我们将暂时“告别一下” TCP,而去领略一下另一个非常重要的协议--UDP的风彩!
===============================================================================
有关“四则运算计算器”示例程序和MathFuncLib.dll的详细介绍,请看《.NET 4.0面向对象编程漫谈
》一书的第24章,读者可以从书的配套资源包中找到下载链接。以下列出CSDN下载频道中的下载链接:
《从面向对象到SOA》正文及源码
(http://download.csdn.net/source/2739426)
以下是本文所附之示例源码的下载链接:
示例源码下载链接
分享到:
相关推荐
填平3D CAD产品文档鸿沟.pdf
PCB微盲孔电镀铜填平影响因素研究.pdf
技术插图:填平3D CAD和产品文档之间的鸿沟.pdf
在本溪组沉积演化规律认识的基础上,利用煤层发育对沉积地貌的填平补齐特征,结合地层压实校正的印模法来恢复本溪组沉积期的古地貌。沉积环境分析表明,本溪组底部的湖田段铝土岩受奥陶系顶面古沟槽的控制,砂体形成于...
银行年报业绩前瞻:预计资产质量拐点确立,行业利润负增缺口将填平.pdf
银行年报业绩前瞻:预计资产质量拐点确立,行业利润负增缺口将填平(2021)(11页).pdf
网上 的参考博客很多很多五花八门,琳琅满目,其实都挺不错的但是呢在实际开发过程中会遇到各种各样的 你想不到的坑,而这些坑只能自己一个人默默的去填平。以下也是本博主参考网络及几何自身项目的记录。
主要分享下自己在开发Web App遇到的问题和过程,以及一些很已经(如何)填平的坑。
微信小程序处于发展中,内嵌的方法仍在不断改善与补充,相信一些坑也会在未来的版本中被填平。 本文基于的基础库版本为1.5.3,内容如有疏漏,欢迎指教。 (此图片来源于网络,如有侵权,请联系删除! ) Dry ...
2021年09月16日
微信小程序处于发展中,内嵌的方法仍在不断改善与补充,相信一些坑也会在未来的版本中被填平。 本文基于的基础库版本为1.5.3,内容如有疏漏,欢迎指教。 Dry goods 【干货】 一. 值 1. setData 1. ...
对于Mason,Pruppacher等人指出的对流暖云大云滴增长速度谷,本文从马尔柯夫型起伏凝结过程的作用,以及它和一次不连续碰并过程在分段模型下的作用观点出发...计算表明,这些过程都是填平大云滴生长沟中的有效的机制。
在典型草型富营养化湖泊——内蒙古鸟粱素海设立试验研究基地,进行了较大规模的生态恢复工程试验,实施了两项草型富营养化湖泊生态恢复技术措施:沉水植物收割工程与芦苇园田化生态管理工程。以机械化方式收割沉水...
八道湾组为伊犁盆地内...八道湾早期,全区分布有伊宁、尼勒克、巩留3个沉积中心,具有填平补齐的特征;八道湾晚期,沉积范围逐步扩大,沼泽大面积分布,为伊犁盆地重要的成煤期。这对于指导研究区煤炭的勘探具有重要意义。
长株潭地区房地产营销策划的现状及对策,唐明皓,苏小林,目前,长株潭地区房地产产品营销形势趋好,住宅缺口逐渐填平,办公用房的供求趋向平衡,写字楼被适度开发。商品房向智能型、环境
黔北煤田是贵州省主要产煤区之一,大地构造位置处于扬子陆块南部被动边缘褶冲带上的毕节弧形构造区、织金宽缓褶皱区和凤冈南北向褶断区,主要含煤地层为上二叠统龙潭组和长兴组。含煤层数多、厚度大,含煤性由南西至北...
六盘水煤田是贵州的主要产煤区之一,地处黔南坳陷六盘水断坳内,含煤地层主要为上二叠统龙潭组、长兴组,含煤层数多、厚度大,含煤性由中部向西北、南东方向逐渐变差。区内含煤地层的发育主要与含煤岩系基底构造和成煤期...
杭东普查区位于鄂尔多斯盆地东胜煤田北部,侏罗系延安组为主要含煤地层,自上而下可划分为3个岩段5个煤组,共含煤8~20层;由于本区处于鄂尔多斯盆地北部边缘附近,延安组沉积时期,沉积环境主要以河流-三角洲相为主,伴有...
MCDaemon-go是most-simple-mcd的前身,在MCDaemon-go中踩的坑和不合理规划,都将在most-simple-mcd中被填平! 使用时遇到了问题 首先查看 ,没有在FAQ中找到解决方案,则查看运行文件目录下,日志目录下的默认...