最近公司项目需要把服务器端C++实现的 websocket 改成websocket secure(websocket + ssl)。百度了一下有一个广泛使用的ssl库,openssl。花了两天时间一顿操作之后客户端用js终于连接上服务器上的wss。但是诡异的是,每次客户端来消息后,服务器调用SSL_read函数总是会读完一个字节后返回,第二次调用(epoll控制)才会读完剩下的字节。
比如客户端发了502字节的数据包,服务器这边epoll响应 调用到SSL_read,会先读1个字节,epoll再次响应,再读剩下的501个字节。等于调用了两次read才把一个数据包读完!
百思不得其解啊,在客户端用wrieshark抓包,客户端从来没发过数据长度位1的有效包。说明不是客户端问题。
只好去翻ssl manual ,怀疑是不是什么参数没设置。也没发现有什么参数可设置的。。
最后,怀疑是协议版本的问题。。然后我把把openssl 用的ssl协议从TSL1.0,改成了TSL1.2,就尼玛好了。。可能是js websocket类用的ssl协议版本高于1.0,结果导致这个问题。
好吧,差点怀疑人生。不知道有没有朋友和我遇到过一样的问题。
顺便把我参考网上的代码封装的openssl类贴一下,一个openSSL类,一个SSL管理类。
//
// Created by jamesjiang on 2018/5/15.
//
#ifndef GAME_OPENSSL_H
#define GAME_OPENSSL_H
#include
#include "common_tcpconnection.h"
#include
#include
using namespace GameJayo::Server;
class openSSL
{
public:
openSSL();
~openSSL();
public:
// 初始化 openssl 上下文
static int32_t init_ssl_ctx();
static void shutdown_ssl();
int32_t creat_ssl(int32_t fd);
int32_t accept_ssl_fd(int32_t fd);
// return > 0 recv 字节数, < 0 error , == 0 判断
int32_t ssl_recv(char* data, int32_t size);
// return > 0 send 字节数, < 0 error , == 0 判断
int32_t ssl_send(char* data, int32_t size);
private:
SSL* m_ssl;
int32_t m_fd;
static SSL_CTX* m_ctx;
public:
void ShowCerts();
};
#endif //GAME_OPENSSL_H
//
// Created by jamesjiang on 2018/5/15.
//
#include "openSSL.h"
#define CA_CERT_FILE "ssl/123u.com_combined.crt"
#define SERVER_CERT_FILE "ssl/123u.com_combined.crt"
#define SERVER_KEY_FILE "ssl/123u.com.key"
SSL_CTX* openSSL::m_ctx = NULL;
openSSL::openSSL()
{
}
openSSL::~openSSL()
{
/* 关闭 SSL 连接 */
if (m_ssl)
{
SSL_shutdown(m_ssl);
/* 释放 SSL */
SSL_free(m_ssl);
}
m_fd = -1;
close(m_fd);
}
int32_t openSSL::init_ssl_ctx()
{
/* SSL 库初始化 */
SSL_library_init();
/* 载入所有 SSL 算法 */
OpenSSL_add_all_algorithms();
/* 载入所有 SSL 错误消息 */
SSL_load_error_strings();
/* 以 TSL1.2 标准兼容方式产生一个 SSL_CTX ,即 SSL Content Text */
m_ctx = SSL_CTX_new(TLSv1_2_server_method());
if (m_ctx == NULL)
{
ERR_print_errors_fp(stdout);
return fail;
}
// 是否要求校验对方证书 此处不验证客户端身份所以为: SSL_VERIFY_NONE
SSL_CTX_set_verify(m_ctx, SSL_VERIFY_NONE, NULL);
// 加载CA的证书
if(!SSL_CTX_load_verify_locations(m_ctx, CA_CERT_FILE, NULL))
{
printf("SSL_CTX_load_verify_locations error!\n");
ERR_print_errors_fp(stderr);
return fail;
}
// 加载自己的证书 此证书用来发送给客户端。 证书里包含有公钥
if(SSL_CTX_use_certificate_file(m_ctx, SERVER_CERT_FILE, SSL_FILETYPE_PEM) <= 0)
{
printf("SSL_CTX_use_certificate_file error!\n");
ERR_print_errors_fp(stderr);
return fail;
}
// 加载自己的私钥 私钥的作用是,ssl握手过程中,对客户端发送过来的随机
//消息进行加密,然后客户端再使用服务器的公钥进行解密,若解密后的原始消息跟
//客户端发送的消息一致,则认为此服务器是客户端想要链接的服务器
if(SSL_CTX_use_PrivateKey_file(m_ctx, SERVER_KEY_FILE, SSL_FILETYPE_PEM) <= 0)
{
printf("SSL_CTX_use_PrivateKey_file error!\n");
ERR_print_errors_fp(stderr);
return fail;
}
// 判定私钥是否正确
if(!SSL_CTX_check_private_key(m_ctx))
{
printf("SSL_CTX_check_private_key error!\n");
ERR_print_errors_fp(stderr);
return fail;
}
return success;
}
int32_t openSSL::creat_ssl(int32_t fd)
{
/* 基于 ctx 产生一个新的 SSL */
m_ssl = SSL_new(m_ctx);
if (m_ssl == NULL)
{
ERR_print_errors_fp(stderr);
return fail;
}
m_fd = fd;
/* 将连接用户的 socket 加入到 SSL */
SSL_set_fd(m_ssl, m_fd);
/* 设置成服务器模式 */
SSL_set_accept_state(m_ssl);
return success;
}
int32_t openSSL::accept_ssl_fd(int32_t fd)
{
/* 建立 SSL 连接. 握手在此完成 */
// 或者 SSL_do_handshake(ssl);
if (SSL_accept(m_ssl) == -1)
{
int icode = -1;
ERR_print_errors_fp(stderr);
int iret = SSL_get_error(m_ssl, icode);
printf("SSL_accept error! code = %d, iret = %d\n", icode, iret);
return fail;
}
return success;
}
int32_t openSSL::ssl_recv(char *data, int32_t size)
{
if (NULL == data || size <= 0)
{
return -1;
}
int32_t recved = 0;
while(true)
{
// int x = recv(m_fd,data,size, 0);
recved = SSL_read(m_ssl, data, size);
int left = SSL_pending(m_ssl);
if (recved > 0)
{
int err = SSL_get_error(m_ssl,recved);
return recved;
}
else
{
// TODO 小心陷入死循环
if (recved < 0 && SSL_get_error(m_ssl,recved) == SSL_ERROR_WANT_READ)
{
printf("got SSL_ERROR_WANT_READ\n");
continue;
}
// recved=0 || no want read
return recved;
}
}
}
int32_t openSSL::ssl_send(char *data, int32_t size)
{
if (NULL == data || size <= 0)
{
return -1;
}
int32_t remainded = size;
int32_t sended = 0;
char* pszTmp = data;
while(remainded > 0)
{
//TODO:检查此处的处理逻辑,是否会造成由于单个连接而拖累整个服务
sended = SSL_write(m_ssl, data, (size_t)remainded);
if (sended > 0)
{
pszTmp += sended;
remainded -= sended;
}
else // sended <= 0
{
//TODO
if (errno != EINTR || errno != EAGAIN)
{
printf(" send data(size:%d) error, msg = %s\n", remainded, strerror(errno));
break;
}
}
}
return (size - remainded);
}
void openSSL::ShowCerts()
{
X509 *cert;
char *line;
cert = SSL_get_peer_certificate(m_ssl);
if (cert != NULL)
{
printf("数字证书信息:\n");
line = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0);
printf("证书: %s\n", line);
free(line);
line = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0);
printf("颁发者: %s\n", line);
free(line);
X509_free(cert);
}
else
printf("无证书信息!\n");
}
// ---- rsa非对称加解密 ---- //
#define KEY_LENGTH 2048 // 密钥长度
#define PUB_KEY_FILE "pubkey.pem" // 公钥路径
// 函数方法生成密钥对
int32_t generateRSAKey(std::string strKey[2])
{
// 公私密钥对
size_t pri_len;
size_t pub_len;
char *pri_key = NULL;
char *pub_key = NULL;
// 生成密钥对
RSA *keypair = RSA_generate_key(KEY_LENGTH, RSA_3, NULL, NULL);
BIO *pri = BIO_new(BIO_s_mem());
BIO *pub = BIO_new(BIO_s_mem());
PEM_write_bio_RSAPrivateKey(pri, keypair, NULL, NULL, 0, NULL, NULL);
PEM_write_bio_RSAPublicKey(pub, keypair);
// 获取长度
pri_len = BIO_pending(pri);
pub_len = BIO_pending(pub);
// 密钥对读取到字符串
pri_key = (char *)malloc(pri_len + 1);
pub_key = (char *)malloc(pub_len + 1);
BIO_read(pri, pri_key, pri_len);
BIO_read(pub, pub_key, pub_len);
pri_key[pri_len] = '\0';
pub_key[pub_len] = '\0';
// 存储密钥对
strKey[0] = pub_key;
strKey[1] = pri_key;
// 存储到磁盘(这种方式存储的是begin rsa public key/ begin rsa private key开头的)
FILE *pubFile = fopen(PUB_KEY_FILE, "w");
if (pubFile == NULL)
{
return fail;
}
fputs(pub_key, pubFile);
fclose(pubFile);
FILE *priFile = fopen(SERVER_KEY_FILE, "w");
if (priFile == NULL)
{
return fail;
}
fputs(pri_key, priFile);
fclose(priFile);
// 内存释放
RSA_free(keypair);
BIO_free_all(pub);
BIO_free_all(pri);
free(pri_key);
free(pub_key);
}
void openSSL::shutdown_ssl()
{
if (m_ctx)
SSL_CTX_free(m_ctx);
}
我们服务器用的是LT模式,用ET模式的ssl_recv函数和ssl_send可能会有bug。
SSLMgr类:
//
// Created by jamesjiang on 2018/5/16.
//
#ifndef GAME_CONNECTOR_SSLMGR_H
#define GAME_CONNECTOR_SSLMGR_H
#include
#include
#include
#include "Common/openSSL.h"
namespace GameJayo { namespace Server {
class SSLMgr
{
public:
SSLMgr()
{
}
~SSLMgr()
{
clear();
}
openSSL* get_ssl(int32_t fd)
{
if (m_clientSSL.count(fd))
{
return m_clientSSL.at(fd);
}
return NULL;
}
void del_ssl(int32_t fd)
{
if (m_clientSSL.count(fd))
{
delete m_clientSSL.at(fd);
m_clientSSL.erase(fd);
}
}
int32_t add_ssl(int32_t fd)
{
del_ssl(fd);
openSSL *ssl = new openSSL();
if (fail == ssl->creat_ssl(fd))
{
return fail;
}
m_clientSSL[fd] = ssl;
return success;
}
int32_t accept_fd(int32_t fd)
{
openSSL *ssl = get_ssl(fd);
if (ssl == NULL)
{
return fail;
}
if (fail == ssl->accept_ssl_fd(fd))
{
return fail;
}
return success;
}
void clear()
{
for(auto &it : m_clientSSL)
{
delete it.second;
}
m_clientSSL.clear();
}
private:
std::map m_clientSSL;
};
}
}
#endif //GAME_CONNECTOR_SSLMGR_H