测试方法
工作中有遇到使用HTTP keep-alive来复用TCP连接来多次发送请求,但是CDN侧有一个逻辑是如果TCP连接60s没有请求过来,CDN则主动断开这条连接。所以,有意测试当CDN断开连接后客户端再次发送请求的情况,测试伪代码如下:
int test()
{
open_tcp_connect();
send_http_request(...);
on_recv_http_response(...);
// 此时http response中 Connection:Keep-Alive 表示复用连接
sleep(65); // CDN会60s断开
send_http_request(...); // 此时发送失败,asio_async_write报错,errno为2
on_recv_http_response(...);
}
errno为2的定义是没有文件描述符,感觉像是socket出现了问题。
#define ENOENT 2 /* No such file or directory */
分析问题
使用netstat -annp查看连接状态处于close_wait状态,则说明在CDN关闭连接后,客户端没有关闭连接。
此时CDN应该处于time_wait的状态,所以客户端的这种行为是对CDN资源大大的消耗。
继续tcpdump抓包,可以很明显的看到CDN发送的fin包,但客户端没有发送fin给CDN
那处于close_wait状态的连接什么时候关闭呢?如果上层没有继续在发送数据的话,会一直处于close_wait状态。
如果客户端再次发送数据的话,则会发送失败,且boost马上关闭socket。抓包分析如下:
可以看到,此时第二次发送的数据已经从网卡发送出去了,然后boost发送fin包来关闭socket。那么发送出去的数据也就是白白浪费了手机流量,即使这种情况下boost不主动关闭socket,CDN也会回复rst包,因为之前的连接CDN已经超时关闭了。
boost库真的存在这种问题吗?
这个问题其实很基础的,一般的网络事件库设计都会提供注册网络socket断开的回调函数来让业务层关闭socket。或者自己实现的epoll的逻辑会当recv返回0时关闭socket,但是我目前没有查到boost的asio组件提供这样的接口或者回调。
google搜索后也存在一些这样的贴子,传送门1,传送门2
boost库为什么这么设计?
从接口上看asio的api和操作系统提供操作socket的api很类似,就是简单的send/recv接口。操作系统是利用了epoll/select监听socket来感知socket的变化,然后用recv的返回值来判断socket发生了什么事情。 但是asio已经拥有了epoll的功能了,但是却没有提供错误回调函数,所以,我认为是设计上的缺陷。
规避问题的方法1
规避问题的方法其实很简单,客户端在CDN超时之前主动关闭socket即可。因为客户端一般知道CDN超时的时间t1,所以客户端主动关闭socket的时间t2比t1配置的小一点。同时客户端也监控当发送的逻辑返回errno为2或者54时,主动设置t1为原来的2/3。
规避问题的方法2
对于http长链接的在请求完成后,再次调用recv,则如果CDN关闭了socket,则recv函数则可以感知到。