C 语言本身没有内置的 SSL/TLS 支持,SSL/TLS 是一个复杂的加密协议栈,需要借助第三方库来实现,最常用、最权威的库是 OpenSSL。

(图片来源网络,侵删)
当我们在 C 语言中谈论 "SSL Socket" 时,我们通常指的是:使用 OpenSSL 库,在标准的 TCP Socket 之上,构建一个安全的、加密的通信通道。
这个过程可以形象地理解为:
- 建立地基:使用标准的
socket(),bind(),listen(),accept()(服务器端) 或socket(),connect()(客户端) 函数创建和连接一个普通的 TCP Socket。 - 安装安全门锁:在 TCP 连接建立后,使用 OpenSSL 的函数对这个 Socket 进行“包装”或“升级”,将其从一个明文的 Socket 变成一个加密的 SSL Socket。
- 安全通信:之后,你不再使用
send()/recv(),而是使用 OpenSSL 提供的SSL_write()/SSL_read()函数进行数据收发,所有数据都会在发送前自动加密,在接收后自动解密。
核心组件
在开始编程之前,需要了解 OpenSSL 中几个核心的数据结构:
SSL_CTX(SSL Context): SSL 上下文,你可以把它看作是 SSL/TLS 连接的“工厂”或“配置中心”,它包含了 SSL/TLS 的全局配置,比如使用的协议版本(TLS 1.2, 1.3)、加密套件、证书、私钥等,创建一个SSL对象时,通常需要一个SSL_CTX对象。SSL: 代表一个具体的 SSL/TLS 连接,每个客户端连接到服务器时,都会创建一个独立的SSL对象,它包含了该连接的状态信息,比如当前加密会话、密钥等,所有的读写操作都是通过这个SSL对象来完成的。BIO(Basic Input/Output): 这是 OpenSSL 的 I/O 抽象层,它是一个强大的工具,可以将 SSL/TLS 流连接到任何底层的 I/O 通道上,比如文件描述符(Socket)、内存、甚至自定义的回调函数,我们最常用的就是将 SSL 连接到 Socket 的文件描述符上。
编程步骤
下面我们分别介绍 SSL 服务器和客户端的编程流程。

