-1

I am using the following code to read a psuedo-HTTP request response. It works sometimes but not always and I do not understand.

Background: I have a device that takes HTTP GET requests and sends a chunked HTTP response. In one case, the response is not a proper chunked HTTP response. It leaves out the null chunk that indicates the end of data. I have fixed that problem in the device, but I am trying to figure out how to read the non-comforming HTTP response. I found code from Create http request using TcpClient that sometimes works and sometimes doesn't and I do not understand why.

If I use the code unaltered, it works fine. If I use it by replacing the "www.bing.com" with my device's IP, "192.1.168.89" in both places the string appears, for example, and change the GET command line to "GET /index.htm HTTP/1.1", it works fine. This version of the command returns a web page that is constructed by the device and sends several TCP buffers (about 1400 bytes in my device) of chunked data.

However, if I change to another command that my device understands, "GET /request.htm?T HTTP/1.1", but returns less than 500 bytes of chunked data, then I never see the response. In fact it never gets past the call to "CopyToAsync(memory)" and I do not understand why. The device sees the request, parses it and sends a proper HTTP response. (I know it is a proper response because I have code that uses HTTPClient to read the response and it sees the response fine. And I see the response data from the device side is exactly the same going out in both cases. I can see the device data because I am writing the device's firmware and can change it to printf() the data being sent out to the TCP routines.)

Anyone have an explanation for why the code below isn't always seeing a response?

private static async Task<string> HttpRequestAsync() {
    string result = string.Empty;

    using (var tcp = new TcpClient("www.bing.com", 80))
    using (var stream = tcp.GetStream())
    {
        tcp.SendTimeout = 500;
        tcp.ReceiveTimeout = 1000;
        // Send request headers
        var builder = new StringBuilder();
        builder.AppendLine("GET /?scope=images&nr=1 HTTP/1.1");
        builder.AppendLine("Host: www.bing.com");
        //builder.AppendLine("Content-Length: " + data.Length);   // only for POST request
        builder.AppendLine("Connection: close");
        builder.AppendLine();
        var header = Encoding.ASCII.GetBytes(builder.ToString());
        await stream.WriteAsync(header, 0, header.Length);

        // Send payload data if you are POST request
        //await stream.WriteAsync(data, 0, data.Length);

        // receive data
        using (var memory = new MemoryStream())
        {
            await stream.CopyToAsync(memory);
            memory.Position = 0;
            var data = memory.ToArray();

            var index = BinaryMatch(data, Encoding.ASCII.GetBytes("\r\n\r\n")) + 4;
            var headers = Encoding.ASCII.GetString(data, 0, index);
            memory.Position = index;

            if (headers.IndexOf("Content-Encoding: gzip") > 0)
            {
                using (GZipStream decompressionStream = new GZipStream(memory, CompressionMode.Decompress))
                using (var decompressedMemory = new MemoryStream())
                {
                    decompressionStream.CopyTo(decompressedMemory);
                    decompressedMemory.Position = 0;
                    result = Encoding.UTF8.GetString(decompressedMemory.ToArray());
                }
            }
            else
            {
                result = Encoding.UTF8.GetString(data, index, data.Length - index);
                //result = Encoding.GetEncoding("gbk").GetString(data, index, data.Length - index);
            }
        }

        //Debug.WriteLine(result);
        return result;
    }
}

private static int BinaryMatch(byte[] input, byte[] pattern)
{
    int sLen = input.Length - pattern.Length + 1;
    for (int i = 0; i < sLen; ++i)
    {
        bool match = true;
        for (int j = 0; j < pattern.Length; ++j)
        {
            if (input[i + j] != pattern[j])
            {
                match = false;
                break;
            }
        }
        if (match)
        {
            return i;
        }
    }
    return -1;
}

=====================

Let me edit the function above to show what it is now and maybe clarify things.

static async Task<byte[]> getTcpClientHttpDataRequestAsync(string ipAddress, string request)
        {
            string result = string.Empty;
            List<byte> arrayList = new List<byte>();

            using (var tcp = new TcpClient("192.168.1.89", 80))
            using (var stream = tcp.GetStream())
            using (var memory = new MemoryStream())
            {
                tcp.SendTimeout = 500;
                tcp.ReceiveTimeout = 10000;
                tcp.NoDelay = true;
                // Send request headers
                var builder = new StringBuilder();
                builder.AppendLine("GET /request.htm?x01011920000000000001 HTTP/1.1");
                builder.AppendLine("Host: 192.168.1.89");
                builder.AppendLine("Connection: Close");
                builder.AppendLine();
                var header = Encoding.ASCII.GetBytes(builder.ToString());

                Console.WriteLine("======");
                Console.WriteLine(builder.ToString());
                Console.WriteLine("======");

                await stream.WriteAsync(header, 0, header.Length);

                do { } while (stream.DataAvailable == 0);

                Console.WriteLine("Data available");

                bool done = false;
                do
                {
                    int next = stream.ReadByte();

                    if (next < 0)
                    {
                        done = true;
                    }
                    else
                    {
                        arrayList.Add(Convert.ToByte(next));
                    }

                } while (stream.DataAvailable && !done);

                byte[] data = arrayList.ToArray();

                return data;
            }
        }

The GET command is what my device is responding to. If the command starts with 'x' as shown then it responds with a proper HTTP response and the function above reads the data. If it starts with 'd' it is missing the 0 length chunk at the end and the function above never sees any data from the device.

With Wireshark, I am seeing the following responses for the 'x' and 'd' commands.

The 'x' command returns 2 TCP frames with the following data:

0000   1c 6f 65 d3 f0 e2 4c 60 de 41 3f 67 08 00 45 00   .oe...L`.A?g..E.
0010   00 9c 00 47 00 00 64 06 d2 49 c0 a8 01 59 c0 a8   ...G..d..I...Y..
0020   01 22 00 50 05 5d fc f5 9e 72 ad 75 e3 2c 50 18   .".P.]...r.u.,P.
0030   00 01 a9 cd 00 00 48 54 54 50 2f 31 2e 31 20 32   ......HTTP/1.1 2
0040   30 30 20 4f 4b 0d 0a 43 6f 6e 6e 65 63 74 69 6f   00 OK..Connectio
0050   6e 3a 20 63 6c 6f 73 65 0d 0a 43 6f 6e 74 65 6e   n: close..Conten
0060   74 2d 54 79 70 65 3a 20 74 65 78 74 2f 68 74 6d   t-Type: text/htm
0070   6c 0d 0a 43 61 63 68 65 2d 43 6f 6e 74 72 6f 6c   l..Cache-Control
0080   3a 20 6e 6f 2d 63 61 63 68 65 0d 0a 54 72 61 6e   : no-cache..Tran
0090   73 66 65 72 2d 45 6e 63 6f 64 69 6e 67 3a 20 63   sfer-Encoding: c
00a0   68 75 6e 6b 65 64 0d 0a 0d 0a                     hunked....

0000   1c 6f 65 d3 f0 e2 4c 60 de 41 3f 67 08 00 45 00   .oe...L`.A?g..E.
0010   00 45 00 48 00 00 64 06 d2 9f c0 a8 01 59 c0 a8   .E.H..d......Y..
0020   01 22 00 50 05 5d fc f5 9e e6 ad 75 e3 2c 50 18   .".P.].....u.,P.
0030   00 01 fc 20 00 00 30 30 31 0d 0a 2b 0d 0a 30 30   ... ..001..+..00
0040   37 0d 0a 01 85 86 00 00 0d 0a 0d 0a 30 30 30 0d   7...........000.
0050   0a 0d 0a                                          ...

By comparison the 'd' command returns data in 2 TCP frames as:

0000   1c 6f 65 d3 f0 e2 4c 60 de 41 3f 67 08 00 45 00   .oe...L`.A?g..E.
0010   00 9c 00 4e 00 00 64 06 d2 42 c0 a8 01 59 c0 a8   ...N..d..B...Y..
0020   01 22 00 50 05 5e d3 c3 f9 f5 69 cc 6d a3 50 18   .".P.^....i.m.P.
0030   00 01 30 ae 00 00 48 54 54 50 2f 31 2e 31 20 32   ..0...HTTP/1.1 2
0040   30 30 20 4f 4b 0d 0a 43 6f 6e 6e 65 63 74 69 6f   00 OK..Connectio
0050   6e 3a 20 63 6c 6f 73 65 0d 0a 43 6f 6e 74 65 6e   n: close..Conten
0060   74 2d 54 79 70 65 3a 20 74 65 78 74 2f 68 74 6d   t-Type: text/htm
0070   6c 0d 0a 43 61 63 68 65 2d 43 6f 6e 74 72 6f 6c   l..Cache-Control
0080   3a 20 6e 6f 2d 63 61 63 68 65 0d 0a 54 72 61 6e   : no-cache..Tran
0090   73 66 65 72 2d 45 6e 63 6f 64 69 6e 67 3a 20 63   sfer-Encoding: c
00a0   68 75 6e 6b 65 64 0d 0a 0d 0a                     hunked....

0000   1c 6f 65 d3 f0 e2 4c 60 de 41 3f 67 08 00 45 00   .oe...L`.A?g..E.
0010   00 36 00 4f 00 00 64 06 d2 a7 c0 a8 01 59 c0 a8   .6.O..d......Y..
0020   01 22 00 50 05 5e d3 c3 fa 69 69 cc 6d a3 50 18   .".P.^...ii.m.P.
0030   00 01 64 c2 00 00 30 30 37 0d 0a 01 90 91 00 00   ..d...007.......
0040   0d 0a 0d 0a                                       ....

The only discernible differences that I see is that in the second frame of the 'd' command it is missing a 1 byte chunk that is part of our protocol (and shouldn't have any effect on the TCP/HTTP function) and the last 7 bytes of data that the 'x' command provides, which is the 0 length chunk expected for HTTP.

Going back to the code in HttpRequestAsync(), if the 'd' command is sent then the code never sees stream.DataAvailable become true, even though the data has been sent. Why?

  • 1
    I don't see any `stream.Read()` as per [this example](https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.tcpclient?view=netframework-4.8) – Arthur Rey Jan 19 '20 at 22:08
  • @ArthurRey stream.CopyToAsync is the 'read' equivalent. Some time ago I experienced a similar behaviour. In my case it was caused by a huge difference in speed between my 2 devices (client and server). I figured out that I can solve it with a System.Threading.Thread.Sleep(50); Maybe you just have a timing error. You could try and put some 'sleeps' at different points. Just to check if that's fixing the problem. Even though you have to look for a less dirty method. In my case the response was sent too late and the response was rejected – Sheradil Jan 19 '20 at 22:28
  • @Sheradil the last thing you want to do with high bit rate I/O is insert a `Sleep()` as that can lead to dropped packets –  Jan 19 '20 at 22:34
  • _"...but returns less than 500 bytes of chunked data, then I never see the response..."_ - that can happen when the response is small; you are using buffered I/O and the server doesn't `Close()` the stream, the system is waiting for more bytes that never come. Have the server `Close()` or `Flush()` the stream so that your read code gets the response sooner –  Jan 19 '20 at 22:38
  • @MickyD The device is closing the connection. – larrycook99 Jan 19 '20 at 23:33
  • @Sheradil II see the data response sent from the device, via printfs in the device, immediately after receiving the GET. – larrycook99 Jan 19 '20 at 23:35
  • @Sheradil I did do an experiment where I added "do{} while(!stream.DataAvailable); Console.WriteLine("Data is available");" between the stream.WriteAsync() and the stream.CopyToAsync() calls and there is data available. It is just not getting read asyncronously. So, I added a loop to call stream.ReadByte() to read all of the data and all of the expected data is there. I just don't understand why stream.CopyToAsync() isn't always reading that data. – larrycook99 Jan 19 '20 at 23:40
  • If your device is closing the connection, then `CopyToAsync` should complete. I recommend installing Wireshark and investigating the packets. – Stephen Cleary Jan 20 '20 at 20:36
  • Show us the server code –  Jan 20 '20 at 21:03
  • @MickyD Yeah, you don't want that. Definitely true, just wanted to check if that's actually the problem. – Sheradil Jan 21 '20 at 19:44
  • My "experiment" results above were misstated. It only worked if I sent a proper HTTP response. If the response does not contain the last 0 length chunk, then DataAvailable is never true. However, I see the data being sent via Wireshark. So, the data has to be there being held somewhere but where and how can I get to it? – larrycook99 Jan 25 '20 at 00:18

1 Answers1

0
await stream.CopyToAsync()

will not complete until

stream.DataAvailable == false

You have indicated to the server, in the headers that you will close the TCP connection when done, but have not done so. The server will eventually close the connection when it thinks you're gone. The server is not obligated to obey your "Connection: close" request and that should be indicated in the headers the server returns.

Before you call stream.CopyToAsync() you should check the headers to determine if what Content-Length has been supplied and pass a buffer length to stream.CopyToAsync() and then call TcpClient.Close()

Rowan Smith
  • 1,815
  • 15
  • 29
  • The code being returned is chunked, so there is no Content-Length. Further, if I check for stream.DataAvailable before the call to stream.CopyToAsync(), the function is never called because DataAvailable is never true. – larrycook99 Jan 25 '20 at 00:14
  • A chunk is preceded by the size of the chunk. But if you're not getting any data then how did the getbyte test you mentioned get data? Can you post a minimal working example? – Rowan Smith Jan 25 '20 at 05:32
  • I was wrong about the example working. When it worked, it was because I was sending a proper HTTP response. See the edited example above. – larrycook99 Jan 25 '20 at 15:18