C++葵花宝典|OpenSSL API入门和踩坑大全


文章目录

  • SSL学习笔记
    • OpenSSL库基础
    • 示例
      • Server
      • Client
      • 证书生成
    • 问题记录
      • 获取错误原因
      • 数据发送对方接受不到
      • SSL_connect在服务端异常时阻塞卡住
      • SSL_shutdown崩溃Broken pipe
        • 问题
        • 解决方式
        • 原理
  • 参考

SSL学习笔记 OpenSSL库基础 根据 官方的例子,OpenSSL常用的结构体和函数如下:
  • 初始化OpenSSL库
    • SSL_library_init():初始化SSL算法库函数
    • SSL_load_error_strings():载入所有SSL 错误消息
    • OpenSSL_add_all_algorithms(): 载入所有SSL 算法
  • 加载和验证证书
    • 通过SSL_CTX_new(SSL_METHOD *method)创建一个SSL_CTX *实例,用来保存证书的私钥。其中method通过TLS_client_method()、TLS_server_method() 和TLS_method()创建(一些写法中会提到SSLv23_method(),SSLv23_server_method(), SSLv23_client_method()等API,官方文档 明确提到已经过时了)。
    • 通过SSL_CTX_use_certificate_file()和SSL_CTX_use_PrivateKey_file()载入证书和私钥,SSL_CTX_check_private_key()来检查是否匹配。
  • 创建SSL实例,绑定socket
    • SSL_new(ctx)来创建一个ssl的实例,这个实例后续握手、读写、关闭等需要
    • 通过SSL_set_fd()来把SSL实力和socket句柄关联起来
  • SSL握手,有2种方式
    • SSL_accept(),被动的等待客户端,注意这里会阻塞
    • SSL_do_handshake
  • 收发
    • SSL_read()/SSL_write()来进行数据加解密和读写
    • 注意需要对返回值调用SSL_get_error()进一步判断,如果返回的是SSL_ERROR_WANT_READ,SSL_ERROR_WANT_WRITE代表需要再次调用
  • 释放
    • 释放ssl实例,SSL_shutdown()来关闭ssl连接,SSL_free()释放内存,别忘记还要调用close()关闭socket句柄
    • 释放ctx上下文实例,因为ctx是全局的,应该在程序退出前调用SSL_CTX_free()进行释放
