【问题标题】:Creating HTTP client to download a webpage for offline viewing in C在 C 中创建 HTTP 客户端以下载网页以供离线查看
【发布时间】:2015-02-09 20:42:48
【问题描述】:

我正在创建一个基于命令行参数下载网页的 HTTP 客户端。它接受参数,查找域名以获取 IP 地址,创建套接字,连接到服务器并发送 GET 请求并等待回复。这一切都很好,但是当我使用缓冲区和 while 循环阅读我的回复时,我也收到了一些不可读的字符。如果您运行代码并查看 html,您会在页面上到处看到不可读的字符。

我的代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    int socket_desc, i, bytes_read;    
    char server_reply[1024], ip[100], request[100];;
    char *hostname = argv[1];
    struct sockaddr_in server;
    struct hostent *he;
    struct in_addr **addr_list;
    FILE *fp;

    if ((he = gethostbyname(hostname)) == NULL) {
        //gethostbyname failed
        herror("gethostbyname\n");
        return 1;
    }

    addr_list = (struct in_addr **) he->h_addr_list;

    for(i = 0; addr_list[i] != NULL; i++) {
        //Return the first one;
        strcpy(ip , inet_ntoa(*addr_list[i]) );
    }

    //Create socket
    socket_desc = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_desc == -1) {
        printf("Could not create socket!\n");
    }

    server.sin_addr.s_addr = inet_addr(ip);
    server.sin_family = AF_INET;
    server.sin_port = htons(80);

    //Connect to remote server
    if (connect(socket_desc , (struct sockaddr *)&server , sizeof(server)) < 0) {
        printf("connect error!\n");
        return 1;
    }

    printf("Connected...\n");

    //Send some data
    snprintf(request, 99, "GET / HTTP/1.1\r\n"
            "Host: %s\r\n"
            "\r\n\r\n", hostname
    );

    if (send(socket_desc, request, strlen(request), 0) < 0) {
        puts("Send failed!\n");
        return 1;
    }
    puts("Data Sent...\n");

    //Receive a reply from the server

    fp = fopen("/home/localusr/Desktop/ouput.html", "w+");

    while (bytes_read = read(socket_desc, server_reply, sizeof(server_reply)) > 0) {
        fputs(server_reply, fp);
        memset(server_reply, 0, sizeof(server_reply));
    } 
    do {
        bytes_read = read(socket_desc, server_reply, sizeof(server_reply));
        fputs(server_reply, fp);
        memset(server_reply, 0, sizeof(server_reply));
    } while (bytes_read > 0);

    printf("reply received...\n");

    fclose(fp);
    close(socket_desc);

    return 0;
}

抱歉,缩进不佳的代码。非常感谢任何帮助。我正在使用 Ubuntu 机器并使用 gcc 来编译我的代码。

编辑:

