用 epoll 编写一个高并发网络程序是很常见的任务,但在 epoll 中加入 ssl 层的支持则是一个不常见的场景。腾讯 WeTest 服务器压力测产品,在用户反馈中收到了不少支持 https 协议的请求。基于此,本文介绍了在基于 epoll 的高并发机器人框架中加入 openssl,实现对 https 支持时的基本实现思路。
2014 年,谷歌在其官方博客中发布公告称,为了打造更安全的互联网环境,谷歌搜索引擎将尝试把 “是否使用安全加密”(HTTPS)作为搜索排名算法中的一个参考因素,使用加密技术的网站将得到更多的展示机会,排名相对同类网站也更有优势。面对运营商的 http 劫持,广告嵌入,将产品页面重定向到其他页面,http 站点通常束手无策。所以仅仅是为了加密流量,https 的部署也将成为大势所趋。
腾讯 WeTest 服务器性能测试原本的简单模式,主要针对以 http 协议为主的轻量级场景(游戏业务一般会采用更复杂的协议)。而在上线之后,收到了不少需要 https 测试的用户反馈,由此决定在我们使用的压测框架中加入 https 支持。
腾讯 WeTest 服务器性能测试是一个基于 epoll 的高并发机器人网络行为模拟框架。其中的网络传输模块,是用单线程 epoll 的多路复用方式,将多个机器人和服务器的交互包进行非阻塞高速转发。配合以 Linux 系统层面的一些配置优化,就可以达到单进程几千的机器人数量。
后台开发同学,一般在自己的 web 服务器中加 https 的配置相对常见,但自己到 socket 层去写 https 的代码实现,这个需求还真不太多。动手之前,我们调研了 https 层可用的库,最常见的就是 OpenSSL 了。像 curl 也有 https 的相应支持,不过考虑到要在 tcp socket(epoll)这一层实现,还是选择了 OpenSSL。
在介绍 OpenSSL 之前,首先要介绍下 https。https 是什么?https 就是 http+tls/ssl(下文简称 ssl)。从网络协议的层面来说,tcp 是传输层协议,http 是应用层协议,ssl 就是为了给应用层的 http 报文加密,专门加在 tcp 和 http 之间的一层安全协议。网络上对 https 协议进行介绍的好文很多,如:http://www.cnblogs.com/LittleHann/p/3741907.html
,详细阐述了 https 的原理,这里就不再赘述。
OpenSSL 就是在常用的 socket 层连接建好之后,完成 ssl 层的连接建立、收发包、连接释放,其实调用的基本思路还是很清晰的。我们以本文中要实现的 client 侧为例,如下图所示:
可以看到,就是在普通的 socket 建立好 tcp 连接后,再用 SSL_connect 建立 ssl 层的连接。然后用 SSL_read/SSL_write 替代 recv/send 进行收发数据,并在 close socket 的前后释放 ssl 层的资源即可。
由于已经实现了基于 epoll 的客户端数据收发和 http 协议的解析,所以这两者都不是本文的重点——下文主要介绍的是在 epoll 的框架中使用 openssl 收发数据时,需要注意的地方。
看到这个标题,肯定有同学会纳闷:tcp 本来不就是全双工的么,https 是在 tcp 层之上的,怎么还会单独拎出这个来说?没错,tcp 是全双工的,但 openssl 的实现,不代表你能像普通 socket 一样在收发两个通道上随意操作。
要点 1:OpenSSL 并发读写,是不安全的
其实 OpenSSL 官方的文档上还没找到直接的话术指明同一个 SSL 不能两个线程并发读写,但实际上,外网上、km 上都有文章说在多线程并发情况下读写会引起程序崩溃。想来是 SSL 对象内部实现中,维护了共享的状态变量或者缓存区之类的资源,并发读写时会改坏数据导致崩溃。可以通过初始化时设置加锁回调的方式来避免(http://linux.die.net/man/3/crypto_set_locking_callback),但锁终究对性能有不小的影响。
不过 gaps 现有的实现是单进程的,即单进程中通过 epoll 完成了多个机器人连接的收发数据,所以并不存在多线程并发的问题,也无需加锁。由此,小标题的 “全双工实现” 其实更严格说是” 单进程情况下读写互不干扰的双工实现 “。
要点 2:OpenSSL 的建链、收包、发包接口,其是否阻塞都随 socket 本身属性而变,所以 OpenSSL 可以非阻塞使用
在我们的场景下,用 epoll 来维护机器人的并发建连接和收发包,当然希望任何一个动作都是非阻塞的,这样才能将多路复用的功效发挥到极致。那现在加了个 ssl2 进去,是否还能保持这一点?答案是能。所以,这里的要点是,OpenSSL 的建立连接、收包、发包,都可以是非阻塞的。
建立连接不用上图中的 SSL_connect,而用 SSL_do_handshake。这样,如果 socket 本身设置为非阻塞的,那这个操作也就不会阻塞,而是有三种返回可能:
1)返回 0:
意味着 ssl 层的交互阻塞了。直观地去理解,虽然这时候 tcp 已经连好了,但总要去收发些握手数据什么的来建立 ssl 层连接吧,而这个过程收发数据阻塞了。此时,用 SSL_get_error() 可以获取具体的错误码:若是 SSL_ERROR_WANT_READ 或 SSL_ERROR_WANT_WRITE,就在 epoll 中关注该连接的可读或可写事件,并在事件被触发时接着调用 SSL_do_handshake,直到返回下面的 1。
2)返回 1:
ssl 层建链数据交互完成,可以开始收发业务数据了
3)<0:
协议或连接层各种异常出错,不再详述。
非阻塞建立 SSL 连接的过程如图所示:
建链之后,就是收发数据了。由于 socket 为非阻塞,所以收发数据的函数 SSL_read、SSL_write 一样会非阻塞。他们的参数和普通的 recv/send 等读写类函数很像,就是传入 buff 和 length 这些。需要注意的在于,和 SSL_do_handshake 一样,如果返回值大于 0,表示成功收发了业务层数据;如果返回值等于 0,则需要判断下错误码是不是 SSL_ERROR_WANT_READ 或 SSL_ERROR_WANT_WRITE,即读写阻塞了。
发包,即发送一个请求到 http 服务器的逻辑如下图:
可以看出,发包的逻辑和普通的使用 epoll 发包的逻辑大概相同,区别在于以下几点:
1)SSL_write 替代了普通的 send
2)SSL_write 也会阻塞。只是,我们这里只关注写阻塞(即图中的错误码为 SSL_ERROR_WANT_WRITE),然后加入 epoll,关注 socket 的可写事件。
上面的第 2 点就是 openSSL 比较奇葩的一个地方了:调用 SSL_write 发包,可能返回的是一个 SSL_ERROR_WANT_READ,即发包可能阻塞在读操作!无法理解吧。其实这个是因为在 http 的底层,会有一个重协商的过程,这个过程,相当于在业务数据正在单向地收或发的时候,突然在 ssl 链路层要去交互协议数据,重建链接了——那这个时候,重协商协议数据交互是双方的,client 可能刚好在 recv 协议数据时被阻塞了,那就只能乖乖地等 socket 可读了——SSL_write 在这种情况下,会返回一个 SSL_ERROR_WANT_READ,等待可读。而下次可读事件发生时,还需要重复调用 SSL_write,直到 SSL_write 成功......是不是有点奇怪,epoll 告知我们 socket 可读了,我们居然要对 socket 调用写操作......
重协商的原理网上也有很多,这里不详述。只是,我们在全双工的模式下,对于 SSL_write 操作,只认为写阻塞是正常的!一旦因为重协商发生而产生读阻塞,我们就认为链路出现问题了——否则,无法真正实现收发互不考虑的全双工,这个会在半双工的时候具体介绍。
收包,即接收服务器侧返回的 http 响应的逻辑如下图:
可以看到,收包的逻辑和发包类似,也是有可能会因为重协商产生写阻塞,我们在全双工实现的做法,一样是认为出错。
要点 3:当 SSL_read 或 SSL_write 阻塞时,需要在 SSL 对象上重复调用该操作直到收发完成
要点 3 正是我们上面提到的奇葩之处。这也是在 OpenSSL 的官方文档中说明了的:
所以,我们如果需要真正支持重协商,就必须有一种半双工的实现——这种实现会在收发包阻塞在对应的操作后,记录一个中间状态,不处理当前不期望的收或发,直到之前被阻塞的操作完成。这种情况下,相当于对这个自定义的状态维护了一个状态机。由于实际实现非常复杂,所以代码细节就不在这里贴了。概括一下,大概是下面的这个状态机转移图和一些要点:
如上图:
1)“正常状态” 可以认为连接当前是空闲的,不需要收发数据;
2)正常态下有客户端数据要发送,则调用 SSL_write 接口,如果阻塞,则会进入图左的两个状态;
3)正常态下 epoll 提示有服务端返回的数据可读,则调用 SSL_read 接口,如果阻塞,则会进入图右的两个状态;
4)在外侧的四种状态下,不是当前期望的操作,都不会处理:如阻塞在等待读/写时,epoll 的可写/可读事件都不理会,又如,阻塞在任何一种状态时,客户的发包请求都会入队列;
5)红字标出的两个状态和平时普通 socket+epoll 的操作刚好相反,值得留意。
如此,一个半双工的 https 客户端实现就有了。但它的缺陷很明显:每次读、写操作都可能阻塞另一个方向上的数据传输,性能会有急剧的下降。由于通常服务器端并不推荐重协商的过程,所以这种情况也是很少见的。因而,全双工的实现加了开关,当普通 https 服务器进行压测时,关闭开关,保证性能;当面对真有重协商这种特殊需求的服务器时,才打开开关。
下面,我们来看一下如何在简单模式中进行 https 页面的服务器性能测试。
1)点击服务器性能测试产品首页(http://wetest.qq.com/gaps/ )中的快捷入口:HTTP 直压。模式选择简单模式,名称和描述可以自己填写。(图中示例起始人数 50 人,每隔 60 秒增加 50 人,加到 200 人为上限)
点击左侧 “HTTP 直压 “进入压测
2)新建一个客户端请求,接口压测包括读写接口,读接口基本是 GET 请求,写接口基本是 POST 请求。GET 请求使用 url 请求参数,填写测试用例的基础数值,选择正确的 URL
3)随后进行 Header 的配置,Header 的名称在选定 URL 的内,打开 URL 的链接(推荐使用 chrome 浏览器),敲击 F12 并刷新页面,选定 Network-Name-Headers-Request Headers(Header 的名称与值均在内查看,如下图所示)
到这里,基本就完成了对 https 的配置过程了,是不是很简单?下面动图可以再回顾一下操作的流程:
腾讯 WeTest 服务器性能测试运用了沉淀十多年的内部实践经验总结,通过基于真实业务场景和用户行为进行压力测试,帮助游戏开发者发现服务器端的性能瓶颈,进行针对性的性能调优,降低服务器采购和维护成本,提高用户留存和转化率。
功能目前免费对外开放中,欢迎大家的体验!
体验地址:http://WeTest.qq.com/gaps
如果对使用当中有任何疑问,欢迎联系腾讯 WeTest 企业 qq:800024531