推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接

HTTPS 与 HTTP 的区别

我在前一篇 文章 中提到过当我们使用 HTTP 协议去访问 “https://” 加密的网页时,它是会失败的。比如,现代的银行业网页、各种的机关单位网上的服务厅,都是要采用 HTTPS 加密协议,而 HTTP 是明文传输的。我们所写的 HTTP 请求代码访问这些网页肯定是失败的。那么,HTTPS 与 HTTP 的区别是什么呢?

HTTPS(Hypertext Transfer Protocol Secure)是 HTTP(Hypertext Transfer Protocol)的安全版本,它通过在 HTTP 基础上添加 SSL/TLS(Secure Sockets Layer/Transport Layer Security)加密层来保护网络通信的安全性。以下是使用 HTTPS 的原因以及它与 HTTP 的主要区别:

使用 HTTPS 的原因

  • 数据加密
    • 保护隐私:HTTPS 使用 SSL/TLS 加密技术对传输中的数据进行加密,确保数据在客户端和服务器传输之间时不会被窃听或篡改。例如,用户在网站上输入的密码、信用卡信息等敏感数据在传输过程中会被加密,从而防止中间人攻击。
    • 防止数据泄露:加密可以防止网络中的恶意设备(如路由器、代理服务器等)窃取或篡改数据。
  • 身份验证
    • 证书验证:HTTPS 使用数字证书来验证服务器的身份。当客户端与服务器建立连接时,服务器会提供一个由受信任的证书颁发机构(CA)签发的数字证书。客户端可以通过验证证书的有效性来确认服务器的身份,从而防止中间人攻击。
    • 防止钓鱼网站:通过证书验证,用户可以更放心地访问网站,避免被钓鱼网站欺骗。
  • 完整性保护
    • 防止数据篡改:SSL/TLS 协议不仅加密数据,还提供数据完整性保护。通过使用哈希算法和数字签名,可以确保数据在传输过程中未被篡改。
  • 搜索引擎优化(SEO)
    • 搜索排名优势:许多搜索引擎(如 Google)会优先推荐使用 HTTPS 的网站,因为它们被认为是更安全的。使用 HTTPS 可以提高网站在搜索引擎结果页面(SERP)中的排名。
  • 遵守法规和行业标准
    • 数据保护法规:许多国家和地区的数据保护法规(如欧盟的 GDPR)要求网站在处理用户数据时必须采取适当的安全措施。使用 HTTPS 是满足这些法规要求的一种有效方式。
    • 行业最佳实践:在金融、医疗、电子商务等行业,使用 HTTPS 是一种标准做法,以保护用户的隐私和数据安全。

HTTPS 与 HTTP 的主要区别

  • 协议层
    • HTTP:工作在应用层,基于 TCP 协议进行数据传输。
    • HTTPS:在 HTTP 的基础上增加了 SSL/TLS 层,用于加密和保护数据传输。
  • 端口号
    • HTTP:默认使用端口号 80。
    • HTTPS:默认使用端口号 443。
  • 数据传输安全性
    • HTTP:数据以明文形式传输,容易被窃听和篡改。
    • HTTPS:数据通过 SSL/TLS 加密传输,确保数据的保密性、完整性和身份验证。
  • 证书
    • HTTP:不需要证书。
    • HTTPS:需要一个由受信任的证书颁发机构(CA)签发的 SSL/TLS 证书。证书用于验证服务器的身份,并建立加密连接。
  • 性能
    • HTTP:通常比 HTTPS 稍快,因为没有加密和解密的开销。
    • HTTPS:由于加密和解密操作,可能会比 HTTP 稍慢。但随着硬件性能的提升和 SSL/TLS 协议的优化,这种性能差距已经非常小。
  • 浏览器支持
    • HTTP:所有浏览器都支持。
    • HTTPS:现代浏览器都支持 HTTPS,并且会优先推荐使用 HTTPS 的网站。如果网站仅支持 HTTP,浏览器可能会显示安全警告。

假设你访问一个在线银行网站:

  • 使用 HTTP 的情况:你的用户名、密码和账户信息在传输过程中以明文形式发送,容易被网络中的恶意设备窃取。
  • 使用 HTTPS 的情况:你的数据会被加密,即使数据被截获,攻击者也无法解密,从而保护了你的隐私和资金安全。

HTTPS 的 SSL/TLS 加密技术