示例 官方的例子可以参考:OpenSSL官网:Simple TLS Server
我的代码可以参见 code/tcp/05-simple-multi-thread-in-ssl/server/ssl_server.cpp
Server
/** @file main.h * @brief OpenSSL库使用示例,为了兼顾简单和实用,这里拿PerConnectionPerThread模型来演示。即一个连接一个线程 *,注意:实际中为了更高的性能,通常是搭配epoll I/O复用来实现。有单Reactor单线程,单Reactor多线程,主从Reactor多线程几种模型 * @author teng.qing * @date 2021/5/13 */#include #include #include #include #include #include #include #include #include #include /** @fn create_socket * @brief 创建一个监听的socket * @param [in]listenPort:监听端口 * @return */ int create_socket(int listenPort) { int sockFd = 0; struct sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_port = htons(listenPort); addr.sin_addr.s_addr = htonl(INADDR_ANY); sockFd = socket(PF_INET, SOCK_STREAM, 0); if (sockFd < 0) { printf("create socket error:%d", errno); exit(EXIT_FAILURE); }// SO_REUSEADDR int yesReuseAddr = 1; if (::setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &yesReuseAddr, sizeof(yesReuseAddr)) == -1) { std::cout << "setsockopt error:" << errno << std::endl; return 0; }if (bind(sockFd, (struct sockaddr *) &addr, sizeof(addr)) < 0) { std::cout << "Unable to bind:" << errno << std::endl; exit(EXIT_FAILURE); }if (listen(sockFd, SOMAXCONN) < 0) { std::cout << "Unable to listen:" << errno << std::endl; exit(EXIT_FAILURE); }return sockFd; }/** @fn init_openssl * @brief 全局初始化openssl库,只需要调用一次 * @return */ void init_openssl() { SSL_load_error_strings(); // 载入所有SSL 错误消息 OpenSSL_add_all_algorithms(); // 加载所有支持的算法 }/** @fn cleanup_openssl * @brief 退出前清理openssl * @return */ void cleanup_openssl() { EVP_cleanup(); }/** @fn create_context * @brief 创建一个全局SSL_CTX,存储证书等信息 * @return */ SSL_CTX *create_context() { const SSL_METHOD *method; SSL_CTX *ctx; /* 以SSL V2 和 V3 标准兼容方式产生一个SSL_CTX ,即SSL Content Text */ /* 也可以用SSLv2_server_method() 或SSLv3_server_method() 单独表示V2 或V3 标准*/ //method = SSLv3_server_method(); method = SSLv23_server_method(); ctx = SSL_CTX_new(method); if (!ctx) { perror("Unable to create SSL context"); ERR_print_errors_fp(stderr); exit(EXIT_FAILURE); }return ctx; }/** @fn configure_context * @brief 设置证书 * @param [in]ctx: SSL上下文 * @param [in]certPath: 证书文件 * @param [in]privateKeyPath: 私钥文件 * @return */ void configure_context(SSL_CTX *ctx, std::string certPath, std::string privateKeyPath) { SSL_CTX_set_ecdh_auto(ctx, 1); // 载入用户的数字证书, 此证书用来发送给客户端。证书里包含有公钥 if (SSL_CTX_use_certificate_file(ctx, certPath.c_str() /*"cert.pem"*/, SSL_FILETYPE_PEM) <= 0) { ERR_print_errors_fp(stderr); exit(EXIT_FAILURE); }// 载入用户私钥 if (SSL_CTX_use_PrivateKey_file(ctx, privateKeyPath.c_str()/*"key.pem"*/, SSL_FILETYPE_PEM) <= 0) { ERR_print_errors_fp(stderr); exit(EXIT_FAILURE); }// 检查用户私钥是否正确 if (!SSL_CTX_check_private_key(ctx)) { ERR_print_errors_fp(stdout); exit(1); } }/** @fn opensslErrorCheck * @brief opensslErrorCheck * @param [in]ssl: SSL实例 * @param [in]retCode: SSL_read/SSL_write返回值 * @param [in]isError: 是否确实发生了错误 * @return */ void opensslErrorCheck(SSL *ssl, int retCode, bool &isError) { // 处理ssl的错误码 int sslErr = SSL_get_error(ssl, retCode); isError = true; switch (sslErr) { case SSL_ERROR_WANT_READ: std::cout << "SSL_ERROR_WANT_READ" << std::endl; isError = false; break; case SSL_ERROR_WANT_WRITE: std::cout << "SSL_ERROR_WANT_WRITE" << std::endl; isError = false; break; case SSL_ERROR_NONE: // 没有错误发生,这种情况好像没怎么遇到过 std::cout << "SSL_ERROR_WANT_WRITE" << std::endl; break; case SSL_ERROR_ZERO_RETURN:// == 0 ,代表对端关闭了连接 std::cout << "SSL remote close the connection" << std::endl; break; case SSL_ERROR_SSL: std::cout << "SSL error:" << sslErr << std::endl; break; default: std::cout << "SSL unknown error:" << sslErr << std::endl; break; } }/** @fn * @brief * @param [in]socketFd: 客户端的socket文件句柄 * @param [in]ctx:全局的上下文,保存有证书信息等 * @return */ void onHandleClient(int socketFd, SSL_CTX *ctx) { std::cout << "new connection coming" << std::endl; SSL *ssl; const char reply[] = "test\n"; // 基于ctx 产生一个新的SSL ssl = SSL_new(ctx); // 将连接用户的socket 加入到SSL SSL_set_fd(ssl, socketFd); auto t1 = std::chrono::steady_clock::now(); // 建立SSL 连接 int ret = SSL_accept(ssl); if (ret > 0) { std::cout << "ssl handshake success" << std::endl; // 发消息给客户端 //SSL_write(ssl, reply, strlen(reply)); auto t2 = std::chrono::steady_clock::now(); auto timeSpan = std::chrono::duration_cast::chrono::duration>(t2 - t1); std::cout << "SSL_accept cost " << timeSpan.count() * 1000 << " ms." << std::endl; while (true) { char tempBuf[512] = {}; int recvLen = SSL_read(ssl, tempBuf, sizeof(tempBuf)); if (recvLen > 0) { std::cout << "客户端发来数据,len=" << recvLen << ",content=" << tempBuf << std::endl; // echo std::cout << "SSL_write " << std::string(tempBuf, recvLen) << std::endl; ret = SSL_write(ssl, tempBuf, recvLen); if (ret <= 0) { std::cout << "SSL_write return <=0,ret=" << recvLen << std::endl; bool isError = false; opensslErrorCheck(ssl, recvLen, isError); if (isError) { std::cout << "SSL_write error,close" << std::endl; break; } } } else { // SSL_read <= 0 ,进一步检查openssl 的错误码,判断具体原因 std::cout << "SSL_read return <=0,ret=" << recvLen << std::endl; bool isError = true; opensslErrorCheck(ssl, recvLen, isError); if (isError) { std::cout << "SSL_read error,close" << std::endl; break; } }/* 这里是TCP处理的流程,针对openssl,还需进一步针对<=0进行判断 * else if (recvLen == 0) { std::cout << "客户端主动断开连接,退出接收流程" << std::endl; break; } else { std::cout << "发生其他错误,no=" << errno << ",desc=" << strerror(errno) << std::endl; }*/ } } else { int code = SSL_get_error(ssl, ret); auto reason = ERR_reason_error_string(code); if (code == SSL_ERROR_SYSCALL) { std::cout << "ssl handshake error:errno=" << errno << ",reason:" << strerror(errno) << std::endl; } else { std::cout << "ssl handshake error:code=" << code << ",reason:" << reason << std::endl; }ERR_print_errors_fp(stderr); }std::cout << "cleanup ssl connection" << std::endl; // 关闭SSL 连接 SSL_shutdown(ssl); // 释放SSL SSL_free(ssl); // 关闭socket close(socketFd); }int main() { int sockFd; SSL_CTX *ctx; // 捕获SIG_IGN信号,解决Broken pipe导致进程崩溃问题 signal(SIGPIPE, SIG_IGN); init_openssl(); ctx = create_context(); configure_context(ctx, "../ssl/google.com.pem", "../ssl/google.com.key"); //configure_context(ctx, "../ssl/zhaogang.com.pem", "../ssl/zhaogang.com.key"); std::cout << "listen at :8433" << std::endl; sockFd = create_socket(8433); /* Handle connections */ while (true) { struct sockaddr_in addr{}; socklen_t len = sizeof(addr); // 阻塞,直到有新的连接到来 int clientFd = accept(sockFd, (struct sockaddr *) &addr, &len); if (clientFd < 0) { perror("Unable to accept\n"); break; }// 单独起1个线程处理客户端逻辑(错误的用法,这里只是为了演示,实战中需要使用epoll多路复用技术) std::thread task(onHandleClient, clientFd, ctx); task.detach(); }// 关闭监听socket文件句柄 close(sockFd); // 退出前释放全局的上下文 SSL_CTX_free(ctx); // 清理openssl cleanup_openssl(); return 0; }

