WCF Data transfer buffered VS streamed

WCF Data Transfer buffered VS streamed

在Data Transfer中,我们会经常听到开发提到buffer mode和stream mode。对于不了解Data Transfer逻辑的同学来说,很难通过字面意思理解这两种传输方式分别是什么以及他们有什么区别。今天这篇文章将尽量为大家解释这两种传输方式的原理和区别。

  • Buffered vs Streamed概述
  • Buffered vs Streamed效率探究
  • Summary
  • 关于目前Data Transfer的改进建议

Buffered vs Streamed概述

Buffered和Streamed是WCF中的两种传输数据的模式。我们可以直接引用微软对两种模式的解释:

  • Buffered transfers hold the entire message in a memory buffer until
    the transfer is complete. A buffered message must be completely
    delivered before a receiver can read it.

  • Streamed transfers expose the message as a stream. The receiver
    starts processing the message before it is completely delivered.

Buffered Mode只有将想要发送的对象完整传输到接收端后,接收端才能操作该对象。而Streamed Mode可以在对象传输过程中操作传输对象。显然,当需要传输的数据较大且不需要得到完整对象就能继续操作的情况下,Streamed Mode是更好的选择。
WCF应用streamed mode的方式很简单,只需要在代码里给binding的TransferMode赋值,或者通过修改App.config/web.config,修改binding的attribute。

注意:目前Streamed只支持NetTcpBinding,BasicHttpBinding,WSHttpBinding

下面用一个简单的例子来说明上述解释:

Client端Code:

static void TestStream()
        {
            using (var sm = new FileStream(@"C:\lg.zip", FileMode.Open))
            {
                BasicHttpBinding binding = new BasicHttpBinding();
                binding.SendTimeout = ((BasicHttpBinding)binding).SendTimeout = TimeSpan.FromMinutes(100);
                binding.TransferMode = TransferMode.Streamed;
                var factory = new ChannelFactory<ITService>(binding, new EndpointAddress($"http://{host}:14011/TService/Streamed"));
                mChannel = factory.CreateChannel();
                Console.WriteLine("Start send Stream. TransferMode is Streamed . Time now is {0}", DateTime.Now.ToLongTimeString());
                mChannel.OneWayStream(sm);
            }
            using (var sm = new FileStream(@"C:\lg.zip", FileMode.Open))
            {
                BasicHttpBinding binding = new BasicHttpBinding();
                binding.SendTimeout = ((BasicHttpBinding)binding).SendTimeout = TimeSpan.FromMinutes(100);
                //binding.TransferMode = TransferMode.Streamed;
                var factory = new ChannelFactory<ITService>(binding, new EndpointAddress($"http://{host}:14011/TService/Buffered"));
                mChannel = factory.CreateChannel();
                Console.WriteLine("Start send Stream. TransferMode is Buffered . Time now is {0}", DateTime.Now.ToLongTimeString());
                mChannel.OneWayStream(sm);
            }
            Console.ReadKey();
        }

Server端code:

 public void OneWayStream(Stream stream)
        {
            Console.WriteLine("Got Stream. Time Now:{0}", DateTime.Now.ToLongTimeString());
            using (stream)
            {

            }
        }

Server端配置文件片段:

