Win IOCP中使用openssl

1 什麼是IOCP

什麼不知道什麼是IOCP?那你可就out了。IOCP(I/O Completion Port),常稱I/O完成端口。 IOCP模型屬於一種通訊模型,適用於能控制併發執行的高負載服務器的一個技術。 通俗一點說,就是用於高效處理很多很多的客戶端進行數據交換的一個模型。或者可以說,就是能異步I/O操作的模型(哈哈,摘錄自百度百科)。IOCP是Windows平臺特有的一種特性(雖然linux上有epoll,但絕對沒有IOCP強大)。基本上Win平臺使用了IOCP作爲應用程序使用的socket複用技術,性能絕對不會差。這裏不再贅述IOCP優點,主要講下IOCP是如何結合openssl實現強大的支持ssl協議的網絡通信庫。

完全理解本文的技術,需要對IOCP和openssl編程有一定的基礎。

2 關鍵技術

2.1 IOCP使用openssl實現難點

google了一下關於openssl如何使用IOCP,找到可用的信息很少,分析與IOCP結合技術難點,其主要原因在於ssl的協議在TCP協議之上,屬於應用層協議。

那麼問題來了,IOCP的工作原理是綁定套接字端口,如果有數據收發消息,會通知調用層有事件到來,並且數據已經被系統接收完成。而我們都知道ssl編程時,由於SSL的複雜性:SSL握手(密鑰協商和交換),數據收發(SSL_write和SSL_read,兩個函數分別是將明文通過加密通道發送到對端,接收對端發送過來的加密數據並解密後拷貝到數據緩衝區),很難將此和IOCP技術融合到一起,下面是一段SSL客戶端實現僞代碼:

1.  SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //創建TCP套接字
2.  connect(sclient , (struct sockaddr *)&client, sizeof(client)) //連接服務端
3.  SSL_CTX *ctx = SSL_CTX_new (meth);//創建SSL上下文
3.  ssl = SSL_new (ctx);//創建SSL對象
5.  SSL_set_fd(ssl, sclient);//將明文套接字關聯到SSL
6.  SSL_write …  SSL_read… //數據的收發

2.2 IOCP如何集成SSL

2.1章節寫了編寫SSL客戶端的僞代碼,其中關聯服務socket套接字使用的函數是SSL_set_fd,該函數的實現如下:

int SSL_set_fd(SSL *s, int fd)
{
    int ret = 0;
    BIO *bio = NULL;

    bio = BIO_new(BIO_s_socket());

    …
    BIO_set_fd(bio, fd, BIO_NOCLOSE);
    SSL_set_bio(s, bio, bio);   //關鍵函數
    ret = 1;
err:
    return (ret);
}

void SSL_set_bio(SSL *s, BIO *rbio, BIO *wbio)
{
    ...
    s->rbio = rbio;
    s->wbio = wbio;
}

從上面兩個函數的調用,我們看到了SSL_set_fd函數將套接字封裝成了兩個BIO(rbio 用於接收,wbio 用於發送, rbio 和wbio 都關聯到了fd),並賦值給了SSL對象。我們看下SSL結構體的定義

struct ssl_st {
    …
   /* used by SSL_read */
    BIO *rbio;
    /* used by SSL_write */
    BIO *wbio;
    …

};

ssl_st結構中的註釋已經寫的很明確了。SSL的讀寫都是基於BIO操作的,那麼有沒有可能我們將IOCP將數據先,寫入BIO_s_mem然後再使用SSL_write和SSL_reade從BIO中讀取數據呢?方案當然是可行的。只不過這裏需要注意的是,直接操作收發密文數據不再是用SSL_write和SSL_read,而是使用BIO_read、BIO_write配合SSL_*接口使用,另SSL處於SSL握手階段,SSL_reade是沒有應用數據的。


3 實現原理

3.1 主要口依賴

  • BIO_write BIO的寫入數據(從對端收到密文數據)
  • BIO_read BIO的讀取數據(讀取本地SSL中要發送的密文數據)
  • SSL_write SSL寫入數據(將明文數據發送到對端)
  • SSL_read SSL讀取數據(從SSL中讀取對端發送的明文數據)

基於上面四個接口,實現從IOCP中事件的出發,將密文使用BIO接口讀寫到對應的BIO中,緊接着使用SSL接口讀寫數據,實現明文加密,密文解密。

3.2 服務端實現主要流程圖

- 握手(握手時沒有應用數據,無需使用SSL_讀或寫)


