【网络编程】按 HTTPS 加密协议实现客户端请求的 C 代码
文章目录
推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[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 在网络协议层级中的位置可以直观为以下关系。
实现这个请求过程,我们可以采用递归函数的思想,实现 “HTTPS 递归请求” 以克服重定向问题。我们需在检查重定向之后,放置判断是否递归执行函数的语句。由于我们只关心最后那次的报文返回情况,在递归展开之后的最内层返回响应报文后,递归回溯的每一层都返回这个响应报文。检查重定向,应该放在该递归函数的最后。
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
该函数是一个递归查询函数,流程图如下(就是前文的那张图,我只是把它搬下来而已),返回的是 “最终响应报文”
// 发送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 的客户端请求方案的解决办法,读者可以对比这篇 文章 来看。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)