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
连接文中提到,如果发送端和接收端都是WCF程序,NetTcpBinding的效率是要优于HttpBinding的,而我们在内网无延迟的测试中也证明了这个情况。但是如果我们想建立一个可以在恶劣网络条件下仍然能保持较高传输效率的Large Data Transfer体系,目前来看NetTcpBinding恐怕无法满足我们的要求。
在研究的最后,我将发送端和接收端机器之前的带宽限制为5500kbps,延迟保持100ms不变。
- BasicHttpBinding
- BasicHttpBinding with Mtom
这两种情况都几乎可以将带宽占满,BasicHttpBinding with Mtom的表现要相对好一些。
Summary
Binding | Buffered内网无延迟 | Buffered内网100ms延迟 | Streamed内网无延迟 | Streamed内网100ms延迟 | 100ms延迟+5500kbps |
---|---|---|---|---|---|
NetTcpBinding | 8761kb/s | 437kb\s | 14571kb/s | 449kb/s | |
BasicHttpBinding | 7419kb/s | 234kb/s | 3778kb/s | 1002kb/s | 宽度满占用 |
BasicHttpBinding With Mtom | 9514kb/s | 194kb/s | 8570bk/s | 1315kb\s | 宽带满占用 |
为了更明确network delay对于NetTcpBinding的影响,另外测试了50ms延迟下的情况
Binding | Streamed内网50ms延迟 | Buffered内网50ms延迟 |
---|---|---|
NetTcpBinding | 671kb/s | 602kb/s |
BasicHttpBinding | 1139kb/s | 315kb/s |
BasicHttpBinding With Mtom | 1401kb/s | 334kb/s |
从目前的测试结果来看,考虑的适应大多数网络情况,我们应该采取基于HttpBinding with Mtom+Stream的解决方案。但是由于NetTcpBinding Transport是connection-based的传输方式,相比HttpBinding,稳定性会更有保证。
关于目前Data Transfer的改进建议
目前Data Transfer普遍还在使用buffered mode的传输方式,且实现方法和本文例子中的方式基本一致,在存在延迟的情况下效率会大打折扣。建议放弃这种传输方式。
目前Http Stream Mode采取了将数据先缓存在本地,之后将读取本地文件的stream异步发送到目的端。并且创建了很多数据单元来进行上述操作。目前这套逻辑遇到过连接异常断开的问题,且未找到原因。