《.NET 4.0网络开发入门之旅》7:填平缓冲区陷阱
时间:2011-01-18 来源:金旭亮
注:
这是一个针对 网络开发领域初学者 的系列文章,可作为《.NET 4.0 面向对象编程漫谈 》一书的扩充阅读,写作过程中我假设读者可以对照阅读此书的相关章节,不再浪费笔墨重复介绍相关的内容。
对于其他类型的读者,除非您已经有相应的.NET 技术背景与一定的开发经验,否则,阅读中可能会遇到困难。
我希望这系列文章能让读者领略到网络开发的魅力!
另外,这些文章均为本人原创,请读者尊重作者的劳动,我允许大家出于知识共享的目的自由转载这些文章及相关示例,但未经本人许可,请不要用于商业盈利目的。
本文如有错误,敬请回贴指正。
谢谢大家!
金旭亮
=================================================
点击以下链接阅读本系列前面的文章:
1 《 开篇语—— 无网不胜》
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章,读者可以从书的配套资源包中找到下载链接。以下列出博客园中的下载链接:
《从面向对象到SOA》正文及源码
(http://files.cnblogs.com/bitfan/%e4%bb%8e%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e5%88%b0SOA.rar)
以下是本文所附之示例源码的下载链接:
示例源码下载链接