zoukankan      html  css  js  c++  java
  • 《.NET 4.0网络开发入门之旅》7:填平缓冲区陷阱

    《.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)

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

  • 相关阅读:
    iOS开发之视频播放
    iOS开发之Copy & MutableCopy及深复制 & 浅复制
    iOS开发之JSON & XML
    iOS开发之NSObject的多线程
    iOS开发之单例模式
    iOS开发之Run Loop
    taro开发微信小程序-页面开发规范
    视频Video放器的部分实例方法
    Input框搜索关键字高亮显示
    vue上拉加载下拉加载
  • 原文地址:https://www.cnblogs.com/bitfan/p/1938365.html
Copyright © 2011-2022 走看看