epoll + 非阻塞IO + openssl

epoll搭配非阻塞IO可以更为高效。
但openssl在搭配非阻塞IO时会使得SSL_read, SSL_accept等函数直接返回,导致无法连接上,而循环SSL_accept又会阻塞程序,因为你也不知道下一个连接是什么时候,故非阻塞IO在搭配openssl时较为艰难
但是在采用epoll后,下一次连接到达时epoll会返回,由此可以确定有客户端想要建立连接,在此为什么不说是建立ssl连接呢?因为ssl连接和tcp不是一个概念,epoll只能确定的是由客户端想要建立tcp连接,如果程序在处理连接时死循环,而恰巧客户端只建立tcp连接而不进行ssl握手,那你的程序会卡死。
所以可以在循环中加入循环次数,如果想要建立连接则会在很多时间内连接上,否则则会由于循环次数耗尽跳出循环。同样的,当有读事件到达时可以循环调用SSL_read。

/*************************************************************************
    > File Name: epoll_ssl.cc
    > Author: hsz
    > Brief:
    > Created Time: Tue 15 Mar 2022 04:58:28 PM CST
 ************************************************************************/
// utils和log可以去掉,自己编写的库,方便调试
#include <utils/utils.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/epoll.h>
#include <openssl/crypto.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <map>
#include <log/log.h>

#define LOG_TAG "epoll ssl"

#define LOCAL_IP "172.25.12.215"
#define LOCAL_PORT 8000

#define CA_CERT_FILE        "ca.crt"
#define SERVER_KEY_FILE     "server.key"
#define SERVER_CERT_FILE    "server.crt"

int CreateSocket()
{
    int fd = ::socket(AF_INET, SOCK_STREAM, 0);
    assert(fd > 0 && "socket error");

    sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(LOCAL_IP);
    addr.sin_port = htons(LOCAL_PORT);

    int flag = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));

    int code = ::bind(fd, (sockaddr *)&addr, sizeof(addr));
    assert(code == 0 && "bind error");

    code = ::listen(fd, 1024);
    assert(code == 0 && "listen error");

    timeval tv = {0, 500 * 1000};
    setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(timeval));
    return fd;
}

void catch_signal(int sig)
{
    LOG_ASSERT(false, "");
}

