做h5游戏,之前是自己写的epoll网络通信,所以开始项目的时候都没有多想就直接自己写了一个websocket网络,而不是使用第三方的ws库。一直用都没有出现问题,但是项目上线前一周前端对接sdk的时候说平台只支持https不支持http,所以后端必须的使用wss通信,惊闻这个消息的时候已经周四了,离上线还有三天时间。想着如果用完全不了解的第三方库还不知道会出现什么问题,而且第三方库引入项目估计网络层全部都是修改,动静太大,时间上来不及,考虑之后还是决定自己写,自己用c++和openssl只用了一天时间就实现了,最后拿到线上也没有出现任何问题。下面简单介绍下wss的相关处理方式。wss其实就是在ws的基础上增加了一层ssl封装,所以听起来麻烦,真正实现其实不难。
首先分配和初始化服务器上下文句柄,代码如下:
bool WSSSocketThread::Init()
{
SSL_library_init();//初始化库
SSL_load_error_strings();//加载错误信息
m_ctx = SSL_CTX_new(SSLv23_method());//SSLv23_server_method or SSLv23_client_method
if (nullptr == m_ctx)
{
WRITE_ERROR_LOG("SSL_CTX_new error");
return false;
}
if(m_clientMethod)
return CSocketThread::Init();
SSL_CTX_set_verify(m_ctx, SSL_VERIFY_NONE, nullptr);//取消验证前端证书
if (1 != SSL_CTX_use_certificate_file(m_ctx, m_cert.data(), SSL_FILETYPE_PEM))//加载证书
{
WRITE_ERROR_LOG("SSL_CTX_use_certificate_file error");
return false;
}
if (1 != SSL_CTX_use_certificate_chain_file(m_ctx, m_cert.data()))//加载证书链
{
WRITE_ERROR_LOG("SSL_CTX_use_certificate_chain_file error");
return false;
}
if (1 != SSL_CTX_use_PrivateKey_file(m_ctx, m_key.data(), SSL_FILETYPE_PEM))//加载密钥
{
WRITE_ERROR_LOG("SSL_CTX_use_PrivateKey_file error");
return false;
}
if (1 != SSL_CTX_check_private_key(m_ctx))//验证密钥
{
WRITE_ERROR_LOG("SSL_CTX_check_private_key error");
return false;
}
return CSocketThread::Init();
}
初始化之后就可以启动监听,监听处理和普通的网络编程一样,就是创建socket,绑定socket,开启监听端口。在收到客户端连接之后也就是在调用accept函数之后需要立马对连接的socket进行ssl初始化,初始化代码如下:
bool CWSSClientSocket::OnAccept()
{
m_ssl = g_pSocketThread->GetNewSSL();//SSL_new(m_ctx)
if (nullptr == m_ssl)
{
WRITE_ERROR_LOG("SSL_new error port=%u ip=%s",GetPort(),GetHost());
return false;
}
if (1 != SSL_set_fd(m_ssl, (int)GetSocket()))
{
WRITE_ERROR_LOG("SSL_set_fd error port=%u ip=%s",GetPort(), GetHost());
return false;
}
SSL_set_accept_state(m_ssl);//设置为接受连接socket 并且和客户端开启ssl握手
return true;
}
对soket进行初始化之后下面一步就是等待ssl握手,ssl需要握手成功之后才会进行数据通信,所以在ssl没有握手成功之前,socket有可写或者是可读事件都需要调用ssl握手函数。ssl握手函数处理如下:
bool CClientSocket::SSLHandshake()
{
auto nRet = SSL_do_handshake(GetSSL());
if (1 == nRet)//握手成功
{
SetStatus(enmSocketStatus_SSLConnected);
g_pSocketThread->EpollMod(this);
return true;
}
auto nErr = SSL_get_error(GetSSL(), nRet);
if (nErr == SSL_ERROR_WANT_READ || nErr == SSL_ERROR_WANT_WRITE)//正在进行握手
return true;
WRITE_ERROR_LOG("SSL_do_handshake error port=%u ip=%s ret=%d err=%d", GetPort(), GetHost(), nRet, nErr);
return false;//握手发生错误
}
如果握手成功那么就可以进行数据通信了,注意ssl握手成功之后wss还需要进行ws握手,握手流程见上一篇博文。ssl接受数据的代码处理如下:
int32_t CClientSocket::HttpsIn()
{
if (!IsSSLConencted())
return SSLHandshake() ? S_OK : E_UNKNOW;
int nCount = 0;
int nRecvSize = 0;
while (++nCount < 10)
{
if (m_pRecvBuf->IsFull())//满了
{
g_pSocketThread->EpollMod(this);
g_pServerBase->PushSocketEvent(enmEventType_Data, this);
return S_OK;
}
NetBuffer arrBuf[2];
m_pRecvBuf->GetWriteEnable(arrBuf);
int nRet = SSL_read(GetSSL(), arrBuf[0].buf, arrBuf[0].len);
if (nRet > 0)
{
m_pRecvBuf->Add(nRet);
nRecvSize += nRet;
}
else
{
auto nErr = SSL_get_error(GetSSL(), nRet);
if (nErr != SSL_ERROR_WANT_READ)
return E_UNKNOW;//socket 已经发生了错误
break;
}
}
if (nCount >= 10)//达到了循环次数
g_pSocketThread->EpollMod(this);
if (nRecvSize > 0)//接收到了数据处理
g_pServerBase->PushSocketEvent(enmEventType_Data, this);
return S_OK;
}
接收数据的方式和普通的recv函数区别不大,由于是异步通信,所以网络层只负责数据收发和socket连接处理,不进行任何数据处理,ws的握手留给其他线程去实现。ssl发包的处理和普通的异步send函数有所区别,代码如下:
int32_t CClientSocket::HttpsOut()
{
if (!IsSSLConencted())
return SSLHandshake() ? S_OK : E_UNKNOW;
int nCount = 0;
while (++nCount < 10)
{
if (m_pSendBuf->IsEmpty())return S_OK;
NetBuffer arrBuf[2];
m_pSendBuf->GetReadEnable(arrBuf);
if (GetLastSendSize() > 0)//上次数据没有发送成功需要用相同的参数发送
{
int nRet = SSL_write(GetSSL(), arrBuf[0].buf, GetLastSendSize());
if (nRet >0 && (uint32_t)nRet == GetLastSendSize())//实际测试的时候发现没有发送完所有数据返回的是0
{
m_pSendBuf->Remove(GetLastSendSize());
SetLastSendSize(0);
continue;
}
auto nErr = SSL_get_error(GetSSL(), nRet);
if (nErr != SSL_ERROR_WANT_WRITE)
return E_UNKNOW;//socket 已经发生了错误
return S_OK;
}
int nRet = SSL_write(GetSSL(), arrBuf[0].buf, arrBuf[0].len);
if (nRet > 0 && (uint32_t)nRet == arrBuf[0].len)
m_pSendBuf->Remove(arrBuf[0].len);
else
{
auto nErr = SSL_get_error(GetSSL(), nRet);
if(nErr != SSL_ERROR_WANT_WRITE)
return E_UNKNOW;//socket 已经发生了错误
SetLastSendSize(arrBuf[0].len);//保存参数
return S_OK;
}
}
if (nCount >= 10)//达到了循环次数
g_pSocketThread->EpollMod(this);
return S_OK;
}
ssl发包的时候可能一次发送不完,测试发现如果发送不完SSL_write函数会返回0,这个时候需要保存本次发送的参数,也就是调用函数的时候使用的第二和第三个参数,等待下次可写的时候使用相同的参数调用。剩下的就是socket关闭和服务器关闭的时候句柄的析构了,不在多说。
整体的收包处理就是 SSL_read接收数据-> ws拆包->二进制组包->内部协议逻辑处理。发包处理为:编码二进制协议->添加ws协议头->SSL_write发送。看网上有文章说ssl其实还有重协商的流程,我在项目中没有支持,线上运行之后上面这套流程也没有出现过任何问题,所以实际是可行的。完整代码传送地址为:https://github.com/mouzey/c-project
来源:CSDN
作者:m08090420
链接:https://blog.csdn.net/m08090420/article/details/103752942