orb.ws.require.lib--> <script type="text/javascript">/*
be2

be2 不应该在那里。 * 也得到'@'符号

【问题讨论】:

  • 请不要在未阅读必要文档的情况下尝试自己执行 HTTP。您在这里做错了几件事,这表明您不知道正确的 HTTP。
  • 我已阅读 RFC 2616,我的 GET 请求符合标准,这没有问题。服务器回复 200 OK 并给我 html。它只是我的缓冲区,不断添加它未读取的字符。您的评论非常无益,实际上表明您对 GET 请求知之甚少。我不能使用 wget,因为我正在尝试用 C 自己制作程序,而没有使用许多可以为我完成它的库。
  • 使用像libcurl这样的HTTP客户端库; HTTP 协议足够复杂;不要花费数月或数年的时间来重新实现它。
  • @WilliamGoodwin:Steffen 的评论很到位。您对 HTTP 的工作原理没有很好的理解,而您的代码反映了这一点。您的请求中有 1 个过多的 CRLF,并且在读取失败之前盲目读取入站数据的读取循环是读取服务器响应的完全错误的方式。你显然错过了RFC 2616 Section 4.4。您必须分析响应标头以了解如何正确读取响应正文。请参阅 stackoverflow.com/a/19211701/65863 了解您必须实施的逻辑示例。

标签: c sockets http buffer webpage


【解决方案1】:

您是否尝试过使用 telnet 访问网页?

请执行以下操作:

telnet [主机名] [端口]

在 telnet shell 中输入:

GET / HTTP/1.1
Host: [hostname]
<return>

(注意Host之后的额外回报!

请同时发布 telnet 的结果和代码的结果

  • 编辑 *

发现问题:

您使用 fputs 而不是 fwrite。 fputs 需要一个字符串,它通过查找 NULL 字符来检测。

但是,在您的情况下,不承诺此类 NULL 字符,因此您必须明确。 作为奖励,您的程序现在终止并将输入刷新到文件。 修复:

用下面的 do while 循环替换你的 while 和 do while 循环:

do
{
    int write;
    bytes_read = read(socket_desc, server_reply, sizeof(server_reply));
    write = fwrite(server_reply, 1, bytes_read, fp);
    printf("Written %d bytes_read: %d\n", write, bytes_read);
    memset(server_reply, 0, sizeof(server_reply));
    fflush(fp);
} while (bytes_read > 0); // This termination is wrong! You should look at Content-Length from the server's reply to detect the actual length

现在可以使用了....

【讨论】:

  • 我返回的 html 很大,但是使用 telnet 我没有得到任何随机字符,而我的缓冲区在 html 中读取得到。我已经编辑了我的问题,以给出一个简短的例子来说明我的缓冲区的 html 是什么样的。
  • 我运行了您的代码,通过添加 fflush 并删除另一个 do { } while 循环对其进行了一些修复。您的程序没有终止,但输出看起来不错...您要访问哪个网站,您得到的输出到底是什么?在这里添加完整的输出(尝试一个内容很少的网站),我会将它与我得到的内容进行比较。 HTML 协议确实有些复杂,但根据您的需要,您做对了。
  • 我尝试了您的代码,它可以编译并运行,但它并没有终止,也没有足够的读入来获取所有的 html。我还没有研究过 fflush,所以我可能会看看 fflush 是如何工作的..
  • 修改了我的答案,看看现在是否完整
  • @Morpfh - 在一般情况下你是正确的,但是,请注意他的 while 循环中的这些行: fputs(server_reply, fp); memset(server_reply, 0, sizeof(server_reply));这意味着整个缓冲区被清零,因此您的描述在这里无效
【解决方案2】:

编辑:在这里发表我的评论:

请注意,例如www.bbc.co.uk 响应标头说“Transfer-Encoding: chunked”,这意味着每个块都有一个十六进制数字表示长度,后跟数据,然后是 \r\n。

也就是说,根据您的示例:

be2\r\n => 0xbe2\r\n => 3042\r\n

“下面是 3042 字节”(在 \r\n aka CRLF 或十六进制 0d0a 之后)。

Example 的块:

e\r\nStack Exchange
|  | ||||||||||||||
|  | +............+
|  |        |
|  |        +-------- 14 bytes
|  +----------------- \r\n
+-------------------- 0x0e == 14 dec in hex

旧:


您可以通过以下方式正确终止读取字节,而不是 memset 等:

while ( (bytes_read = read(socket_desc, server_reply, sizeof(server_reply) - 1)) > 0) {
    server_reply[bytes_read] = 0x00;

在此之后,bytes_read 之外的任何内容都不会被 fputs 编辑。


当您将memset 整个缓冲区写入0读入整个缓冲区时——memset 无效,除非读取小于缓冲区大小。您只需在完全 (1024) 读取和写入 1024 + 垃圾时覆盖所有零,直到第一个零。


read() 返回读取的字节数。通过将server_reply[bytes_read] 设置为0,您实际上终止了实际数据。把它变成一个C字符串。如果不将最后一个字节设置为零,fputs() 将在bytes_read 之后继续输出垃圾,直到第一个零或崩溃。

换一种说法; read() 最多读取 size 字节,不在乎它是否全为零字节。如果您告诉read() 读取 356GiB 的数据,并且文件描述符提供 356GiB 的零(如 0x00 字节,而不是 ASCII 0) - 这就是您得到的。

您的套接字确实以零终止交付。它像您的服务器一样提供零字节作为数据的一部分。假设您传输了零字节的图像或其他一些数据;换句话说:它不是一个以零结尾的字符串read()gets。

还要注意sizeof 后面的- 1 – 为空字节腾出空间。

fputs 然而 写入直到第一个终止空字节,但不将其包含在输出中(如果您正在写入缓冲 string数据)。


示例:

char buf[8];

Char 未初始化并包含垃圾。例如它可能是:

buf[0] == 0x13
buf[1] == 0x0a
buf[2] == 0x00
buf[3] == 0x65
buf[4] == 0x78
buf[5] == 0xf3
buf[6] == 0x00
buf[7] == 0xaf

除了 buf 你还有随机垃圾,例如

buf[7+1] == 0xde
buf[7+2] == 0xa0
buf[7+3] == 0x33
buf[7+3] == 0x00

bytes_read = read(soc, buf, 8); soc 提供:'ABCDEFG'

缓冲区现在是:

buf[0] == 0x41 (A)
buf[1] == 0x42 (B)
buf[2] == 0x43 (C)
buf[3] == 0x44 (D)
buf[4] == 0x45 (E)
buf[5] == 0x46 (F)
buf[6] == 0x47 (G)
buf[7] == 0xaf (H)

但是buf[7]以外的字节仍然充满了垃圾;并且您的 fputs() 将读取数据并将其传递给文件,直到第一个零为止。

这就是为什么你会说:

bytes_read = read(soc, buf, 7);
buf[bytes_read] = 0x00;

现在我们只阅读 A-G。最后一个字节设置为 0。

这里fputs(buf, fh) 会写到第一个\0,换句话说ABCDEFG

如果服务器现在在下一次运行时只提供两个字节:

buf[0] == 0x48 (H)
buf[1] == 0x5A (Z)

那么 bytes_read 将是 2 并且声明:

buf[bytes_read] = 0x00 ===> buf[2] = 0x00

给你

buf[0] == 0x48 (H)
buf[1] == 0x5A (Z)
buf[2] == 0x00 (0x00) <<--- nulled out
                      +---.
buf[3] == 0x44 (D)    |    \
buf[4] == 0x45 (E)    |     \
buf[5] == 0x46 (F)    |      }--->>> garbage from previous read.
buf[6] == 0x47 (G)    |     / 
buf[7] == 0x00 (0x00) |    /
                      +---/

这里fputs(buf, fh) 会写到第一个\0,换句话说HZ

【讨论】:

  • 回复的大小会因请求的网页 html 而异。我目前正在 www.bbc.co.uk 上对其进行测试,但 www.macmillandictionary.com 的 html 少得多。大多数网页虽然有超过 1024 字节的 html,所以这就是为什么我试图清除缓冲区并返回到它已满的位置并读取它。这可行,但每个缓冲区的末尾是随机字符所在的位置放置,我不能只删除缓冲区末尾的一组字符,因为没有统一。当我使用您的代码正确终止 read_bytes 时,我什么也没读。
【解决方案3】:

read() 不会为空终止字节。但是fputs() 依赖于空终止,因此如果要传递给fputs(),则必须在char 数组的末尾附加0x00。

【讨论】:

    猜你喜欢
    • 2011-05-15
    • 1970-01-01
    • 1970-01-01
    • 2013-05-25
    • 1970-01-01
    • 2013-03-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多