Client
/** @file main.h * @brief * @author teng.qing * @date 2021/5/13 */// openssl #include #include // socket #include #include #include #include #include #include #include void showCerts(SSL *ssl) { X509 *cert; char *line; cert = SSL_get_peer_certificate(ssl); if (cert != nullptr) { std::cout << "数字证书信息:" << std::endl; line = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0); std::cout << "证书: " << line << std::endl; free(line); line = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0); std::cout << "颁发者: " << line << std::endl; free(line); X509_free(cert); } else { std::cout << "无证书信息!" << std::endl; } }/** @fn init_openssl * @brief 全局初始化openssl库,只需要调用一次 * @return */ void init_openssl() { //SSL_library_init(); SSL_load_error_strings(); // 载入所有SSL 错误消息 OpenSSL_add_all_algorithms(); // 加载所有支持的算法 }/** @fn create_context * @brief 创建一个全局SSL_CTX,存储证书等信息 * @return */ SSL_CTX *create_context() { const SSL_METHOD *method; SSL_CTX *ctx; /* 以SSL V2 和 V3 标准兼容方式产生一个SSL_CTX ,即SSL Content Text */ /* 也可以用SSLv2_server_method() 或SSLv3_server_method() 单独表示V2 或V3 标准*/ //method = SSLv3_server_method(); method = SSLv23_client_method(); ctx = SSL_CTX_new(method); if (!ctx) { perror("Unable to create SSL context"); ERR_print_errors_fp(stderr); exit(EXIT_FAILURE); }return ctx; }/** @fn create_socket * @brief 创建一个监听的socket * @param [in]listenPort:监听端口 * @return */ int create_socket(std::string serverIp, uint16_t serverPort) { int sockFd = 0; struct sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_port = htons(serverPort); addr.sin_addr.s_addr = inet_addr(serverIp.c_str()); sockFd = socket(PF_INET, SOCK_STREAM, 0); if (sockFd < 0) { printf("create socket error:%d", errno); exit(EXIT_FAILURE); }int ret = connect(sockFd, (struct sockaddr *) &addr, sizeof(sockaddr_in)); if (ret != 0) { std::cout << "Connect err:" << errno << std::endl; exit(errno); }return sockFd; }/** @fn opensslErrorCheck * @brief opensslErrorCheck * @param [in]ssl: SSL实例 * @param [in]retCode: SSL_read/SSL_write返回值 * @param [in]isError: 是否确实发生了错误 * @return */ void opensslErrorCheck(SSL *ssl, int retCode, bool &isError) { // 处理ssl的错误码 int sslErr = SSL_get_error(ssl, retCode); isError = true; switch (sslErr) { case SSL_ERROR_WANT_READ: std::cout << "SSL_ERROR_WANT_READ" << std::endl; isError = false; break; case SSL_ERROR_WANT_WRITE: std::cout << "SSL_ERROR_WANT_WRITE" << std::endl; isError = false; break; case SSL_ERROR_NONE: // 没有错误发生,这种情况好像没怎么遇到过 std::cout << "SSL_ERROR_WANT_WRITE" << std::endl; break; case SSL_ERROR_ZERO_RETURN:// == 0 ,代表对端关闭了连接 std::cout << "SSL remote close the connection" << std::endl; break; case SSL_ERROR_SSL: std::cout << "SSL error:" << sslErr << std::endl; break; default: std::cout << "SSL unknown error:" << sslErr << std::endl; break; } }int main() { SSL_CTX *ctx = nullptr; // 初始化openssl init_openssl(); std::cout << "init openssl success" << std::endl; // 初始化socket,同步连接远端服务器 //int socketFd = create_socket("10.0.72.202", 8433); int socketFd = create_socket("10.80.0.17", 8000); std::cout << "tcp connect remote success" << std::endl; // 创建SSL_CTX上下文 ctx = create_context(); // 绑定socket句柄到SSL实例上 SSL *ssl = SSL_new(ctx); SSL_set_fd(ssl, socketFd); // 建立SSL链接,握手 std::cout << "SSL_connect 2s later will connect and do hand shake..." << std::endl; std::this_thread::sleep_for(std::chrono::seconds(2)); std::cout << "SSL_connect " << std::endl; int ret = SSL_connect(ssl); if (ret <= 0) { ERR_print_errors_fp(stderr); return 0; } std::cout << "handshake success" << std::endl; // 显示对方证书信息 std::cout << "Connected with " << SSL_get_cipher(ssl) << " encryption" << std::endl; showCerts(ssl); std::cout << "send hello server" << std::endl; std::string msg = "hello serve"; SSL_write(ssl, msg.c_str(), msg.length()); // wait server response char tempBuf[256] = {}; ret = SSL_read(ssl, tempBuf, sizeof(tempBuf)); if (ret <= 0) { std::cout << "SSL_read return <=0,ret=" << ret << std::endl; bool isError = false; opensslErrorCheck(ssl, ret, isError); if (isError) { std::cout << "SSL_read error,close" << std::endl; } }std::this_thread::sleep_for(std::chrono::seconds(5)); std::cout << "exit ..." << std::endl; SSL_shutdown(ssl); // 关闭SSL连接 SSL_free(ssl); // 释放SSL资源 close(socketFd); // 关闭socket文件句柄 SSL_CTX_free(ctx); // 释放SSL_CTX上下文资源return 0; }