<endpoint address="http://10.2.66.62:14011/TService/Streamed" binding="basicHttpBinding" bindingConfiguration="streamedHttpBinding" contract="TransferService.ITService">
        </endpoint>
        <endpoint address="http://10.2.66.62:14011/TService/Buffered" binding="basicHttpBinding" bindingConfiguration="bufferedHttpBinding" contract="TransferService.ITService">
        </endpoint>
        <endpoint address="http://10.2.66.62:14011/TService/Mtom" binding="basicHttpBinding" bindingConfiguration="mtomHttpBinding" contract="TransferService.ITService">
        </endpoint>
        <endpoint address="http://10.2.66.62:14011/TService/Buffered/Mtom" binding="basicHttpBinding" bindingConfiguration="bufferedMtomHttpBinding" contract="TransferService.ITService">
        </endpoint>
        <endpoint address="net.tcp://10.2.66.62:14012/TService/Streamed" binding="netTcpBinding" bindingConfiguration="streamednetTcpBinding" contract="TransferService.ITService">
        </endpoint>
        <endpoint address="net.tcp://10.2.66.62:14012/TService/Buffered" binding="netTcpBinding" bindingConfiguration="bufferednetTcpBinding" contract="TransferService.ITService">
        </endpoint>
 <basicHttpBinding>
        <binding name="streamedHttpBinding" receiveTimeout="10:10:10" maxReceivedMessageSize="2147483647" transferMode="Streamed" messageEncoding="Text" maxBufferPoolSize="524288" maxBufferSize="2147483647" allowCookies="true">
          <readerQuotas maxDepth="320" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="65536" maxNameTableCharCount="65536"/>
        </binding>
        <binding name="bufferedHttpBinding" receiveTimeout="10:10:10" maxReceivedMessageSize="2147483647" maxBufferPoolSize="524288" maxBufferSize="2147483647" allowCookies="true">
          <readerQuotas maxDepth="320" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="65536" maxNameTableCharCount="65536"/>
        </binding>
        <binding name="mtomHttpBinding" receiveTimeout="10:10:10" maxReceivedMessageSize="2147483647" transferMode="Streamed" messageEncoding="Mtom" maxBufferPoolSize="524288" maxBufferSize="2147483647" allowCookies="true">
          <readerQuotas maxDepth="320" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="65536" maxNameTableCharCount="65536"/>
        </binding>
      <basicHttpBinding>
        <binding name="streamedHttpBinding" receiveTimeout="10:10:10" maxReceivedMessageSize="2147483647" transferMode="Streamed" messageEncoding="Text" maxBufferPoolSize="524288" maxBufferSize="2147483647" allowCookies="true">
          <readerQuotas maxDepth="320" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="65536" maxNameTableCharCount="65536"/>
        </binding>
        <binding name="bufferedHttpBinding" receiveTimeout="10:10:10" maxReceivedMessageSize="2147483647" maxBufferPoolSize="524288" maxBufferSize="2147483647" allowCookies="true">
          <readerQuotas maxDepth="320" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="65536" maxNameTableCharCount="65536"/>
        </binding>
        <binding name="bufferedMtomHttpBinding" receiveTimeout="10:10:10" maxReceivedMessageSize="2147483647" maxBufferPoolSize="524288" messageEncoding="Mtom" maxBufferSize="2147483647" allowCookies="true">
          <readerQuotas maxDepth="320" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="65536" maxNameTableCharCount="65536"/>
        </binding>
        <binding name="mtomHttpBinding" receiveTimeout="10:10:10" maxReceivedMessageSize="2147483647" transferMode="Streamed" messageEncoding="Mtom" maxBufferPoolSize="524288" maxBufferSize="2147483647" allowCookies="true">
          <readerQuotas maxDepth="320" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="65536" maxNameTableCharCount="65536"/>
        </binding>
      </basicHttpBinding>
      <netTcpBinding>
        <binding name="streamednetTcpBinding" receiveTimeout="10:10:10" transferMode="Streamed" maxReceivedMessageSize="2147483647">
          <readerQuotas maxDepth="320" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="4096" maxNameTableCharCount="65536"/>
        </binding>
        <binding name="bufferednetTcpBinding" receiveTimeout="10:10:10" maxReceivedMessageSize="2147483647">
          <readerQuotas maxDepth="320" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="4096" maxNameTableCharCount="65536"/>
        </binding>
      </netTcpBinding>

测试文件大小340MB,测试结果如下:

Client端
这里写图片描述
Server端
这里写图片描述

