OpenSSL:C 语言 TLS 客户端完整示例
目录
- 概述
- 快速上手:三步跑起来
- 环境准备:安装与版本
- [编译与链接速查(gcc / pkg-config / CMake)](#编译与链接速查(gcc / pkg-config / CMake))
- 核心对象与典型调用顺序
- 程序整体逻辑(九部分)
- [TLS 握手与应用数据(示意)](#TLS 握手与应用数据(示意))
- [OpenSSL 1.1.x 风格完整示例](#OpenSSL 1.1.x 风格完整示例)
- [OpenSSL 3.x 风格完整示例](#OpenSSL 3.x 风格完整示例)
- [1.1.x 与 3.x 主要差异对照](#1.1.x 与 3.x 主要差异对照)
- [各 API 要点速查](#各 API 要点速查)
- [SSL_read / SSL_write 与「明文/密文」](#SSL_read / SSL_write 与「明文/密文」)
- [SSL_read / SSL_write 内部分层流程(Mermaid)](#SSL_read / SSL_write 内部分层流程(Mermaid))
- [返回值与 SSL_get_error 速查](#返回值与 SSL_get_error 速查)
- [进阶一小步:SNI 与系统默认证书校验](#进阶一小步:SNI 与系统默认证书校验)
- 教学代码局限与生产环境建议
- 常见问题排查
- 免责声明
概述
本文说明如何用 C 语言配合 OpenSSL 完成 TLS 客户端的常见流程(初始化、TCP、握手、SSL_read / SSL_write),并对比 1.1.x 与 3.x 差异,便于学习与查阅。示例侧重教学演示;正文后半有「最小校验 + SNI」片段,仍不等于生产级 TLS 客户端。
快速上手:三步跑起来
| 步骤 | 你要做的事 |
|---|---|
| 1 | 安装开发包:头文件 openssl/ssl.h + 库 libssl / libcrypto(见下节) |
| 2 | 用 gcc 或 CMake 编译链接(见「编译与链接速查」) |
| 3 | 运行:./ssl_client www.example.com(或你编译出的可执行文件名) |
心智模型(一层图):
text
你的 HTTP/自定义协议明文
│
▼
SSL_write / SSL_read (只处理明文)
│
▼
OpenSSL TLS 记录层(自动加解密)
│
▼
TCP socket(connect 已通)
│
▼
互联网
环境准备:安装与版本
先确认版本(与文档、编译选项是否匹配):
bash
openssl version
pkg-config --modversion openssl # 若已安装 pkg-config 配置
| 环境 | 安装方式(示例) |
|---|---|
| Debian / Ubuntu | sudo apt install libssl-dev |
| Fedora / RHEL | sudo dnf install openssl-devel |
| macOS(Homebrew) | brew install openssl,按 brew info openssl 提示设置 PKG_CONFIG_PATH 或 -I/-L |
| Windows | 推荐 vcpkg :vcpkg install openssl,或安装官方预编译 SDK 后配置 INCLUDE/LIB;也可用 MSYS2 的 mingw-w64-*-openssl |
开发时常用头文件与库:
| 用途 | 头文件 | 链接 |
|---|---|---|
| TLS 接口 | openssl/ssl.h |
-lssl |
| 错误与部分底层 | openssl/err.h |
通常与 -lcrypto 一起 |
| 3.x Provider | openssl/provider.h |
同上 |
多数 Unix 上顺序为:-lssl -lcrypto (若遇未定义引用,可尝试把 -lssl 写在 -lcrypto 前或后,视链接器而定)。
编译与链接速查(gcc / pkg-config / CMake)
一行 gcc(Linux 等,库在默认搜索路径)
bash
gcc -O2 -Wall ssl_client.c -o ssl_client -lssl -lcrypto
使用 pkg-config(路径非标准时更方便)
bash
gcc -O2 -Wall ssl_client.c -o ssl_client $(pkg-config --cflags --libs openssl)
CMake 最小示例(跨平台常用)
cmake
cmake_minimum_required(VERSION 3.16)
project(ssl_client C)
find_package(OpenSSL REQUIRED)
add_executable(ssl_client ssl_client.c)
target_link_libraries(ssl_client PRIVATE OpenSSL::SSL OpenSSL::Crypto)
配置时若 CMake 找不到 OpenSSL,可指定:
bash
cmake -B build -DOPENSSL_ROOT_DIR=/path/to/openssl ...
核心对象与典型调用顺序
| 对象 | 角色 | 典型数量 |
|---|---|---|
SSL_CTX |
客户端「上下文」:验证模式、默认选项等;可复用到多个连接 | 每进程或每类客户端常建 1 个 |
SSL |
单次 TLS 会话:绑定一个已 connect 的 socket fd |
每个 TCP 连接 1 个 |
每个 TCP 连接重复
通常一次
SSL_CTX_new(TLS_client_method)
socket + connect
SSL_new(ctx)
SSL_set_fd(ssl, sock)
SSL_connect(ssl)
SSL_read / SSL_write
SSL_free → close
程序整体逻辑(九部分)
| 部分 | 内容 |
|---|---|
| 1 | 创建 SSL_CTX(或 3.x 下加载 Provider),记录握手参数与 TLS 相关状态 |
| 2 | 将主机名解析为 IP(示例 1.1 用 gethostbyname,3.x 示例推荐 getaddrinfo) |
| 3 | 建立到服务器 443 的 TCP 连接(此时尚未加密) |
| 4 | SSL_new / SSL_set_fd / SSL_connect:在 TCP 上完成 TLS 握手 |
| 5 | 打印协商得到的对称加密套件(如 SSL_get_cipher) |
| 6 | 打印对端 X.509 的 Subject / Issuer(不等于已验证证书) |
| 7 | SSL_write 发送简单 HTTP 请求(明文应用数据) |
| 8 | SSL_read 循环读取响应(应用层得到明文) |
| 9 | SSL_free / close / SSL_CTX_free(3.x 再 OSSL_PROVIDER_unload)等资源清理 |
流程串:DNS(或解析)→ TCP → TLS 握手 → 查看套件与证书 → 加密信道上读写 HTTP → 清理。
九部分一览(时间轴示意):
text
[1 初始化/CTX] → [2 解析主机] → [3 TCP connect]
→ [4 SSL_connect 握手] → [5 套件] [6 证书信息]
→ [7 SSL_write 请求] → [8 SSL_read 响应] → [9 释放]
TLS 握手与应用数据(示意)
握手阶段由 SSL_connect 驱动(内部多次读写 socket);成功后应用层只用 SSL_read / SSL_write 传明文。
Server TCP OpenSSL App Server TCP OpenSSL App loop [握手多轮] SSL_connect(一次调用) 发送/接收握手记录 字节流 字节流 字节流 握手完成 SSL_write(HTTP 明文) TLS 密文记录 TLS 密文记录 SSL_read → HTTP 明文
OpenSSL 1.1.x 风格完整示例
基于 OpenSSL 1.1.x ,适用于 Linux / Unix 类环境编译运行。
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#define SERVER_PORT "443"
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "Usage: %s <hostname>\n", argv[0]);
exit(1);
}
const char *hostname = argv[1];
/* 第 1 部分:创建 SSL_CTX */
SSL_library_init();
OpenSSL_add_all_algorithms();
SSL_load_error_strings();
const SSL_METHOD *method = TLS_client_method();
SSL_CTX *ctx = SSL_CTX_new(method);
if (!ctx) {
ERR_print_errors_fp(stderr);
exit(1);
}
/* 第 2 部分:解析主机名 */
struct hostent *host = gethostbyname(hostname);
if (!host) {
fprintf(stderr, "DNS resolution failed\n");
SSL_CTX_free(ctx);
exit(1);
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(SERVER_PORT));
memcpy(&server_addr.sin_addr, host->h_addr_list[0], sizeof(struct in_addr));
/* 第 3 部分:建立 TCP 连接 */
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
SSL_CTX_free(ctx);
exit(1);
}
if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
close(sock);
SSL_CTX_free(ctx);
exit(1);
}
/* 第 4 部分:建立 SSL 连接 */
SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, sock);
if (SSL_connect(ssl) <= 0) {
ERR_print_errors_fp(stderr);
SSL_free(ssl);
close(sock);
SSL_CTX_free(ctx);
exit(1);
}
/* 第 5 部分:打印加密套件 */
printf("Cipher: %s\n", SSL_get_cipher(ssl));
/* 第 6 部分:打印服务器证书 */
X509 *cert = SSL_get_peer_certificate(ssl);
if (cert) {
char subject[256], issuer[256];
X509_NAME_oneline(X509_get_subject_name(cert), subject, sizeof(subject));
X509_NAME_oneline(X509_get_issuer_name(cert), issuer, sizeof(issuer));
printf("Subject: %s\n", subject);
printf("Issuer: %s\n", issuer);
X509_free(cert);
} else {
printf("No certificate provided\n");
}
/* 第 7 部分:发送 HTTP 请求 */
const char *request =
"GET / HTTP/1.0\r\n"
"Host: %s\r\n"
"\r\n";
char req[512];
snprintf(req, sizeof(req), request, hostname);
SSL_write(ssl, req, strlen(req));
SSL_shutdown(ssl);
/* 第 8 部分:读取服务器响应 */
char buf[4096];
int bytes;
while ((bytes = SSL_read(ssl, buf, sizeof(buf) - 1)) > 0) {
buf[bytes] = '\0';
printf("%s", buf);
}
/* 第 9 部分:清理资源 */
SSL_free(ssl);
close(sock);
SSL_CTX_free(ctx);
return 0;
}
编译示例:
bash
gcc ssl_client.c -o ssl_client -lssl -lcrypto
./ssl_client www.example.com
OpenSSL 3.x 风格完整示例
要点:Provider 、可省略旧式全局初始化、解析主机名推荐 getaddrinfo。
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/socket.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/provider.h>
#define SERVER_PORT "443"
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "Usage: %s <hostname>\n", argv[0]);
return 1;
}
const char *hostname = argv[1];
/* 第 1 部分:OpenSSL 3.x 初始化 */
OSSL_PROVIDER *prov_default = OSSL_PROVIDER_load(NULL, "default");
if (!prov_default) {
fprintf(stderr, "Failed to load default provider\n");
return 1;
}
SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
if (!ctx) {
ERR_print_errors_fp(stderr);
OSSL_PROVIDER_unload(prov_default);
return 1;
}
/* 第 2 部分:主机名解析 */
struct addrinfo hints = {0}, *res;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(hostname, SERVER_PORT, &hints, &res) != 0) {
perror("getaddrinfo");
SSL_CTX_free(ctx);
OSSL_PROVIDER_unload(prov_default);
return 1;
}
struct sockaddr_in *addr = (struct sockaddr_in *)res->ai_addr;
/* 第 3 部分:建立 TCP 连接 */
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
freeaddrinfo(res);
SSL_CTX_free(ctx);
OSSL_PROVIDER_unload(prov_default);
return 1;
}
if (connect(sock, (struct sockaddr *)addr, sizeof(*addr)) < 0) {
perror("connect");
close(sock);
freeaddrinfo(res);
SSL_CTX_free(ctx);
OSSL_PROVIDER_unload(prov_default);
return 1;
}
freeaddrinfo(res);
/* 第 4 部分:SSL 连接 */
SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, sock);
if (SSL_connect(ssl) <= 0) {
ERR_print_errors_fp(stderr);
SSL_free(ssl);
close(sock);
SSL_CTX_free(ctx);
OSSL_PROVIDER_unload(prov_default);
return 1;
}
printf("Cipher: %s\n", SSL_get_cipher(ssl));
X509 *cert = SSL_get_peer_certificate(ssl);
if (cert) {
char subject[256], issuer[256];
X509_NAME_oneline(X509_get_subject_name(cert), subject, sizeof(subject));
X509_NAME_oneline(X509_get_issuer_name(cert), issuer, sizeof(issuer));
printf("Subject: %s\n", subject);
printf("Issuer: %s\n", issuer);
X509_free(cert);
}
const char *request =
"GET / HTTP/1.0\r\n"
"Host: %s\r\n"
"\r\n";
char req[512];
snprintf(req, sizeof(req), request, hostname);
SSL_write(ssl, req, strlen(req));
SSL_shutdown(ssl);
char buf[4096];
int bytes;
while ((bytes = SSL_read(ssl, buf, sizeof(buf) - 1)) > 0) {
buf[bytes] = '\0';
printf("%s", buf);
}
SSL_free(ssl);
close(sock);
SSL_CTX_free(ctx);
OSSL_PROVIDER_unload(prov_default);
return 0;
}
1.1.x 与 3.x 主要差异对照
| 项目 | OpenSSL 1.1.x | OpenSSL 3.x |
|---|---|---|
| 算法提供方式 | 多与库编译绑定 | Provider 机制,可加载 default(及可选 fips 等) |
| 旧初始化 | SSL_library_init()、OpenSSL_add_all_algorithms()、SSL_load_error_strings() |
多数场景可省略;以 Provider / 库默认行为为主 |
SSL_CTX_new(TLS_client_method()) |
✅ 常用 | ✅ 仍推荐 |
| 主机名解析 | 示例用 gethostbyname(偏旧) |
推荐 getaddrinfo(IPv4/IPv6、可移植性更好) |
各 API 要点速查
| API / 片段 | 含义 |
|---|---|
SSL_CTX |
「配置中心」:TLS 版本范围、套件、验证策略等;一个 SSL_CTX 可服务多连接 |
gethostbyname / getaddrinfo |
网络层:域名 → 地址;生产更推荐 getaddrinfo |
socket + connect |
明文 TCP;TLS 跑在 TCP 之上 |
SSL_new / SSL_set_fd / SSL_connect |
绑定 fd 并完成握手(ClientHello、协商、密钥等) |
SSL_get_cipher |
查看最终协商的对称算法(如 TLS_AES_256_GCM_SHA384) |
SSL_get_peer_certificate |
取对端证书;打印信息 ≠ 验证通过 |
SSL_write / SSL_read |
应用层与 明文 打交道;加解密在库内完成 |
SSL_free / close / SSL_CTX_free |
C 库需手动释放;3.x 另需 OSSL_PROVIDER_unload |
SSL_read / SSL_write 与「明文/密文」
结论 :SSL_write 传入的是明文 ;SSL_read 返回缓冲区中的也是解密后的明文。线路上由 OpenSSL 完成分段、加密、MAC/AEAD、发送与逆过程。
| 操作 | 交给 OpenSSL 的缓冲区 | 经 TCP 发送/接收的载荷(概念上) |
|---|---|---|
SSL_write(ssl, buf, len) |
明文 | 密文(及 TLS 记录封装) |
SSL_read(ssl, buf, len) |
(输出)明文 | 从 socket 读入的是密文记录,经解密后写入 buf |
错误用法(重复加解密):
c
/* 错误:不要先自行加密再 SSL_write */
/* encrypt(data); SSL_write(ssl, data, len); */
/* 错误:不要对 SSL_read 结果再 decrypt */
/* SSL_read(ssl, buf, len); decrypt(buf); */
正确用法 :直接对应用数据调用 SSL_write / SSL_read。
分层理解:
- 握手阶段涉及非对称密钥交换等;应用数据阶段
SSL_read/SSL_write使用已协商的对称密钥保护流量。
SSL_read / SSL_write 内部分层流程(Mermaid)
TCP
OpenSSL TLS 记录层
应用层
SSL_write(ssl, 明文, len)
SSL_read(ssl, buf, len)
分段 / 加密 / MAC 或 AEAD
收记录 / 校验 / 解密
send 密文记录
recv 密文记录
返回值与 SSL_get_error 速查
SSL_read / SSL_write / SSL_connect 返回值 ≤ 0 时,必须用 SSL_get_error(ssl, ret) 区分「重试」「关闭」「真错」。
SSL_get_error 结果 |
常见含义 | 应用层建议 |
|---|---|---|
SSL_ERROR_NONE |
无错误(与当前调用不搭配时出现,多表示逻辑误用) | 查文档与调用顺序 |
SSL_ERROR_WANT_READ |
当前需先等 可读 再重试同一操作 | 配合 select/poll/非阻塞 fd |
SSL_ERROR_WANT_WRITE |
当前需先等 可写 再重试 | 同上 |
SSL_ERROR_ZERO_RETURN |
对端有序关闭(收到 close_notify) | 视为 EOF,停止读 |
SSL_ERROR_SYSCALL |
底层 read/write 出错或意外 EOF |
看 errno;可用 ERR_print_errors_fp 辅助 |
SSL_ERROR_SSL |
协议或库内错误 | ERR_print_errors_fp(stderr) |
阻塞 socket 教学代码 里常省略重试循环;非阻塞或事件驱动 时必须按 WANT_READ / WANT_WRITE 重入。
进阶一小步:SNI 与系统默认证书校验
访问公网 HTTPS 虚拟主机时,服务端往往依赖 SNI 选证书;务必在 SSL_connect 之前设置主机名(OpenSSL 常用宏):
c
/* 需已包含 openssl/ssl.h(其中提供 SSL_set_tlsext_host_name 宏) */
/* 在 SSL_new 之后、SSL_connect 之前: */
if (!SSL_set_tlsext_host_name(ssl, hostname)) {
fprintf(stderr, "SNI failed\n");
/* 清理并退出 */
}
使用系统默认 CA 校验对端证书(仍建议再校验主机名):
c
/* 在 SSL_CTX_new 之后、SSL_connect 之前: */
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
if (!SSL_CTX_set_default_verify_paths(ctx)) {
fprintf(stderr, "verify paths failed\n");
}
说明:SSL_VERIFY_PEER 会要求验证证书链;主机名是否与 hostname 一致 还需 X509_check_host 等(OpenSSL 1.0.2+)或对 X509 自行校验,生产环境请查阅当前版本手册中的 hostname verification 推荐写法。
| 项 | 教学示例 | 加上本节后 |
|---|---|---|
| SNI | 可能缺,多站点 HTTPS 易失败或证书奇怪 | 与浏览器行为更接近 |
| 证书 | 不校验,易被中间人 | 链由系统 CA 校验;主机名仍需单独处理 |
教学代码局限与生产环境建议
| 点 | 说明 |
|---|---|
| 证书 | 示例未做链校验、主机名匹配、有效期、吊销等;浏览器/安全客户端必须做 |
| SNI | 访问虚拟主机 HTTPS 时常需 SSL_set_tlsext_host_name(或等价 API),否则可能握手失败或证书不匹配 |
| 关闭顺序 | 先 SSL_write 再立刻 SSL_shutdown 再读,对部分服务端或协议细节可能不理想;生产宜按半关闭、错误码与 SSL_get_error 规范处理 |
| 可移植性 | Windows 上需 Winsock 与不同链接库;示例以 POSIX 为主 |
| HTTP/1.0 | 仅为演示;现代服务多为 HTTP/1.1 / 2 / 3 |
常见问题排查
| 现象 | 可能原因 | 可尝试 |
|---|---|---|
找不到 openssl/ssl.h |
未装 -dev 包或包含路径未指定 |
安装 libssl-dev / openssl-devel;Windows 设 INCLUDE 或用 CMake OPENSSL_ROOT_DIR |
链接报错 undefined reference to SSL_* |
未链 -lssl 或顺序不对 |
gcc ... -lssl -lcrypto;CMake 用 OpenSSL::SSL |
SSL_connect 失败 |
协议/套件不匹配、证书校验失败、缺 SNI、防火墙 | ERR_print_errors_fp;对 HTTPS 加上 SSL_set_tlsext_host_name;临时调试用 openssl s_client -connect host:443 |
| 能握手但 HTTP 异常 | HTTP 版本、Host 头、HTTP/2 非明文升级路径 | 改用 GET / HTTP/1.1 + Host;HTTP/2 需 ALPN(SSL_CTX_set_alpn_protos 等),超出本文入门范围 |
| Windows 与 Linux 行为不一致 | Winsock 初始化、路径、库位数(x86/x64) | Windows 需 WSAStartup;库与编译目标一致 |
命令行对照服务器(不写代码也能测 TLS):
bash
openssl s_client -connect example.com:443 -servername example.com
其中 -servername 即 SNI,应与浏览器访问的域名一致。
免责声明
示例与说明仅供学习;TLS/证书行为以所用 OpenSSL 版本、系统根证书与具体服务器配置为准。部署到生产前须完成安全审计与正确的主机名与证书验证策略。
主题:OpenSSL C 语言 TLS 客户端完整示例(1.1.x / 3.x、上手与排查)。