下面我将为你提供一个详细的、可运行的 C 语言示例,并分步解释其中的关键点。

linux socket 获取网页源码
(图片来源网络,侵删)

核心原理

要获取网页源码,你需要执行以下步骤:

  1. 创建 Socket:使用 socket() 函数创建一个套接字,这是网络通信的端点。
  2. 建立连接:使用 connect() 函数将你的客户端 Socket 连接到目标 Web 服务器的 Socket(通常是 80 端口,用于 HTTP)。
  3. 发送 HTTP 请求:通过 send() 函数,向服务器发送一个符合 HTTP 协议的 GET 请求,这个请求包含了你想要获取的网页路径(如 /index.html)和主机名。
  4. 接收 HTTP 响应:使用 recv() 函数循环从服务器接收数据,服务器会返回一个 HTTP 响应,其中包含响应头和响应体(即网页源码)。
  5. 解析和提取:你需要对接收到的数据进行解析,找到 HTTP 响应头和响应体之间的分隔符(通常是 \r\n\r\n),然后提取出响应体部分。
  6. 关闭连接:使用 close() 函数关闭 Socket,释放资源。

完整 C 语言代码示例

这个示例会获取 example.com 首页的源码并打印到终端。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#define PORT 80
#define BUFFER_SIZE 4096
void error(const char *msg) {
    perror(msg);
    exit(1);
}
int main(int argc, char *argv[]) {
    int sockfd;
    struct sockaddr_in serv_addr;
    struct hostent *server;
    char buffer[BUFFER_SIZE];
    char request[BUFFER_SIZE];
    // 检查命令行参数
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <hostname> [path]\n", argv[0]);
        fprintf(stderr, "Example: %s example.com /index.html\n", argv[0]);
        exit(1);
    }
    const char *hostname = argv[1];
    const char *path = (argc > 2) ? argv[2] : "/"; // 默认路径为根目录 /
    // 1. 创建 Socket
    // AF_INET 表示 IPv4
    // SOCK_STREAM 表示 TCP
    // 0 表示使用默认协议 (TCP)
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        error("ERROR opening socket");
    }
    // 2. 获取服务器信息
    server = gethostbyname(hostname);
    if (server == NULL) {
        fprintf(stderr, "ERROR, no such host as %s\n", hostname);
        exit(1);
    }
    // 3. 设置服务器地址结构
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    memcpy(&serv_addr.sin_addr.s_addr, server->h_addr, server->h_length);
    serv_addr.sin_port = htons(PORT); // 将端口号从主机字节序转换为网络字节序
    // 4. 连接到服务器
    if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        error("ERROR connecting");
    }
    printf("Connected to %s on port %d\n", hostname, PORT);
    // 5. 构造 HTTP GET 请求
    // 请求格式: GET /path HTTP/1.0\r\nHost: hostname\r\n\r\n
    snprintf(request, BUFFER_SIZE, "GET %s HTTP/1.0\r\nHost: %s\r\n\r\n", path, hostname);
    printf("\n--- Sending HTTP Request ---\n%s\n---------------------------\n", request);
    // 6. 发送 HTTP 请求
    if (send(sockfd, request, strlen(request), 0) < 0) {
        error("ERROR writing to socket");
    }
    // 7. 接收 HTTP 响应
    printf("\n--- Receiving HTTP Response ---\n");
    int total_received = 0;
    int header_end_found = 0;
    char *body_start = NULL;
    while (1) {
        int n = recv(sockfd, buffer, BUFFER_SIZE - 1, 0);
        if (n <= 0) {
            break; // 连接关闭或出错
        }
        buffer[n] = '\0'; // 确保字符串以 null 
        // 8. 解析响应,找到响应体的开始位置
        if (!header_end_found) {
            body_start = strstr(buffer, "\r\n\r\n");
            if (body_start) {
                header_end_found = 1;
                // 打印响应头
                long header_length = body_start - buffer;
                fwrite(buffer, 1, header_length, stdout);
                printf("\n--- HTTP Response Body ---\n");
                // 打印响应体
                body_start += 4; // 跳过 "\r\n\r\n"
                printf("%s", body_start);
            } else {
                // 还没找到头尾,继续打印缓冲区内容(可能是响应头的一部分)
                printf("%s", buffer);
            }
        } else {
            // 已经找到响应头,直接打印响应体
            printf("%s", buffer);
        }
        total_received += n;
    }
    printf("\n--- End of Response ---\n");
    printf("Total bytes received: %d\n", total_received);
    // 9. 关闭连接
    close(sockfd);
    return 0;
}