從圖中可以看到,IOCP轉發SSL協議協議時的函數調用,通過BIO_write寫入本地SSL握手數據,使用BIO_read讀取握手數據。


- 1.iocp響應WSARecv使用結果收取對端密文數據
- 2.使用BIO_write寫入iocp收到的密文數據到RECV bio
- 3.使用SSL_read讀取明文數據
- 4.使用SSL_write寫入要發送的明文數據
- 5.使用BIO_read讀取要發送到對端的密文數據,使用WSASend發送到對端

4 樣例代碼

- 文件關鍵結構定義:

enum OVERLAPPED_TYPE{
    RECV = 0,
    SEND,
    CONNECT
};

enum ADDRESS_TYPE{
    LOCAL = 0,
    REMOTE
};

enum SOCKET_STATUS{
    NONE        = 0x0,
    ACCEPTING    = 0x1,
    CONNECTING    = 0x2,
    HANDSHAKING    = 0x4,
    CONNECTED    = 0x8,
    RECEIVING    = 0x10,
    SENDING        = 0x20,
    CLOSING        = 0x40,
    CLOSED        = 0x80,
    OPERATING    = ACCEPTING | CONNECTING | HANDSHAKING | RECEIVING | SENDING
};

struct session;

struct session_overlapped
{
    OVERLAPPED overlapped;
    DWORD result;
    session *psession;
};

struct session
{
    SOCKET s; // handle to socket
    char socket_buffer[2][BUFFER_SIZE]; // memory used for read/write from/to socket
    char ssl_buffer[2][BUFFER_SIZE]; // memory used for read/write from/to ssl memory bio
    DWORD ssl_buffer_size[2]; // indicates the bytes of valid data in ssl_buffer
    unsigned int status; // stores current socket status, bit-masked value of one or more of SOCKET_STATUS
    session_overlapped overlapped[3]; // structure for overlapped operations
    WSABUF wsabuf[2]; // structure used for pass buffer to overlapped operations
    DWORD bytes_transferred[2]; // store the bytes of buffer that received/sent from/to the socket
    DWORD wsa_flags[2]; // store the flags send/receive from overlapped operations, not used
    SSL *ssl; // SSL structure used by OpenSSL
    BIO *bio[2]; // memory BIO used by OpenSSL
    ssl_lock lock; // synchronization object for multiple-thread data access
};

- ocp處理關鍵流程

bool session_process(session *psession)
{
    bool fatal_error_occurred = false;
    if(nullptr != psession->ssl)
    {
        if(psession->bytes_transferred[RECV] > 0)
        {
            int bytes = BIO_write(psession->bio[RECV], psession->socket_buffer[RECV], psession->bytes_transferred[RECV]);
            if(bytes == psession->bytes_transferred[RECV])
            {
                psession->bytes_transferred[RECV] = 0;
            }
        }

        if(psession->ssl_buffer_size[RECV] == 0)
        {
            int bytes = 0;
            do
            {
                bytes = SSL_read(psession->ssl, psession->ssl_buffer[RECV], BUFFER_SIZE);

                if ((HANDSHAKING == (psession->status & HANDSHAKING)) && SSL_is_init_finished(psession->ssl))
                {
                    psession->status &= ~HANDSHAKING;
                    psession->status |= CONNECTED;

                    app_on_session_connect(psession);
                }

                if (bytes > 0)
                {
                    psession->ssl_buffer_size[RECV] = bytes;
                    app_on_session_recv(psession);
                    psession->ssl_buffer_size[RECV] = 0;
                }

            } while (bytes > 0);
        }

        if(psession->ssl_buffer_size[SEND] > 0)
        {
            int bytes = SSL_write(psession->ssl, psession->ssl_buffer[SEND], psession->ssl_buffer_size[SEND]);
            if(bytes == psession->ssl_buffer_size[SEND])
            {
                psession->ssl_buffer_size[SEND] = 0;
            }
        }

        if(psession->wsabuf[SEND].len == 0 && (0 != psession->s_listening || BIO_pending(psession->bio[SEND])))
        {
            int bytes = BIO_read(psession->bio[SEND], psession->socket_buffer[SEND], BUFFER_SIZE);
            if(bytes > 0)
            {
                psession->wsabuf[SEND].len = bytes;
            }
        }

        if(fatal_error_occurred)
            session_close(psession);
    }

    session_send(psession);
    session_recv(psession);    
    return !fatal_error_occurred;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章