SSL (Secure Sockets Layer): 最初的加密协议,由 Netscape 开发。现已过时,存在已知漏洞(如 POODLE, DROWN),不应再使用。

TLS (Transport Layer Security): SSL 的继任者和现代化版本。目前广泛使用的是 TLS 1.2 和 TLS 1.3(最新、最安全、最快速)。当我们说“SSL/TLS”时,通常指的就是 TLS。

我们专注于 TLS 加密协议。加密协议 TLS 工作原理可以理解为 “握手”。当客户端(浏览器)首次连接到 HTTPS 网站时,会进行一个复杂的协商过程,称为 TLS 握手。以下是简化步骤(以 TLS 1.2 为例,TLS 1.3 更高效):

  • Client Hello: 客户端向服务器发送支持的 TLS 版本、支持的加密套件列表(包含加密算法、密钥交换算法、哈希算法)、一个随机数。

  • Server Hello: 服务器选择双方都支持的最高 TLS 版本和最强加密套件,发送自己的随机数,并发送其数字证书。

  • 证书验证: 客户端验证服务器证书

    • 证书是否由受信任的证书颁发机构签发?
    • 证书是否在有效期内?
    • 证书中的域名是否与正在访问的网站域名匹配?
    • 证书是否被吊销?(通过 OCSP 或 CRL 检查)验证失败会触发警告(如浏览器显示红色警告页)。
  • 密钥交换:

    • 客户端生成一个 Pre-Master Secret。
    • 客户端用服务器证书中的公钥加密这个 Pre-Master Secret,发送给服务器。
    • 只有拥有对应私钥的服务器才能解密 Pre-Master Secret。
    • (在 TLS 1.3 和某些现代套件中,使用更安全的 Diffie-Hellman 密钥交换,无需传输 Pre-Master Secret)
  • 生成会话密钥: 客户端和服务器使用 Client Random、Server Random 和 Pre-Master Secret (或 DH 交换结果),通过相同的算法独立计算出相同的主密钥。主密钥再派生出用于实际数据传输的会话密钥(对称密钥)。

  • 完成握手: 双方交换加密的“Finished”消息,验证握手过程是否成功且未被篡改。

业务拆解

我们可以参考这篇 文章 的思路。

我们可以先进行普通的 HTTP 协议的客户端请求;然后检查返回的响应是否包含重定向的要求;如果不是则结束执行;如果是,则转换 HTTP 协议为 HTTPS 协议,实行 TLS 加密,完成客户端与服务端的握手,而后才开始发送请求报文。

SSL/TLS 是一个位于传输层和应用层之间的安全协议。它为应用层协议提供了透明的安全保护,使得应用层协议可以专注于业务逻辑,而无需直接处理加密和身份验证的细节。通过 SSL/TLS,应用层协议可以安全地传输数据,同时保持与底层传输层协议的兼容性。
在这里插入图片描述
SSL/TLS 在网络协议层级中的位置可以直观为以下关系。

Created with Raphaël 2.3.0 应用层(如 HTTP、HTTPS、SMTP、FTP 等) SSL/TLS 层(提供加密、身份验证和数据完整性保护) 传输层(TCP 或 UDP) 网络层(IP) 链路层(以太网等) 物理层

实现这个请求过程,我们可以采用递归函数的思想,实现 “HTTPS 递归请求” 以克服重定向问题。我们需在检查重定向之后,放置判断是否递归执行函数的语句。由于我们只关心最后那次的报文返回情况,在递归展开之后的最内层返回响应报文后,递归回溯的每一层都返回这个响应报文。检查重定向,应该放在该递归函数的最后。

Created with Raphaël 2.3.0 发起 HTTP 请求 接受响应报文 检查响应报文是否有重定向要求 提取重定向域名 与新域名进行 SSL/TLS 握手 发起 HTTPS 请求 接受最终响应报文 yes no

C 代码实现

读者可按照上面的流程图来思考以下给出的代码。

准备工作

准备头文件

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <errno.h>
#include <netdb.h>
#include <fcntl.h>

// 添加OpenSSL支持
#include <openssl/ssl.h>
#include <openssl/err.h>

准备宏

