基于epoll的非阻塞OpenSSL服务器实现求助
嘿,太能体会你现在的挫败感了——折腾了好久的非阻塞OpenSSL+epoll服务器,思路耗光的滋味真的糟透了!我之前做类似实现的时候踩过一堆坑,结合你贴的代码片段,给你梳理下核心的注意点和修复方向:
先划几个非阻塞OpenSSL+epoll配合的致命坑
SSL层必须单独设置非阻塞
我一开始也犯过这个错:只给socket加了O_NONBLOCK,结果epoll事件触发了,但SSL_accept()死活卡着,查了半天才发现——SSL对象本身也得设非阻塞!每个新建的SSL对象必须调用:SSL_set_nonblock(ssl, 1); SSL_set_mode(ssl, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_MODE_ENABLE_PARTIAL_WRITE);第一行是开非阻塞,第二行是让SSL支持部分写入和移动写缓冲,适配非阻塞场景。
动态调整epoll监听事件是核心
非阻塞SSL的握手、读写都不是一次就能完成的,必须根据SSL_accept()/SSL_read()/SSL_write()的返回值,动态修改epoll对该fd的监听事件:- 调用
SSL_accept()返回SSL_ERROR_WANT_WRITE:说明现在需要等fd可写才能继续握手,立刻给这个fd注册EPOLLOUT事件; - 返回
SSL_ERROR_WANT_READ:改回监听EPOLLIN; - 握手完成后,再根据业务需求(比如要给客户端发响应),动态加/减
EPOLLOUT监听。
别一上来就给所有fd固定监听EPOLLIN|EPOLLOUT,会导致大量不必要的事件触发,拖垮性能。
- 调用
给每个连接维护状态上下文
你代码里没看到这部分,但这是必须的!每个连接要把fd、SSL*、当前状态(握手进行中/已连接/关闭中)、未发完的缓冲这些打包成一个结构体,比如:typedef struct { int fd; SSL* ssl; enum { STATE_HANDSHAKE, STATE_CONNECTED, STATE_CLOSING } state; char write_buf[BUFFERSIZE]; size_t write_len; size_t write_pos; } ClientConn;然后把这个结构体的指针存在epoll_event的
data.ptr里,而不是只用data.fd——不然你根本不知道这个fd对应的SSL对象是谁,也没法处理半完成的读写操作。非阻塞读写必须循环/分阶段处理
拿你定义的TESTSTRING来说,非阻塞下SSL_write()大概率没法一次性把整个字符串发完,这时候要把未发送的内容存在write_buf里,等epoll通知fd可写的时候继续发。举个简单的写逻辑:ClientConn* conn = ev->data.ptr; ssize_t ret = SSL_write(conn->ssl, conn->write_buf + conn->write_pos, conn->write_len - conn->write_pos); if (ret > 0) { conn->write_pos += ret; if (conn->write_pos == conn->write_len) { // 数据发完了,去掉EPOLLOUT监听,避免无效触发 struct epoll_event upd_ev = {0}; upd_ev.events = EPOLLIN | EPOLLET; upd_ev.data.ptr = conn; epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &upd_ev); } } else if (ret == SSL_ERROR_WANT_WRITE) { // 需要等可写,确保epoll监听EPOLLOUT struct epoll_event upd_ev = {0}; upd_ev.events = EPOLLIN | EPOLLOUT | EPOLLET; upd_ev.data.ptr = conn; epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &upd_ev); } else { // 处理错误,关闭连接 SSL_shutdown(conn->ssl); SSL_free(conn->ssl); close(conn->fd); free(conn); }
针对你贴的代码片段的具体建议
先把你贴的截断代码整理好:
#define _GNU_SOURCE #include <unistd.h> #include <string.h> #include <asm-generic/socket.h> #include <openssl/crypto.h> #include <openssl/evp.h> #include <complex.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <wait.h> #include <arpa/inet.h> #include <errno.h> #include <sys/socket.h> #include <sys/epoll.h> #include <openssl/ssl.h> #include <openssl/err.h> #define PORT 8443 #define BUFFERSIZE 4096 #define MAXEVENTS 16 #define TESTSTRING "<html><p>I am a message</p></html>" int init(int * sfd); void runServer(int sfd); void eventLoop(struct epoll_event * ev, int...
这里给你补几个必加的点:
- init函数里必须设socket为非阻塞:
int init(int *sfd) { *sfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); struct sockaddr_in addr = {0}; addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(*sfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { perror("bind"); return -1; } if (listen(*sfd, SOMAXCONN) == -1) { perror("listen"); return -1; } fcntl(*sfd, F_SETFL, O_NONBLOCK); // 确保监听socket也是非阻塞,配合epoll边缘触发 return 0; } - OpenSSL全局初始化必须做:
在main函数开头加:SSL_load_error_strings(); OpenSSL_add_ssl_algorithms(); const SSL_METHOD *method = TLS_server_method(); SSL_CTX *ctx = SSL_CTX_new(method); if (!ctx) { ERR_print_errors_fp(stderr); exit(EXIT_FAILURE); } // 加载证书和私钥!没有这个SSL握手直接失败 if (SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM) <= 0 || SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM) <= 0) { ERR_print_errors_fp(stderr); exit(EXIT_FAILURE); } - 新连接处理逻辑:
当epoll触发监听sfd的EPOLLIN事件时,循环accept所有待处理的连接:while (1) { struct sockaddr in_addr; socklen_t in_len = sizeof(in_addr); int client_fd = accept4(sfd, &in_addr, &in_len, SOCK_NONBLOCK); if (client_fd == -1) { if (errno != EAGAIN && errno != EWOULDBLOCK) { perror("accept4"); } break; } // 创建连接上下文 ClientConn* conn = malloc(sizeof(ClientConn)); conn->fd = client_fd; conn->ssl = SSL_new(ctx); SSL_set_fd(conn->ssl, client_fd); SSL_set_nonblock(conn->ssl, 1); // 关键!设SSL为非阻塞 conn->state = STATE_HANDSHAKE; memset(conn->write_buf, 0, BUFFERSIZE); conn->write_len = 0; conn->write_pos = 0; // 注册epoll事件,开始监听读事件 struct epoll_event ev = {0}; ev.events = EPOLLIN | EPOLLET; ev.data.ptr = conn; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev); } - TESTSTRING要加HTTP头:
直接发HTML字符串浏览器会解析失败,要补响应头:snprintf(conn->write_buf, BUFFERSIZE, "HTTP/1.1 200 OK\r\nContent-Length: %lu\r\nContent-Type: text/html\r\n\r\n%s", (unsigned long)strlen(TESTSTRING), TESTSTRING); conn->write_len = strlen(conn->write_buf); conn->write_pos = 0;
最后提醒下:关闭连接的时候要按顺序来——先调用SSL_shutdown()(非阻塞下可能要调用两次),然后SSL_free(ssl),再close(fd),最后free上下文结构体,不然会有资源泄漏。
备注:内容来源于stack exchange,提问作者Fabrice