如何编译和运行

  1. 保存代码:将上面的代码保存为 get_webpage.c

  2. 编译:打开终端,使用 gcc 进行编译。

    linux socket 获取网页源码
    (图片来源网络,侵删)
    gcc get_webpage.c -o get_webpage
  3. 运行:执行编译后的程序,并传入目标主机名和可选的路径。

    # 获取 example.com 首页
    ./get_webpage example.com
    # 获取 example.com 的某个特定页面
    ./get_webpage example.com /index.html

代码分步详解

  1. 包含头文件

    • sys/socket.h:核心 Socket 函数(socket, connect, send, recv, close)。
    • netinet/in.h:定义了地址结构 sockaddr_in 和字节序转换函数 htons
    • netdb.h:提供了主机信息查询函数 gethostbyname
    • arpa/inet.h:提供了 IP 地址转换函数。
  2. socket()

    • AF_INET:指定使用 IPv4 地址族。
    • SOCK_STREAM:指定使用面向连接的 TCP 协议。
    • 返回一个文件描述符 sockfd,后续所有操作都通过这个描述符进行。
  3. gethostbyname()

    linux socket 获取网页源码
    (图片来源网络,侵删)
    • 这个函数将主机名(如 "example.com")解析为 struct hostent 结构体,其中包含了服务器的 IP 地址信息。
  4. sockaddr_in 结构体

    • sin_family:必须设置为 AF_INET
    • sin_port:端口号。htons() 函数将主机字节序(你的电脑上的顺序)转换为网络字节序(大端序),这是网络通信的标准。
    • sin_addr:服务器的 IP 地址,我们使用 memcpygethostbyname 得到的地址复制过来。
  5. connect()

    尝试与服务器建立 TCP 连接,如果成功,就可以开始双向数据传输了。

  6. 构造 HTTP 请求

    • snprintf 用于格式化字符串,创建一个合法的 HTTP GET 请求。
    • GET / HTTP/1.0\r\n:请求方法、路径和 HTTP 版本。HTTP/1.0 是一个较简单的版本,连接在请求响应后会自动关闭。
    • Host: example.com\r\n这是必须的,HTTP/1.1 协议要求,用于虚拟主机场景,告诉服务器你要访问的是哪个域名。
    • \r\n\r\n:一个空行,标志着 HTTP 请求头的结束。
  7. send()recv()

    • send() 将构造好的 HTTP 请求发送到服务器。
    • recv() 是一个阻塞函数,它会等待接收数据,我们用一个 while 循环来持续接收,直到服务器关闭连接(recv 返回 0)或发生错误(返回 -1)。
  8. 解析响应

    • HTTP 响应由 响应头响应体 组成,它们之间用 \r\n\r\n 分隔。
    • strstr(buffer, "\r\n\r\n") 函数用于在接收到的数据块中查找这个分隔符。
    • 第一次找到分隔符时
      • 打印分隔符之前的所有内容(即响应头)。
      • 计算响应体的起始位置。
    • 之后:直接打印缓冲区中的所有内容,因为它们都属于响应体。
  9. close()

    关闭 Socket,释放系统资源,这是一个良好的编程习惯。

重要注意事项

  • HTTP/1.1 vs HTTP/1.0:示例中使用的是 HTTP/1.0,因为它在响应结束后会自动关闭连接,简化了客户端的逻辑,如果使用 HTTP/1.1,服务器可能会使用 "Keep-Alive" 机制保持连接,你需要额外处理连接的关闭逻辑(根据 Content-Length 判断响应是否接收完整,或者等待服务器主动关闭连接)。
  • 错误处理:代码中使用了简单的 perrorexit 来处理错误,在实际的生产环境中,你需要更健壮的错误处理和恢复机制。
  • HTTPS:这个例子只能获取 HTTP 协议的内容,如果要获取 HTTPS (加密) 内容,过程会复杂得多,需要实现 SSL/TLS 握手过程,通常使用 OpenSSL 这样的库来完成。
  • 缓冲区大小BUFFER_SIZE 定义了每次接收的最大字节数,如果网页很大,recv 会被多次调用,循环处理是必要的。