c++ WebSocket Secure服务器(wss服务器)

时光怂恿深爱的人放手 提交于 2020-01-21 05:41:39

做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

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!