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;
}