muduo的inspect库以及TCP的Keep-Alive时间分析


一:源码分析

    今天剖析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,可是我只是在浏览器上输入了一次命令啊。百思不得骑*,然后百度了一下,原来是这样的:


    好吧,那么一次性并发了两个TCP连接也就不足为奇了。再往下看,由于61169(简称69)端口的TCP连接先收到相应,69端口和http server先建立连接,然后携带http请求包发往http server,我推测浏览器这时候会关掉另一支连接,也就是70端口的连接,因为GET请求已经可以通过更快更早的连接发送出去了,多余的一斤没用了。然后直接发送FIN,70端口的TCP连接就断掉了。那么接下来就是已建立的连接之间交换数据,http server发送应答包。
    但是,我们之前说过,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功能,你直接通过发送接收的函数返回值就可以知道网络是否异常。设置的方法(应用层):

    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  
   所以系统默认时间那么长,当然不可能自己的服务器先发送keep alive了。然后客户端发送了三次keep alive,即便收到应答了,客户端浏览器可能自身的设定还是主动关闭了连接,这个我们从抓包信息就可以看出来。

   我把几个值都用sysctl修改之后,重新执行了一遍程序流程,抓包如下:


 
    仔细看看,现在果然是服务器192.168.33.147向客户端发送keep alive了,并且第一次发送的时间间隔就是10秒,与我设置的一样。

    另外,keep alive机制不针对正常关闭情况,甚至你kill调对端进程,也是由read等函数处理的。keep alive只能用于对端设备断电,网络故障的情况。我今天惨痛的试验了一下午,一直想kill,或者什么来replay这种情况,结果证明我是错的。

    今天感觉很无力,早上到中午学了个GDB,下午到晚上被这个keep alive整了好几个小时,一直想不通为什么我设置的服务器tcp的keep alive,但是是客户端发送的。后来才知道这是时间的原因,差点都想放弃了,这个东西。
  

    感谢: http://blog.csdn.net/g1036583997/article/details/52803660



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值