【问题标题】:Why does this C# TcpClient code not always see a response?为什么这个 C# TcpClient 代码并不总是看到响应?
【发布时间】:2020-01-25 08:53:56
【问题描述】:

我正在使用以下代码来读取伪 HTTP 请求响应。它有时有效,但并非总是有效,我不明白。

背景:我有一个接收 HTTP GET 请求并发送分块 HTTP 响应的设备。在一种情况下,响应不是正确的分块 HTTP 响应。它省略了指示数据结束的空块。我已经在设备中解决了这个问题,但我试图弄清楚如何读取不一致的 HTTP 响应。我从Create http request using TcpClient 找到了有时有效有时无效的代码,我不明白为什么。

如果我使用未更改的代码,它可以正常工作。例如,如果我通过将“www.bing.com”替换为设备的 IP 来使用它,则两个地方都会出现“192.1.168.89”字符串,并将 GET 命令行更改为“GET /index.htm HTTP/1.1 “,它工作正常。此版本的命令返回一个由设备构建的网页,并发送多个 TCP 缓冲区(在我的设备中约为 1400 字节)的分块数据。

但是,如果我更改为我的设备可以理解的另一个命令“GET /request.htm?T HTTP/1.1”,但返回的分块数据少于 500 字节,那么我永远看不到响应。事实上,它永远不会超过对“CopyToAsync(内存)”的调用,我不明白为什么。设备看到请求,对其进行解析并发送正确的 HTTP 响应。 (我知道这是一个正确的响应,因为我有使用 HTTPClient 读取响应的代码并且它看到响应很好。而且我看到来自设备端的响应数据在两种情况下都是完全相同的。我可以看到设备数据,因为我正在编写设备的固件,并且可以将其更改为 printf() 发送到 TCP 例程的数据。)

任何人都可以解释为什么下面的代码并不总是看到响应?

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;
}

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

让我编辑上面的函数以显示它现在是什么,也许可以澄清一些事情。

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;
            }
        }

GET 命令是我的设备所响应的。如果命令以 'x' 开头,如图所示,那么它会以正确的 HTTP 响应进行响应,并且上面的函数会读取数据。如果它以 'd' 开头,则最后缺少长度为 0 的块,并且上面的函数永远不会看到来自设备的任何数据。

使用 Wireshark,我看到以下对“x”和“d”命令的响应。

'x' 命令返回 2 个 TCP 帧,其中包含以下数据:

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                                          ...

相比之下,'d' 命令以 2 个 TCP 帧返回数据:

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                                       ....

我看到的唯一可辨别的区别是,在“d”命令的第二帧中,它缺少一个 1 字节块,它是我们协议的一部分(并且不应该对 TCP/HTTP 函数有任何影响)以及“x”命令提供的最后 7 个字节的数据,这是 HTTP 预期的 0 长度块。

回到 HttpRequestAsync() 中的代码,如果发送了“d”命令,那么代码永远不会看到 stream.DataAvailable 变为 true,即使数据已经发送。为什么?

【问题讨论】:

  • 我没有看到任何stream.Read() this example
  • @ArthurRey stream.CopyToAsync 是“读取”等效项。前段时间我经历过类似的行为。就我而言,这是由我的 2 台设备(客户端和服务器)之间的速度差异造成的。我发现我可以用 System.Threading.Thread.Sleep(50); 解决它。也许你只是有一个时间错误。您可以尝试在不同的点放置一些“睡眠”。只是为了检查这是否解决了问题。即使您必须寻找一种不太脏的方法。在我的情况下,响应发送得太晚并且响应被拒绝
  • @Sheradil 对高比特率 I/O 做的最后一件事是插入 Sleep(),因为这会导致丢包
  • "...但返回少于 500 字节的分块数据,然后我永远看不到响应..." - 当响应很小时会发生这种情况;您正在使用缓冲 I/O 并且服务器没有Close() 流,系统正在等待更多永远不会到来的字节。让服务器 Close()Flush() 流,以便您读取的代码更快地得到响应
  • @MickyD 设备正在关闭连接。

标签: c# httpclient tcpclient


【解决方案1】:
await stream.CopyToAsync()

直到

stream.DataAvailable == false

您已在标头中向服务器表明您将在完成后关闭 TCP 连接,但尚未这样做。当服务器认为你离开时,它最终会关闭连接。服务器没有义务遵守您的“连接:关闭”请求,这应该在服务器返回的标头中指明。

在调用stream.CopyToAsync() 之前,您应该检查标头以确定是否提供了Content-Length,并将缓冲区长度传递给stream.CopyToAsync(),然后调用TcpClient.Close()

【讨论】:

  • 返回的代码是分块的,所以没有Content-Length。此外,如果我在调用 stream.CopyToAsync() 之前检查 stream.DataAvailable,则永远不会调用该函数,因为 DataAvailable 永远不会为真。
  • 一个块的前面是块的大小。但是,如果您没有获得任何数据,那么您提到的 getbyte 测试是如何获得数据的?你能发布一个最小的工作示例吗?
  • 我对示例的工作有误。当它起作用时,是因为我发送了正确的 HTTP 响应。请参阅上面的编辑示例。
猜你喜欢
  • 2021-08-17
  • 1970-01-01
  • 1970-01-01
  • 2023-03-28
  • 1970-01-01
  • 1970-01-01
  • 2020-04-29
  • 1970-01-01
  • 2017-10-16
相关资源
最近更新 更多