解决snmp数据溢出的参考文章 NetPerSec 获取网络流量部分源码分析 之一

本文解析了NetperSec的源码,详细介绍了其如何利用SNMP和MIB-II进行网络流量统计。NetperSec是一款开源的Windows网络流量监控工具,能够实时监控系统网络流量并以图形方式展示。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、 NetperSec 简介
    NetperSec 是一款 Windows 下的网络流量统计工具。它能够实时的监视系统内全部或者某个网络接口上的网络流量,无论这些流量是向内的还是向外的,并且以图形化的方式显示通信的即时速率和平均速率。同时, NetperSec 还会在系统托盘的图标上已动态条形图或者柱状图来反映接收和发送的网络流量。更重要的一点,它是开源的,这得以让我们通过分析它的源码来了解它的工作机理,感谢作者的无私。界面如下:




二、SNMP 和 MIB 简介

    通过 NetperSec 官方的简单介绍,我们得知 NetperSec 获取网络流量信息用到了 SNMP(Simple Network Management Protocol)--简单网络管理协议。下面就先介绍一下 SNMP 和 MIB。
   
    SNMP 规范定义了每个 SNMP 代理都支持的 MIB(Management Information Base 管理信息库)集合。第一组集合对象称为 MIB-I,并且记录在 RFC 1156 文档中;随着时间的推移,这些对象扩展了,并且对象集合变成了 MIB-II,RFC 1213 就是用来描述 MIB-II 的规范。MIB-II 对象提供了关于设备网络情况的一般信息,是一个被划分为若干组的集合。MIB 对象是按层次树形结构组织的,该树上每个分支有自己唯一的名字和数值标识符,从根到各分支的组织形式如下图:
   

   
    一般用带点(.)表示法来表示对象标识符,也就是用点号(.)来分隔各个分支名或者分支标识符。通常根节点(.)是隐含的,不用写出。如 mib-2 对象的完整路径表示为 iso.org.dod.internet.mgmt.mib-2 或者 1.3.6.1.2.2 。
   
    MIB 包含的对象代表一个设备的物理特性,或是代表一个代理所包含的其他信息。这些信息可以是分离元素的形式,如 sysDescr 此类独立对象,也可以是二维表形式。这些表中存储着一个 MIB 对象中的几个实例或相关信息。在 MIB-II中定义的 interface 组,它的标识符是 iso.org.dod.internet.mgmt.mib-2.interface 或者 1.3.6.1.2.1.2 。在此组中的对象代表网络物理接口和网络设备中的相关信息。另外,与性能相关的信息也被收集和存储在这个组内。
   
    interface 组中包括 ifNumber 对象,该对象表明网络设备中网络接口总数。另外,interface 组中还包括 ifTable 对象,它包括一个表格。这个表格对应设备中的每一个接口,每个接口一行。这个表格用 ifIndex 来索引,并且包含从 1 到 ifNumber 对象值之间的值。ifIndex 对象也唯一的指示了表中每一行所代表的接口。ifTable 包括 22 个对象。对 Linux 系统查询 interface 表时,显示的输出与下面的内容类似:
   
    interfaces.ifNumber.0 = 2
    interfaces.ifTable.ifEntry.ifIndex.1 = 1
    interfaces.ifTable.ifEntry.ifIndex.2 = 2
    interfaces.ifTable.ifEntry.ifDescr.1 = "lo0" Hex: 6C 6F 30
    interfaces.ifTable.ifEntry.ifDescr.2 = "eth0" Hex: 65 74 68 30
    interfaces.ifTable.ifEntry.ifType.1 = softwareloopback(24)
    interfaces.ifTable.ifEntry.ifType.2 = ethernet-csmacd(6)
    interfaces.ifTable.ifEntry.ifMtu.1 = 3924
    interfaces.ifTable.ifEntry.ifMtu.2 = 1500
    ......
    ......
    interfaces.ifTable.ifEntry.ifInOctets.1 = 373912
    interfaces.ifTable.ifEntry.ifInOctets.2 = 50240
    ......
    ......
    interfaces.ifTable.ifEntry.ifOutOctets.1 = 381334
    interfaces.ifTable.ifEntry.ifOutOctets.2 = 17402
    ......
    ......
    interfaces.ifTable.ifEntry.ifSpecific.1 = OID: .ccitt.nullOID
    interfaces.ifTable.ifEntry.ifSpecific.2 = OID: .ccitt.nullOID
           
   
    下图所示的分层结构表示 interfaces 组,该图中展示了这些对象的不同组,包括接口特性以及传入、传出通信计数器。
   

   
    在 NetperSec 中,主要用到了上面对象中的 ifDescr、ifInOctets 和 IfOutOctets。按照 MIB 的描述格式,这三个对象可以依次描述为:
   
    对象名:     ifDescr
    OID:       ifEntry.2
    对象类型:   DisplayString[255]
    访问模式:   只读
    描述:      该接口的一个字符串描述,包括操作系统角度获得的接口名,可能包括以下值:eth0、ppp0、 lo0
    对象名:    ifInOctets
    OID:       ifEntry.10
    对象类型: Counter
    访问模式: 只读
    描述:      从该接口上接受到的字节数,包括任何数据链路组帧的字节
    对象名:    ifOutOctets
    OID:      ifEntry.16
    对象类型: Counter
    访问模式: 只读
    描述:      在该接口上发送的字节数,包括任何数据链路组帧的字节
   

