11

The following method (in which I hopefully did not make any mistakes while dramatically simplifying it for this post) is working properly and set up using the Streamed transfer mode over the net.tcp protocol. The problem is that the performance is significantly worse than downloading the same file through IIS over http. Why would this be, and what things can I change to improve the performance?

Stream WebSiteStreamedServiceContract.DownloadFile( string filePath ) {
    return File.OpenRead( filePath );
}

Finally, is WCF taking responsibility for properly disposing my stream, and does it do a good job at this? If not, what am I supposed to do instead?

Thank you.

Greg Smalter
  • 6,571
  • 9
  • 42
  • 63
  • Hi Greg! I 'm struggling over setting up a streamed channel via netTcp. By your question, it seems that you've been successful in doing the same. Can you please share info about server and client configurations? Thanks much in advance!! – Nayan Oct 07 '11 at 08:36
  • @Nayan I think you should make a new question and point me to it. The answer will probably end up being too big for comments. – Greg Smalter Oct 12 '11 at 16:53

6 Answers6

31

After 8 months of working on this problem, 3 of them with Microsoft, here is the solution. The short answer is that the server side (the side sending the large file) needed to use the following for a binding:

 <customBinding>
  <binding name="custom_tcp">
   <binaryMessageEncoding />
   <tcpTransport connectionBufferSize="256192" maxOutputDelay="00:00:30" transferMode="Streamed">
   </tcpTransport>
  </binding>
 </customBinding>

The key here being the connectionBufferSize attribute. Several other attributes may need to be set (maxReceivedMessageSize, etc.), but connectionBufferSize was the culprit.

No code had to be changed on the server side.

No code had to be changed on the client side.

No configuration had to be changed on the client side.

Here is the long answer:

I suspected all along that the reason net.tcp over WCF was slow was because it was sending small chunks of information very frequently rather than larger chunks of information less often, and that this made it perform poorly over high latency networks (the internet). This turned out to be true, but it was a long road to get there.

There are several attributes on the netTcpBinding that sound promising: maxBufferSize being the most obvious, and maxBytesPerRead and others sounding hopeful as well. In addition to those, it is possible to create more complicated streams than the one in the original question - you can specify the buffer size there as well - on both the client and the server side. The problem is that none of this has any impact. Once you use a netTcpBinding, you are hosed.

The reason for this is that adjusting maxBufferSize on a netTcpBinding adjusts the buffer on the protocol layer. But nothing you can do to a netTcpBinding will ever adjust the underlying transport layer. This is why we failed for so long to make headway.

The custom binding solves the problem because increasing the connectionBufferSize on the transport layer increases the amount of information sent at once, and thus the transfer is much less susceptible to latency.

In solving this problem, I did notice that maxBufferSize and maxBytesPerRead did have a performance impact over low latency networks (and locally). Microsoft tells me that maxBufferSize and connectionBufferSize are independent and that all combinations of their values (equal to one another, maxBufferSize larger than connectionBufferSize, maxBufferSize smaller than connectionBufferSize) are valid. We are having success with a maxBufferSize and maxBytesPerRead of 65536 bytes. Again, though, this had very little impact on high-latency network performance (the original problem).

If you are wondering what maxOutputDelay is for, it is the amount of time that is allotted to fill the connection buffer before the framework throws an IO exception. Because we increased the buffer size, we also increased the amount of time allotted to fill the buffer.

With this solution, our performance increased about 400% and is now slightly better than IIS. There are several other factors that affect the relative and absolute performance over IIS over HTTP and WCF over net.tcp (and WCF over http, for that matter), but this was our experience.

Greg Smalter
  • 6,571
  • 9
  • 42
  • 63
  • Greg, you say No configuration had to be changed on the client side - how so? I am currently using basicHttpBinding on both server and client, and Mtom, which requires configs on both client and service, thus i fail to see how the client can magically recognise a new customBinding node? – joedotnot Jun 21 '11 at 17:00
  • @joe, we were previously using a NetTcpBinding (we create a binding and then pass it into a ChannelFactory - we do not use the auto-generated proxy way of doing things), and we are continuing to use a NetTcpBinding. I'm pretty sure nothing changed at all, but I'll stop short of promising that and say what my real point was, which was that none of the paramter values (MaxBytesPerRead, etc.) on the client side were the cause of the original problem. – Greg Smalter Jun 21 '11 at 18:29
3

I don't know the answer to your first question (I think you need to provide more code to show what you are doing for both your tests), but your second question regarding the disposal of the stream, the answer is that you need to do it yourself.

Here is an excellent blog entry which has some great code for just this purpose.

Sailing Judo
  • 11,083
  • 20
  • 66
  • 97
2

I took @Greg-Smalter advice and changes ConnectionBufferSize on NetTcpBinding using reflection and that solved my problem. Streaming large documents is screaming fast now. Here is the code.

        var transport = binding.GetType().GetField("transport", BindingFlags.NonPublic | BindingFlags.Instance)
            ?.GetValue(binding);

        transport?.GetType().GetProperty("ConnectionBufferSize", BindingFlags.Public | BindingFlags.Instance)?.SetValue(transport, 256192);
Mayank
  • 8,777
  • 4
  • 35
  • 60
  • We have a WCF service that could not be easily changed (in deployment, with live users) hosted in IIS which was using this. Which turned into a problem reflecting this (or swapping to a CustomBinding per other answers); turns out you can do this with a little hackery around the magic Configure() function. Adding this to the service .svc worked: – Adam Frisby Apr 26 '18 at 14:04
  • (See reply further down - insufficient space here in the comment to paste code.) – Adam Frisby Apr 26 '18 at 14:08
0

Here is another way to do it without having to deal with Reflection. Just wrap NetTcpBinding into CustomBinding.

var binding = new CustomBinding(new NetTcpBinding
{
     MaxReceivedMessageSize = 2147483647,
     MaxBufferSize = 2147483647,
     MaxBufferPoolSize = 2147483647,
     ReceiveTimeout = new TimeSpan(4, 1, 0),
     OpenTimeout = new TimeSpan(4, 1, 0),
     SendTimeout = new TimeSpan(4, 1, 0),
     CloseTimeout = new TimeSpan(4, 1, 0),
     ReaderQuotas = XmlDictionaryReaderQuotas.Max,
     Security =
            {
                Mode = SecurityMode.None,
                Transport = {ClientCredentialType = TcpClientCredentialType.None}
            },
            TransferMode = TransferMode.Streamed,
            HostNameComparisonMode = HostNameComparisonMode.StrongWildcard
        });

binding.Elements.Find<TcpTransportBindingElement>().ConnectionBufferSize = 665600;
Mayank
  • 8,777
  • 4
  • 35
  • 60
0

The above reflection based answer does work for us. If you need to do this via a hosted IIS/WCF service; you can use the magic Configure function declaration to get access to the Binding to do this:

public static void Configure(ServiceConfiguration config)  
{  
    NetTcpBinding tcpBinding = new NetTcpBinding { /* Configure here */ };

    var transport = tcpBinding.GetType().GetField("transport", BindingFlags.NonPublic | BindingFlags.Instance)
            ?.GetValue(tcpBinding);
    transport?.GetType().GetProperty("ConnectionBufferSize", BindingFlags.Public | BindingFlags.Instance)?.SetValue(transport, 256192);

    ServiceEndpoint se = new ServiceEndpoint(ContractDescription.GetContract(typeof(IService)), tcpBinding , new EndpointAddress("net.tcp://uri.foo/bar.svc"))
    {
        ListenUri = new Uri("net.tcp://uri.foo/bar.svc")
    };

    config.AddServiceEndpoint(se);

    config.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true });  
    config.Description.Behaviors.Add(new ServiceDebugBehavior { IncludeExceptionDetailInFaults = true });  
}  
Adam Frisby
  • 358
  • 5
  • 20
  • I would recommend to avoid using reflection and use `CustomBinding` instead. I think it is much cleaner that way. – Mayank Apr 26 '18 at 14:14
  • It probably is; but this is a production service, and changing the binding that way makes my spider sense tingle on the existing deployed clients; the above has been confirmed to not break compatibility with them, CustomBinding has not (although I am about to test that.) – Adam Frisby Apr 30 '18 at 16:49
0

Thanks for the information in this post. There's not a lot of information out there about this issue. I want to add some additional details that maybe helpful for others.

Most of the responses here seem to indicate that people are using a single NetTcp endpoint or they are not hosting the WCF inside IIS.

If you are using multiple netTcp endpoints in the same wcf service and its hosted inside IIS or using a container that uses WAS, you may run into these issues.

  1. If you change the ConnectionBufferSize for one NetTcp endpoint, all of them must be changed and they must be the same value. Apparently this is a requirement when being hosted in WAS (which is what IIS uses). (https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/how-to-host-a-wcf-service-in-was) Apparently its not an issue if you are self-hosting the webservice, but I have not verified this. So if you've added a customBinding for one NetTcp, but you're getting an activation exception about a TransportManager being missing, this is your problem.
  2. I couldn't get this this to work using the magic Configure method. Even with the basic NetTcp binding without much changes, I was getting a null response object. I'm sure there's a way to fix that, but I didn't have the time to keep digging into this.
  3. The way that worked for me was to use a customBinding. If you are using Transport security with Windows authentication you can just add <windowsStreamSecurity /> So the binding ends up looking like this:
<binding name="CustomTcpBinding_ServerModelStreamed">
     <windowsStreamSecurity />
     <binaryMessageEncoding />
     <tcpTransport connectionBufferSize="5242880" maxReceivedMessageSize="2147483647" maxBufferSize ="2147483647" transferMode="Streamed" />
</binding>

You shouldn't have to change your client NetTcp configurations, of course that's if you are not using any additional features that aren't reflected here. For customBinding, the order of the sections are important. https://learn.microsoft.com/en-us/dotnet/framework/wcf/extending/custom-bindings