(图片来源网络,侵删)
准备工作:编译时链接 OpenSSL
在编译你的 C 程序时,你需要链接 OpenSSL 库,通常使用以下命令:
# 对于简单的程序 gcc -o myssl_app myssl_app.c -lssl -lcrypto # 对于更复杂的程序,可能需要指定路径 gcc -o myssl_app myssl_app.c -I/usr/local/ssl/include -L/usr/local/ssl/lib -lssl -lcrypto
-lssl: 链接 OpenSSL 的 SSL 库。-lcrypto: 链接 OpenSSL 的加密算法库(libcrypto),libssl依赖它。-I和-L: OpenSSL 不在系统默认路径下,需要指定其头文件和库文件的路径。
SSL 服务器端实现
服务器端需要持有自己的证书和私钥,以便向客户端证明自己的身份。
步骤:
- 初始化 OpenSSL 库。
- 创建 SSL 上下文 (
SSL_CTX)。 - 加载证书和私钥到上下文中。
- 创建 TCP Socket,并绑定、监听。
- 进入主循环,接受客户端连接。
- 将新的 Socket 连接“包装”成 SSL 连接 (
SSL)。 - 执行 SSL 握手。
- 通过 SSL 连接进行安全通信 (
SSL_write/SSL_read)。 - 关闭连接和清理资源。
示例代码框架:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
// 初始化 OpenSSL
void init_openssl() {
SSL_load_error_strings();
OpenSSL_add_ssl_algorithms();
}
// 清理 OpenSSL
void cleanup_openssl() {
EVP_cleanup();
}
// 创建 SSL 上下文
SSL_CTX *create_context() {
const SSL_METHOD *method;
SSL_CTX *ctx;
// 使用 TLSv1_2_server_method 或 TLS_server_method
method = TLS_server_method();
ctx = SSL_CTX_new(method);
if (!ctx) {
perror("Unable to create SSL context");
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
return ctx;
}
// 配置上下文,加载证书和私钥
void configure_context(SSL_CTX *ctx) {
// 加载证书文件
if (SSL_CTX_use_certificate_file(ctx, "server-cert.pem", SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
// 加载私钥文件
if (SSL_CTX_use_PrivateKey_file(ctx, "server-key.pem", SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
}
int main() {
int sock;
struct sockaddr_in addr;
init_openssl();
SSL_CTX *ctx = create_context();
configure_context(ctx);
// 1. 创建 TCP Socket
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("Unable to create socket");
exit(EXIT_FAILURE);
}
addr.sin_family = AF_INET;
addr.sin_port = htons(4433);
addr.sin_addr.s_addr = INADDR_ANY;
// 2. 绑定和监听
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("Unable to bind");
exit(EXIT_FAILURE);
}
if (listen(sock, 1) < 0) {
perror("Unable to listen");
exit(EXIT_FAILURE);
}
// 3. 接受客户端连接
printf("Server is listening on port 4433...\n");
int client = accept(sock, (struct sockaddr*)&addr, (socklen_t*)&sizeof(addr));
if (client < 0) {
perror("Unable to accept");
exit(EXIT_FAILURE);
}
// 4. 将 Socket 包装成 SSL 连接
SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, client);
// 5. 执行 SSL 握手
if (SSL_accept(ssl) <= 0) {
ERR_print_errors_fp(stderr);
} else {
char reply[1024] = "Hello, this is a secure SSL server!";
// 6. 通过 SSL 安全通信
SSL_write(ssl, reply, strlen(reply));
}
// 7. 关闭连接
SSL_shutdown(ssl);
SSL_free(ssl);
close(client);
close(sock);
// 8. 清理
SSL_CTX_free(ctx);
cleanup_openssl();
return 0;
}
SSL 客户端实现
客户端需要验证服务器的证书(可选,但强烈推荐)。
步骤:
- 初始化 OpenSSL 库。
- 创建 SSL 上下文 (
SSL_CTX)。 - (可选但推荐) 加载 CA 证书,用于验证服务器证书。
- 创建 TCP Socket。
- 连接到服务器。
- 将 Socket 连接“包装”成 SSL 连接 (
SSL)。 - 执行 SSL 握手。
- 通过 SSL 连接进行安全通信 (
SSL_write/SSL_read)。 - 关闭连接和清理资源。
示例代码框架:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
// ... (init_openssl 和 cleanup_openssl 函数与服务器端相同) ...
// 创建 SSL 上下文
SSL_CTX *create_context() {
const SSL_METHOD *method;
SSL_CTX *ctx;
method = TLS_client_method();
ctx = SSL_CTX_new(method);
if (!ctx) {
perror("Unable to create SSL context");
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
return ctx;
}
// 配置客户端上下文,加载 CA 证书用于验证
void configure_context(SSL_CTX *ctx) {
// 加载受信任的 CA 证书
if (SSL_CTX_load_verify_locations(ctx, "ca-cert.pem", NULL) <= 0) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
// 设置验证深度和是否必须验证
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
SSL_CTX_set_verify_depth(ctx, 1);
}
int main() {
int sock;
struct sockaddr_in addr;
SSL *ssl;
SSL_CTX *ctx;
init_openssl();
ctx = create_context();
configure_context(ctx);
// 1. 创建 TCP Socket
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("Unable to create socket");
exit(EXIT_FAILURE);
}
addr.sin_family = AF_INET;
addr.sin_port = htons(4433);
// 替换为你的服务器 IP
if (inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) <= 0) {
perror("Invalid address/ Address not supported");
exit(EXIT_FAILURE);
}
// 2. 连接服务器
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("Unable to connect");
exit(EXIT_FAILURE);
}
// 3. 将 Socket 包装成 SSL 连接
ssl = SSL_new(ctx);
SSL_set_fd(ssl, sock);
// 4. 执行 SSL 握手
if (SSL_connect(ssl) <= 0) {
ERR_print_errors_fp(stderr);
} else {
char buf[1024] = {0};
// 5. 通过 SSL 安全通信
SSL_read(ssl, buf, sizeof(buf));
printf("Server says: %s\n", buf);
}
// 6. 关闭连接
SSL_shutdown(ssl);
SSL_free(ssl);
close(sock);
// 7. 清理
SSL_CTX_free(ctx);
cleanup_openssl();
return 0;
}
| 函数类别 | 服务器端 | 客户端 | 描述 |
|---|---|---|---|
| 上下文 | SSL_CTX_new(TLS_server_method()) |
SSL_CTX_new(TLS_client_method()) |
创建 SSL 上下文 |
SSL_CTX_use_certificate_file() |
SSL_CTX_load_verify_locations() |
加载证书/CA 证书 | |
SSL_CTX_use_PrivateKey_file() |
加载私钥 | ||
| 连接 | SSL_new() |
SSL_new() |
从上下文创建一个 SSL 连接对象 |
SSL_set_fd(ssl, socket_fd) |
SSL_set_fd(ssl, socket_fd) |
将 SSL 连接绑定到 Socket 文件描述符 | |
SSL_accept(ssl) |
SSL_connect(ssl) |
执行 SSL/TLS 握手 | |
| 通信 | SSL_write(ssl, data, len) |
SSL_write(ssl, data, len) |
加密发送数据 |
SSL_read(ssl, buf, len) |
SSL_read(ssl, buf, len) |
接收并解密数据 | |
| 关闭 | SSL_shutdown(ssl) |
SSL_shutdown(ssl) |
通知对方关闭 SSL 连接 |
SSL_free(ssl) |
SSL_free(ssl) |
释放 SSL 连接对象 | |
SSL_CTX_free(ctx) |
SSL_CTX_free(ctx) |
释放 SSL 上下文 |
获取证书和私钥
要运行上面的示例,你需要生成一些测试用的证书文件,你可以使用 OpenSSL 的命令行工具来完成:

