一:源码分析
今天剖析muduo inspect库。muduo_inspect库通过HTTP的方式为服务器提供监控接口。比如提供以下功能:
- 接收了多少个TCP连接
- 当前有多少个活动连接
- 一共响应了多少次请求
- 每次请求的平均响应时间为多少ms
- ...
inspect库主要有Inspector类组成,它包含了一个HttpServer对象,并同过ProcessInspector类返回进程信息,ProcessInfo类获取进程相关信息。
以查看进程id为例,用法如下:
浏览器输入ip,端口,"proc"是进程模块,“pid”是命令,获取进程pid。当然也可以获取其他资源。
首先我们来看它的类:
class Inspector : boost::noncopyable
{
public:
typedef std::vector<string> ArgList;
typedef boost::function<string (HttpRequest::Method, const ArgList& args)> Callback;
Inspector(EventLoop* loop,
const InetAddress& httpAddr,
const string& name);
~Inspector();
/// Add a Callback for handling the special uri : /mudule/command
//添加监控器对应的回调函数
//如 add("proc", "pid", ProcessInspector::pid, "print pid")
//http:/192.168.33.147:12345/proc/pid这个http请求就会相应调用ProcessInspector::pid来处理
void add(const string& module, //模块
const string& command, //命令
const Callback& cb, //回调执行方法
const string& help); //帮助信息
void remove(const string& module, const string& command);
private:
typedef std::map<string, Callback> CommandList; //命令列表,对于客端发起的每个命令,有一个回调函数处理
typedef std::map<string, string> HelpList; //针对客端命令的帮助信息列表
void start();
void onRequest(const HttpRequest& req, HttpResponse* resp);
HttpServer server_;
boost::scoped_ptr<ProcessInspector> processInspector_; //暴露的接口,进程磨成
boost::scoped_ptr<PerformanceInspector> performanceInspector_; //性能模块
boost::scoped_ptr<SystemInspector> systemInspector_; //系统模块
MutexLock mutex_;
std::map<string, CommandList> modules_; //模块,对应佑趍odule -- command -- callback
std::map<string, HelpList> helps_; //帮助,对应于module -- command -- help
};
然后是构造函数:
Inspector::Inspector(EventLoop* loop,
const InetAddress& httpAddr,
const string& name)
: server_(loop, httpAddr, "Inspector:"+name), //初始化http服务器
processInspector_(new ProcessInspector), //进程检查器
systemInspector_(new SystemInspector) //系统检查器
{
assert(CurrentThread::isMainThread()); //只能在主线程构造
assert(g_globalInspector == 0);
g_globalInspector = this; //注册全局观察器
server_.setHttpCallback(boost::bind(&Inspector::onRequest, this, _1, _2)); //注册http服务器的回调函数
processInspector_->registerCommands(this); //注册命令,实际上就是填充两个map对应的命令,回调函数,help信息
systemInspector_->registerCommands(this);
//这样子做法是为了防止竞态问题
//如果直接调用start,(当前线程不是loop所在的I/O线程,是主线程),那么有可能,当前构造函数还没返回,
//I/O线程可能已经收到了http客户端的请求了(因为这是?1 HttpServer已经启动),那么就会回调Inspector::onRequest,
//而这时候构造函数还没返回,也就是对象还没完全构造好,会出现core dump
loop->runAfter(0, boost::bind(&Inspector::start, this)); // little race condition
}
我们看到构造函数中除了注册onRequset()函数(这个待会分析),还对成员processInspector(进程观察器)的注册了registerCommands()函数,这实际上是注册命令的函数,我们可以将它理解为为观察器inspector添加功能。比如上文图中使用的获取进程id功能,也可以注册获取连接数功能,线程数功能等。
我们来看它怎么实现的,进程连接器processInspector类是这样的:
class ProcessInspector : boost::noncopyable
{
public:
void registerCommands(Inspector* ins); //注册命令结口
//向外部提供的四个回调函数
static string overview(HttpRequest::Method, const Inspector::ArgList&);
static string pid(HttpRequest::Method, const Inspector::ArgList&);
static string procStatus(HttpRequest::Method, const Inspector::ArgList&);
static string openedFiles(HttpRequest::Method, const Inspector::ArgList&);
static string threads(HttpRequest::Method, const Inspector::ArgList&);
static string username_;
};
它只有一个给总观察器注册功能的方法,注册的功能将会配合它自己的四个回调函数,当客端访问某个功能时,回调函数执行即可。
registerCommands()函数如下:
void ProcessInspector::registerCommands(Inspector* ins)
{//将监控器传递进来,监控器添加4个回调函数,muduo库当前仅支持这四种,注册四个命令
ins->add("proc", "overview", ProcessInspector::overview, "print basic overview");
ins->add("proc", "pid", ProcessInspector::pid, "print pid");
ins->add("proc", "status", ProcessInspector::procStatus, "print /proc/self/status");
// ins->add("proc", "opened_files", ProcessInspector::openedFiles, "count /proc/self/fd");
ins->add("proc", "threads", ProcessInspector::threads, "list /proc/self/task");
}
举个例子执行返回pid的函数如下:
string ProcessInspector::pid(HttpRequest::Method, const Inspector::ArgList&)
{
char buf[32];
snprintf(buf, sizeof buf, "%d", ProcessInfo::pid());
return buf;
}
所以目前来说,观察器Inspector的所有功能可以说已经安装完毕,就等客端连接了。
那我们来看一下客户端连接后处理的onRequest()函数:
//http响应处理函数
void Inspector::onRequest(const HttpRequest& req, HttpResponse* resp)
{
if (req.path() == "/") //如果请求路径为"/",
{
string result;
MutexLockGuard lock(mutex_);
//遍历helps_,格式如下(模块,命令,函数):
// /proc/pid print pid
// /proc/status print /proc/self/status
for (std::map<string, HelpList>::const_iterator helpListI = helps_.begin();
helpListI != helps_.end();
++helpListI)
{
const HelpList& list = helpListI->second;
for (HelpList::const_iterator it = list.begin();
it != list.end();
++it)
{
result += "/";
result += helpListI->first; //外部迭代器,first是module,比如上面的proc
result += "/";
result += it->first; //内部爹抬起第一个,first是command,比如上面的
size_t len = helpListI->first.size() + it->first.size();
result += string(len >= 25 ? 1 : 25 - len, ' ');
result += it->second; //help
result += "\n";
}
}
//封装http响应包,体部就是上述信息
resp->setStatusCode(HttpResponse::k200Ok);
resp->setStatusMessage("OK");
resp->setContentType("text/plain");
resp->setBody(result);
}
else
{ //分割/proc/pid,把它们push_back按顺序进vecor<string>
//如果不是根路径,需要对请求路径进行分割,调用split函数,分割"/",将得到的字符串保存在result中
std::vector<string> result = split(req.path());
// boost::split(result, req.path(), boost::is_any_of("/"));
//std::copy(result.begin(), result.end(), std::ostream_iterator<string>(std::cout, ", "));
//std::cout << "\n";
bool ok = false;
if (result.size() == 0)
{
//这种情况是错误的,因为ok仍为false
LOG_DEBUG << req.path();
}
else if (result.size() == 1)
{
//只有module没有command,发送一张图片,貌似是全白图片
string module = result[0];
if (module == "favicon.ico")
{
resp->setStatusCode(HttpResponse::k200Ok);
resp->setStatusMessage("OK");
resp->setContentType("image/png");
resp->setBody(string(favicon, sizeof favicon));
ok = true;
}
else
{
LOG_ERROR << "Unimplemented " << module;
}
}
else
{
string module = result[0]; //查找模块
std::map<string, CommandList>::const_iterator commListI = modules_.find(module);
if (commListI != modules_.end())
{
string command = result[1]; //result[1]就是command
const CommandList& commList = commListI->second;
CommandList::const_iterator it = commList.find(command); //在命令列表中查找命令
if (it != commList.end())
{
//如果有额外的参数,如/proc/threads/1/2/3
ArgList args(result.begin()+2, result.end());
if (it->second)
{
resp->setStatusCode(HttpResponse::k200Ok);
resp->setStatusMessage("OK");
resp->setContentType("text/plain");
const Callback& cb = it->second; //回调函数
resp->setBody(cb(req.method(), args)); //体部包含回调函数的返回值,是字符串
ok = true;
}
}
}
}
if (!ok)
{
resp->setStatusCode(HttpResponse::k404NotFound);
resp->setStatusMessage("Not Found");
}
//resp->setCloseConnection(true);
}
}
该函数根据不同的请求做出相应的处理,实际上就是将请求中模块(比如进程proc模块),命令(比如pid),回调函数(函数名也叫pid)一一分析出来,然后调用回调函数获得相应的结果,结果是字符串形式,比如pid号,然后把它们封装成http响应包,然后它的成员http server就会把该响应包发送回去,客户端浏览器就可以看到想要的结果了。
不过,我们发现,onRequest()函数没有对连接之后的是否断开进行处理,那么无效连接什么时候断开呢?下面会分析。
二:抓包分析
我对上述过程使用tcpdump抓包,并用wireshark做了分析,下面说一下我的感悟,不过先上一下图。
这张图片有点大,是因为我抓了所有的包。现在说一下分析,首先开始的FIN,ACK不用管,是我之前程序遗留下的。
从SYN开始看,客户端浏览器ip是192.168.33.1,服务器是192.168.33.147。一开始有点奇怪,这怎么有着么多的SYN,仔细一看竟然发起了两个连接,它们的进程号也不相同,分别是61169和61170,可是我只是在浏览器上输入了一次命令啊。百思不得骑*,然后百度了一下,原来是这样的:
但是,我们之前说过,onRequest()函数并没有对连接进行处理,万一连接无效了呢,难道会一直占用资源?
这时候,TCP的keep alive机制就派上用场了,我们知道一个新链接建立的时候,会new一个TcpConnection,它是http server的成员。TcpConnection的构造函数中就对所维护的tcp连接执行了set keepalive操作,实际上就是用setsockopt设置的。
我们看wireshark的图的黑色部分,那不正是keep alive吗。算一下数目,发现192.168.33.1一共向192.168.33.147发送了3个keep alive,然后三次后它直接关闭了连接。不过,我们在TcpConnection中设置的sockfd的TCP_KEEPALIVE,但是我们发现192.168.33.1是客户端的ip地址啊,为什么不是我们设置的服务器的keep alive起作用呢?
这个问题让我纳闷了一下午,后来百度出来,关于keep alive,linux下是由参数的,都在/proc/sys/net/ipv4/目录下,分别是三个文件:
- tcp_keepalive_probes (探测次数,默认值为9次)
- tcp_keepalive_time (探测超时,默认7200秒)
- keepalive_intvl (探测间隔,默认75秒)
对于一个已经建立的tcp连接。如果在keepalive_time时间内双方没有任何的数据包传输,则开启keepalive功能的一端将发送 eepalive数据包,若没有收到应答,则每隔keepalive_intvl时间再发送该数据包,发送keepalive_probes次。一直没有 收到应答,则发送rst包关闭连接。若收到应答,则将计时器清零。例如:
sk->keepalive_probes = 3;
sk->keepalive_time = 30;
sk->keepalive_intvl = 1;
意思就是说对于tcp连接,如果一直在socket上有数据来往就不会触发keepalive,但是如果30秒一直没有数据往来,则keep alive开始工作:发送探测包,受到响应则认为网络,是好的,结束探测;如果没有相应就每隔1秒发探测包,一共发送3次,3次后仍没有响应,就关闭连接,也就是从网络开始断到你的socket能够意识到网络异常,最多花33秒。但是如果没有设置keep alive,可能你在你的socket(阻塞性)的上面,接收: recv会一直阻塞不能返回,除非对端主动关闭连接,因为recv不知道socket断了。发送:取决于数据量的大小,只要底层协议站的buffer能放 下你的发送数据,应用程序级别的send就会一直成功返回。 直到buffer满,甚至buffer满了还要阻塞一段时间试图等待buffer空闲。所以你对send的返回值的检查根本检测不到失败。开启了keep alive功能,你直接通过发送接收的函数返回值就可以知道网络是否异常。设置的方法(应用层):
sk->keepalive_time = 30;
sk->keepalive_intvl = 1;
意思就是说对于tcp连接,如果一直在socket上有数据来往就不会触发keepalive,但是如果30秒一直没有数据往来,则keep alive开始工作:发送探测包,受到响应则认为网络,是好的,结束探测;如果没有相应就每隔1秒发探测包,一共发送3次,3次后仍没有响应,就关闭连接,也就是从网络开始断到你的socket能够意识到网络异常,最多花33秒。但是如果没有设置keep alive,可能你在你的socket(阻塞性)的上面,接收: recv会一直阻塞不能返回,除非对端主动关闭连接,因为recv不知道socket断了。发送:取决于数据量的大小,只要底层协议站的buffer能放 下你的发送数据,应用程序级别的send就会一直成功返回。 直到buffer满,甚至buffer满了还要阻塞一段时间试图等待buffer空闲。所以你对send的返回值的检查根本检测不到失败。开启了keep alive功能,你直接通过发送接收的函数返回值就可以知道网络是否异常。设置的方法(应用层):
int keepalive = 1; // 开启keepalive属性
int keepidle = 60; // 如该连接在60秒内没有任何数据往来,则进行探测
int keepinterval = 5; // 探测时发包的时间间隔为5 秒
int keepcount = 3; // 探测尝试的次数.如果第1次探测包就收到响应了,则后2次的不再发.
setsockopt(rs, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepalive , sizeof(keepalive ));
setsockopt(rs, SOL_TCP, TCP_KEEPIDLE, (void*)&keepidle , sizeof(keepidle ));
setsockopt(rs, SOL_TCP, TCP_KEEPINTVL, (void *)&keepinterval , sizeof(keepinterval ));
setsockopt(rs, SOL_TCP, TCP_KEEPCNT, (void *)&keepcount , sizeof(keepcount ));
另外一种方法是使用sysctl命令修改,直接修改就算是root用户也不被允许。
- sysctl net.ipv4.tcp_keepalive_probes=5
- sysctl net.ipv4.tcp_keepalive_time=10
- sysctl net.ipv4.tcp_keepalive_time=5
我把几个值都用sysctl修改之后,重新执行了一遍程序流程,抓包如下:
仔细看看,现在果然是服务器192.168.33.147向客户端发送keep alive了,并且第一次发送的时间间隔就是10秒,与我设置的一样。
另外,keep alive机制不针对正常关闭情况,甚至你kill调对端进程,也是由read等函数处理的。keep alive只能用于对端设备断电,网络故障的情况。我今天惨痛的试验了一下午,一直想kill,或者什么来replay这种情况,结果证明我是错的。
今天感觉很无力,早上到中午学了个GDB,下午到晚上被这个keep alive整了好几个小时,一直想不通为什么我设置的服务器tcp的keep alive,但是是客户端发送的。后来才知道这是时间的原因,差点都想放弃了,这个东西。