使用TcpClient在C#中创建的非常奇怪的HTTP客户端

8

我正在实现一个简单的HTTP客户端,它只是连接到Web服务器并获取其默认主页。这是它的代码,它可以很好地工作:

using System;
using System.Net.Sockets;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            TcpClient tc = new TcpClient();
            tc.Connect("www.google.com", 80);

            using (NetworkStream ns = tc.GetStream())
            {
                System.IO.StreamWriter sw = new System.IO.StreamWriter(ns);
                System.IO.StreamReader sr = new System.IO.StreamReader(ns);

                string req = "";
                req += "GET / HTTP/1.0\r\n";
                req += "Host: www.google.com\r\n";
                req += "\r\n";

                sw.Write(req);
                sw.Flush();

                Console.WriteLine("[reading...]");
                Console.WriteLine(sr.ReadToEnd());
            }
            tc.Close();
            Console.WriteLine("[done!]");
            Console.ReadKey();
        }
    }
}

当我从上面的代码中删除下面这行时,程序会在sr.ReadToEnd处阻塞。
req += "Host: www.google.com\r\n";

我甚至用 sr.Read 替换了 sr.ReadToEnd,但仍然无法读取任何内容。我使用 Wireshark 查看发生了什么: 使用 Wireshark 捕获数据包的屏幕截图 http://www.imagechicken.com/uploads/1252514718052893500.jpg 如您所见,在我的 GET 请求之后,Google 没有响应,请求一次又一次地重新发送。看来我们必须在 HTTP 请求中指定Host 部分。奇怪的是我们不必这样做。我使用 telnet 发送了这个请求并从 Google 得到了响应。我还捕获了由 telnet 发送的请求,它和我的请求完全相同。
我尝试了许多其他网站(例如 Yahoo、Microsoft),但结果都是一样的。
那么,telnet 中的延迟是否会导致 Web 服务器有所不同(因为在 telnet 中我们实际上是一个一个字符地输入,而不是将它们一起发送在一个数据包中)?
另一个奇怪的问题是,当我将 HTTP/1.0 改为 HTTP/1.1 时,程序总是在 sr.ReadToEnd 行上阻塞。我猜这是因为 Web 服务器没有关闭连接。

一种解决方案是使用 Read(或 ReadLine)和 ns.DataAvailable 读取响应。但我无法确定是否已经读取了所有响应内容。如何读取响应并确保 HTTP/1.1 请求中没有剩余的字节?


注意:如W3所述,

Host 请求头字段必须附带所有的HTTP/1.1请求。

(我在我的 HTTP/1.1 请求中已经这样做了)。但是我没有看到类似的要求适用于HTTP/1.0。此外,使用 telnet 发送没有Host头的请求也可以正常工作。


更新:

TCP段中的Push标志已设置为1。我还尝试了netsh winsock reset来重置我的TCP/IP堆栈。测试计算机上没有防火墙或反病毒软件。实际上,数据包已发送,因为另一台计算机上安装了Wireshark可以捕获它。

我也尝试了其他请求。例如:

string req = "";
req += "GET / HTTP/1.0\r\n";
req += "s df slkjfd sdf/ s/fd \\sdf/\\\\dsfdsf \r\n";
req += "qwretyuiopasdfghjkl\r\n";
req += "Host: www.google.com\r\n";
req += "\r\n";

在所有类型的请求中,如果我省略了 Host: 部分,Web 服务器不会响应,而如果有了 Host: 部分,即使是无效的请求(就像上面的请求一样),也会得到响应(通过 400:HTTP 错误请求)。 nos 表示在他的机器上不需要 Host: 部分,这让情况更加奇怪。

我不确定这是否是问题,但你应该在HTTP响应中使用内容长度来确定应该读取多少字节,然后从响应体中读取这么多字节,这样做是否更合适? - Aziz
@Aziz。也许这是一个好的解决方案,而不是使用ReadToEnd。但在问题的第一部分中,我没有从服务器接收到任何东西(甚至一个字节)。 - Isaac
该代码在这里使用 Host 标头与否都可以正常工作。GET 请求的 TCP 段设置 PUSH 位吗?如果没有设置,可能会导致重传,虽然你无法对此做太多事情。 - nos
@nos - 谢谢你,nos。我添加了一些关于你提示的细节。 - Isaac
1
@Aziz - 需要记住的一点是,并非所有 HTTP 1.1 中的 HTTP 响应都使用“Content-Length”头。有些响应使用“Transfer-Encoding: chunked”头,需要完全不同的读取模型。 - Remy Lebeau
伙计们,Telnet 绝对不会发送 \r\n 而不是 \n 到主机。 - Pavel Radzivilovsky
5个回答