#define HTTP_VERSION     "HTTP/1.1"        //   一开始采用 HTTP 协议访问
#define CONNECTION_TYPE "Connection: close\r\n"        //   成功执行 HTTP 事务后
#define USER_AGENT      "User-Agent: Mozilla/5.0 (X11; Linux x86_64)\r\n"       //  Linux 系统终端采用的浏览器代理
#define ACCEPT_HEADER   "Accept: */*\r\n"  //   接受所有资源
#define BUFFER_SIZE     (4096)               //   缓冲区 4 KB
#define MAX_REDIRECTS   (5)                  //   最多重定向 5 次

实现网络连接

DNS 域名查询,函数 gethostbyname 来自头文件 <netdb.h>

static char *host_to_ip(const char *hostname) {            // 从网络端解析域名为字符串的点分十进制 IP 地址
    struct hostent *host_entry = gethostbyname(hostname);
    if (host_entry) {
        return inet_ntoa(*(struct in_addr*)*host_entry->h_addr_list);
    }
    return NULL;
}

针对该 DNS 查询函数所获取的 IP 地址创建网络 I/O 套接字 sockfd,端口可选 80 表示 HTTP 协议,443 表示 HTTPS 协议的监听端口。

// 创建套接字(支持指定端口)
static int create_socket(char *ip, int port) {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        return -1;
    }

    struct sockaddr_in sin = {0};
    sin.sin_family = AF_INET;
    sin.sin_port = htons(port);
    if (inet_pton(AF_INET, ip, &sin.sin_addr) <= 0) {   //  点分十进制改成 16 进制的 IP
        perror("Invalid address");
        close(sockfd);
        return -1;
    }

    if (0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in))) {
        perror("connection failed");
        close(sockfd);
        return -1;
    }

    // 设置为非阻塞模式
    fcntl(sockfd, F_SETFL, O_NONBLOCK);
    return sockfd;
}

重定向的处理代码

检测重定向

一份响应报文的状态码部分是可以告诉我们是否需要重定向。

// 获取HTTP状态码
int get_status_code(const char *response) {
    int status_code = 0;
    char status_line[128] = {0};
    sscanf(response, "HTTP/%*[^ ] %d", &status_code);   //  从字符串 response 中提取 HTTP 状态码
    return status_code;
}

提取重定向的 URL

重定向的响应报文的 Header 有一个字段是 “Location”,那个就是重定向的 URL.

// 从响应头中提取Location
char *get_location(const char *response) {
    const char *location_start = strstr(response, "Location: ");
    if (!location_start) return NULL;
    
    location_start += 10; // 跳过"Location: "
    const char *location_end = strstr(location_start, "\r\n");
    if (!location_end) return NULL;
    
    size_t len = location_end - location_start;
    char *location = malloc(len + 1);
    strncpy(location, location_start, len);
    location[len] = '\0';
    
    return location;
}

解析 URL

本函数的作用就是把 URL 三分开来,协议 + 域名 + 资源路径。这三个字段都会汇集到下文的集成函数 https_send_request 函数中。

// 解析URL,提取协议、主机、端口和路径
static void parse_url(const char *url, char **protocol, char **host, char **path, int *port) {
    char *url_copy = strdup(url);     // 它会————分配一块动态内存————来存储字符串的副本,并返回指向这块内存的指针。
    char *p = url_copy;
    
    // 提取协议
    *protocol = "http";
    if (strstr(url, "https://") == url) {
        *protocol = "https";
        p += 8;
        *port = 443;
    } else if (strstr(url, "http://") == url) {
        p += 7;
        *port = 80;
    }
    
    // 提取主机和路径
    *host = p;
    char *slash = strchr(p, '/');
    if (slash) {
        *slash = '\0'; // 分隔主机和路径
        *path = slash + 1;     // 相对路径,或者资源
    } else {
        *path = "/";
    }
    
    // 检查是否包含端口号
    char *colon = strchr(*host, ':');
    if (colon) {
        *colon = '\0';
        *port = atoi(colon + 1);    // atoi 是 C 语言标准库中的一个函数,用于将字符串转换为意思整数。它的全称是 "ASCII to Integer",即“将 ASCII 字符串转换为整数”。
    }
    
    // 创建副本并释放临时内存
    // strdup 是一个在 C 语言中常用的函数,用于复制一个字符串。它的名字来源于 "string duplicate",即“字符串复制”。
    // 它会————分配一块动态内存————来存储字符串的副本,并返回指向这块内存的指针。
    *host = strdup(*host);
    *path = strdup(*path);
    free(url_copy);
}

执行客户端和服务器的 TLS 握手

