- 初始化 OpenSSL
- 创建 TCP Socket
- 创建 SSL 上下文
- 创建 SSL 结构体
- 建立 SSL 连接
- 通过 SSL 进行加密通信
- 关闭和清理
下面我将分为 服务器端 和 客户端 两部分,并提供完整的代码示例。
准备工作:安装 OpenSSL
在开始之前,请确保你的系统上安装了 OpenSSL 开发库。
- 在 Ubuntu/Debian 上:
sudo apt-get update sudo apt-get install libssl-dev
- 在 CentOS/RHEL 上:
sudo yum install openssl-devel
- 在 macOS 上 (使用 Homebrew):
brew install openssl
核心概念
- SSL_CTX (SSL Context): SSL 上下文,它是一个结构体,包含了 SSL 会话所需的所有信息,比如密码套件列表、证书、私钥等,我们可以把它看作是一个“工厂”或“模板”,用于创建多个 SSL 结构体。
- SSL (SSL Structure): SSL 结构体,它代表一个具体的 SSL 连接,当你需要处理一个客户端连接时,你会从 SSL_CTX 创建一个 SSL 结构体。
- BIO (Basic I/O): OpenSSL 的 I/O 抽象层,通常我们直接使用标准的
read和write函数,这背后是通过BIO来实现的。BIO提供了一个统一的接口来处理各种 I/O,包括文件、Socket、内存等。
客户端 示例
客户端的流程相对简单:连接服务器 -> 建立 SSL 连接 -> 加密通信。
客户端代码 (ssl_client.c)
#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>
#define PORT 4433
#define SERVER_IP "127.0.0.1" // 服务器 IP 地址
// 初始化 OpenSSL
void init_openssl() {
SSL_load_error_strings();
OpenSSL_add_ssl_algorithms();
}
// 清理 OpenSSL
void cleanup_openssl() {
EVP_cleanup();
}
// 创建并建立 TCP Socket
int create_socket() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("Unable to create socket");
exit(EXIT_FAILURE);
}
return sock;
}
int main() {
int sock;
SSL_CTX *ctx;
SSL *ssl;
char buffer[1024] = {0};
// 1. 初始化 OpenSSL
init_openssl();
// 2. 创建 SSL 上下文
ctx = SSL_CTX_new(TLS_client_method());
if (!ctx) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
// 3. 创建 TCP Socket
sock = create_socket();
// 4. 设置服务器地址并连接
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
perror("Invalid address/ Address not supported");
exit(EXIT_FAILURE);
}
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("Connection Failed");
exit(EXIT_FAILURE);
}
printf("Connected to server.\n");
// 5. 创建 SSL 结构体并关联 Socket
ssl = SSL_new(ctx);
SSL_set_fd(ssl, sock);
// 6. 建立 SSL 连接 (执行 TLS 握手)
if (SSL_connect(ssl) <= 0) {
ERR_print_errors_fp(stderr);
} else {
printf("SSL connection established.\n");
}
// 7. 通过 SSL 进行加密通信
// 发送数据
char *msg = "Hello from SSL Client!";
SSL_write(ssl, msg, strlen(msg));
// 接收数据
int bytes = SSL_read(ssl, buffer, sizeof(buffer) - 1);
if (bytes > 0) {
buffer[bytes] = '\0';
printf("Server says: %s\n", buffer);
}
// 8. 关闭连接和清理
SSL_shutdown(ssl);
SSL_free(ssl);
close(sock);
SSL_CTX_free(ctx);
cleanup_openssl();
return 0;
}
服务器端 示例
服务器端的流程更复杂一些:创建 Socket -> 绑定 -> 监听 -> 接受连接 -> 建立 SSL 连接 -> 加密通信。
重要提示: 服务器需要证书和私钥来证明自己的身份,这里我们使用 OpenSSL 自带的测试证书,你可以从 OpenSSL 官网下载。
-
下载并解压 OpenSSL。
-
进入
apps目录,运行以下命令生成测试证书和私钥:# 生成私钥 openssl genrsa -out server.key 2048 # 生成证书签名请求 (CSR) openssl req -new -key server.key -out server.csr # 使用私钥和 CSR 生成自签名证书 openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
你会需要回答一些问题,比如国家、组织等,最终你会得到
server.key和server.crt两个文件,将它们和你的服务器代码放在同一个目录下。
服务器端代码 (ssl_server.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#define PORT 4433
// 初始化 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;
// 使用 TLS 服务器方法
method = TLS_server_method();
// 创建 SSL 上下文
ctx = SSL_CTX_new(method);
if (!ctx) {
perror("Unable to create SSL context");
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
// 加载证书
if (SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
// 加载私钥
if (SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
// 验证私钥是否正确
if (!SSL_CTX_check_private_key(ctx)) {
fprintf(stderr, "Private key does not match the public certificate\n");
exit(EXIT_FAILURE);
}
return ctx;
}
// 处理客户端连接
void handle_connection(SSL *ssl, int client_sock) {
char buffer[1024] = {0};
int bytes;
// 接收客户端数据
bytes = SSL_read(ssl, buffer, sizeof(buffer) - 1);
if (bytes > 0) {
buffer[bytes] = '\0';
printf("Client says: %s\n", buffer);
} else {
ERR_print_errors_fp(stderr);
}
// 发送响应数据
char *response = "Hello from SSL Server!";
SSL_write(ssl, response, strlen(response));
// 关闭 SSL 连接
SSL_shutdown(ssl);
SSL_free(ssl);
close(client_sock);
}
int main() {
int server_sock, client_sock;
struct sockaddr_in serv_addr, cli_addr;
SSL_CTX *ctx;
// 1. 初始化 OpenSSL
init_openssl();
// 2. 创建 SSL 上下文
ctx = create_context();
// 3. 创建 TCP Socket
server_sock = socket(AF_INET, SOCK_STREAM, 0);
if (server_sock < 0) {
perror("Unable to create socket");
exit(EXIT_FAILURE);
}
// 设置 SO_REUSEADDR 选项,避免地址占用问题
int opt = 1;
if (setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
// 绑定地址和端口
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT);
if (bind(server_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("Bind failed");
exit(EXIT_FAILURE);
}
printf("Server bound to port %d\n", PORT);
// 监听连接
if (listen(server_sock, 10) < 0) {
perror("Listen failed");
exit(EXIT_FAILURE);
}
printf("Server is listening...\n");
// 主循环,接受客户端连接
while (1) {
socklen_t cli_len = sizeof(cli_addr);
client_sock = accept(server_sock, (struct sockaddr *)&cli_addr, &cli_len);
if (client_sock < 0) {
perror("Accept failed");
continue;
}
printf("New connection from %s:%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
// 4. 为新连接创建 SSL 结构体
SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, client_sock);
// 5. 建立 SSL 连接 (执行 TLS 握手)
if (SSL_accept(ssl) <= 0) {
ERR_print_errors_fp(stderr);
} else {
printf("SSL connection established with client.\n");
}
// 6. 处理加密通信
handle_connection(ssl, client_sock);
}
// 清理 (这部分代码通常在服务器关闭时执行)
SSL_CTX_free(ctx);
close(server_sock);
cleanup_openssl();
return 0;
}
编译和运行
你需要使用 gcc 来编译这些 C 文件,并链接 OpenSSL 库。
编译命令:
# 编译服务器 gcc -o ssl_server ssl_server.c -lssl -lcrypto # 编译客户端 gcc -o ssl_client ssl_client.c -lssl -lcrypto
运行步骤:
-
确保你生成了
server.crt和server.key文件,并且它们与ssl_server.c在同一目录。 -
首先在终端运行服务器:
./ssl_server
你会看到 "Server is listening..." 的输出。
-
然后在另一个终端运行客户端:
./ssl_client
-
预期输出:
-
服务器端终端:
Server bound to port 4433 Server is listening... New connection from 127.0.0.1:54321 (端口号可能不同) SSL connection established with client. Client says: Hello from SSL Client! -
客户端终端:
Connected to server. SSL connection established. Server says: Hello from SSL Server!
-
常见问题和注意事项
-
-lssl -lcrypto的顺序很重要 在链接时,通常需要-lcrypto在-lssl之前,这是因为libssl库依赖于libcrypto库。 -
错误处理 OpenSSL 的函数在出错时通常会返回
0或NULL,你应该调用ERR_print_errors_fp(stderr);来打印详细的错误信息到标准错误输出,这对于调试至关重要。 -
证书验证 (客户端) 上面的客户端代码没有验证服务器的证书,在生产环境中,这是不安全的,你应该让客户端加载一个受信任的 CA 证书列表,然后使用
SSL_get_verify_result()或SSL_CTX_set_verify()来验证服务器证书的有效性(比如是否由可信的 CA 签发、是否过期、域名是否匹配等)。 -
内存泄漏
SSL_new,SSL_CTX_new等函数会分配内存,请务必确保在程序结束时调用对应的SSL_free,SSL_CTX_free等函数来释放资源,否则会导致内存泄漏。 -
现代 TLS 版本 示例代码中使用了
TLS_server_method()和TLS_client_method(),这是推荐的做法,因为它们会自动协商使用客户端和服务器都支持的、最新的安全 TLS 版本,避免使用过时的SSLv23_method或特定版本如TLSv1_2_method,除非你有特殊需求。