3

这与使用TcpClient有关。

我知道这篇文章很旧了。我提供这些信息只是为了万一其他人遇到同样的问题。将这个答案视为所有以上答案的补充。

一些服务器需要HTTP主机头,因为它们被设置为在一个IP地址上托管多个域名。作为一般规则,总是发送主机头。一个好的服务器将回复“未找到”。有些服务器根本不会回复。

当从流中读取数据的调用被阻塞时,通常是因为服务器正在等待发送更多的数据。当HTTP 1.1规范未严格遵循时,通常会出现这种情况。为了证明这一点,请尝试省略最后的CR LF序列,然后从流中读取数据——调用读取将等待,直到客户端超时或服务器通过终止连接放弃等待为止。

希望这能让您有所启发...


2
我在所有问题中找到了一个:
我如何读取响应并确保我读取了HTTP/1.1请求中的全部响应?
那是一个我可以回答的问题!
你目前使用的所有方法都是同步的,这很容易使用,但并不可靠。一旦你有一个相当大的响应并只获得其中的一部分,你就会遇到问题。
为了最可靠地实现TcpClient连接,你应该使用所有异步方法和回调函数。相关的方法如下:
1)通过TcpClient.BeginConnect(...)创建连接,回调调用TcpClient.EndConnect(...)
2)使用TcpClient.GetStream()。BeginWrite(...)发送请求,回调调用TcpClient.GetStream()。EndWrite(...)
3)使用TcpClient.GetStream()。BeginRead(...)接收响应,回调调用TcpClient.GetStream()。EndRead(...),将结果附加到StringBuilder缓冲区,然后再次调用TcpClient.GetStream()。BeginRead(...)(具有相同的回调),直到收到0字节的响应。
正是这个最终步骤(反复调用BeginRead直到读到0字节)解决了获取响应、整个响应以及仅响应的问题。所以帮助我们TCP。
希望这能帮到你!

0

我相信ReadToEnd会等待连接关闭。但它似乎没有关闭。你应该持续读取它。然后它将按照你的期望工作。

//Console.WriteLine(sr.ReadToEnd());
var bufout = new byte[1024];
int readlen=0;
do
{
    readlen = ns.Read(bufout, 0, bufout.Length);
    Console.Write(System.Text.Encoding.UTF8.GetString(bufout, 0, readlen));
} while (readlen != 0);

0

我建议您在本地机器上安装标准、经过充分测试和广泛接受的Web服务器,例如Apache HTTPD或IIS,并针对其进行代码测试。

配置您的Web服务器以响应无Host头(例如IIS中的默认Web应用程序),然后查看是否一切正常。

归根结底,您无法真正了解幕后发生的事情,因为您无法控制像Google、Yahoo等网站/ Web应用程序。
例如,网站管理员可以配置该站点,使得在使用HTTP协议的TCP端口80上没有传入TCP连接的默认应用程序。
但是,他/她可能希望在使用TELNET协议的TCP端口23连接时配置默认的telnet应用程序。


-2

尝试直接使用System.Net.Sockets.TcpClient而不是System.Net.WebClient:

using System;
using System.Net;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            WebClient wc = new WebClient();
            Console.WriteLine("[requesting...]");
            Console.WriteLine(wc.DownloadString("http://www.google.com"));
            Console.WriteLine("[done!]");
            Console.ReadKey();
        }
    }
}

1
@Remy Lebeau - 谢谢但我必须使用TcpClient因为我想在更底层上完成这个任务。 - Isaac
@Remy Lebeau - 所以这不是对问题的回答,只会分散其他人的注意力,因为他们认为“他有一个答案”:/ - Isaac
3
如果你一定要使用TcpClient,那么你真的需要阅读实际的HTTP规范。你原来的读取代码在许多情况下都不能正常工作,因为像Aziz之前所说的,ReadToEnd() 是处理它们的错误方式。 - Remy Lebeau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接