初始化本机的证书链、加密方式等等复杂的设定,并加载到结构体 SSL_CTX 之中。这是一站式处理的结构体,作为用户不需要一个一个的加载这些复杂的东西。这是执行 TLS 握手过程所必须的变量类型。

// 全局SSL上下文
SSL_CTX *ssl_ctx = NULL;

// 初始化OpenSSL
static void init_openssl() {
    SSL_library_init();                             // 1. 初始化 OpenSSL 库
    SSL_load_error_strings();                       // 2. 加载 SSL 错误信息
    OpenSSL_add_all_algorithms();                   // 3. 加载所有加密算法
    ssl_ctx = SSL_CTX_new(TLS_client_method());     // 4. 创建客户端 SSL 上下文
    if (!ssl_ctx) {                                 // 5. 错误处理
        perror("Unable to create SSL context");
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }

    // 设置协议版本 - 支持 TLS 1.2 及以上
    SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_2_VERSION);
    
    // 启用服务器名称指示 (SNI)
    SSL_CTX_set_options(ssl_ctx, SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);   //  按位与运算
    
    // 设置验证模式 - 验证服务器证书
    SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, NULL);
    
    // 加载信任的CA证书 - 使用系统默认CA证书
    if (!SSL_CTX_set_default_verify_paths(ssl_ctx)) {
        fprintf(stderr, "Failed to set default CA verify paths\n");
        ERR_print_errors_fp(stderr);
    }
}

// 清理OpenSSL资源
static void cleanup_openssl() {
    if (ssl_ctx) {
        SSL_CTX_free(ssl_ctx);
        ssl_ctx = NULL;
    }
    EVP_cleanup();
}

SSL_connect(ssl) 是头文件 <openssl/ssl.h> 集成的握手函数,SSL 对象是一个会话对象,我们将会在下文的集成函数 https_send_request 函数中说明 SSL 对象是会关联前面所创建的网络套接字 sockfd,也就知道了连接信息了。更有意思的是该函数还为 SSL_connect(ssl) 添加超时处理,即没有立即收到握手过程完成的消息也没关系,我在这里非阻塞地等待 2 秒钟,这就是 select 事件通知机制。

// 执行SSL握手
static int perform_ssl_handshake(SSL *ssl, int sockfd) {
    //  SSL *ssl:OpenSSL 的 SSL 会话对象
    //  int sockfd:底层 TCP 套接字文件描述符

    int ssl_ret;                //  存储 SSL_connect() 的返回值
    int retry_count = 0;        //  当前重试次数计数器
    const int max_retries = 3;  //  最多三次重试
    
    while ((ssl_ret = SSL_connect(ssl)) != 1) {     //  SSL_connect(ssl) 返回值不等于 1(握手未完成)
        int ssl_err = SSL_get_error(ssl, ssl_ret);  //  SSL_get_error() 解析具体错误代码
        
        switch (ssl_err) {
            //  以下两个错误都执行同样的代码
            case SSL_ERROR_WANT_READ:
            case SSL_ERROR_WANT_WRITE:
                // 等待套接字可读/可写
                fd_set fdset;
                FD_ZERO(&fdset);
                FD_SET(sockfd, &fdset);
                
                struct timeval tv;
                tv.tv_sec = 2;
                tv.tv_usec = 0;
                
                int select_ret = select(sockfd + 1, 
                                       (ssl_err == SSL_ERROR_WANT_READ) ? &fdset : NULL,
                                       (ssl_err == SSL_ERROR_WANT_WRITE) ? &fdset : NULL,
                                       NULL, &tv);
                //  这是分支填写,确定自己对哪个 I/O 感兴趣而后监听,没错误就是成功握手,有错误就是超时了,现在挂起线程等他们至多 2 秒的响应
                  
                if (select_ret <= 0) {
                    fprintf(stderr, "SSL handshake timeout or error\n");
                    return 0;
                }
                break;
                
            case SSL_ERROR_SYSCALL:
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    //  EWOULDBLOCK 是操作阻塞, EAGAIN 表示重试资源
                    if (++retry_count > max_retries) {
                        fprintf(stderr, "SSL handshake max retries exceeded\n");
                        return 0;
                    }
                    usleep(100000); // 100ms
                    break;
                }
                // 其他错误直接退出
                return 0;
                
            default:
                fprintf(stderr, "SSL handshake failed with error %d\n", ssl_err);
                ERR_print_errors_fp(stderr);
                return 0;
        }
    }
    return 1;       //  握手完成,密钥互通
}

