You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于epoll的非阻塞OpenSSL服务器实现求助

基于epoll的非阻塞OpenSSL服务器实现求助

嘿,太能体会你现在的挫败感了——折腾了好久的非阻塞OpenSSL+epoll服务器,思路耗光的滋味真的糟透了!我之前做类似实现的时候踩过一堆坑,结合你贴的代码片段,给你梳理下核心的注意点和修复方向:

先划几个非阻塞OpenSSL+epoll配合的致命坑

  1. 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支持部分写入和移动写缓冲,适配非阻塞场景。

  2. 动态调整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,会导致大量不必要的事件触发,拖垮性能。
  3. 给每个连接维护状态上下文
    你代码里没看到这部分,但这是必须的!每个连接要把fdSSL*、当前状态(握手进行中/已连接/关闭中)、未发完的缓冲这些打包成一个结构体,比如:

    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对象是谁,也没法处理半完成的读写操作。

  4. 非阻塞读写必须循环/分阶段处理
    拿你定义的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

火山引擎 最新活动