OpenSSL:C 语言 TLS 客户端完整示例

OpenSSL:C 语言 TLS 客户端完整示例

目录

  1. 概述
  2. 快速上手:三步跑起来
  3. 环境准备:安装与版本
  4. [编译与链接速查(gcc / pkg-config / CMake)](#编译与链接速查(gcc / pkg-config / CMake))
  5. 核心对象与典型调用顺序
  6. 程序整体逻辑(九部分)
  7. [TLS 握手与应用数据(示意)](#TLS 握手与应用数据(示意))
  8. [OpenSSL 1.1.x 风格完整示例](#OpenSSL 1.1.x 风格完整示例)
  9. [OpenSSL 3.x 风格完整示例](#OpenSSL 3.x 风格完整示例)
  10. [1.1.x 与 3.x 主要差异对照](#1.1.x 与 3.x 主要差异对照)
  11. [各 API 要点速查](#各 API 要点速查)
  12. [SSL_read / SSL_write 与「明文/密文」](#SSL_read / SSL_write 与「明文/密文」)
  13. [SSL_read / SSL_write 内部分层流程(Mermaid)](#SSL_read / SSL_write 内部分层流程(Mermaid))
  14. [返回值与 SSL_get_error 速查](#返回值与 SSL_get_error 速查)
  15. [进阶一小步:SNI 与系统默认证书校验](#进阶一小步:SNI 与系统默认证书校验)
  16. 教学代码局限与生产环境建议
  17. 常见问题排查
  18. 免责声明

概述

本文说明如何用 C 语言配合 OpenSSL 完成 TLS 客户端的常见流程(初始化、TCP、握手、SSL_read / SSL_write),并对比 1.1.x 与 3.x 差异,便于学习与查阅。示例侧重教学演示;正文后半有「最小校验 + SNI」片段,仍不等于生产级 TLS 客户端。


快速上手:三步跑起来

步骤 你要做的事
1 安装开发包:头文件 openssl/ssl.h + 库 libssl / libcrypto(见下节)
2 gccCMake 编译链接(见「编译与链接速查」)
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 推荐 vcpkgvcpkg 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 会话:绑定一个已 connectsocket 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 建立到服务器 443TCP 连接(此时尚未加密)
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、上手与排查)。

相关推荐
zly35002 小时前
centos7 mysql 无法被远程连接
数据库·mysql
廿一夏2 小时前
MySql的增删改查
数据库·mysql·dba
瀚高PG实验室2 小时前
HGDB 4.5.8.8开启oracle兼容执行带聚合函数的SQL导致数据库进程被信号11杀死
数据库·sql·oracle·瀚高数据库
上海云盾-小余2 小时前
服务器被入侵后如何快速止损?从排查到加固的应急处置全流程
网络·网络协议·tcp/ip·安全·web安全
炘爚2 小时前
日志系统整体设计步骤以及功能函数梳理
运维·服务器·数据库
_下雨天.2 小时前
PostgreSQL日常维护
数据库·postgresql
神の愛2 小时前
本地连接MySql数据库报错??
数据库·mysql
黑牛儿2 小时前
MySQL 索引实战详解:为什么B+类型的索引查询更快
数据库·mysql
克莱因3582 小时前
思科 动态路由协议RIP配置
网络·指令·cisco·rip路由