集成的 HTTPS 请求函数 https_send_request

该函数是一个递归查询函数,流程图如下(就是前文的那张图,我只是把它搬下来而已),返回的是 “最终响应报文”

Created with Raphaël 2.3.0 发起 HTTP 请求 接受响应报文 检查响应报文是否有重定向要求 提取重定向域名 与新域名进行 SSL/TLS 握手 发起 HTTPS 请求 接受最终响应报文 yes no
// 发送HTTP请求(支持重定向和HTTPS)
char *http_send_request(const char *url, int redirect_count) {
    if (redirect_count > MAX_REDIRECTS) {
        fprintf(stderr, "Too many redirects (> %d)\n", MAX_REDIRECTS);
        return NULL;
    }

    char *protocol, *host, *path;       // HTTP/HTTPS协议,域名,资源路径
    int port = 80;                      // 首先尝试用 HTTP 协议,对应端口是 80,之后有可能变为 443
    parse_url(url, &protocol, &host, &path, &port);
    
    if (!host || !path) {               // 检查内存
        fprintf(stderr, "Invalid URL: %s\n", url);
        free(host);
        free(path);
        return NULL;
    }

    char *ip = host_to_ip(host);
    if (!ip) {
        fprintf(stderr, "DNS resolution failed for %s\n", host);
        free(host);
        free(path);
        return NULL;
    }

    int sockfd = create_socket(ip, port);
    if (sockfd < 0) {
        free(host);
        free(path);
        
        return NULL;
    }

    SSL *ssl = NULL;        //  建立连接
    //  https 的分支,一旦重定向出现 https 的处理思路
    if (strcmp(protocol, "https") == 0) {   
        if (!ssl_ctx) {
            fprintf(stderr, "SSL context not initialized\n");
            close(sockfd);
            free(host);
            free(path);
            
            return NULL;
        }
        
        ssl = SSL_new(ssl_ctx);             //  创建新连接对象(基于上下文)
        if (!ssl) {
            fprintf(stderr, "SSL_new failed\n");
            close(sockfd);
            free(host);
            free(path);
            return NULL;
        }

        SSL_set_fd(ssl, sockfd);            //  关联套接字文件描述符

        // 设置SNI扩展
        SSL_set_tlsext_host_name(ssl, host);

        if (!perform_ssl_handshake(ssl, sockfd)) {
            fprintf(stderr, "SSL handshake failed for %s\n", host);
            SSL_free(ssl);
            close(sockfd);
            free(host);
            free(path);
            return NULL;
        }


        // 验证证书
        X509 *cert = SSL_get_peer_certificate(ssl);
        if (!cert) {
            fprintf(stderr, "No server certificate presented\n");
            SSL_free(ssl);
            close(sockfd);
            free(host);
            free(path);
            return NULL;
        }

        // 验证证书结果
        long verify_result = SSL_get_verify_result(ssl);
        if (verify_result != X509_V_OK) {
            fprintf(stderr, "Certificate verification failed: %s (%ld)\n", 
                    X509_verify_cert_error_string(verify_result), verify_result);
            X509_free(cert);
            SSL_free(ssl);
            close(sockfd);
            free(host);
            free(path);
            return NULL;
        }
        
        X509_free(cert);
    }

    char request[BUFFER_SIZE] = {0};        //  请求报文
    snprintf(request, sizeof(request),
        "GET /%s %s\r\n"
        "Host: %s\r\n"
        "%s"
        "%s"
        "%s"
        "\r\n",
        path, HTTP_VERSION,
        host,
        USER_AGENT,
        ACCEPT_HEADER,
        CONNECTION_TYPE
    );

    // 发送请求
    ssize_t sent;
    if (ssl) {
        sent = SSL_write(ssl, request, strlen(request));    //  SSL可用的话,加密发送数据
    } else {
        sent = send(sockfd, request, strlen(request), 0);   //  不可用的话。就明文发送
    }
    
    if (sent < 0) {                             //  发送不成功
        perror("send failed");
        if (ssl) SSL_free(ssl);
        close(sockfd);
        free(host);
        free(path);
        
        return NULL;
    }

    // 接收响应
    size_t total_size = 0;
    size_t buffer_capacity = BUFFER_SIZE;
    char *response = malloc(buffer_capacity);
    if (!response) {
        perror("malloc failed");
        if (ssl) SSL_free(ssl);
        close(sockfd);
        free(host);
        free(path);
        
        return NULL;
    }


    // 设置为非阻塞模式用于超时处理
    fcntl(sockfd, F_SETFL, O_NONBLOCK);

    fd_set fdread;
    struct timeval tv;
    tv.tv_sec = 5;
    tv.tv_usec = 0;

    while (1) {
        FD_ZERO(&fdread);
        FD_SET(sockfd, &fdread);

        int ready = select(sockfd + 1, &fdread, NULL, NULL, &tv);
        if (ready < 0) {
            perror("select error");
            break;
        }
        if (ready == 0) {
            printf("Timeout waiting for response\n");
            break;
        }

        char buffer[BUFFER_SIZE] = {0};
        ssize_t len;
        if (ssl) {
            len = SSL_read(ssl, buffer, sizeof(buffer));        //  解密接收数据
        } else {
            len = recv(sockfd, buffer, sizeof(buffer), 0);      //  收取杂文
        }

        if (len < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) continue;
            perror("recv error");
            break;
        }
        if (len == 0) break;

        if (total_size + len >= buffer_capacity) {
            buffer_capacity *= 2;
            char *new_buf = realloc(response, buffer_capacity);
            if (!new_buf) {
                perror("realloc failed");
                free(response);
                if (ssl) SSL_free(ssl);
                close(sockfd);
                free(host);
                free(path);
                
                return NULL;
            }
            response = new_buf;
        }

        memcpy(response + total_size, buffer, len);
        total_size += len;
    }

    if (total_size < buffer_capacity) {
        response[total_size] = '\0';
    } else {
        char *new_buf = realloc(response, total_size + 1);
        if (new_buf) {
            response = new_buf;
            response[total_size] = '\0';    // 确保始终有终止符
        }
    }

    // 清理资源
    if (ssl) {
        SSL_shutdown(ssl);
        SSL_free(ssl);
    }
    close(sockfd);
    free(host);
    free(path);
    

    // 检查重定向
    int status_code = get_status_code(response);
    if (status_code >= 300 && status_code < 400) {
        char *location = get_location(response);
        if (location) {
            printf("Redirecting to: %s\n", location);
            char *redirect_response = http_send_request(location, redirect_count + 1);      //  递归发送重定向
            free(location);     // 回溯
            free(response);     // 回溯
            return redirect_response;       //  强行终止递归函数的回溯
        }
    }

    return response;
}