int main(int argc, char **argv)
{
    signal(SIGSEGV, catch_signal);
    int srvFd = CreateSocket();
    SSL_library_init();
    SSL_load_error_strings();
    OpenSSL_add_all_algorithms();

    int efd = epoll_create(1024);
    assert(efd > 0);
    printf("epoll fd %d\n", efd);

    epoll_event events[1024];
    epoll_event ev;
    ev.data.fd = srvFd;
    ev.events = EPOLLET | EPOLLIN;
    epoll_ctl(efd, EPOLL_CTL_ADD, srvFd, &ev);

    std::map<int, SSL *> sslMap;
    SSL_CTX *ctx = nullptr;

    ctx = SSL_CTX_new(SSLv23_server_method());
    assert(ctx);
    // 不校验客户端证书
    SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr);
    // 加载CA的证书  
    if(!SSL_CTX_load_verify_locations(ctx, CA_CERT_FILE, nullptr)) {
        printf("SSL_CTX_load_verify_locations error!\n");
        return -1;
    }
    // 加载自己的证书  
    if(SSL_CTX_use_certificate_file(ctx, SERVER_CERT_FILE, SSL_FILETYPE_PEM) <= 0) {
        printf("SSL_CTX_use_certificate_file error!\n");
        return -1;
    }

    // 加载私钥
    if(SSL_CTX_use_PrivateKey_file(ctx, SERVER_KEY_FILE, SSL_FILETYPE_PEM) <= 0) {
        printf("SSL_CTX_use_PrivateKey_file error!\n");
        return -1;
    }

    // 判定私钥是否正确  
    if(!SSL_CTX_check_private_key(ctx)) {
        printf("SSL_CTX_check_private_key error!\n");
        return -1;
    }

    printf("CA证书、本地证书、私钥均已加载完毕\n");
    printf("server is listening...\n");

    static const char *https_response = 
        "HTTP/1.1 200 OK\r\nServer: httpd\r\nContent-Length: %d\r\nConnection: keep-alive\r\n\r\n";

    while (true) {
        printf("epoll wait...\n");
        int nev = epoll_wait(efd, events, sizeof(events) / sizeof(epoll_event), -1);
        if (nev < 0) {
            printf("epoll_wait error. [%d,%s]", errno, strerror(errno));
            break;
        }

        for (size_t i = 0; i < nev; ++i) {
            auto &event = events[i];
            if (event.data.fd == srvFd) {  // accept
                sockaddr_in addr;
                socklen_t len = sizeof(addr);
                int cfd = ::accept(srvFd, (sockaddr *)&addr, &len);
                if (cfd > 0) {
                    printf("accept client %d [%s:%d]\n", cfd, inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
                    SSL *ssl = SSL_new(ctx);
                    bool isSSLAccept = true;
                    if (ssl == nullptr) {
                        printf("SSL_new error.\n");
                        continue;
                    }
                    // static struct timeval tv = {0, 500 * 1000};
                    // setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(timeval));
                    // setsockopt(cfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(timeval));
                    int flags = ::fcntl(cfd, F_GETFL, 0);
                    ::fcntl(cfd, F_SETFL, flags | O_NONBLOCK);

                    SSL_set_fd(ssl, cfd);
                    int code;
                    int retryTimes = 0;
                    uint64_t begin = Time::SystemTime();
                    // 防止客户端连接了但不进行ssl握手, 单纯的增大循环次数无法解决问题,本地大概循环4000次,chrome连接会循环20000多次
                    while ((code = SSL_accept(ssl)) <= 0 && retryTimes++ < 100) {
                        if (SSL_get_error(ssl, code) != SSL_ERROR_WANT_READ) {
                            printf("ssl accept error. %d\n", SSL_get_error(ssl, code));
                            break;
                        }
                        msleep(1);
                    }
                    uint64_t end = Time::SystemTime();

                    printf("code %d, retry times %d\n", code, retryTimes);
                    if (code != 1) {
                        isSSLAccept = false;
                        ::close(cfd);
                        SSL_free(ssl);
                        continue;
                    }

                    printf("ssl accept success. cost %lu ms\n", end - begin);
                    ev.data.fd = cfd;
                    ev.events = EPOLLET | EPOLLIN;
                    epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &ev);
                    sslMap.insert(std::make_pair(cfd, ssl));
                } else {
                    perror("accept error");
                }
                continue;
            }

            auto it = sslMap.find(event.data.fd);
            assert(it != sslMap.end());

            if (event.events & (EPOLLRDHUP | EPOLLHUP)) {
                printf("client %d quit!\n", event.data.fd);
                close(event.data.fd);
                SSL_shutdown(it->second);
                SSL_free(it->second);
                sslMap.erase(it);
                epoll_ctl(efd, EPOLL_CTL_DEL, event.data.fd, nullptr);
                continue;
            }

            if (event.events & EPOLLIN) {
                char buf[1024] = {0};
                int readSize = SSL_read(it->second, buf, sizeof(buf));
                if (readSize <= 0) {
                    printf("SSL_read error. %d\n", SSL_get_error(it->second, readSize));
                    continue;
                }
                printf("read: %d\n%s\n", readSize, buf);

                char sendBuf[1024] = {0};
                int fmtSize = sprintf(sendBuf, https_response, readSize);

                printf("*********************\n%s*********************\n", sendBuf);
                int writeSize = SSL_write(it->second, sendBuf, strlen(sendBuf));    // 发送响应头
                printf("format size %d, write size %d\n", fmtSize, writeSize);
                if (writeSize <= 0) {
                    printf("SSL_write error. %d\n", SSL_get_error(it->second, writeSize));
                }
                writeSize = SSL_write(it->second, buf, readSize);   // 发送响应主体
                if (writeSize <= 0) {
                    printf("SSL_write error. %d\n", SSL_get_error(it->second, writeSize));
                }
                printf("format size %d, write size %d\n", fmtSize, writeSize);
            }
        }
    }

    for (auto it : sslMap) {
        close(it.first);
        SSL_free(it.second);
    }

    SSL_CTX_free(ctx);
    close(srvFd);
    close(efd);
    return 0;
}

使用chrome进行https连接结果
返回的内容就是chrome的http请求
请添加图片描述
请添加图片描述

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Epoll+线程池的工作原理如下: 1. 单线程创建epoll并等待,有I/O请求(socket)到达时,将其加入epoll并从线程池中取一个空闲工作者线程,将实际的业务交由工作者线程处理。 2. 当多个任务到来时,Epoll及时响应并将任务下发给特定的处理线程,完成对应的任务。 3. 如果只用单线程进行listen轮询监听的话,效率上实在是太低。而借助epoll的话就会很完美的解决这个问题。 4. 使用线程池的缘由是为了避免频繁创建和销毁线程,提高线程的复用率和效率。 代码示例: ```python import socket import threading import queue import select # 定义线程池类 class ThreadPool: def __init__(self, max_workers): self.max_workers = max_workers self._workers = [] self._task_queue = queue.Queue() self._init_workers() # 初始化线程池 def _init_workers(self): for i in range(self.max_workers): worker = threading.Thread(target=self._worker) worker.start() self._workers.append(worker) # 工作者线程 def _worker(self): while True: try: func, args, kwargs = self._task_queue.get() func(*args, **kwargs) except Exception as e: print(e) # 提交任务 def submit(self, func, *args, **kwargs): self._task_queue.put((func, args, kwargs)) # 定义服务端类 class Server: def __init__(self, host, port, max_workers): self.host = host self.port = port self.max_workers = max_workers self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.server_socket.bind((self.host, self.port)) self.server_socket.listen(5) self.thread_pool = ThreadPool(self.max_workers) # 处理客户端请求 def handle_request(self, client_socket, client_address): print(f"Connected by {client_address}") while True: data = client_socket.recv(1024) if not data: break client_socket.sendall(data) client_socket.close() # 运行服务端 def serve_forever(self): print(f"Server is running on {self.host}:{self.port}") while True: client_socket, client_address = self.server_socket.accept() self.thread_pool.submit(self.handle_request, client_socket, client_address) # 运行服务端 if __name__ == '__main__': server = Server('localhost', 8888, 10) server.serve_forever() ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值