证书生成
生成自签名证书命令如下:
  1. 生成私钥
$ openssl genrsa -out google.com.key 2048

  1. 生成CSR(证书签名请求)
$ openssl req -new -out google.com.csr -key google.com.keyCountry Name (2 letter code) [AU]:CN State or Province Name (full name) [Some-State]:Shanghai Locality Name (eg, city) []:Shanghai Organization Name (eg, company) [Internet Widgits Pty Ltd]:Google Ltd Organizational Unit Name (eg, section) []:google.com Common Name (e.g. server FQDN or YOUR name) []:*.google.com #这一项必须和你的域名一致 Email Address []:kefu@google.com A challenge password []:fG!#tRru An optional company name []:Google.com

  1. 生成自签名证书(100年过期)
$ openssl x509 -req -in google.com.csr -out google.com.cer -signkey google.com.key -CAcreateserial -days 36500

  1. 生成服务器crt格式证书
$ openssl x509 -inform PEM -in google.com.cer -out google.com.crt

  1. 生成PEM公钥
$ openssl x509 -in google.com.crt -outform PEM -out google.com.pem

最后,google.com.pem 和 google.com.key 是本程序需要的 公钥和私钥
附录:
  • 生成IOS客户端p12格式根证书(输入密码fG!#tRru)
$ openssl pkcs12 -export -clcerts -in google.com.cer -inkey google.com.key -out google.com.p12

  • 生成Android客户端bks格式证书
# 略

问题记录 获取错误原因
SSL_get_error(),ERR_reason_error_string()
int ret = SSL_accept(ssl); if (ret <= 0) { int code = SSL_get_error(ssl, ret); auto reason = ERR_reason_error_string(code); std::cout << "ssl handshake error:code=" << code << ",reason:" << reason << std::endl; ERR_print_errors_fp(stderr); }

数据发送对方接受不到
当 SSL_read 和 SSL_write 返回值<=0时,需要调用SSL_get_error(ssl, ret)再次判断,如果是SSL_ERROR_WANT_READ 或者 SSL_ERROR_WANT_WRITE,则需要再次调用SSL_read或者SSL_write。
拿我修改的evpp增加openssl代码举例:
// add openssl support int serrno = 0; ssize_t n = !ssl_ ? input_buffer_.ReadFromFD(chan_->fd(), &serrno) : evpp::ssl::SSL_read(ssl_, &input_buffer_, &serrno); if (n > 0) { msg_fn_(shared_from_this(), &input_buffer_); } else if (ssl_) { // <= 0 , 需要进一步处理SSL错误码 // deal SSL_ERROR_WANT_READ/SSL_ERROR_WANT_WRITE // 这里如果不这么处理,则导致接收数据不完整,从而出错 switch (serrno) { case SSL_ERROR_WANT_READ: chan_->EnableReadEvent(); break; case SSL_ERROR_WANT_WRITE: chan_->EnableWriteEvent(); break; case SSL_ERROR_ZERO_RETURN://SSL has been shutdown,相当于原生write()返回0,代表对端关闭连接(正常情况) DLOG_TRACE << "SSL has been shutdown(" << serrno << ")."; HandleError(); break; case SSL_ERROR_SSL: DLOG_TRACE << "SSL has error(" << serrno << ")."; HandleError(); break; case SSL_ERROR_NONE: // 0,have none error DLOG_TRACE << "SSL has error none"; break; default: DLOG_TRACE << "SSL Connection has been aborted(" << serrno << ")."; HandleError(); break; } } else if (n == 0) { // remote close the connection if (type() == kOutgoing) { // This is an outgoisslsssng connection, we own it and it's done. so close it DLOG_TRACE << "fd=" << fd_ << ". We read 0 bytes and close the socket."; status_ = kDisconnecting; HandleClose(); } else { // Fix the half-closing problem : https://github.com/chenshuo/muduo/pull/117chan_->DisableReadEvent(); if (close_delay_.IsZero()) { DLOG_TRACE << "channel (fd=" << chan_->fd() << ") DisableReadEvent. delay time " << close_delay_.Seconds() << "s. We close this connection immediately"; DelayClose(); } else { // This is an incoming connection, we need to preserve the // connection for a while so that we can reply to it. // And we set a timer to close the connection eventually. DLOG_TRACE << "channel (fd=" << chan_->fd() << ") DisableReadEvent. And set a timer to delay close this TCPConn, delay time " << close_delay_.Seconds() << "s"; delay_close_timer_ = loop_->RunAfter(close_delay_, std::bind(&TCPConn::DelayClose, shared_from_this())); // TODO leave it to user layer close. } } } else { // n < 0 if (EVUTIL_ERR_RW_RETRIABLE(serrno)) { DLOG_TRACE << "errno=" << serrno << " " << strerror(serrno); } else { DLOG_TRACE << "errno=" << serrno << " " << strerror(serrno) << " We are closing this connection now."; HandleError(); } }

SSL_connect在服务端异常时阻塞卡住
如果connect()系统调用成功,但是调用SSL_connect的时候卡住,则代表对方没有响应我们的握手请求,可能是出BUG了或者其他原因,此时我们需要一种机制能监测出来。通常有2种思路:
  • 通过定时器,超过一定时间调用SSL_shutdown(ssl),SSL_connect()会立即返回。
  • 通过非阻塞I/O+select I/O复用增加重试机制达到超时监测的目标。
2种方案各有优劣,在evpp增加openssl的支持中,我使用了第二种方式(参考Stackoverflow):
// no-blocking io std::random_device rd; std::default_random_engine gen = std::default_random_engine(rd()); std::uniform_int_distribution dis(5, 10); // c++11 的随机数 int maxTimes = dis(gen); // 最大次数,5 - 10 次 int curTimes = 0; int ret = -1; // 这里限制超时时间 while (curTimes < maxTimes) { // 100 - 200 ms超时 ret = SSL_connect(ssl); if (ret == -1) { fd_set fds; FD_ZERO(&fds); FD_SET(sockfd, &fds); int ret = SSL_get_error(ssl, -1); switch (ret) { case SSL_ERROR_WANT_READ: case SSL_ERROR_WANT_WRITE: { struct timeval t{}; t.tv_sec = 0; t.tv_usec = 100 * 1000; // 100 ms select(sockfd + 1, &fds, nullptr, nullptr, &t); curTimes++; break; } default: break; } } else { break; } }if (ret <= 0) { LOG_WARN << "SSL_connect error:" << ret; conn_fn_(TCPConnPtr(new TCPConn(loop_, "", sockfd, laddr, remote_addr_, 0))); return; }

SSL_shutdown崩溃Broken pipe
问题 写了个性能测试工具,每 20毫秒启动一个协程 ,进行TCP连接和SSL握手,然后关闭。
【C++葵花宝典|OpenSSL API入门和踩坑大全】服务端运行过程中,程序有时在SSL_shutdown处崩溃,如果降低客户端连接频率(比如1秒),该问题不出现。
static inline void SSL_free_(SSL *&ssl) { if (ssl) { ::SSL_shutdown(ssl); // crash ::SSL_free(ssl); ssl = nullptr; } }

解决方式 根据:socket编程—— 服务器遇到Broken Pipe崩溃 ,是因为对一个对端已经关闭的socket调用两次write,第二次将会生成SIGPIPE信号,该信号默认结束进程。
解决方法是:捕获SIG_IGN信号,以避免进程退出
signal(SIGPIPE, SIG_IGN);

这样, 第二次调用write方法时,会返回-1,同时errno置为SIGPIPE,程序便能知道对端已经关闭。
原理 引用socket编程—— 服务器遇到Broken Pipe崩溃:
具体的分析可以结合TCP的"四次握手",TCP是全双工的信道,可以看作两条单工信道,TCP连接两端的两个端点各负责一条。当对端调用close时,虽然本意是关闭整个两条信道,但本端只是收到FIN包。按照TCP协议的语义,表示对端只是关闭了其所负责的那一条单工信道,仍然可以继续接收数据。也就是说,因为TCP协议的限制,一个端点无法获知对端的socket是调用了close还是shutdown。
对一个已经收到FIN包的socket调用read方法,如果接收缓冲已空,则返回0,这就是常说的表示连接关闭。 但第一次对其调用write方法时,如果发送缓冲没问题,会返回正确写入(发送)。但发送的报文会导致对端发送RST报文,因为对端的socket已经调用了close,完全关闭,既不发送,也不接收数据。所以,第二次调用write方法(假设在收到RST之后),会生成SIGPIPE信号,导致进程退出。
参考
  • OpenSSL官网:Simple TLS Server
  • IBM社区:Secure programming with the OpenSSL API(客户端)
  • SSL协议原理详解
  • SSL原来介绍+OpenSSL API使用教程
  • OpenSSL Libraries
  • websocket协议实现及基于muduo库的功能扩展 https/ws/wss
  • websocket-muduo

    推荐阅读