主函数与接口函数

我们围绕着上文复杂的 https_send_request ,我们做一个简单的接口函数。他是先拼接来自 Linux 命令行输入的 域名和资源,并且以 HTTP 协议试探性地发起请求。之后发生重定向,函数 https_send_request 会处理这个情况的。

int https_response_entry(char *domain, char *resource) {
    // 初始化OpenSSL
    init_openssl();
    
    // 构建初始URL
    char initial_url[256];
    snprintf(initial_url, sizeof(initial_url), "http://%s/%s", domain, resource);   //  先尝试用 HTTP
    
    char *response = http_send_request(initial_url, 0);
    if (response) {
        printf("\n----- Final Response -----\n%s\n", response);
        free(response);
    } else {
        printf("Request failed\n");
    }
    
    // 清理OpenSSL资源
    cleanup_openssl();
}

主函数就简单许多了,

int main(int argc, char *argv[]) {
    if (argc < 3) {
        fprintf(stderr, "Usage: %s <domain> <resource>\n", argv[0]);
        return -1;
    }

    https_response_entry(argv[1], argv[2]);
    
    return 0;
}

代码执行

检查头文件和对应库函数

我是在 Linux 系统下编译 C 代码的。我们先检查头文件,

qiming@qiming:/$ cd usr/include/
qiming@qiming:/usr/include$ ls

查到有这个文件即可

openssl

我们可打开这个文件夹