(图片来源网络,侵删)
-
生成 CA 证书 (用于签名服务器证书)
openssl genrsa -out ca-key.pem 2048 openssl req -new -x509 -days 365 -key ca-key.pem -out ca-cert.pem -subj "/CN=My Test CA"
-
生成服务器私钥和证书签名请求 (CSR)
openssl genrsa -out server-key.pem 2048 openssl req -new -key server-key.pem -out server-csr.pem -subj "/CN=localhost"
-
使用 CA 签名服务器 CSR,生成服务器证书
openssl x509 -req -days 365 -in server-csr.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem
你将拥有 ca-cert.pem, server-key.pem, 和 server-cert.pem 这三个文件,可以用于上面的示例程序了。
高级主题与注意事项
- 错误处理:OpenSSL 的错误处理通过
ERR_get_error()等函数获取错误堆栈,并使用ERR_print_errors_fp()打印到文件,务必对每个可能失败的 OpenSSL 函数调用进行错误检查。 - 非阻塞 I/O:在高性能服务器中,Socket 和 SSL I/O 通常设置为非阻塞模式,配合
select(),poll(), 或epoll来实现事件驱动。 - SNI (Server Name Indication):如果一台服务器需要提供多个域名的 HTTPS 服务,客户端需要在握手时告诉它自己想访问哪个域名,这可以通过
SSL_set_tlsext_host_name()函数实现。 - 性能:SSL/TLS 握手是计算密集型操作,开销较大,对于高并发场景,通常会使用 会话恢复 或 会话缓存 来避免重复握手,提高性能。
希望这份详细的指南能帮助你理解和使用 C 语言中的 SSL Socket 编程!
