拓冰建站拓冰建站
首页 / 资讯中心 / 正文

C语言实现HTTPS客户端:OpenSSL库集成与SSL/TLS加密通信实践

1. 从HTTP到HTTPS为什么我们需要SSL/TLS如果你写过C语言的网络程序用socket、connect、send、recv这几个函数实现一个HTTP客户端可能觉得网络通信也就那么回事。但当你把协议从HTTP换成HTTPS尝试连接https://开头的网址时大概率会碰一鼻子灰。服务器要么直接断开连接要么返回一堆你看不懂的二进制乱码。这不是你的代码错了而是你撞上了一堵叫做“SSL/TLS”的加密墙。简单来说HTTPS就是在HTTP下面垫了一层SSL/TLS。HTTP是明文传输的你在网络上发的每一个请求、服务器回的每一个响应就像寄明信片沿途经过的任何一个路由器、网关甚至是不怀好意的邻居都能看得一清二楚。你的账号密码、聊天记录、银行卡号在明文传输下几乎等于裸奔。SSL/TLS协议就是为了解决这个问题而生的它在你的应用程序客户端和服务器之间建立了一条加密的、经过身份验证的安全通道。所有数据在进入网络之前都会被加密变成一堆乱码只有持有正确密钥的对方才能解密还原。同时客户端还能通过数字证书验证服务器的身份确保你连接的不是一个假冒的钓鱼网站。在C语言里实现一个HTTPS客户端本质上就是要在传统的TCP Socket通信流程中插入SSL/TLS握手和加解密的环节。你不能再用send直接发“GET / HTTP/1.1”这样的明文了而是要把这个请求交给一个SSL库比如OpenSSL让它帮你加密后再通过socket发出去同样从socket收来的加密数据也要先交给SSL库解密才能得到可读的HTTP响应。这听起来有点绕但核心流程可以概括为创建TCP连接 - 初始化SSL上下文 - 基于TCP连接创建SSL会话对象 - 进行SSL握手交换密钥、验证证书 - 握手成功后使用SSL对象的读写函数进行加密通信 - 通信结束清理资源。接下来我们就一步步拆解这个流程用最基础的C语言和OpenSSL库实现一个能真正抓取HTTPS网页内容的客户端。2. 环境准备与OpenSSL库的集成在动手写代码之前第一件事是把“武器库”准备好。在C语言的世界里实现SSL/TLS功能OpenSSL是事实上的标准选择。它庞大、复杂但也功能强大、历经考验。我们的简单实现将基于它来完成。2.1 OpenSSL库的安装与链接如果你在Linux或macOS上开发通常可以通过包管理器直接安装开发库。例如在Ubuntu或Debian上你需要安装libssl-dev包这个包包含了编译所需的头文件.h和链接库.so或.a。sudo apt-get update sudo apt-get install libssl-dev在macOS上使用Homebrew安装会非常方便brew install openssl安装完成后关键是要确认你的编译器能找到头文件和库文件。有时候特别是macOS上Homebrew安装的OpenSSL它可能不在系统默认的搜索路径/usr/include,/usr/lib里而是安装在/usr/local/opt/openssl这样的独立目录下。这时你需要在编译命令中显式指定路径。一个典型的编译命令看起来像这样gcc -o https_client https_client.c -lssl -lcrypto-lssl和-lcrypto就是告诉链接器我们要使用名为libssl和libcrypto的库。如果遇到“找不到-lssl”的错误你可能需要加上-L选项来指定库文件的具体路径例如-L/usr/local/opt/openssl/lib。注意OpenSSL库本身又依赖很多其他的加密算法库libcrypto就是其核心加密库libssl是建立在它之上的SSL/TLS协议库。两者通常需要同时链接。2.2 初始化OpenSSL被忽略的关键第一步这是很多新手会踩的第一个坑直接调用SSL_new()之类的函数导致程序崩溃。在使用OpenSSL任何函数之前必须对其进行初始化。这是因为OpenSSL需要加载它支持的各类加密算法、错误字符串等信息。初始化工作主要靠两个函数完成SSL_library_init(): 注册所有SSL/TLS协议需要用到的算法比如AES, RSA, SHA256等。OpenSSL_add_all_algorithms(): 实际上在现代OpenSSL中SSL_library_init()通常就足够了但为了兼容性和确保万无一失显式调用这个函数来加载所有算法也是一个好习惯。SSL_load_error_strings(): 这个函数非常重要。它加载了人类可读的错误描述信息。没有它当SSL操作失败时你调用ERR_error_string()得到的只是一串数字错误码像“336445449”这样完全无法调试。调用这个函数后错误信息会变成“SSL3_GET_RECORD:wrong version number”这样有意义的描述。对应的在程序结束前应该进行清理工作。虽然对于简单的一次性程序操作系统会回收资源但养成好习惯总是对的。你可以调用EVP_cleanup()和ERR_free_strings()。一个标准的初始化代码块如下#include openssl/ssl.h #include openssl/err.h int main() { // 初始化OpenSSL SSL_library_init(); OpenSSL_add_all_algorithms(); SSL_load_error_strings(); // ... 你的主要逻辑代码 ... // 程序结束前清理对于简单示例有时可省略 EVP_cleanup(); ERR_free_strings(); return 0; }把这一步做好就为后续的SSL操作铺平了道路。接下来我们就可以开始构建SSL通信的核心组件了。3. 构建SSL上下文与连接对象初始化完OpenSSL库之后我们并不能立刻开始加密通信。需要先搭建一个“舞台”这个舞台就是SSL_CTXSSL上下文然后为每一次具体的连接创建一个“演员”也就是SSL对象。上下文负责管理全局的设置比如用什么协议版本、如何验证证书而SSL对象则代表一次具体的、状态完整的SSL连接。3.1 创建与配置SSL上下文SSL_CTX创建上下文使用SSL_CTX_new函数你需要告诉它使用哪种“方法”Method。这个方法决定了协议版本和模式客户端/服务器。对于客户端我们最常用的是SSLv23_client_method()。这个名字有点历史遗留问题它并不意味着使用不安全的SSLv2或v3而是表示“与服务器协商使用双方都支持的最高版本的TLS协议”。这是一种兼容性最好的做法。SSL_CTX *ctx SSL_CTX_new(SSLv23_client_method()); if (ctx NULL) { ERR_print_errors_fp(stderr); // 打印错误信息到标准错误输出 exit(EXIT_FAILURE); }创建好上下文后有几项关键的配置需要设置设置验证模式可选但强烈推荐默认情况下OpenSSL客户端不会验证服务器的证书。这意味着你虽然建立了加密连接但无法确认对方是不是你真正想连的服务器中间人攻击依然可能发生。通过SSL_CTX_set_verify函数可以设置验证模式。SSL_VERIFY_PEER要求验证对端服务器证书。通常我们还需要设置一个验证深度SSL_CTX_set_verify_depth(ctx, 4);表示允许证书链最大深度为4这足以应对大多数情况。加载受信任的根证书CA证书验证服务器证书时需要依据一套受信任的根证书CA证书。你的操作系统或浏览器通常自带了一套。在Linux上它们通常在/etc/ssl/certs/目录下。你可以用SSL_CTX_load_verify_locations函数来加载一个目录或一个特定的证书文件。如果这一步不做即使设置了SSL_VERIFY_PEER验证也会因为找不到可信的根而失败。if (!SSL_CTX_load_verify_locations(ctx, NULL, /etc/ssl/certs)) { fprintf(stderr, 加载CA证书目录失败。\n); // 处理错误 }3.2 建立TCP连接SSL的运输层SSL/TLS是应用层协议它需要运行在一个可靠的传输层协议之上几乎总是TCP。所以在创建SSL对象之前我们必须先用标准的Berkeley Socket API建立一个到目标服务器的TCP连接。这个过程和你写普通HTTP客户端一模一样域名解析使用getaddrinfo将主机名如www.example.com解析为IP地址。这个函数能优雅地处理IPv4和IPv6。创建Socketsocket()系统调用。建立连接connect()系统调用。这里有一个细节HTTPS的标准端口是443而不是HTTP的80。所以你在getaddrinfo的提示中或直接调用connect时端口号应指定为443。#include sys/types.h #include sys/socket.h #include netdb.h int sockfd; struct addrinfo hints, *res, *p; memset(hints, 0, sizeof hints); hints.ai_family AF_UNSPEC; // IPv4或IPv6都可以 hints.ai_socktype SOCK_STREAM; // TCP流socket int status getaddrinfo(“www.example.com”, “443”, hints, res); if (status ! 0) { fprintf(stderr, “getaddrinfo error: %s\n”, gai_strerror(status)); exit(1); } // 遍历所有返回的地址尝试连接 for(p res; p ! NULL; p p-ai_next) { sockfd socket(p-ai_family, p-ai_socktype, p-ai_protocol); if (sockfd -1) continue; if (connect(sockfd, p-ai_addr, p-ai_addrlen) 0) { break; // 连接成功 } close(sockfd); // 这个地址失败关闭socket尝试下一个 } freeaddrinfo(res); if (p NULL) { fprintf(stderr, “无法连接到服务器\n”); exit(1); } // 此时 sockfd 是一个已连接到 www.example.com:443 的TCP socket拿到这个sockfd之后我们才算是准备好了SSL通信的“传输管道”。3.3 创建SSL会话对象并绑定Socket现在我们可以将SSL上下文和TCP连接组合起来了。使用SSL_new函数传入之前创建好的ctx来生成一个代表本次SSL连接的SSL对象。SSL *ssl SSL_new(ctx); if (ssl NULL) { ERR_print_errors_fp(stderr); close(sockfd); SSL_CTX_free(ctx); exit(EXIT_FAILURE); }创建成功后需要将这个SSL对象与我们刚刚建立的TCP socket进行绑定。这是通过SSL_set_fd函数完成的if (!SSL_set_fd(ssl, sockfd)) { ERR_print_errors_fp(stderr); SSL_free(ssl); close(sockfd); SSL_CTX_free(ctx); exit(EXIT_FAILURE); }这个调用并不立即发起任何网络通信它只是建立了SSL对象内部状态与一个文件描述符我们的socket的关联。至此所有的准备工作都已就绪最关键的“握手”阶段即将开始。4. 核心握手与证书验证前面所有的初始化、创建、绑定都是为了这一刻SSL握手Handshake。这是整个HTTPS连接建立过程中最复杂、也最核心的一步。客户端和服务器通过握手协商出后续通信所用的加密套件、交换密钥并完成身份认证。4.1 执行SSL握手连接在代码中握手就是一次函数调用SSL_connect。这个函数会阻塞直到握手完成或失败。它会通过我们绑定的sockfd与服务器进行好几轮网络报文交换。int ret SSL_connect(ssl); if (ret ! 1) { // 握手失败 int err SSL_get_error(ssl, ret); fprintf(stderr, “SSL连接失败错误码%d\n”, err); ERR_print_errors_fp(stderr); // 打印详细的错误信息 // ... 清理资源 ... exit(EXIT_FAILURE); } printf(“SSL握手成功\n”);如果SSL_connect返回1表示握手成功。如果返回0或负数则表示失败。失败的原因可能多种多样网络不通、服务器不支持客户端提出的协议版本、证书有问题等等。SSL_get_error和ERR_print_errors_fp是此时排查问题的利器。4.2 理解与处理证书验证握手成功只代表加密通道建立起来了但不代表服务器身份可信。这就是证书验证的作用。即使你没有像3.1节那样设置SSL_VERIFY_PEER握手也可能成功但连接是不安全的。在设置了证书验证后你需要在握手成功后主动检查验证结果。使用SSL_get_verify_result函数long verify_result SSL_get_verify_result(ssl); if (verify_result ! X509_V_OK) { fprintf(stderr, “证书验证失败错误%s\n”, X509_verify_cert_error_string(verify_result)); // 尽管握手成功但出于安全考虑应该终止连接 // ... 清理资源 ... exit(EXIT_FAILURE); } printf(“服务器证书验证通过。\n”);常见的验证错误包括X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY找不到签发服务器证书的根证书CA证书。这就是为什么之前要正确加载CA证书目录。X509_V_ERR_CERT_NOT_YET_VALID或X509_V_ERR_CERT_HAS_EXPIRED证书尚未生效或已过期。X509_V_ERR_HOSTNAME_MISMATCH证书中的域名与您实际连接的主机名不匹配。例如证书是发给www.google.com的但你连接的是mail.google.com。对于开发测试或者连接一个使用自签名证书的内部服务器时你可能会选择跳过证书验证。但这在生产环境或涉及敏感信息的场景下是极其危险的行为。如果必须跳过可以通过SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL)来设置。更好的做法是将服务器的自签名证书导入为受信任的根证书。4.3 获取连接信息与密码套件握手成功后你可以获取一些关于当前连接的有趣信息这有助于调试和理解// 获取协议版本如 “TLSv1.2” printf(“使用的协议%s\n”, SSL_get_version(ssl)); // 获取协商出的加密套件如 “ECDHE-RSA-AES256-GCM-SHA384” printf(“使用的加密套件%s\n”, SSL_get_cipher(ssl)); // 获取服务器证书可选可用于进一步检查证书信息 X509 *server_cert SSL_get_peer_certificate(ssl); if (server_cert) { char *subject X509_NAME_oneline(X509_get_subject_name(server_cert), NULL, 0); char *issuer X509_NAME_oneline(X509_get_issuer_name(server_cert), NULL, 0); printf(“服务器证书主题%s\n”, subject); printf(“签发者%s\n”, issuer); free(subject); free(issuer); X509_free(server_cert); }了解这些信息能让你确信连接确实按照你期望的安全标准建立了。至此一条经过认证和加密的SSL/TLS通道已经准备就绪可以开始传输应用层数据也就是HTTP请求和响应了。5. 加密通信发送请求与接收响应握手成功且证书验证通过后我们手中的ssl对象就变成了一个安全的“双向管道”。你不能再用普通的send()和recv()在这个sockfd上读写数据了因为它们操作的是原始的TCP流。所有需要通过这个连接收发的数据都必须使用OpenSSL提供的SSL读写函数它们会在内部自动完成加密和解密。5.1 使用SSL_write发送HTTP请求发送数据使用SSL_write函数。它的接口和标准的write或send很像但作用在SSL*对象上。const char *request “GET / HTTP/1.1\r\n” “Host: www.example.com\r\n” “User-Agent: My-HTTPS-Client/1.0\r\n” “Connection: close\r\n” “\r\n”; // 注意最后的空行表示请求头结束 int bytes_written SSL_write(ssl, request, strlen(request)); if (bytes_written 0) { int err SSL_get_error(ssl, bytes_written); fprintf(stderr, “SSL写入失败错误%d\n”, err); // 处理错误可能是连接中断 } printf(“成功发送 %d 字节的HTTP请求。\n”, bytes_written);SSL_write会将你的明文请求request根据当前SSL会话协商出的加密算法和密钥进行加密然后将加密后的数据通过底层的TCP socket发送出去。你不需要关心分片、填充、加密块这些细节OpenSSL都处理好了。注意SSL_write不一定一次调用就能把你给它的所有数据都发出去它可能受底层TCP发送缓冲区的影响。对于较长的请求体你可能需要在一个循环中调用它直到所有数据写完。但对于一个简单的GET请求头一次调用通常就够了。5.2 使用SSL_read接收HTTP响应接收数据使用SSL_read函数。它从SSL连接中读取解密后的明文数据。由于TCP是流式协议且SSL记录层也有自己的分帧你无法预知一次SSL_read能拿到多少数据。因此循环读取是标准做法。char buffer[4096]; // 4KB的缓冲区 int total_bytes_read 0; int bytes_read; printf(“开始接收响应…\n”); while ((bytes_read SSL_read(ssl, buffer, sizeof(buffer) - 1)) 0) { // 确保缓冲区以NULL结尾便于作为字符串处理HTTP响应是文本 buffer[bytes_read] ‘\0’; printf(“%s”, buffer); // 或者将数据存储起来 total_bytes_read bytes_read; } if (bytes_read 0) { int err SSL_get_error(ssl, bytes_read); // SSL_ERROR_ZERO_RETURN 表示对方正常关闭了连接这通常是预期的 // SSL_ERROR_WANT_READ/WRITE 在非阻塞模式下会出现我们这里是阻塞模式一般不会 if (err ! SSL_ERROR_ZERO_RETURN) { fprintf(stderr, “SSL读取错误%d\n”, err); } } printf(“\n响应接收完毕共 %d 字节。\n”, total_bytes_read);这个循环会一直执行直到服务器关闭连接对应SSL_read返回0或者发生错误返回负数。对于HTTP/1.1协议服务器在发送完整个响应体后通常会关闭TCP连接如果请求头中Connection是close这会导致SSL_read返回0循环退出。你打印出来的buffer内容就是完整的HTTP响应包括状态行如HTTP/1.1 200 OK、响应头和响应体HTML等。一个简单的客户端到这里就算完成了它的使命发送请求获取响应。5.3 处理阻塞I/O与超时我们上面的例子使用的是阻塞I/O即SSL_connect、SSL_write、SSL_read都会一直等待直到操作完成或出错。这在简单场景下没问题但在需要同时处理多个连接或需要响应性的程序中阻塞I/O会是个问题。OpenSSL也支持非阻塞I/O。你需要将底层的socket设置为非阻塞模式使用fcntl设置O_NONBLOCK标志然后SSL函数在数据未就绪时会返回SSL_ERROR_WANT_READ或SSL_ERROR_WANT_WRITE错误而不是阻塞。你的程序需要根据这个错误码在适当的时机比如使用select或epoll检测到socket可读/可写时重试该操作。这大大增加了代码的复杂度对于初学者建议先从阻塞模式理解原理。另一个重要问题是超时。网络是不稳定的服务器可能无响应。在阻塞模式下如果没有设置socket超时你的程序可能会永远卡在SSL_connect或SSL_read上。你可以使用setsockopt函数为sockfd设置SO_RCVTIMEO接收超时和SO_SNDTIMEO发送超时选项。struct timeval timeout; timeout.tv_sec 10; // 10秒超时 timeout.tv_usec 0; setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)timeout, sizeof(timeout)); setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, (const char*)timeout, sizeof(timeout));设置之后如果connect、send、recv以及基于它们的SSL_函数在指定时间内没有完成就会失败并设置errno为EAGAIN或EWOULDBLOCK。这对于构建健壮的客户端至关重要。6. 资源清理与错误处理C语言编程尤其是涉及网络和外部库时资源管理和错误处理是重中之重。内存泄漏、文件描述符泄漏在长期运行的程序中是致命的。OpenSSL对象也需要遵循严格的创建和释放配对。6.1 正确的资源释放顺序当通信结束或发生错误时必须按顺序清理资源。一个推荐的顺序是关闭SSL连接使用SSL_shutdown。这个函数会向对方发送“关闭通知”警报优雅地结束SSL连接。它通常需要调用两次一次发送关闭通知一次接收对方的确认。但在简单的客户端场景中服务器在发送完HTTP响应后常常会直接关闭底层TCP连接这时第一次SSL_shutdown就可能失败。一个常见的模式是// 尝试优雅关闭 SSL_shutdown(ssl); // 即使失败也继续清理释放SSL对象使用SSL_free(ssl)。这个调用会释放该SSL会话占用的所有内存。关闭TCP Socket使用close(sockfd)或closesocket(sockfd)Windows。释放SSL上下文使用SSL_CTX_free(ctx)。如果你的程序只建立一次连接可以在所有连接都处理完后释放它。如果你的程序需要建立多个SSL连接应该复用同一个ctx在程序最终退出时才释放它。一个完整的清理代码块可能如下if (ssl) { SSL_shutdown(ssl); // 尝试优雅关闭 SSL_free(ssl); ssl NULL; } if (sockfd 0) { close(sockfd); sockfd -1; } if (ctx) { SSL_CTX_free(ctx); ctx NULL; } // 最后可选的全局清理 EVP_cleanup(); ERR_free_strings();6.2 OpenSSL错误队列的排查技巧OpenSSL有自己的错误队列。当SSL函数调用失败时错误信息会被压入这个队列而不是设置全局的errno。这就是为什么我们一直使用ERR_print_errors_fp(stderr)来打印错误。错误队列是线程相关的。有时候一个函数失败可能会在队列中留下多个错误码。ERR_print_errors_fp会打印出整个队列这对于调试非常有用。你也可以用ERR_get_error一次弹出一个错误码然后自己用ERR_error_string将其转换为可读信息。unsigned long err; while ((err ERR_get_error()) ! 0) { char *err_str ERR_error_string(err, NULL); fprintf(stderr, “OpenSSL Error: %s\n”, err_str); }养成在每次SSL函数调用失败后立即打印错误队列的习惯能帮你快速定位问题是出在证书验证、握手协议不匹配还是网络I/O上。6.3 常见问题与调试心得在实现过程中你几乎一定会遇到一些问题。以下是一些常见坑点和调试建议“SSL连接失败错误1”错误码1通常是SSL_ERROR_SSL表示发生了SSL协议级别的错误。具体原因需要看错误队列。常见原因包括协议版本不匹配服务器只支持TLS1.3而客户端方法配置不当、证书问题自签名证书未加载、密码套件不匹配等。务必使用ERR_print_errors_fp查看详细描述。“证书验证失败”首先检查是否加载了正确的CA证书路径。在Linux上可以尝试SSL_CTX_set_default_verify_paths(ctx);这个函数会加载OpenSSL编译时设置的默认证书路径通常就是/etc/ssl/certs。如果连接的是自签名证书要么将证书添加到受信任库要么在测试时暂时关闭验证并清楚其风险。程序编译通过但运行时崩溃最常见的原因是忘记调用SSL_library_init()等初始化函数。确保所有OpenSSL函数调用都在初始化之后。内存泄漏使用如Valgrind之类的工具检查。确保每个SSL_new都有对应的SSL_free每个SSL_CTX_new都有对应的SSL_CTX_free。连接超时或无响应检查网络是否通畅ping一下域名检查防火墙是否屏蔽了443端口。在代码中设置socket超时选项避免程序永久挂起。接收到的响应不完整或乱码确保你正确处理了SSL_read的循环。HTTP响应可能很大你的缓冲区可能一次装不下。另外确保你没有把二进制数据如图片内容误当作字符串打印printf遇到\0会终止。调试网络程序一个非常有效的方法是使用抓包工具如Wireshark。你可以捕获lo本地回环或你的物理网卡上的流量并设置过滤条件tcp.port 443。虽然数据是加密的但你可以看到TCP握手、TLS握手Client Hello, Server Hello等的报文序列这对于判断连接是否成功建立、卡在哪一步非常有帮助。例如如果你看到了Client Hello但没有Server Hello那问题很可能出在服务器端或网络链路上。7. 完整代码示例与封装思路将前面所有步骤整合起来下面是一个极简但功能完整的HTTPS客户端代码示例。它连接到一个指定的主机和路径获取内容并打印到标准输出。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/socket.h #include netdb.h #include arpa/inet.h #include openssl/ssl.h #include openssl/err.h #define REQUEST_FORMAT “GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: Simple-HTTPS-Client\r\nConnection: close\r\n\r\n” #define BUFFER_SIZE 4096 int main(int argc, char **argv) { if (argc ! 3) { fprintf(stderr, “用法%s 主机名 路径\n”, argv[0]); fprintf(stderr, “示例%s www.example.com /\n”, argv[0]); return EXIT_FAILURE; } const char *hostname argv[1]; const char *path argv[2]; int port 443; // HTTPS默认端口 int sockfd -1; SSL_CTX *ctx NULL; SSL *ssl NULL; // 1. 初始化OpenSSL SSL_library_init(); OpenSSL_add_all_algorithms(); SSL_load_error_strings(); // 2. 创建SSL上下文 ctx SSL_CTX_new(SSLv23_client_method()); if (!ctx) { ERR_print_errors_fp(stderr); goto cleanup; } // 可选加载系统信任的CA证书Linux常见路径 if (!SSL_CTX_set_default_verify_paths(ctx)) { fprintf(stderr, “警告无法加载默认CA证书路径。\n”); // 继续执行但证书验证可能会失败 } // 设置验证服务器证书 SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); // 3. 建立TCP连接 struct addrinfo hints, *result, *rp; memset(hints, 0, sizeof(hints)); hints.ai_family AF_UNSPEC; hints.ai_socktype SOCK_STREAM; char port_str[10]; snprintf(port_str, sizeof(port_str), “%d”, port); int ret getaddrinfo(hostname, port_str, hints, result); if (ret ! 0) { fprintf(stderr, “解析地址失败%s\n”, gai_strerror(ret)); goto cleanup; } for (rp result; rp ! NULL; rp rp-ai_next) { sockfd socket(rp-ai_family, rp-ai_socktype, rp-ai_protocol); if (sockfd -1) continue; if (connect(sockfd, rp-ai_addr, rp-ai_addrlen) 0) { break; // 连接成功 } close(sockfd); sockfd -1; } freeaddrinfo(result); if (sockfd -1) { fprintf(stderr, “无法连接到 %s:%d\n”, hostname, port); goto cleanup; } // 4. 创建SSL对象并绑定socket ssl SSL_new(ctx); if (!ssl) { ERR_print_errors_fp(stderr); goto cleanup; } if (!SSL_set_fd(ssl, sockfd)) { ERR_print_errors_fp(stderr); goto cleanup; } // 5. 执行SSL握手 ret SSL_connect(ssl); if (ret ! 1) { fprintf(stderr, “SSL握手失败\n”); ERR_print_errors_fp(stderr); goto cleanup; } printf(“[*] 已成功与 %s 建立SSL连接。协议%s 密码套件%s\n”, hostname, SSL_get_version(ssl), SSL_get_cipher(ssl)); // 6. 验证服务器证书 long verify_result SSL_get_verify_result(ssl); if (verify_result ! X509_V_OK) { fprintf(stderr, “[!] 证书验证失败%s\n”, X509_verify_cert_error_string(verify_result)); // 在实际应用中这里应该终止连接。此处为演示继续。 } else { printf(“[*] 服务器证书验证通过。\n”); } // 7. 发送HTTP请求 char request[1024]; snprintf(request, sizeof(request), REQUEST_FORMAT, path, hostname); int bytes_sent SSL_write(ssl, request, strlen(request)); if (bytes_sent 0) { fprintf(stderr, “发送请求失败\n”); ERR_print_errors_fp(stderr); goto cleanup; } printf(“[*] 请求已发送 (%d 字节)。\n”, bytes_sent); // 8. 接收HTTP响应 char buffer[BUFFER_SIZE]; int total_received 0; int bytes_received; printf(“[*] 开始接收响应…\n”); printf(“—–响应开始—–\n”); while ((bytes_received SSL_read(ssl, buffer, sizeof(buffer) - 1)) 0) { buffer[bytes_received] ‘\0’; printf(“%s”, buffer); total_received bytes_received; } printf(“\n—–响应结束—–\n”); printf(“[*] 共接收 %d 字节。\n”, total_received); if (bytes_received 0) { int err SSL_get_error(ssl, bytes_received); if (err ! SSL_ERROR_ZERO_RETURN) { fprintf(stderr, “读取响应时发生错误\n”); ERR_print_errors_fp(stderr); } } // 9. 清理资源 cleanup: if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); } if (sockfd 0) close(sockfd); if (ctx) SSL_CTX_free(ctx); // 可选的全局清理 // EVP_cleanup(); // ERR_free_strings(); return 0; }你可以将这段代码保存为https_client.c用之前提到的命令编译gcc -o https_client https_client.c -lssl -lcrypto然后运行./https_client www.example.com /来测试。对于更复杂的应用比如需要处理重定向、解析响应头、处理分块传输编码、保持连接复用等这个基础版本就显得力不从心了。一个自然的封装思路是将SSL连接的管理创建、握手、读写、关闭封装成一个结构体比如struct ssl_connection和相关函数。再往上可以封装一个简单的HTTP客户端库处理请求构建、响应解析、状态码判断等。这会让你的主业务逻辑更清晰也更容易应对不同的网络场景。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门