SocketChannelSslImpl类中带override关键字的方法都是覆盖基类SocketChannelImpl的同名方法,也是实现SSL通信与非SSL通信调用透明的关键。不带override关键字的方法都是SSL通信相关方法,这些方法里有openssl的函数调用。不带override的方法中有静态和非静态之分,静态方法在进程中只会被调用一次,与具体Channel对象无关。SocketChannel外部不需要调用非静态的ssl相关方法。
因为是非阻塞的socket,SSL_do_handshake()和SSL_write()、SSL_read()返回值并不完全能判断是否出错,还需要SSL_get_error()获取错误码。SSL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE都是正常的。
网上的大部分openssl例子程序是按顺序调用openssl函数简单实现同步ssl通信,在非阻塞IO应用中,ssl通信要复杂许多。SocketChannelSslImpl实现的是非阻塞的ssl通信,从该类的实现上看整个通信过程并非完全线性的。下面的SSL通信图更清晰地说明了Nebula框架中SSL通信是如何实现的:
SocketChannelSslImpl中的静态方法在进程生命期内只需调用一次,也可以理解成SSL_CTX_new()、SSL_CTX_free()等方法只需调用一次。更进一步理解SSL_CTX结构体在进程内只需要创建一次(在Nebula中分别为Server和Client各创建一个)就可以为所有SSL连接所用;当然,为每个SSL连接创建独立的SSL_CTX也没问题(Nebula 0.4中实测过为每个Client创建独立的SSL_CTX),但一般不这么做,因为这样会消耗更多的内存资源,并且效率也会更低。
建立SSL连接时,客户端调用SSL_connect(),服务端调用SSL_accept(),许多openssl的demo都是这么用的。Nebula中用的是SSL_do_handshake(),这个方法同时适用于客户端和服务端,在兼具client和server功能的服务更适合用SSL_do_handshake()。注意调用SSL_do_handshake()前,如果是client端需要先调用SSL_set_connect_state(),如果是server端则需要先调用SSL_set_accept_state()。非阻塞IO中,SSL_do_handshake()可能需要调用多次才能完成握手,具体调用时机需根据SSL_get_error()获取错误码SSL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE判断需监听读事件还是写事件,在对应事件触发时再次调用SSL_do_handshake()。详细实现请参考SocketChannelSslImpl的Send和Recv方法。
关闭SSL连接时先调用SSL_shutdown()正常关闭SSL层连接(非阻塞IO中SSL_shutdown()亦可能需要调用多次)再调用SSL_free()释放SSL连接资源,最后关闭socket连接。SSL_CTX无须释放。整个SSL通信顺利完成,Nebula 0.4在开多个终端用shell脚本死循环调用curl简单压测中SSL client和SSL server功能一切正常:
while : do curl -v -k -H "Content-Type:application/json" -X POST -d '{"hello":"nebula ssl test"}' https://192.168.157.168:16003/test_ssl done测试方法如下图: