HttpWebRequest is slow with chunked data

2020-04-10 03:33发布

问题:

I'm using HttpWebRequest to connect to my in-house built HTTP server. My problem is that it is a lot slower than connecting to the server via for instance PostMan (https://chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnpjigdojojpjoooidkmcomcm?hl=en), which is probably using the built-in functions in Chrome to request data.

The server is built using this example on MSDN (http://msdn.microsoft.com/en-us/library/dxkwh6zw.aspx) and uses a buffer size of 64. The request is a HTTP request with some data in the body.

When connecting via PostMan, the request is split into a bunch of chunks and BeginRecieve() is called multiple times, each time receiving 64B and taking about 2 milliseconds. Except the last one, which receives less than 64B.

But when connecting with my client using HttpWebRequest, the first BeginRecieve() callback receives 64B and takes about 1 ms, the following receives only 47B and takes almost 200 ms, and finally the third receives about 58B and takes 2ms.

What is up with the second BeginRecieve? I note that the connection is established as soon as I start to write data to the HttpWebRequest input stream, but the data reception does not start until I call GetResponse().

Here is my HttpWebRequest code:

var request = (HttpWebRequest)WebRequest.Create(url);

request.Method = verb;
request.Timeout = timeout;
request.Proxy = null;
request.KeepAlive = false;
request.Headers.Add("Content-Encoding", "UTF-8");
System.Net.ServicePointManager.Expect100Continue = false;
request.ServicePoint.Expect100Continue = false;

if ((verb == "POST" || verb == "PUT") && !String.IsNullOrEmpty(data))
{
    var dataBytes = Encoding.UTF8.GetBytes(data);

    try
    {
        var dataStream = request.GetRequestStream();
        dataStream.Write(dataBytes, 0, dataBytes.Length);
        dataStream.Close();
    }
    catch (Exception ex)
    {
        throw;
    }

}

WebResponse response = null;
try
{
    response = request.GetResponse();
}
catch (Exception ex)
{
    throw;
}

var responseReader = new StreamReader(rStream, Encoding.UTF8);
var responseStr = responseReader.ReadToEnd();

responseReader.Close();
response.Close();

What am I doing wrong? Why is it behaving so much differently than a HTTP request from a web browser? This is effectively adding 200ms of lag to my application.

回答1:

This looks like a typical case of the Nagle algorithm clashing with TCP Delayed Acknowledgement. In your case you are sending a small Http Request (~170 bytes according to your numbers). This is likely less than the MSS (Maximum Segment Size) meaning that the Nagle Algorithm will kick in. The server is probably delaying the ACK resulting in a delay of up to 500 ms. See links for details.

You can disable Nagle via ServicePointManager.UseNagleAlgorithm = false (before issuing the first request), see MSDN.

Also see Nagle’s Algorithm is Not Friendly towards Small Requests for a detailed discussion including a Wireshark analysis.

Note: In your answer you are running into the same situation when you do write-write-read. When you switch to write-read you overcome this problem. However I do not believe you can instruct the HttpWebRequest (or HttpClient for that matter) to send small requests as a single TCP write operation. That would probably be a good optimization in some cases. Althought it may lead to some additional array copying, affecting performance negatively.



回答2:

200ms is the typical latency of the Nagle algorithm. This gives rise to the suspicion that the server or the client is using Nagling. You say you are using a sample from MSDN as the server... Well there you go. Use a proper server or disable Nagling.

Assuming that the built-in HttpWebRequest class has an unnecessary 200ms latency is very unlikely. Look elsewhere. Look at your code to find the problem.



回答3:

It seems like HttpWebRequest is just really slow.

Funny thing: I implemented my own HTTP client using Sockets, and I found a clue to why HttpWebRequest is so slow. If I encoded my ASCII headers into its own byte array and sent them on the stream, followed by the byte array encoded from my data, my Sockets-based HTTP client behaved exactly like HttpWebRequest: first it fills one buffer with data (part of the header), then it uses another buffer partially (the rest of the header), waits 200 ms and then sends the rest of the data.

The code:

TcpClient client = new TcpClient(server, port);
NetworkStream stream = client.GetStream();

// Send this out
stream.Write(headerData, 0, headerData.Length);
stream.Write(bodyData, 0, bodyData.Length);
stream.Flush();

The solution was of course to append the two byte arrays before sending them out on the stream. My application is now behaving as espected.

The code with a single stream write:

TcpClient client = new TcpClient(server, port);
NetworkStream stream = client.GetStream();

var totalData = new byte[headerBytes.Length + bodyData.Length];
Array.Copy(headerBytes,totalData,headerBytes.Length);
Array.Copy(bodyData,0,totalData,headerBytes.Length,bodyData.Length);

// Send this out
stream.Write(totalData, 0, totalData.Length);
stream.Flush();

And HttpWebRequest seems to send the header before I write to the request stream, so it might be implemented somewhat like my first code sample. Does this make sense at all?

Hope this is helpful for anyone with the same problem!



回答4:

Try this: you need to dispose of your IDisposables:

var request = (HttpWebRequest)WebRequest.Create(url);

request.Method = verb;
request.Timeout = timeout;
request.Proxy = null;
request.KeepAlive = false;
request.Headers.Add("Content-Encoding", "UTF-8");
System.Net.ServicePointManager.Expect100Continue = false;
request.ServicePoint.Expect100Continue = false;

if ((verb == "POST" || verb == "PUT") && !String.IsNullOrEmpty(data))
{
    var dataBytes = Encoding.UTF8.GetBytes(data);

    using (var dataStream = request.GetRequestStream())
    {
        dataStream.Write(dataBytes, 0, dataBytes.Length);
    }
}

string responseStr;
using (var response = request.GetResponse())
{
    using (var responseReader = new StreamReader(rStream, Encoding.UTF8))
    {
        responseStr = responseReader.ReadToEnd();
    }
}


标签: c# .net http web