从测试结果可以看出,通过Streamed Mode发送的Stream,接收端在1s之内就可以得到Stream对象并操作。而通过Buffered Mode发送的Stream,接收端在接近20s之后才获得Stream对象,由于client和server机器在同一网段,足以将完整的Stream发送到目的端。

Buffered vs Streamed效率探究

  • Buffered Performance

    通过上面的阐述和测试,我们知道Buffered Mode实际上是一种类似于同步的传输方式。在实际的数据传输过程中,我们不可能将Stream本身完整发送到目的端,所以buffered mode的Data Transfer实现类似如下。为了防止磁盘读写对测试结果造成影响,本文所有Send操作会将本地FileStream读取到MemoryStream中,再向接收端发送,接收端只读取接收到的Buffer或者Stream,不会写入磁盘中。
    Client端Code:

innerStream = new FileStream(@"C:\test.zip", FileMode.Open);
            int read = 0;
            int offset = 0;
            var buffer = new byte[64 * 1024];
            while ((read = innerStream.Read(buffer, 0, 64 * 1024)) != 0)
            {
                memory.Write(buffer, 0, read);
                offset += read;
            }
var buffer = new byte[64 * 1024];
            int read = 0;
            Stopwatch watch = new Stopwatch();
            memory.Position = 0;
            watch.Start();
            while ((read = memory.Read(buffer, 0, 64 * 1024)) != 0)
            {
                mStream.Write(buffer, 0, read);
            }
            mStream.Flush();
            //mStream.Position = 0;
            watch.Stop();
            Console.WriteLine("Send time:{0}", watch.ElapsedMilliseconds);

Server端Code:

public void PutBuffer(byte[] buffer, int totalSize)
        {            
            Console.WriteLine("Time now is :{0}. Receive buffer:{1}, total size:{2}",
                DateTime.Now.ToLongTimeString(), buffer.Length, totalSize);
        }

仍然以340MB大小文件为例,测试不同网络条件下buffered mode的传输表现,server端WCF配置文件参考第一节。

  • 内网无延迟

    • BasicHttpBinding
      发送端数据
      这里写图片描述

    • BasicHttpBinding with Mtom

这里写图片描述

Mtom是为HttpBinding提供的传输优化机制

  • NetTcpBinding

    发送端数据
    这里写图片描述

在理想的网络条件下,buffered mode在NetTcpBinding和BasicHttpBinding中的表现区别不大,NetTcpBinding比BasicHttpBinding略快一些,如果BasicHttpBinding应用了Mtom的编码模式,与NetTcpBinding几乎没有效率上的区别。

  • 内网延迟100ms

    • BasicHttpBinding

    发送数据端
    这里写图片描述

    • BasicHttpBinding with Mtom

这里写图片描述

  • NetTcpBinding

    发送端数据
    这里写图片描述

    我们看到在高延迟下buffered mode的表现是灾难性的,从理想网络条件下的接近9M/s的速度骤减到只有200~400kb/s。由于每次传输buffer都是同步传输,都会受到网络延迟的影响,显然这个影响的程度是我们不能接受的。

疑问:我们看到NetTcpBinding下每次调用OperationContract方法所花费的时间是BasicHttpBinding下花费时间的一半左右,原因尚待研究。

对象完整发送完全之前接收端无法操作对象,是buffered mode的特性之一。对于大数据,如果我们一次将大数据发送到目的端,很容易导致内存溢出。如果分为多次发送,网络延迟会施加在每次传输中,造成的效率影响十分巨大。综上,buffered mode的传输方式不适合应用在较大数据的传输方案中。

现在我们需要寻找一种调用传输(OperationContract)次数少(最好是一次),且不会在单次传输中出现内存溢出的传输方式。让我们来看看Streamed Mode是否是那个救世主。

  • Streamed Performance

    Streamed Mode的特性决定了我们可以先将Stream对象发送到接收端,然后在发送端将数据写入Stream中,接收端从Stream中直接读取数据。这种情况下,我们可以使用WCF提供的异步方式来发送。

