我的CSDN博客:http://blog.csdn.net/bitfan我的新浪微博:http://t.sina.com.cn/jinxuliang

金旭亮

让技术变得有趣,将学习升级为探索
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

《.NET 4.0网络开发入门之旅》7:填平缓冲区陷阱

Posted on 2011-01-18 14:58  金旭亮  阅读(1512)  评论(4编辑  收藏  举报

《.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章,读者可以从书的配套资源包中找到下载链接。以下列出博客园中的下载链接:

(https://files.cnblogs.com/bitfan/%e4%bb%8e%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e5%88%b0SOA.rar)

    以下是本文所附之示例源码的下载链接:

我的CSDN博客:http://blog.csdn.net/bitfan我的新浪微博:http://t.sina.com.cn/jinxuliang