使用一个支持 SSL/TLS 的网络库来处理底层的 TCP 通信、握手和加密/解密,而你的业务逻辑则专注于处理 HTTP 请求和生成 HTTP 响应。

(图片来源网络,侵删)
目前最常用和最推荐的库是 OpenSSL,下面我将详细介绍如何使用 OpenSSL 来构建一个简单的 HTTPS 监听器。
核心概念
- HTTPS = HTTP + SSL/TLS:HTTPS 本质上是在 HTTP 协议(应用层)和 TCP 协议(传输层)之间加入了一个 SSL/TLS 加密层。
- SSL/TLS 握手:当客户端(如浏览器)连接到 HTTPS 服务器时,它们会进行一个“握手”过程,这个过程包括:
- 协商一个加密算法(如 AES-256, RSA)。
- 验证服务器的身份(通过数字证书)。
- 生成并交换对称密钥,用于后续的加密通信。
- 数字证书:这是服务器身份的“身份证”,它由受信任的证书颁发机构(CA)签发,包含服务器的公钥和一些身份信息,你需要为你的服务器准备一个证书文件(通常是
.crt或.pem)和对应的私钥文件(通常是.key)。- 自签名证书:用于开发和测试,它不受公共 CA 信任,所以浏览器会显示“不安全”的警告。
- 权威机构签名的证书:用于生产环境,需要购买或申请(如 Let's Encrypt 提供免费证书)。
准备工作:安装 OpenSSL 和生成证书
安装 OpenSSL
在 Linux (Debian/Ubuntu) 上:
sudo apt-get update sudo apt-get install libssl-dev
在 macOS 上 (使用 Homebrew):
brew install openssl
在 Windows 上,可以从 OpenSSL 官网 下载安装包。

(图片来源网络,侵删)
生成自签名证书和私钥
我们将创建一个名为 server.pem 的文件,它同时包含了证书和私钥(OpenSSL 可以方便地处理这种格式)。
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes
命令解释:
req -x509: 生成一个 X.509 格式的自签名证书。-newkey rsa:4096: 同时生成一个新的 4096 位的 RSA 密钥对。-keyout key.pem: 将私钥保存到key.pem文件。-out cert.pem: 将证书保存到cert.pem文件。-sha256: 使用 SHA-256 算法签名。-days 365: 证书有效期 365 天。-nodes: "No DES" 的缩写,表示私钥不使用密码加密,这样程序读取时更方便(但安全性较低,生产环境应使用密码保护)。
为了方便,我们将这两个文件合并成一个 server.pem:
cat cert.pem key.pem > server.pem
你有了 server.pem 文件,可以开始写代码了。
使用 OpenSSL 实现 HTTPS 监听器(C 语言)
下面的代码实现了一个简单的 HTTPS 服务器,它会监听 4433 端口,当有客户端连接时,它会发送一个 "Hello, HTTPS World!" 的响应。
#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 CERT_FILE "server.pem"
// 初始化 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);
}
return ctx;
}
// 配置 SSL 上下文,加载证书和私钥
void configure_context(SSL_CTX *ctx) {
// 加载证书
if (SSL_CTX_use_certificate_file(ctx, CERT_FILE, SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
// 加载私钥
if (SSL_CTX_use_PrivateKey_file(ctx, CERT_FILE, SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
}
int main() {
int sock;
struct sockaddr_in addr;
SSL_CTX *ctx;
SSL *ssl;
int client;
// --- 1. 初始化 ---
init_openssl();
ctx = create_context();
configure_context(ctx);
// --- 2. 创建 TCP 监听套接字 ---
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(PORT);
addr.sin_addr.s_addr = INADDR_ANY;
// 绑定套接字
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);
}
printf("HTTPS Server listening on port %d...\n", PORT);
// --- 3. 接受客户端连接 ---
client = accept(sock, (struct sockaddr*)&addr, (socklen_t*)&addr);
if (client < 0) {
perror("Unable to accept");
exit(EXIT_FAILURE);
}
// --- 4. 创建 SSL 连接 ---
ssl = SSL_new(ctx);
SSL_set_fd(ssl, client);
// 执行 SSL 握手
if (SSL_accept(ssl) <= 0) {
ERR_print_errors_fp(stderr);
} else {
char response[] = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello, HTTPS World!";
// 通过 SSL 连接发送数据
SSL_write(ssl, response, strlen(response));
printf("HTTPS message sent.\n");
}
// --- 5. 清理 ---
SSL_free(ssl);
close(client);
SSL_CTX_free(ctx);
close(sock);
cleanup_openssl();
return 0;
}
如何编译和运行
-
保存代码:将上面的代码保存为
https_server.c。 -
确保证书存在:确保
server.pem文件和https_server.c在同一个目录下。 -
编译:你需要链接 OpenSSL 的库 (
ssl和crypto)。gcc https_server.c -o https_server -lssl -lcrypto
-
运行:
./https_server
你会看到输出:
HTTPS Server listening on port 4433... -
测试:
- 打开你的浏览器。
- 在地址栏输入
https://localhost:4433。 - 由于我们使用的是自签名证书,浏览器会显示一个“不安全”的警告,点击“高级”,然后选择“继续访问”。
- 如果一切正常,你将看到页面内容:
Hello, HTTPS World!
代码分步解析
-
init_openssl()和cleanup_openssl():这是 OpenSSL 的标准初始化和清理函数,用于注册必要的算法和错误处理机制。
-
create_context():- 创建一个
SSL_CTX(SSL Context) 对象,这个对象可以看作是一个“工厂”或“配置模板”,用于后续创建多个 SSL 连接,我们指定了TLS_server_method来告诉 OpenSSL 我们要创建一个 TLS 服务器。
- 创建一个
-
configure_context():- 这是 HTTPS 的核心,它加载了之前生成的
server.pem文件,其中包含了服务器的证书和私钥,服务器在握手时会使用这些信息来证明自己的身份。
- 这是 HTTPS 的核心,它加载了之前生成的
-
TCP 监听部分:
- 这部分和普通的 C 语言 TCP 服务器完全一样:创建套接字 (
socket),绑定地址 (bind),开始监听 (listen),它只负责建立底层的 TCP 连接。
- 这部分和普通的 C 语言 TCP 服务器完全一样:创建套接字 (
-
SSL 连接部分:
accept():接受来自客户端的 TCP 连接,返回一个新的套接字描述符client。SSL_new():基于之前创建的ctx,创建一个新的SSL对象,代表一个具体的 SSL 连接。SSL_set_fd():将底层的 TCP 套接字 (client) 与这个SSL对象关联起来。SSL_accept():这是关键的一步!它会执行 SSL/TLS 握手过程,它会读取客户端的 "ClientHello" 消息,验证客户端,并协商加密参数,如果成功,后续的通信就是加密的了。SSL_write()和SSL_read():- 一旦握手成功,你就不能再使用
write()和read()了。 - 必须使用 OpenSSL 提供的
SSL_write()和SSL_read()函数,这些函数会在数据发送/接收前自动进行加密/解密,对上层应用是透明的。
- 一旦握手成功,你就不能再使用
SSL_free()和SSL_CTX_free():释放 SSL 连接和上下文对象所占用的资源。
进阶和注意事项
- 多线程/多进程:上面的例子一次只能处理一个客户端,在生产环境中,你需要使用多线程(每个连接一个线程)、I/O 多路复用(如
select,poll,epoll)或线程池来同时处理成千上万个并发连接。 - 错误处理:示例中的错误处理比较简单,一个健壮的服务器应该对
SSL_accept,SSL_read,SSL_write等函数的返回值进行更细致的检查,并根据不同的错误类型采取不同的恢复或关闭连接的措施。 - HTTP 协议解析:这个例子只是简单地发送了一个固定的字符串,一个真正的 Web 服务器需要解析 HTTP 请求头(如
GET /index.html HTTP/1.1),然后根据请求的路径和内容动态生成响应,你可以自己写解析逻辑,也可以集成像 libhttpd 或 mongoose 这样的轻量级 HTTP 库。 - 性能:SSL/TLS 握手是比较耗时的,为了优化性能,可以使用 会话恢复 机制,避免为每个连接都重新进行完整的握手,OpenSSL 支持会话缓存和 TLS 会话票证来实现这一点。