Client端Code:

var doc = new Document() { Stream = mStream, Length = innerStream.Length };
                Stopwatch watch = new Stopwatch();
                watch.Start();                 
                var result = mChannel.BeginTransferDocument(doc, OnTransferCompleted, mChannel);
                Thread start = new Thread(SendThread);
                start.IsBackground = true;
                start.Start();
                result.AsyncWaitHandle.WaitOne();


static void SendThread()
        {
            var buffer = new byte[64 * 1024];
            int read = 0;
            Stopwatch watch = new Stopwatch();
            watch.Start();
            while ((read = memory.Read(buffer, 0, 64 * 1024)) != 0)
            {
                mStream.Write(buffer, 0, read);
            }
            mStream.Flush();
            watch.Stop();
            Console.WriteLine("Send time:{0}",watch.ElapsedMilliseconds);
            doc.Stream.Position = 0;
        }

Contract:

[OperationContract]
 void TransferDocument(Document document);

[OperationContract(AsyncPattern = true)]
IAsyncResult BeginTransferDocument(Document document,
AsyncCallback callback, object asyncState);

void EndTransferDocument(IAsyncResult result);

[MessageContract]
    public class Document
    {
        Stream stream;
        long length = 0L;

        [MessageBodyMember]
        public Stream Stream
        {
            get { return stream; }
            set { stream = value; }
        }

        [MessageHeader(MustUnderstand = true)]
        public long Length
        {
            get { return length; }
            set { length = value; }
        }
    }

Server端Code:

public void TransferDocument(Document document)
        {
            Console.WriteLine("Begin TransferDocument");
            var buffer = new byte[64 * 1024];
            int offset = 0;
            int read = 0;
            int readTimes = 0;
            while (true)
            {
                if ((read = document.Stream.Read(buffer, 0, 64 * 1024)) == 0)
                {
                    Thread.Sleep(1000);
                    continue;
                }
                break;
            }

                Console.WriteLine("Time now :{0} Current read:{1}", DateTime.Now.ToLongTimeString(), read);
                offset += read;
                readTimes++;
                while (offset < document.Length)
                {
                    read = document.Stream.Read(buffer, 0, 64 * 1024);
                    if (read != 0)
                    {
                        readTimes++;
                        Console.WriteLine("Time now :{0} Current read:{1}", DateTime.Now.ToLongTimeString(), read);
                    }
                    offset += read;           
                Console.WriteLine("Total size:{0}. Total read times is {1}", offset.ToString(), readTimes);
            }
        }

注意:在Streamed传输中,如果要传输的对象不只是Stream,那么必须给对象添加MessageContract,而不是DataContract,其中Stream属性是Message Body。

从代码中可以看到,我们只调用了一次OperationContract方法,之后都是在操作传递过去的Stream,这符合了我们要减少直接调用WCF OperationContract的目的。

所有测试条件与Buffered Mode相同,我们来看一下Streamed Mode在不同网络条件下的表现。

  • 内网无延迟

    • BasicHttpBinding

    发送端数据
    这里写图片描述

接收端数据
这里写图片描述

疑问:在BasicHttpBinding且Transfer Mode为Streamed情况下,接收端每次只能读取1536byte,无论maxBytesPerRead设置为多少。目前还未找到原因,这是制约传输效率的重要原因。

  • BasicHttpBinding with Mtom

    发送端数据
    这里写图片描述
    接收端数据
    这里写图片描述

    疑问:在BasicHttpBinding 将编码模式改为Mtom后,变为每次可读取4096byte。

  • NetTcpBinding

    发送端数据
    这里写图片描述
    接收端数据
    这里写图片描述

    疑问:在NetTcpBinding中我们也遇到了有趣的事情,maxBytesPerRead是65536且maxBufferSize是2147483647(2G)情况下,接收端每次最多只能读取65529且下次读取会很少,目前怀疑是TCP的窗口滑动机制在作祟,窗口滑动是TCP的流量控制机制。

