造https client*的记录

最近我们的服务器需要嵌入HTTP服务,需要支持httpclient和httpserver,httpclient要同时支持https。
我们现在服务器进程之间的网络通信使用的是自有实现,它使用io多路复用技术。因为http只是在tcp之上进行明文传输而已,所以实现也包括了一个简陋的httpclient和httpserver,还基于openssl实现玩具版的https,但基本是不能在生产中使用的。
本来我想着用开源库,但看了下好像与epoll结合也挺麻烦,就先自己手撸*试试。虽然过程痛苦,但加深了对相关知识的理解,比如之前写的理解https的验证过程。以下是造*过程中遇到的一些问题。

SSL_METHOD

通过SSL_CTX *SSL_CTX_new(const SSL_METHOD *method);创建SSL_CTX对象,method又通过一系列的函数来创建,这些函数的参数都是void,返回值都是const SSL_METHOD *,其中SSLv23_method是通用版本,实际使用的SSL/TLS版本通过与对方协商确定,支持SSLv2,SSLv3,TLSv1,TLSv1.1,TLSv1.2,其他的比如TLSv1_2_method则只支持TLSv1.2协议。
更多细节可通过man SSL_CTX_new了解。

信任server端证书

测试环境webserver使用的是自签名证书,所以httpclient需要跳过证书验证步骤,直接信任webserver发来的证书。
查了一下网上说,需要自己注册一个验证证书的回调函数,我实际使用中发现,不对SSL_CTX对象调用SSL_CTX_load_verify_locations和SSL_CTX_use_certificate_file就不会去验证证书。

HTTP POST body常见数据格式

参考HTTP POST body常见的四种数据格式,我们使用的是application/x-www-form-urlencoded格式,这是浏览器的原生form表单格式,上文中说“如果不设置Content-Type属性,则默认以application/x-www-form-urlencoded方式传输数据”,我猜这个默认是浏览器给加的吧,反正我如果不手动添加“Content-Type: application/x-www-form-urlencoded”的header,那边的webserver不认我的请求。

URL编码

上面的x-www-form-urlencoded格式数据需要进行URL编码,这种编码很简单,需要把英文字母、阿拉伯数字、以及“-_.~”之外的字符都进行编码,空格编码成+,其他字符编码成%后面加字符十六进制数字的格式。
比如上面的application/x-www-form-urlencoded格式的body可能是“title=test&sub=%E5%A4%A7%E5%AE%B6”。

chunk编码

有些webserver返回的http报文中,没有Content-Length头部,取而代之的是Transfer-Encoding: chunked,表示包体长度未知或者把包体拆成几个小块传输。每个 chunk 的格式如下所示:

${chunk-length}\r\n${chunk-data}\r\n
其中,${chunk-length} 表示 chunk 的字节长度,使用 16 进制表示,${chunk-data} 为 chunk 的内容
当 chunk 都传输完,需要额外传输 0\r\n\r\n 表示结束

下面是一个例子(来自HTTP 进阶之 chunked 编码):

HTTP/1.1 200 OK
Date: Wed, 01 Jan 2020 08:46:31 GMT
Connection: keep-alive
Transfer-Encoding: chunked

1\r\n
a\r\n
3\r\n
+-=\r\n
14\r\n
ghijklmnopqrstuvwxyz\r\n
0\r\n\r\n

SSL_connect

客户端调用SSL_connect进行握手,如果socket是非阻塞的,SSL_connect会立即返回-1,此时需要调用SSL_get_error获得具体错误,如果是SSL_ERROR_WANT_READ表示还在握手中,等待下次触发读时再调用SSL_connect,如此往复,直到SSL_connect返回1完成握手。

边沿触发下数据的读取

SSL/TLS是介于TCP与HTTP之间的一层,它本身有buffer,epoll触发读调用SSL_read时,先把数据从socket缓存读入SSL缓存,再从SSL缓存读入用户缓存。
没有SSL的时候,epoll触发读了,调用recv或read从socket缓存读到用户缓存,边沿触发模式下一次没读完的话,主动EPOLL_CTL_MOD EPOLLIN事件,此时只要该socket的缓存还有数据可以读,epoll_wait会返回读就绪。
而改为SSL_read后,如果一下把socket缓存都读到了SSL缓存中,而又没有全读入用户缓存,要是没有新数据来到,epoll_wait就不会返回读就绪,SSL缓存中的数据就只能等对端关闭socket的时候触发读了。
应该判断SSL_read的返回值。正数表示读入字节数目,判断返回值如果比SSL_read的第三个参数(最多读取的字节数)小,表示已读取完毕,否则应继续尝试调用SSL_read,而不能像没有SSL的时候直接返回,因为那样就可能会没机会再触发读了。0或负数需要再调用SSL_get_error返回具体错误,如果是SSL_ERROR_WANT_READ表示SSL缓存中已无数据。

上一篇:Vertx之MQTT客户端服务端发送


下一篇:v-model结合select