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