三、 NetperSec 源码分析

    上面介绍了 SNMP 和 MIB 的一些背景知识,下面就详细分析一下 NetperSec 的源码,了解程序是如何使用 SNMP 和 MIB 的。
   
    首先看一下程序的主要流程:进程初始化,运行 NetperSec.cpp 里的 CNetPerSecApp::InitInstance(),在这个函数里,会创建一个 Cwinproc 类的窗体,创建成功后调用 Cwinproc 类的公有函数 Startup()。Cwinproc 类有个公共成员变量 snmp,它是 CSnmp 类的对象。在 Startup() 中首先执行 CSnmp 类的公有函数 Init() 初始化 SNMP 相关的函数,同时分配内存。如果 Init() 初始化成功,则启动定时器,定时器触发后会调用 Cwinproc::OnTimer() ,在 Cwinproc::OnTimer() 中会调用 CSnmp::GetReceivedAndSentOctets() 采集流量信息,然后再由相关的界面线程显示给用户。定时器处理函数 Cwinproc::OnTimer 是通过消息映射中的宏 ON_WM_TIMER 与 Cwinproc 类关联到一起的。
   
    下面就详细介绍一下前面提到的类和函数,同时还会介绍一个叫 CPerfData 的类,它用来通过注册表获取信息。
    首先说说 Cwinproc 类,它声明在 Cwinproc.h,定义在 Cwinproc.cpp 中。Cwinproc 是一个窗体类,它继承自 MFC 的 CWnd 类,下面是它的源码:
   
    class Cwinproc : public CWnd
    {
     // Construction
     public:
         Cwinproc();
         void StartUp( );
       LRESULT OnTaskbarNotify( WPARAM wParam, LPARAM lParam );
       void UpdateTrayIcon( HICON hIcon );
       void ShowPropertiesDlg( );
       UINT GetSNMPValue( LPCSTR pString );
   

     // Attributes
     public:
       CSnmp snmp;
       DlgPropSheet* m_pPropertiesDlg;

       DWORD m_dwStartTime;
       double m_dbTotalBytesRecv;
       double m_dbTotalBytesSent;
       double m_dbRecvWrap;
       double m_dbSentWrap;

       NOTIFYICONDATA m_SystemTray;
       STATS_STRUCT RecvStats[MAX_SAMPLES];
       STATS_STRUCT SentStats[MAX_SAMPLES];

       int GetArrayIndex( );
       void ResetData( );
       void CalcAverages( double dwTotal, DWORD dwTime, DWORD dwBPS, STATS_STRUCT* pStats );
      
    private:
      int m_nArrayIndex;

    // Operations
    public:

    // Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(Cwinproc)
    public:
        virtual void WinHelp(DWORD dwData, UINT nCmd = HELP_CONTEXT);
    //}}AFX_VIRTUAL
   
    // Implementation
    public:
      virtual ~Cwinproc();

    // Generated message map functions
    protected:
    //{{AFX_MSG(Cwinproc)
        afx_msg void OnTimer(UINT nIDEvent);
        afx_msg void OnClose();
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
    };
   
    Cwinproc 主要完成下面的一些功能:
    1,负责初始化 SNMP 库,并定时采集 MIB-II 数据,由 Startup(), OnTimer() 和 CalcAverages 以及两个 STATS_STRUCT 类型作为元素的数组 RecvStats 和 SentStats 实现;
    2,更新系统托盘区动态图标,主要由 UpdateTrayIcon() 完成;
    3,显示设置对话框,在 ShowPropertiesDlg() 中实现;
    4,处理用户在系统托盘区的鼠标操作,由 OnTaskbarNotify() 来完成,这是一个回调函数,在 Cwinproc 的消息映射中将此函数与全局自定义消息 TaskbarCallbackMsg 绑定到一起;
    5,显示帮助信息,在 WinHelp() 中实现;
   
    下面先看看 Cwinproc::Startup() 的实现,源码如下:
   
    void Cwinproc::StartUp( )
    {
       if( snmp.Init( ) == FALSE )
       {
              PostQuitMessage( 0 );
       }
       else
       {
          SetTimer( TIMER_ID_WINPROC, g_nSampleRate, NULL );

              //1.1 init using NIF_ICON
              HICON hIcon;
              if( g_IconStyle == ICON_HISTOGRAM )
                  hIcon = (HICON)LoadImage(AfxGetInstanceHandle(),MAKEINTRESOURCE( IDI_HISTOGRAM ),IMAGE_ICON,16,16,LR_DEFAULTCOLOR);
              else
                  hIcon = (HICON)LoadImage(AfxGetInstanceHandle(),MAKEINTRESOURCE( IDI_BARGRAPH ),IMAGE_ICON,16,16,LR_DEFAULTCOLOR);

              m_SystemTray.cbSize = sizeof( NOTIFYICONDATA );
              m_SystemTray.hWnd   = GetSafeHwnd( );
              m_SystemTray.uID    = 1;
              m_SystemTray.hIcon = hIcon;
              m_SystemTray.uFlags = NIF_MESSAGE | NIF_ICON;
              m_SystemTray.uCallbackMessage = TaskbarCallbackMsg;
              if( !Shell_NotifyIcon(NIM_ADD, &m_SystemTray ) )
                  AfxMessageBox("System tray error.");
         }
       }
      
       函数首先调用 CSnmp::Init() 函数初始化 SNMP 库,如果失败,则执行 PostQuitMessage( 0 ); 退出程序,否则设置定时器并且向系统托盘区注册图标。定时器的触发间隔默认为 1000 毫秒,即1秒,触发间隔可以在通过配置文件设置。
      
       再看下 Cwinproc::OnTimer() 函数,源码:
      
       void Cwinproc::OnTimer( UINT /* nIDEvent */ )
     {
       DWORD s,r, elapsed;
       DWORD dwRecv_bps, dwSent_bps;
       DWORD dwTime;
       double dbRecv, dbSent;
       
       snmp.GetReceivedAndSentOctets( &r, &s );

       //init data?
       if( !m_dbTotalBytesRecv || !m_dbTotalBytesSent )
       {
          m_dbTotalBytesRecv = r + m_dbRecvWrap;
          m_dbTotalBytesSent = s + m_dbSentWrap;
            m_dwStartTime = GetTickCount( );
       }

       //don't depend upon exact WM_TIMER messages, get the true elapsed number of milliseconds
       dwTime = GetTickCount( );
       elapsed = ( dwTime - m_dwStartTime );
       m_dwStartTime = dwTime;

       dbRecv = r + m_dbRecvWrap;
       dbSent = s + m_dbSentWrap;

       //check for integer wrap
       if( dbRecv < m_dbTotalBytesRecv )
       {
          m_dbRecvWrap += 0xffffffff;
          dbRecv = r + m_dbRecvWrap;
       }

       if( dbSent < m_dbTotalBytesSent )
       {
          m_dbSentWrap += 0xffffffff;
          dbSent = s + m_dbSentWrap;
       }

         //total bytes sent or received during this interval
       DWORD total_recv = (DWORD)( dbRecv - m_dbTotalBytesRecv );
       DWORD total_sent = (DWORD)( dbSent - m_dbTotalBytesSent );

         dwRecv_bps = dwSent_bps = 0;
    
         //calc bits per second
       if( elapsed )
       {
          dwRecv_bps = MulDiv( total_recv, 1000, elapsed );
          dwSent_bps = MulDiv( total_sent, 1000, elapsed );
       }

       //convert to double
       double recv_bits = (double)(r);
       double sent_bits = (double)(s);

       //calc the average bps
       CalcAverages( recv_bits, dwTime, dwRecv_bps, &RecvStats[0] );
       CalcAverages( sent_bits, dwTime, dwSent_bps, &SentStats[0] );

       //get the icon for the system tray
       HICON hIcon = pTheApp->m_Icons.GetIcon( &RecvStats[0], &SentStats[0], m_nArrayIndex, g_IconStyle );
       UpdateTrayIcon( hIcon );   
       DestroyIcon( hIcon );

       //increment the circular buffer index
       if( ++m_nArrayIndex >= MAX_SAMPLES )
          m_nArrayIndex = 0;

       //save the totals
       m_dbTotalBytesRecv = dbRecv;
       m_dbTotalBytesSent = dbSent;
     }
             
       首先它调用 CSnmp::GetReceivedAndSentOctets() 函数获得接收和发送的字节 r 和 s。然后通过检查 m_dbTotalBytesRecv 或 m_dbTotalBytesSent 是否为 0 来判断是否是第一次调用 OnTimer() 函数,如果是第一次,就把 r、s 的值直接赋给 m_dbTotalBytesRecv 和 m_dbTotalBytesSent。同时调用 GetTickCount 获取系统启动到现在的毫秒值并保存到 m_dwStartTime 变量中,后面会用到这个值。 如果不是第一次了,就先调用 GetTickCount 获取毫秒数,并用这个毫秒数减去之前保存的 m_dwStartTime 的值。因此可以看出,尽管可以使用 OnTimer 的触发周期作为流浪统计的周期,但由于 OnTimer 在计时上不准确, NetperSec 直接采用比较两次的 GetTickCount 值来获得精确的时间间隔。得到本次采集与上次采集的间隔后,就把本次的系统毫秒值存储在 m_dwStartTime 中以备下次使用。接下来把 snmp 采集到数据加上一个修正值之后存储在临时变量中。修正值 m_dbRecvWrap 和 m_dbSentWrap 的用处在于,由于 r,s 都是 DWORD 类型,他们所能表达的最大值就是 $FFFFFFFF, 一旦 CSnmp::GetReceivedAndSentOctets() 返回的真实数值大于 $FFFFFFFF,那么 r、s 中仅能保存溢出后剩下的部分,如 $100000000 强制转换成 DWORD 之后就是 0,因此就需要在 r、s 溢出之后,通过给 m_dbRecvWrap 或 m_dbSentWrap 加上 $FFFFFFFF,并且让 r、s 分别与 m_dbRecvWrap 和 m_dbSentWrap 修正之后,作为总接收和发送的流量。 if( dbRecv < m_dbTotalBytesRecv ) 是用来判断收到的数据流量是否溢出,而 if( dbSent < m_dbTotalBytesSent ) 则是判断发出的数据流量是否溢出。再接下来就是计算本次采集间隔所发生的流量,分别使用修正过的 dbRecv 和 dbSent 减去对应的 m_dbTotalBytesRecv 和 m_dbTotalBytesSent。有了本次采集间隔的流量,就可以计算本次采集间隔的速率了,通过 MulDiv 函数,先把本次流量扩大1000倍,然后再除以本次采集的间隔时间,然后对结果四舍五入得到本次采集间隔的速率。继续执行,程序通过 Cwinproc::CalcAverages() 计算平均速率,由于 CalcAverages() 需要的参数为 double 型,因此需要先把 r、s 转换一下。再然后就是更新系统托盘图标,步进存储区索引,最后把总流量信息保存到全局的 m_dbTotalBytesRecv 和 m_dbTotalBytesSent 中,以备下一个 OnTimer 时使用。
      
       下面再看看 Cwinproc::CalcAverages() 函数,它负责把采集到的字节数、本次采集间隔内的速率以及系统的毫秒值存储到对应的数组内,并且计算自一段时间以来的网络流量平均值,源码如下:
      
       void Cwinproc::CalcAverages( double dbTotal, DWORD dwTime, DWORD dwBps, STATS_STRUCT* pStats )
     {
        ASSERT( g_nAveragingWindow <= MAX_SAMPLES );

        //set current bps
        pStats[m_nArrayIndex].Bps = dwBps;

        //set total bytes sent or recv
        pStats[m_nArrayIndex].total = dbTotal;

        //set the current time (milliseconds)
        pStats[m_nArrayIndex].time = dwTime;

        //start is the index in the array for calculating averages
        int start = m_nArrayIndex - g_nAveragingWindow;
          if( start < 0 )
               start = MAX_SAMPLES + start;

        //the array entry may not have been filled in yet
        if( pStats[start].total == 0 )
           start = 0;
   
        //set average based upon sampling window size
          double dbSampleTotal = 0;

        //total bytes received/sent in our sampling window
        if( pStats[start].total )
           dbSampleTotal = dbTotal - pStats[start].total;
   
        //elapsed milliseconds
        DWORD dwElapsed = ( pStats[m_nArrayIndex].time - pStats[start].time );
   
        //calc average
        if( dwElapsed )
           pStats[m_nArrayIndex].ave = MulDiv( (DWORD)dbSampleTotal, 1000, dwElapsed );  
     }
      
       这个函数作者加的注释比较多,理解起来也要容易得多。前面几行是将传入的参数保存到元素类型为 STATS_STRUCT 的数组内,STATS_STRUCT 是一个结构体,它的定义可以在 Globals.h 中找到。然后是根据当前平均速率窗口 g_nAveragingWindow 的大小获取计算平均速率的基本点的索引,默认 g_nAveragingWindow 的大小是 30,也就是计算 30 个采集周期的平均速率。如果指定索引处的字节数为 0,则说明采集周期还不够 g_nAveragingWindow 个,需要从索引号为 0 的第一个采集点开始计算平均速率。最后就是用当前点和获取的指定基本点的流量差和时间差计算出平均速率并存储在数组内。
      
       至此,Cwinproc 类中的重要功能函数就分析完了,该类中还有其他几个函数,由于和本文主题不符,这里就不再详细分析,有兴趣的朋友可以自己阅读 NetperSec 的源码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值