qiming@qiming:/usr/include$ cd openssl/
qiming@qiming:/usr/include/openssl$ ls
aes.h       blowfish.h   cmp.h       conftypes.h         crypto.h      dsa.h         engineerr.h  fips_names.h  md2.h         ocsp.h         pkcs12.h    rc4.h        srp.h            store.h     ui.h
asn1err.h   bnerr.h      cmp_util.h  core_dispatch.h     cterr.h       dtls1.h       engine.h     hmac.h        md4.h         opensslv.h     pkcs7err.h  rc5.h        srtp.h           symhacks.h  whrlpool.h
asn1.h      bn.h         cmserr.h    core.h              ct.h          ebcdic.h      e_os2.h      httperr.h     md5.h         ossl_typ.h     pkcs7.h     ripemd.h     ssl2.h           tls1.h      x509err.h
asn1_mac.h  buffererr.h  cms.h       core_names.h        decodererr.h  ecdh.h        err.h        http.h        mdc2.h        param_build.h  proverr.h   rsaerr.h     ssl3.h           trace.h     x509.h
asn1t.h     buffer.h     comperr.h   core_object.h       decoder.h     ecdsa.h       esserr.h     idea.h        modes.h       params.h       provider.h  rsa.h        sslerr.h         tserr.h     x509v3err.h
asyncerr.h  camellia.h   comp.h      crmferr.h           des.h         ecerr.h       ess.h        kdferr.h      objectserr.h  pem2.h         prov_ssl.h  safestack.h  sslerr_legacy.h  ts.h        x509v3.h
async.h     cast.h       conf_api.h  crmf.h              dherr.h       ec.h          evperr.h     kdf.h         objects.h     pemerr.h       randerr.h   seed.h       ssl.h            txt_db.h    x509_vfy.h
bioerr.h    cmac.h       conferr.h   cryptoerr.h         dh.h          encodererr.h  evp.h        lhash.h       obj_mac.h     pem.h          rand.h      self_test.h  stack.h          types.h
bio.h       cmperr.h     conf.h      cryptoerr_legacy.h  dsaerr.h      encoder.h     fipskey.h    macros.h      ocsperr.h     pkcs12err.h    rc2.h       sha.h        storeerr.h       uierr.h

我们发现是可以找到前面所准备的两个头文件

#include <openssl/ssl.h>
#include <openssl/err.h>

接下来要检查对应的库文件,首先是关于 SSL 的库文件

qiming@qiming:/usr$ cd /usr/lib/x86_64-linux-gnu/
qiming@qiming:/usr/lib/x86_64-linux-gnu$ ls

我们可以找到这四个库文件,它使得代码可以选择编译选项 -lssl

libssl3.so                                                                             
libssl.a                                            
libssl.so                                        
libssl.so.3

紧接着是在同一个文件夹下,找到以下三个库文件,它使得代码可以选择编译选项 -lcrypto

libcrypto.a                                 
libcrypto.so                                 
libcrypto.so.3   

编译+运行

代码编译

qiming@qiming:~/share/CTASK/TCP_test$ gcc -o http1 http_1.c  -lssl -lcrypto 

代码执行。我是零声教育的学员,我现在用这个代码访问零声官网www.0voice.com

qiming@qiming:~/share/CTASK/TCP_test$ ./http1 www.0voice.com /
Redirecting to: https://www.0voice.com/

----- Final Response -----
HTTP/1.1 200 OK
Server: nginx/1.16.1
Date: Wed, 18 Jun 2025 18:47:21 GMT
Content-Type: text/html
Content-Length: 67341
Last-Modified: Mon, 02 Sep 2024 02:42:40 GMT
Connection: close
ETag: "66d52620-1070d"
Accept-Ranges: bytes


<!DOCTYPE html>
<html>
<head>

    <!-- Basic Page Needs
    ================================================== -->
    <title>首页-零声教育</title>
    <link rel="stylesheet" href="https://website.0voice.com/ui-website/swiper/swiper.min.css">
    <script src="https://website.0voice.com/ui-website/swiper/swiper.min.js"></script>


    <div>

    <!-- Basic Page Needs
    ================================================== -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="一切只为渴望更优秀的你,让世界没有难学的技术,让工程师的生活丰富多彩。" />
    <meta name="keywords" content="零声,教育,0voice,零声教育,一切只为渴望更优秀的你" />


    <!-- 强制使用webkit内核,作用于360、qq等国产双核浏览器 -->
    <meta name="renderer" content="webkit"/>
////////// 以下内容因篇幅过大而省略  /////////////

我们可以查看该网站是执行 HTTPS 加密传输协议的。
在这里插入图片描述
以上就是 HTTPS 的客户端请求方案的解决办法,读者可以对比这篇 文章 来看。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