虽然存在只读取几个字节的情况,但是由于NetTcpBinding每两次可以读取64k数据,所以在理想网络情况下,NetTcpBinding+Streamed似乎是我们的最优解。

  • 内网100ms延迟

    • BasicHttpBinding
      发送端数据
      这里写图片描述
      接收端数据
      这里写图片描述
      带宽占用峰值
      这里写图片描述

    • BasicHttpBinding with Mtom

    发送端数据
    这里写图片描述
    接收端数据
    这里写图片描述
    带宽占用峰值
    这里写图片描述

    • NetTcpBinding

    发送端数据
    这里写图片描述
    接收端数据
    这里写图片描述

带宽占用峰值
这里写图片描述

出乎我们的意料,在延迟较高的情况下,nettcpbinding的表现令人大跌眼镜,平均速度只有400kb/s,带宽占用也一直稳定在4Mbps左右。反而httpbinding的速度达到甚至超过了直接在server间Copy的速度。
这里写图片描述

我们知道Http是基于tcp的协议,也就是说http协议进行的操作理论上比net.tcp只多不少,为什么会快于net.tcp呢。目前找到的一个比较合理的解释是,在WCF中,tcp transport是connection-based,发送数据包之前需要和对方确认状态,这个过程无疑放大了network delay带来的影响。而http transport则不是connection-based,发送任何相应之前不需要确认状态。

我们会继续研究哪些因素或设定会导致http会快于net.tcp

Choose a transport

连接文中提到,如果发送端和接收端都是WCF程序,NetTcpBinding的效率是要优于HttpBinding的,而我们在内网无延迟的测试中也证明了这个情况。但是如果我们想建立一个可以在恶劣网络条件下仍然能保持较高传输效率的Large Data Transfer体系,目前来看NetTcpBinding恐怕无法满足我们的要求。

在研究的最后,我将发送端和接收端机器之前的带宽限制为5500kbps,延迟保持100ms不变。

  • BasicHttpBinding
    这里写图片描述
  • BasicHttpBinding with Mtom
    这里写图片描述

这两种情况都几乎可以将带宽占满,BasicHttpBinding with Mtom的表现要相对好一些。

Summary

BindingBuffered内网无延迟Buffered内网100ms延迟Streamed内网无延迟Streamed内网100ms延迟100ms延迟+5500kbps
NetTcpBinding8761kb/s437kb\s14571kb/s449kb/s
BasicHttpBinding7419kb/s234kb/s3778kb/s1002kb/s宽度满占用
BasicHttpBinding With Mtom9514kb/s194kb/s8570bk/s1315kb\s宽带满占用

为了更明确network delay对于NetTcpBinding的影响,另外测试了50ms延迟下的情况

BindingStreamed内网50ms延迟Buffered内网50ms延迟
NetTcpBinding671kb/s602kb/s
BasicHttpBinding1139kb/s315kb/s
BasicHttpBinding With Mtom1401kb/s334kb/s

从目前的测试结果来看,考虑的适应大多数网络情况,我们应该采取基于HttpBinding with Mtom+Stream的解决方案。但是由于NetTcpBinding Transport是connection-based的传输方式,相比HttpBinding,稳定性会更有保证。

关于目前Data Transfer的改进建议

目前Data Transfer普遍还在使用buffered mode的传输方式,且实现方法和本文例子中的方式基本一致,在存在延迟的情况下效率会大打折扣。建议放弃这种传输方式。

目前Http Stream Mode采取了将数据先缓存在本地,之后将读取本地文件的stream异步发送到目的端。并且创建了很多数据单元来进行上述操作。目前这套逻辑遇到过连接异常断开的问题,且未找到原因。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值