【C/C++】项目_8_数据挖掘/HTTP协议/非结构化数据存储(filetoblob.cpp),数据管理/监控告警(hsmtable.cpp,tbspaceinfo.cpp)

@TOC


1. 数据挖掘子系统:rc.local

如下数据挖掘dmining和数据交换exptables差不多(exptables自己设计数据表有keyid字段,从其他地方拿数据未必有keyid字段需要指定,也就是数据是增量的,但不提供增量提取方式即没有keyid字段),将数据从数据库中取出导入文件(执行一个sql拿出数据)。vi /etc/rc.local(随操作系统自启动)

在这里插入图片描述

vi /htidc/gz.....sh上面如下,如下要写的其实就是< selectsql >字段里内容。比如取近一小时数据,每次取到都有重复但在入库时处理重复就行,获取到的数据生成xml再入自己库
在这里插入图片描述

在这里插入图片描述

//dminoracle.cpp
#include "_public.h"
#include "_ooci.h"   // oracle数据库是_ooci.h,mysql数据库换成_mysql.h,PostgreSQL数据库换成_postgresql.h
// 主程序的参数
struct st_arg
{
  char connstr[101];
  char charset[51];
  char tname[51];
  char cols[1001];
  char fieldname[1001];
  char fieldlen[501];
  int  exptype;
  char andstr[501];
  char bname[51];
  char ename[51];
  char idfieldname[51];
  char idfilename[301];
  char exppath[301];
  int  timetvl;
} starg;
CLogFile logfile;
connection conn;
// 本程序的业务流程主函数
bool _dmintables();
void EXIT(int sig);
vector<string> vfieldname; // 存放拆分fieldname后的容器
vector<int>    vfieldlen;  // 存放拆分fieldlen后的容器
int maxfieldlen;           // 存放fieldlen的最大值
void SplitFields();        // 拆分fieldname和fieldlen
// 显示程序的帮助
void _help(char *argv[]);
long maxkeyid;   // 已挖掘数据的最大的id
bool LoadMaxKeyid(); // 从记录已获取数据最大id的文件中加载已挖掘数据的最大的id
bool UptMaxKeyid();  // 更新已挖掘数据的最大的id到文件中  
// 把xml解析到参数starg结构中
bool _xmltoarg(char *strxmlbuffer);

int main(int argc,char *argv[])
{
  if (argc!=3) { _help(argv); return -1; }
  // 关闭全部的信号和输入输出
  CloseIOAndSignal();
  // 处理程序退出的信号
  signal(SIGINT,EXIT); signal(SIGTERM,EXIT);
  if (logfile.Open(argv[1],"a+")==false)
  {
    printf("打开日志文件失败(%s)。\n",argv[1]); return -1;
  }
  // 把xml解析到参数starg结构中
  if (_xmltoarg(argv[2])==false) return -1;
  while (true)
  {
    // 连接数据库
    if (conn.connecttodb(starg.connstr,starg.charset) != 0)
    {
      logfile.Write("connect database %s failed.\n",starg.connstr); sleep(starg.timetvl); continue;
    }
    // logfile.Write("export table %s.\n",starg.tname);
    // 挖掘数据的主函数
    if (_dmintables() == false) logfile.Write("export tables failed.\n");
    conn.disconnect();   // 断开与数据库的连接
    sleep(starg.timetvl);
  }
  return 0;
}
void EXIT(int sig)
{
  logfile.Write("程序退出,sig=%d\n\n",sig);

  exit(0);
}

// 显示程序的帮助
void _help(char *argv[])
{
  printf("\n");
  printf("Using:/htidc/public/bin/dminoracle logfilename xmlbuffer\n\n");

  printf("增量挖掘示例:\n");
  printf("Sample:/htidc/public/bin/dminoracle /log/shqx/dminoracle_surfdata_from_qx.log \"<connstr>shqx/pwdidc@snorcl11g_198</connstr><charset>Simplified Chinese_China.ZHS16GBK</charset><tname>T_SURFDATA</tname><cols>obtid,to_char(ddatetime,'yyyymmddhh24miss'),t,p,u,wd,wf,r,vis</cols><fieldname>obtid,ddatetime,t,p,u,wd,wf,r,vis</fieldname><fieldlen>5,14,8,8,8,8,8,8,8</fieldlen><exptype>1</exptype><andstr> and obtid in ('59293','50745')</andstr><bname>SURFDATA_</bname><ename>_from_qx</ename><idfilename>/data/dmin/SURFDATA_from_qx.txt</idfilename><idfieldname>keyid</idfieldname><exppath>/data/shqx/sdata/fromqx</exppath><timetvl>30</timetvl>\"\n\n");
  printf("全量挖掘示例:\n");
  printf("Sample:/htidc/public/bin/dminoracle /log/shqx/dminoracle_obtcode_from_qx.log \"<connstr>shqx/pwdidc@snorcl11g_198</connstr><charset>Simplified Chinese_China.ZHS16GBK</charset><tname>T_OBTCODE</tname><cols>obtid,obtname,provname,lat,lon,height</cols><fieldname>obtid,obtname,provname,lat,lon,height</fieldname><fieldlen>5,30,30,8,8,8</fieldlen><exptype>2</exptype><andstr> and rsts=1 and obtid in ('59293','50745')</andstr><bname>OBTCODE_</bname><ename>_from_qx</ename><exppath>/data/shqx/sdata/fromqx</exppath><timetvl>300</timetvl>\"\n\n");

  printf("本程序是数据中心的公共功能模块,从其它业务系统的数据库中挖掘数据,用于入库到数据中心。\n");
  printf("logfilename是本程序运行的日志文件。\n");
  printf("xmlbuffer为文件传输的参数,如下:\n");
  printf("数据库的连接参数 <connstr>shqx/pwdidc@snorcl11g_198</connstr>\n");
  printf("数据库的字符集 <charset>Simplified Chinese_China.ZHS16GBK</charset> 这个参数要与数据源数据库保持>一致,否则会出现中文乱码的情况。\n");
  printf("待挖掘数据的表名 <tname>T_SURFDATA</tname>\n");
  printf("需要挖掘字段的列表 <cols>obtid,to_char(ddatetime,'yyyymmddhh24miss'),t,p,u,wd,wf,r,vis</cols> 可以采用函数。\n");
  printf("挖掘字段的别名列表 <fieldname>obtid,ddatetime,t,p,u,wd,wf,r,vis</fieldname> 必须与cols一一对应。\n");
  printf("挖掘字段的长度列表 <fieldlen>5,14,8,8,8,8,8,8,8</fieldlen> 必须与cols一一对应。\n");
  printf("挖掘数据的方式 <exptype>1</exptype> 1-增量挖掘;2-全量挖掘,如果是增量挖掘,要求表一定要有表达记录序号的id字段。\n");
  printf("挖掘数据的附加条件 <andstr> and obtid in ('59293','50745')</andstr> 注意,关键字and不能少。\n");
  printf("数据文件的命名的前部分 <bname>SURFDATA_</bname>\n");
  printf("数据文件的命名的后部分 <ename>_from_qx</ename>\n");
  printf("挖掘数据表记录号字段名 <idfieldname>keyid</idfieldname> 当exptype=1时该参数有效。\n");
  printf("已挖掘数据id保存的文件名 <idfilename>/data/dmin/SURFDATA_from_qx.txt</idfilename> 当exptype=1时该参数有效。\n");
  printf("挖掘文件存放的目录 <exppath>/data/shqx/sdata/fromqx</exppath>\n");
  printf("挖掘数据的时间间隔 <timetvl>30</timetvl> 单位:秒,建议大于10。\n");
  printf("以上参数,除了idfieldname、idfilename和andstr,其它字段都不允许为空。\n\n\n");
}

// 把xml解析到参数starg结构中
bool _xmltoarg(char *strxmlbuffer)
{
  memset(&starg,0,sizeof(struct st_arg));
  GetXMLBuffer(strxmlbuffer,"connstr",starg.connstr);
  if (strlen(starg.connstr)==0) { logfile.Write("connstr is null.\n"); return false; }
  GetXMLBuffer(strxmlbuffer,"charset",starg.charset);
  if (strlen(starg.charset)==0) { logfile.Write("charset is null.\n"); return false; }
  GetXMLBuffer(strxmlbuffer,"tname",starg.tname);
  if (strlen(starg.tname)==0) { logfile.Write("tname is null.\n"); return false; }
  GetXMLBuffer(strxmlbuffer,"cols",starg.cols);
  if (strlen(starg.cols)==0) { logfile.Write("cols is null.\n"); return false; }
  GetXMLBuffer(strxmlbuffer,"fieldname",starg.fieldname);
  if (strlen(starg.fieldname)==0) { logfile.Write("fieldname is null.\n"); return false; }
  GetXMLBuffer(strxmlbuffer,"fieldlen",starg.fieldlen);
  if (strlen(starg.fieldlen)==0) { logfile.Write("fieldlen is null.\n"); return false; }
  GetXMLBuffer(strxmlbuffer,"exptype",&starg.exptype);
  if ( (starg.exptype!=1) && (starg.exptype!=2) ) { logfile.Write("exptype is not in (1,2).\n"); return false; }
  GetXMLBuffer(strxmlbuffer,"andstr",starg.andstr);
  if (strlen(starg.andstr)==0) { logfile.Write("andstr is null.\n"); return false; }
  GetXMLBuffer(strxmlbuffer,"bname",starg.bname);
  if (strlen(starg.bname)==0) { logfile.Write("bname is null.\n"); return false; }
  GetXMLBuffer(strxmlbuffer,"ename",starg.ename);
  if (strlen(starg.ename)==0) { logfile.Write("ename is null.\n"); return false; }
  GetXMLBuffer(strxmlbuffer,"idfieldname",starg.idfieldname);
  if ( (starg.exptype==1) && (strlen(starg.idfieldname)==0) ) { logfile.Write("idfieldname is null.\n"); return false; }
  GetXMLBuffer(strxmlbuffer,"idfilename",starg.idfilename);
  if ( (starg.exptype==1) && (strlen(starg.idfilename)==0) ) { logfile.Write("idfilename is null.\n"); return false; }
  GetXMLBuffer(strxmlbuffer,"exppath",starg.exppath);
  if (strlen(starg.exppath)==0) { logfile.Write("exppath is null.\n"); return false; }
  GetXMLBuffer(strxmlbuffer,"timetvl",&starg.timetvl);
  if (starg.timetvl==0) { logfile.Write("timetvl is null.\n"); return false; }
  // 拆分fieldname和fieldlen
  SplitFields();
  // 判断fieldname和fieldlen中元素的个数一定要相同
  if (vfieldname.size() != vfieldlen.size() ) { logfile.Write("fieldname和fieldlen的元素个数不同。.\n"); return false; }
  return true;
}

//////////////////////////////////////////////////1.本程序的业务流程主函数
bool _dmintables()
{
  // 从记录已获取数据最大id的文件中加载已挖掘数据的最大的id
  if (LoadMaxKeyid()==false) { logfile.Write("LoadMaxKeyid() failed.\n"); return false; }

  // 生成挖掘数据的SQL语句
  char strsql[4096];   
  char fieldvalue[vfieldname.size()][maxfieldlen+1]; // 输出变量定义为一个二维数组
  memset(strsql,0,sizeof(strsql));
  if (starg.exptype==1)
    sprintf(strsql,"select %s,%s from %s where 1=1 and %s>%ld %s order by %s",starg.cols,starg.idfieldname,starg.tname,starg.idfieldname,maxkeyid,starg.andstr,starg.idfieldname);
  else
    sprintf(strsql,"select %s from %s where 1=1 %s",starg.cols,starg.tname,starg.andstr);
  sqlstatement stmt(&conn);
  stmt.prepare(strsql);
  for (int ii=0;ii<vfieldname.size();ii++)
  {
    stmt.bindout(ii+1,fieldvalue[ii],vfieldlen[ii]);
  }
  // 如果是增量挖掘,还要绑定id字段
  if (starg.exptype==1) stmt.bindout(vfieldname.size()+1,&maxkeyid);
  
  // 执行挖掘数据的SQL
  if (stmt.execute() != 0)
  {
    logfile.Write("select %s failed.\n%s\n%s\n",starg.tname,stmt.m_cda.message,stmt.m_sql); return false;
  }
  int  iFileSeq=1;   // 待生成文件的序号
  char strFileName[301],strLocalTime[21];
  CFile File;
  while (true)
  {
    memset(fieldvalue,0,sizeof(fieldvalue));   
    if (stmt.next() !=0) break;
    // 把数据写入文件
    if (File.IsOpened()==false)
    {
      memset(strLocalTime,0,sizeof(strLocalTime));
      LocalTime(strLocalTime,"yyyymmddhh24miss");
      memset(strFileName,0,sizeof(strFileName));
      sprintf(strFileName,"%s/%s%s%s_%d.xml",starg.exppath,starg.bname,strLocalTime,starg.ename,iFileSeq++);
      if (File.OpenForRename(strFileName,"w")==false)
      {
        logfile.Write("File.OpenForRename(%s) failed.\n",strFileName); return false;
      }
      File.Fprintf("<data>\n");
    }
    for (int ii=0;ii<vfieldname.size();ii++)
    {
      File.Fprintf("<%s>%s</%s>",vfieldname[ii].c_str(),fieldvalue[ii],vfieldname[ii].c_str());
    }
    File.Fprintf("<endl/>\n");
    
/////////////////////////////////////////////////////////1.1 1000条记录写入一个文件完成
    if (stmt.m_cda.rpc%1000==0)
    {
      File.Fprintf("</data>\n");
      if (File.CloseAndRename()==false)
      {
        logfile.Write("File.CloseAndRename(%s) failed.\n",strFileName); return false;
      }
      // 更新已挖掘数据的最大的id到文件中
      if (UptMaxKeyid()==false) { logfile.Write("UptMaxKeyid() failed.\n"); return false; }
      logfile.Write("create file %s ok.\n",strFileName);
    }
  }

/////////////////////////////////////////////////////1.2 不够1000条的写入一个文件
  if (File.IsOpened()==true)
  {
    File.Fprintf("</data>\n");
    if (File.CloseAndRename()==false)
    {
      logfile.Write("File.CloseAndRename(%s) failed.\n",strFileName); return false;
    }
    // 更新已挖掘数据的最大的id到文件中
    if (UptMaxKeyid()==false) { logfile.Write("UptMaxKeyid() failed.\n"); return false; }
    logfile.Write("create file %s ok.\n",strFileName);
  }
  if (stmt.m_cda.rpc>0) logfile.Write("本次挖掘了%d条记录。\n",stmt.m_cda.rpc);
  return true;
}

/////////////////////////////////////////2.拆分fieldname和fieldlen
void SplitFields()
{
  vfieldname.clear(); vfieldlen.clear(); maxfieldlen=0;  
  CCmdStr CmdStr;
  CmdStr.SplitToCmd(starg.fieldname,",");
  vfieldname.swap(CmdStr.m_vCmdStr);

  int ifieldlen=0;
  CmdStr.SplitToCmd(starg.fieldlen,",");
  for (int ii=0;ii<CmdStr.CmdCount();ii++)
  {  
    CmdStr.GetValue(ii,&ifieldlen);
    if (ifieldlen>maxfieldlen) maxfieldlen=ifieldlen;   // 得到fieldlen的最大值
    vfieldlen.push_back(ifieldlen);
  }
}

/////////////////////////////3.从记录已获取数据最大id的文件中加载已挖掘数据的最大的id
bool LoadMaxKeyid()
{
  if (starg.exptype!=1) return true;
  CFile File;
  if (File.Open(starg.idfilename,"r")==false) 
  {
    logfile.Write("注意,%s文件不存在,程序将从新开始挖掘数据。\n",starg.idfilename); return true;
  }
  char strBuf[21];
  memset(strBuf,0,sizeof(strBuf));
  File.Fread(strBuf,20);
  maxkeyid=atol(strBuf);
  logfile.Write("maxkeyid=%d\n",maxkeyid);
  return true;
}

////////////////////////////4.更新已挖掘数据的最大的id到文件中
bool UptMaxKeyid()
{
  if (starg.exptype!=1) return true;
  CFile File;
  if (File.Open(starg.idfilename,"w")==false) 
  {
    logfile.Write("File.Open(%s) failed.\n",starg.idfilename); return false;
  }
  File.Fprintf("%ld",maxkeyid);
  return true;
}
在这里插入图片描述

在这里插入图片描述

2.HTTP协议:wget,转义

浏览器输入网址后回车,就是向服务端(web系统)发送请求,浏览器就是客户端


在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

注册后会分配一个appkeyid加在如上url后面。数据共享平台也有一个web系统(服务端)用java做的,vi /etc/rc.local如下


在这里插入图片描述

如下http客户端访问不了,startup后可访问
在这里插入图片描述

按照http格式向网站发报文也会得到回应,如下http客户端
在这里插入图片描述

在这里插入图片描述

wgetclient是用wget命令下载,httpclient下载的内容有时候会有断行,wget下载不会,优先wget,wget下载不了的用http。wget支持http,https,ftp,sftp等协议。linux下同步文件rsync,curl,wget,ftp,sftp


在这里插入图片描述

如上调用接口,如下还可以右击网页上图片复制图片地址
在这里插入图片描述

程序在后台跑,有新图就拿下来,wgetclient将网页内容全弄下来,搞清图片命名规律进行解析
//wgetclient.cpp
#include "_public.h"
void EXIT(int sig);
CLogFile       logfile;
int main(int argc, char *argv[])
{
  if(argc!=6)
  {
    printf("Usage:%s weburl tmpfilename outputfilename logfilename charset\n",argv[0]); 
    printf("本程序用于获取WEB网页的内容。\n");
    printf("weburl 网页WEB的地址。\n");
    printf("tmpfilename 获取到的网页的内容存放的全路径的临时文件名,该文件可能是utf-8或其它编码。\n");
    printf("outputfilename 最终的输出文件全路径文件名,该文件是gb18030编码,注意tmpfilename被转换为outputfilename后,tmpfilename文件被自动删除。\n");
    printf("logfilename 本程序的运行产生的日志文件名。\n");
    printf("charset 网页的字符集,如utf-8\n\n");
    exit(1);
  }

  // 关闭全部的信号和输入输出
  // 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程
  // 但请不要用 "kill -9 +进程号" 强行终止
  CloseIOAndSignal(); signal(SIGINT,EXIT); signal(SIGTERM,EXIT);

  // 打开日志文件
  if (logfile.Open(argv[4],"a+") == false)
  {
    printf("logfile.Open(%s) failed.\n",argv[4]); return -1;
  }

  MKDIR(argv[2],true); MKDIR(argv[3],true);
  char strweburl[3001];
  memset(strweburl,0,sizeof(strweburl));
  strncpy(strweburl,argv[1],3000);
 
  char strcmd[3024];
  memset(strcmd,0,sizeof(strcmd));
  snprintf(strcmd,3000,"/usr/bin/wget -c -q -O %s \"%s\" 1>>/dev/null 2>>/dev/null",argv[2],strweburl);
  system(strcmd);
  logfile.Write("%s\n",strcmd);

  char strfilenametmp[301];
  memset(strfilenametmp,0,sizeof(strfilenametmp));
  snprintf(strfilenametmp,300,"%s.tmp",argv[3]);

   // 把获取到的网页转换为中文
  memset(strcmd,0,sizeof(strcmd));
  snprintf(strcmd,256,"iconv -c -f %s -t gb18030 %s -o %s",argv[5],argv[2],strfilenametmp);
  system(strcmd);
  logfile.Write("%s\n",strcmd);
  REMOVE(argv[2]);   // 删除临时文件 
  RENAME(strfilenametmp,argv[3]);
  return 0;
}

void EXIT(int sig)
{
  if (sig > 0) signal(sig,SIG_IGN);
  logfile.Write("catching the signal(%d).\n",sig);
  logfile.Write("wgetclient exit.\n");
  exit(0);
}

/*
除了普通的字母,数字,中文,还有特殊字符,但是规范的使用应该是使用字符转义。
十六进制值 
1. +  URL 中+号表示空格 %2B 
2. 空格 URL中的空格可以用+号或者编码 %20 
3. /  分隔目录和子目录 %2F  
4. ?  分隔实际的 URL 和参数 %3F  
5. % 指定特殊字符 %25  
6. # 表示书签 %23  
7. & URL 中指定的参数间的分隔符 %26  
8. = URL 中指定参数的值 %3D 
*/
//wgetrain24.cpp
#include "_public.h"
void EXIT(int sig);
CLogFile       logfile;
bool GetURL(char *strBuffer,char *strURL,char *strFileName);
int main(int argc, char *argv[])
{
  if(argc!=4)
  {
    printf("Usage:%s logfilename tmpfilename outputfilename\n",argv[0]); 
    printf("Sample:./wgetrain24 /log/shqx/wgetrain24.log /data/wgettmp /data/wfile/zhrain24\n\n");
    printf("本程序用于从中国天气网获取逐小时降雨量实况图。\n");
    printf("中国天气网的url是http://products.weather.com.cn/product/Index/index/procode/JC_JSL_ZH.shtml\n");
    printf("如果中国天气网的url改变,程序也在做改动。\n");
    printf("logfilename 本程序的运行产生的日志文件名。\n");
    printf("tmpfilename 本程序运行产生的临时文件存放的目录。\n");
    printf("获取逐小时降雨量实况图存放的目录。\n\n");
    exit(1);
  }
  // 关闭全部的信号和输入输出
  // 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程
  // 但请不要用 "kill -9 +进程号" 强行终止
  CloseIOAndSignal(); signal(SIGINT,EXIT); signal(SIGTERM,EXIT);
  // 打开日志文件
  if (logfile.Open(argv[1],"a+") == false)
  {
    printf("logfile.Open(%s) failed.\n",argv[1]); return -1;
  }

  MKDIR(argv[2],false); MKDIR(argv[3],false);
  while (true)
  {
    // 调用wgetclient获取网页内容
    char strwgetclient[2001];
    memset(strwgetclient,0,sizeof(strwgetclient));
    snprintf(strwgetclient,2000,"/htidc/public/bin/wgetclient \"http://products.weather.com.cn/product/Index/index/procode/JC_JSL_ZH.shtml\" %s/wgetclient_%d.tmp  %s/wgetclient_%d.html %s/wgetclient.log utf-8",argv[2],getpid(),argv[2],getpid(),argv[2]);
    system(strwgetclient);
    // logfile.Write("%s\n",strwgetclient);
  
    // 打开网页内容文件
    char stroutputfile[301];
    memset(stroutputfile,0,sizeof(stroutputfile));
    snprintf(stroutputfile,300,"%s/wgetclient_%d.html",argv[2],getpid());
    CFile File;
    if (File.Open(stroutputfile,"r")==false)
    {
      logfile.Write("File.Open(%s) failed.\n",stroutputfile); sleep(60); continue;
    }    
    char strBuffer[1001],strURL[501],strFullFileName[301],strFileName[101];  
   
    // 得到全部的图片文件名
    while (true)
    {
      memset(strBuffer,0,sizeof(strBuffer));
      memset(strURL,0,sizeof(strURL));
      memset(strFullFileName,0,sizeof(strFullFileName));
      memset(strFileName,0,sizeof(strFileName));  
      if (File.Fgets(strBuffer,1000)==false) break;  
      if (MatchFileName(strBuffer,"*PWCP_TWC_WEAP_SFER_ER1_TWC_L88_P9_20*.JPG*")==false) continue;  
      // logfile.Write("%s",strBuffer);  
      // 解析出url和文件名
      if (GetURL(strBuffer,strURL,strFileName)==false) continue; 
      // 文件已存在,不采集
      snprintf(strFullFileName,300,"%s/%s",argv[3],strFileName);
      if (access(strFullFileName,F_OK)==0) continue;  
      // 调用wget获取文件
      logfile.Write("download %s ",strFileName);
      memset(strwgetclient,0,sizeof(strwgetclient));
      snprintf(strwgetclient,500,"wget \"%s\" -o %s/wgetrain24.log -O %s",strURL,argv[2],strFullFileName);
      system(strwgetclient);  
      if (access(strFullFileName,F_OK)==0) logfile.WriteEx("ok.\n");
      else logfile.WriteEx("failed.\n");
    }  
    File.CloseAndRemove();
    sleep(60);
  }
  return 0;
}

bool GetURL(char *strBuffer,char *strURL,char *strFileName)
{
  char *start,*end;
  start=end=0;
  if ((start=strstr(strBuffer,"http"))==0) return false;
  if ((end=strstr(start,"\""))==0) return false; //找双引号
  strncpy(strURL,start,end-start);
  strcpy(strFileName,strstr(strURL,"PWCP"));
  return true;
}
void EXIT(int sig)
{
  if (sig > 0) signal(sig,SIG_IGN);
  logfile.Write("catching the signal(%d).\n",sig);
  logfile.Write("wgetclient exit.\n");
  exit(0);
}
在这里插入图片描述

3.非结构化数据存储:blob,pzhrain24file

在这里插入图片描述
// 本程序演示如何把磁盘文件的文本文件写入Oracle的BLOB字段中。
//filetoblob.cpp,实时生成的不要存oracle的blob字段
#include "_ooci.h"
int main(int argc,char *argv[])
{
  // 数据库连接池
  connection conn;  
  // 连接数据库,返回值0-成功,其它-失败
  // 失败代码在conn.m_cda.rc中,失败描述在conn.m_cda.message中。
  if (conn.connecttodb("scott/tiger@snorcl11g_198","Simplified Chinese_China.ZHS16GBK") != 0)
  {
    printf("connect database %s failed.\n%s\n","scott/tiger@orcl",conn.m_cda.message); return -1;
  }  
  // SQL语言操作类
  sqlstatement stmt(&conn);  
  // 为了方便演示,把goods表中的记录全删掉,再插入一条用于测试的数据。
  // 不需要判断返回值
  stmt.prepare("\
    BEGIN\
      delete from goods;\
      insert into goods(id,name,pic) values(1,'商品名称',empty_blob());\
    END;");  
  // 执行SQL语句,一定要判断返回值,0-成功,其它-失败。
  if (stmt.execute() != 0)
  {
    printf("stmt.execute() failed.\n%s\n%s\n",stmt.m_sql,stmt.m_cda.message); return -1;
  }
  // 使用游标从goods表中提取id为1的pic字段
  // 注意了,同一个sqlstatement可以多次使用
  // 但是,如果它的sql改变了,就要重新prepare和bindin或bindout变量
  stmt.prepare("select pic from goods where id=1 for update");
  stmt.bindblob();
  // 执行SQL语句,一定要判断返回值,0-成功,其它-失败。
  if (stmt.execute() != 0)
  {
    printf("stmt.execute() failed.\n%s\n%s\n",stmt.m_sql,stmt.m_cda.message); return -1;
  }
  // 获取一条记录,一定要判断返回值,0-成功,1403-无记录,其它-失败。
  if (stmt.next() != 0) return 0;  
  // 把磁盘文件pic_in.jpg的内容写入BLOB字段,一定要判断返回值,0-成功,其它-失败。
  if (stmt.filetolob((char *)"pic_in.jpg") != 0)
  {
    printf("stmt.filetolob() failed.\n%s\n",stmt.m_cda.message); return -1;
  }
  // 提交事务
  conn.commit();
  return 0;
}
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
// pzhrain24file.cpp
#include "_public.h"
#include "_ooci.h"
#include "_shqx.h"
CLogFile logfile;
CDir Dir;
// 处理数据文件
bool _pzhrain24file(char *strargv2,char *strargv4,char *strargv5);
connection conn;
void EXIT(int sig);

int main(int argc,char *argv[])
{
  if (argc!=7)
  {
    printf("\n本程序用于处理全国逐小时雨量实况图片文件。\n\n");
    printf("/htidc/shqx/bin/pzhrain24file logfilename connstr srcpathname dstpathname tname timetvl\n");
    printf("例如:/htidc/shqx/bin/pzhrain24file /log/shqx/pzhrain24file.log shqx/pwdidc@snorcl11g_198 /data/wfile/zhrain24 /qxfile/zhrain24 T_ZHRAIN24 30\n");
    printf("logfilename 本程序运行的日志文件名。\n");
    printf("connstr 数据库的连接参数。\n");
    printf("srcpathname 原始文件存放的目录,文件命名如PWCP_TWC_WEAP_SFER_ER1_TWC_L88_P9_20191101070000000.JPG。\n");
    printf("dstpathname 目标文件存放的目录,文件按yyyy/mm/dd组织目录,重命名为zhrain24_yyyymmddhh24miss.jpg。\n");
    printf("tname 数据存放的表名。\n");
    printf("timetvl 本程序运行的时间间隔,单位:秒。\n");
    return -1;
  }
  // 关闭全部的信号和输入输出
  CloseIOAndSignal();
  // 处理程序退出的信号
  signal(SIGINT,EXIT); signal(SIGTERM,EXIT);

  if (logfile.Open(argv[1],"a+")==false)
  {
    printf("打开日志文件失败(%s)。\n",argv[1]); return -1;
  }
  logfile.Write("程序启动。\n");
  while (true)
  {
    // logfile.Write("开始扫描目录。\n");
    // 扫描数据文件存放的目录,只匹配"PWCP_TWC_WEAP_SFER_ER1_TWC_L88_P9_20*.JPG"
    if (Dir.OpenDir(argv[3],"PWCP_TWC_WEAP_SFER_ER1_TWC_L88_P9_20*.JPG",1000,true,true)==false)
    {
      logfile.Write("Dir.OpenDir(%s) failed.\n",argv[3]); sleep(atoi(argv[6])); continue;
    }
    // 逐个处理目录中的数据文件
    while (true)
    {
      if (Dir.ReadDir()==false) break;
  
      // 处理数据文件
      if (_pzhrain24file(argv[2],argv[4],argv[5])==false) 
      {
        logfile.WriteEx("失败。\n"); continue;
      }
    }
    // 断开与数据库的连接
    if (conn.m_state==1) conn.disconnect(); 
    sleep(atoi(argv[6]));
  }
  return 0;
}
void EXIT(int sig)
{
  logfile.Write("程序退出,sig=%d\n\n",sig);
  exit(0);
}
     
/////////////////////////////////////////////////处理数据文件
bool _pzhrain24file(char *strargv2,char *strargv4,char *strargv5)
{
  char strddatetime[21];   // 文件的数据时间,格式yyyymmddhh24miss
  memset(strddatetime,0,sizeof(strddatetime));
  strncpy(strddatetime,strstr(Dir.m_FileName,"20"),14);
  
  //搜索文件名PWCP_TWC…中20,后面取14位,重命名为zhrain24_%s.jpg
  char strdstfilename[301];  // 目标文件名,不带路径
  memset(strdstfilename,0,sizeof(strdstfilename));
  snprintf(strdstfilename,300,"zhrain24_%s.jpg",strddatetime);
  
  char strdstfilepath[301];  // 目标文件存放的目录
  memset(strdstfilepath,0,sizeof(strdstfilepath));
  snprintf(strdstfilepath,300,"%s/",strargv4);
  strncat(strdstfilepath,strddatetime,4);     strcat(strdstfilepath,"/");  // 年的子目录
  strncat(strdstfilepath,strddatetime+4,2);   strcat(strdstfilepath,"/");  // 月的子目录
  strncat(strdstfilepath,strddatetime+6,2);   strcat(strdstfilepath,"/");  // 日的子目录

  char strfulldstfilename[301]; // 目标文件名,全路径
  memset(strfulldstfilename,0,sizeof(strfulldstfilename));
  snprintf(strfulldstfilename,300,"%s%s",strdstfilepath,strdstfilename);

  // 如果文件已处理(目标文件已存在),直接返回成功。
  if (access(strfulldstfilename,F_OK) == 0) return true;

  if (conn.m_state==0)
  {
    if (conn.connecttodb(strargv2,"Simplified Chinese_China.ZHS16GBK")!=0)
    {
      logfile.Write("connect database(%s) failed.\n%s\n",strargv2,conn.m_cda.message); return false;
    }
    // logfile.Write("连接数据库成功。\n");
  }
  
  // 把源文件复制到目标文件
  if (COPY(Dir.m_FullFileName,strfulldstfilename)==false) 
  {
    logfile.Write("复制文件COPY(%s,%s)...failed.\n",Dir.m_FullFileName,strfulldstfilename); return false;
  }

  // 把非结构化数据文件写入oracle数据库的表中
  if (FileToTable(&conn,&logfile,strargv5,strfulldstfilename,strddatetime)!=0)
  {
    logfile.Write("把文件%s存入%s...failed.\n",strfulldstfilename,strargv5);  return false;
  }
  logfile.Write("把文件%s存入%s...ok.\n",strfulldstfilename,strargv5);  
  return true;
}
在这里插入图片描述

在这里插入图片描述

如下是目标文件夹并做了重命名,如上程序中COPY函数,是复制并不删除,因为采集文件是增量采集,删除了又重新下载


在这里插入图片描述

在这里插入图片描述

数据库客户端服务端性能不够用文件传输系统


在这里插入图片描述

4.数据管理子系统:数据字典表,分表

Oracle数据字典:是一组表和视图的结构(就像仓库里有什么,每种有多少,哪些空间可用等日记)。自己创建的表,oracle会将这表的信息自动写入系统表,系统表上再创建一视图供客户查看。数据字典中的表是不能被访问修改(自己用的),但是可以访问数据字典的视图就可以知道自己创建的表的详细信息

在这里插入图片描述

在这里插入图片描述

数据字典除了以下三类还有V$_表示性能或参数设置相关数据比如数据库的字符集,会话数,进程数等等。tab也是视图,tab里面表,视图,同义词都可以查到。若是select * from dba_tables;必须是dba用户才可以查
在这里插入图片描述

如下是常用的数据字典表
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

数据量大了会出现性能和数据管理,迁移,备份问题。假如有1亿数据量,不能用exp导出,一个文件有几百G导出要一天。索引设计要合理,不然table scan跑不动。一亿以下的数据单表存放,一亿以上的数据考虑分表,十亿虽然比一亿容量大十倍,但性能不会下降十倍,因为索引(比如按首字母找名字)
在这里插入图片描述

如下并行数据库分布式查询可解决多表性能低的问题
在这里插入图片描述

写一些程序实现数据管理,数据删除(deletables.cpp)或放历史表等等。三种表数据结构一样,迁移数据可写成一个通用功能模块程序。若是不同表,只知道表名去查询数据字典得到数据结构
如下hsmtable.cpp(和deletetable.cpp像)数据源表就是要把数据从哪个表迁移出来,列名从数据字典取出,把列拼成一个字符串,获取列名后把数据rowid从源表查出来,生成插入目的表sql,删除源表sql

//hsmtable.cpp
#include "_public.h"
#include "_ooci.h"
char logfilename[301];
char connstr[101];
char srctname[51];
char dsttname[51];
char where[1024];
char hourstr[101];
char localhour[21];
int  maxcount=1;
connection conn;
CLogFile logfile;
void EXIT(int sig);
// 获取dsttname表全部的列
char strColumnStr[2048];
bool GetColumnStr();
// 显示程序的帮助
void _help(char *argv[]);
bool _hsmtables();

int main(int argc,char *argv[])
{
  if (argc != 2) { _help(argv); return -1; }
  memset(logfilename,0,sizeof(logfilename));
  memset(connstr,0,sizeof(connstr));
  memset(srctname,0,sizeof(srctname));
  memset(dsttname,0,sizeof(dsttname));
  memset(where,0,sizeof(where));
  memset(hourstr,0,sizeof(hourstr));

  GetXMLBuffer(argv[1],"logfilename",logfilename,300);
  GetXMLBuffer(argv[1],"connstr",connstr,100);
  GetXMLBuffer(argv[1],"srctname",srctname,50);
  GetXMLBuffer(argv[1],"dsttname",dsttname,50);
  GetXMLBuffer(argv[1],"where",where,1000);
  GetXMLBuffer(argv[1],"maxcount",&maxcount);
  GetXMLBuffer(argv[1],"hourstr",hourstr,2000);

  if (strlen(logfilename) == 0) { printf("logfilename is null.\n"); return -1; }
  if (strlen(connstr) == 0)     { printf("connstr is null.\n"); return -1; }
  if (strlen(srctname) == 0)    { printf("srctname is null.\n"); return -1; }
  if (strlen(dsttname) == 0)    { printf("dsttname is null.\n"); return -1; }
  if (strlen(where) == 0)       { printf("where is null.\n"); return -1; }
 if ( (maxcount<1) || (maxcount>1000) ) { printf("maxcount %d is invalid,should in 1-1000.\n",maxcount); return -1; }
  if (strlen(hourstr) == 0)     { printf("hourstr is null.\n"); return -1; }
  // 关闭全部的信号和输入输出
  CloseIOAndSignal();
  // 处理程序退出的信号
  signal(SIGINT,EXIT); signal(SIGTERM,EXIT);
  // 打开日志文件
  if (logfile.Open(logfilename,"a+") == false)
  {
    printf("logfile.Open(%s) failed.\n",logfilename); return -1;
  }
  while (true)
  {
    // 判断当前时间是否在启动时间之内
    memset(localhour,0,sizeof(localhour));
    LocalTime(localhour,"hh24");
    if (strstr(hourstr,localhour)==0) { sleep(60); continue; }
    // 连接数据库
    if (conn.connecttodb(connstr,"Simplified Chinese_China.ZHS16GBK") != 0)
    {
      logfile.Write("connect database %s failed.\n",connstr); sleep(60); continue; 
    }
    logfile.Write("from table %s to %s.\n",srctname,dsttname);
    if (_hsmtables() == false) logfile.Write("_hsmtables failed.\n"); 
    conn.disconnect();
    sleep(60); 
  }
  return 0;
}
void EXIT(int sig)
{
  printf("程序退出,sig=%d\n\n",sig);
  exit(0);
}
// 显示程序的帮助
void _help(char *argv[])
{
  printf("\nUsing:/htidc/public/bin/hsmtables \"<logfilename>/log/shqx/hsmtables_SURFDATA.log</logfilename><connstr>shqx/pwdidc@snorcl11g_198</connstr><srctname>T_SURFDATA</srctname><dsttname>T_SURFDATA_HIS</dsttname><where>where ddatetime<sysdate-10</where><maxcount>500</maxcount><hourstr>23,01,02,03,04,05,06</hourstr>\"\n\n");

  printf("这是一个工具程序,用于清理表中的数据。\n");
  printf("<logfilename>/log/shqx/hsmtables_SURFDATA.log</logfilename> 本程序运行日志文件名。\n");
  printf("<connstr>szidc/pwdidc@SZQX_10.153.97.251</connstr> 目的数据库的连接参数。\n");
  printf("<srctname>T_SURFDATA</srctname> 数据源表名。\n");
  printf("<dsttname>T_SURFDATA_HIS</dsttname> 目的数据表名。\n");
  printf("<where>where ddatetime<sysdate-10</where> 待迁移数据的条件。\n");
  printf("<maxcount>500</maxcount> 单次执行数据迁移的记录数,取值在1-1000之间。\n");
  printf("<hourstr>23,01,02,03,04,05,06</hourstr> 本程序启动的时次,小时,时次之间用半角的逗号分隔开。\n\n");
  return;
}

bool _hsmtables()
{
  // 获取dsttname表全部的列
  if (GetColumnStr() == false) return false;
  int  ccount=0;
  char strrowid[51],strrowidn[maxcount][51];
  // 把数据的rowid从srctname表中查出来
  sqlstatement selstmt(&conn);
  selstmt.prepare("select rowid from %s %s",srctname,where);
  selstmt.bindout(1, strrowid,50);
  if (selstmt.execute() != 0)
  {
    logfile.Write("%s failed.\n%s\n",selstmt.m_sql,selstmt.m_cda.message); return false;
  }
  // 生成插入dsttname表和删除srctname表的SQL
  int ii=0;
  char strInsertSQL[10241],strDeleteSQL[10241];
  memset(strInsertSQL,0,sizeof(strInsertSQL));
  memset(strDeleteSQL,0,sizeof(strDeleteSQL));
  sprintf(strInsertSQL,"insert into %s(%s) select %s from %s where rowid in (",dsttname,strColumnStr,strColumnStr,srctname);
  //%s(%s)里面的%s是字段名,字段顺序没必要保持一致,所有把列名从数据字典取出,目的表字段比原表少,只备份一部分字段
  sprintf(strDeleteSQL,"delete from %s where rowid in (",srctname);
  
  char strtemp[11];
  for (ii=0; ii<maxcount; ii++)
  {
    memset(strtemp,0,sizeof(strtemp));
    if (ii==0) sprintf(strtemp,":%d",ii+1);
    if (ii >0) sprintf(strtemp,",:%d",ii+1);
    strcat(strInsertSQL,strtemp);
    strcat(strDeleteSQL,strtemp);
  }
  strcat(strInsertSQL,")");
  strcat(strDeleteSQL,")");

  sqlstatement insstmt(&conn);
  insstmt.prepare(strInsertSQL);
  sqlstatement delstmt(&conn);
  delstmt.prepare(strDeleteSQL);

  for (ii=0; ii<maxcount; ii++)
  {
    insstmt.bindin(ii+1,strrowidn[ii],50);
    delstmt.bindin(ii+1,strrowidn[ii],50);
  }

  // 每maxcount记录就执行一次
  while (true)
  {
    memset(strrowid,0,sizeof(strrowid));
    if (selstmt.next() != 0) break;
    strcpy(strrowidn[ccount],strrowid);
    ccount++;
    if (ccount == maxcount)
    {
      if (insstmt.execute() != 0)
      {
        if (insstmt.m_cda.rc != 1)
        {
          logfile.Write("_hsmtables insert %s failed.\n%s\n",dsttname,insstmt.m_cda.message); return false;
        }
      }
      if (delstmt.execute() != 0)
      {
        logfile.Write("_hsmtables delete %s failed.\n%s\n",dsttname,insstmt.m_cda.message); return false;
      }
      conn.commit();
      memset(strrowidn,0,sizeof(strrowidn));
      ccount=0;
    }

    if (fmod(selstmt.m_cda.rpc,10000) < 1)
    {
      logfile.Write("%s to %s ok(%d).\n",srctname,dsttname,selstmt.m_cda.rpc);
      // 判断当前时间是否在启动时间之内
      memset(localhour,0,sizeof(localhour));
      LocalTime(localhour,"hh24");
      if (strstr(hourstr,localhour)==0) return true;
    }
  }
  // 在以上循环处理的时候,如果不足maxcount,就在这里处理
  for (ii=0; ii<ccount; ii++)
  {
    insstmt.prepare("\
      BEGIN\
        insert into %s(%s) select %s from %s where rowid=:1;\
        delete from %s where rowid=:2;\
      END;",dsttname,strColumnStr,strColumnStr,srctname,srctname);
    insstmt.bindin(1,strrowidn[ii],50);
    insstmt.bindin(2,strrowidn[ii],50);
    if (insstmt.execute() != 0)
    {
      if (insstmt.m_cda.rc != 1)
      {
        logfile.Write("_hsmtables insert %s or delete %s failed.\n%s\n",dsttname,srctname,insstmt.m_cda.message); return false;
      }
    }
  }
  conn.commit(); //一行一行的提交
  logfile.Write("%s to %s finish(%d).\n",srctname,dsttname,selstmt.m_cda.rpc);
  return true;
}

///////////////////////////////////获取dsttname表全部的列
bool GetColumnStr()
{
  memset(strColumnStr,0,sizeof(strColumnStr)); 
  char column_name[51];
  sqlstatement stmt(&conn);
  stmt.prepare("select lower(column_name) from USER_TAB_COLUMNS where table_name=upper('%s') order by column_id",dsttname);
  stmt.bindout(1,column_name,50);
  if (stmt.execute() != 0)
  {
    logfile.Write("%s failed.\n%s\n",stmt.m_sql,stmt.m_cda.message); return false;
  }
  while(true)
  {
    memset(column_name,0,sizeof(column_name));
    if (stmt.next()!=0) break;
    if (stmt.m_cda.rpc>1) strcat(strColumnStr,",");
    strcat(strColumnStr,column_name);
  }
  if (stmt.m_cda.rpc==0) { logfile.Write("表%s不存在。\n",dsttname); return false; }
  return true;
}
在这里插入图片描述

$sqlplus shqx/pwdidc,如下数据都是一小时前生成,需要将后台生成数据脚本启动


在这里插入图片描述

如下是数据迁移程序如何兼容其他数据库,oracle有rowid,mysql和pg都没有rowid但有keyid,取列名时每个数据库的数据字典都不同


在这里插入图片描述

不管id字段是数字还是字符或字符串,我们都可用字符绑定
在这里插入图片描述

如下不同数据库查数据字典得到全部列
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

5.监控告警子系统:分区,mount,fdisk

我们处理后台数据也会有一个web系统,管理参数,展示数据,查询数据等界面(不能让用户去登录plsqldeveloper查询)


在这里插入图片描述

如下是数据的监控,数据少于某数量显示红色


在这里插入图片描述

如下系统告警,监控参数和日志
在这里插入图片描述

数据有延时,统计每隔时次如全国气象站表数据量多少,再用其他程序分析下数据大报是否正常


在这里插入图片描述

在这里插入图片描述

以下是怎么分区安装centos,df -h 显示G。如下日志等等都在sda中即raid1即2个300G中
h

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

以下是收集磁盘空间信息,现在插入一个U盘,fdisk -l可以看到U盘。df可看到%使用率
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

每个服务器上运行一个收集磁盘空间的小程序,收集到磁盘空间后生成xml文件存放在本地目录,通过文件传输系统或ftp将文件传给数据处理服务器(因为有的服务器不安装oracle客户端,只可安装文件传输客户端),统一保存到数据库。


在这里插入图片描述
//diskinfo.cpp,写入xml文件中再入库
#include "_public.h"
void EXIT(int sig);
CLogFile logfile;

int main(int argc,char *argv[])
{
  if (argc != 4)
  {
    printf("\n");
    printf("Using:./diskinfo hostname logfilename outputpath\n");

    printf("Example:/htidc/public/bin/diskinfo 118.89.50.198 /tmp/htidc/log/diskinfo.log /tmp/htidc/monclient\n\n");
    printf("此程序调用df命名,把本服务器的磁盘使用率信息写入xml文件。\n");
    printf("hostname是本服务器的主机名,为了方便识别,也可以用IP。\n");
    printf("logfilename是本程序的日志文件名。\n");
    printf("outputpath是输出的xml文件存放的目录。\n");
    printf("此程序运行在需要监控的服务器上(本程序只适用Linux系统),采集后的xml文件由文件传输程序发送给数据处理服务程序入库。\n\n\n");

    return -1;
  }

  // 关闭全部的信号和输入输出
  // 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程
  // 但请不要用 "kill -9 +进程号" 强行终止
  CloseIOAndSignal(); signal(SIGINT,EXIT); signal(SIGTERM,EXIT);

  if (logfile.Open(argv[2],"a+") == false)
  {
    printf("logfile.Open(%s) failed.\n",argv[2]); return -1;
  }

  FILE *fp=0;

  if ( (fp=popen("df -k --block-size=1M","r")) == NULL )
  {
    logfile.Write("popen(df -k --block-size=1M) failed.\n"); return false;
  }

  char strXMLFileName[301],strLocalTime[21];
  memset(strXMLFileName,0,sizeof(strXMLFileName));
  memset(strLocalTime,0,sizeof(strLocalTime));
  LocalTime(strLocalTime,"yyyymmddhh24miss");
  snprintf(strXMLFileName,300,"%s/diskinfo_%s_%s.xml",argv[3],strLocalTime,argv[1]);

  CFile XMLFile;
  if (XMLFile.OpenForRename(strXMLFileName,"w+") == false )
  {
    logfile.Write("XMLFile.OpenForRename(%s) failed.\n",strXMLFileName); pclose(fp); return -1;
  }

  XMLFile.Fprintf("<data>\n");

  CCmdStr CmdStr;
  char strBuffer[1024],strLine[500];
  
  while (true)
  {
    memset(strBuffer,0,sizeof(strBuffer));

    if (FGETS(fp,strBuffer,500) == false) break;

    // 如果没有找到“%”,就再读取一行,与strBuffer拼起来
    if (strstr(strBuffer,"%") == 0)
    {
      memset(strLine,0,sizeof(strLine));
      if (FGETS(fp,strLine,500) == false) break;
      strcat(strBuffer," "); strcat(strBuffer,strLine);
    }

    // 删除字符串前后的空格和换行符
    DeleteLRChar(strBuffer,' '); DeleteLRChar(strBuffer,'\n');

    // 把字符串中间的多个空格全部转换为一个空格
    UpdateStr(strBuffer,"  "," ");

    // 把全内容全部转换为小写
    ToLower(strBuffer);

    // 除了磁盘信息,还有可能是内存,SMB等其它文件,都丢弃掉
    if (strncmp(strBuffer,"/dev",4) != 0) continue;

    CmdStr.SplitToCmd(strBuffer," ");

    if (CmdStr.CmdCount() != 6) continue;

    char strusep[21];
    memset(strusep,0,sizeof(strusep));
    strcpy(strusep,CmdStr.m_vCmdStr[4].c_str());
    UpdateStr(strusep,"%","");

    char strLocalTime[21];
    memset(strLocalTime,0,sizeof(strLocalTime));
    LocalTime(strLocalTime,"yyyymmddhh24miss");
    XMLFile.Fprintf(\
            "<nodip>%s</nodip>"\
            "<crttime>%s</crttime>"\
            "<filesystem>%s</filesystem>"\
            "<total>%0.02f</total>"\
            "<used>%0.02f</used>"\
            "<available>%0.02f</available>"\
            "<usep>%0.02f</usep>"\
            "<mount>%s</mount><endl/>\n",
             argv[1],
             strLocalTime,
             CmdStr.m_vCmdStr[0].c_str(),
             atof(CmdStr.m_vCmdStr[1].c_str())/1024.0,
             atof(CmdStr.m_vCmdStr[2].c_str())/1024.0,
             atof(CmdStr.m_vCmdStr[3].c_str())/1024.0,
             (atof(CmdStr.m_vCmdStr[2].c_str())/atof(CmdStr.m_vCmdStr[1].c_str()))*100.0,
             CmdStr.m_vCmdStr[5].c_str());
  }

  XMLFile.Fprintf("</data>\n");

  pclose(fp);

  XMLFile.CloseAndRename();

  logfile.Write("create %s ok.\n",strXMLFileName);

  exit(0);
}

void EXIT(int sig)
{
  if (sig > 0) signal(sig,SIG_IGN);
  logfile.Write("catching the signal(%d).\n",sig);
  logfile.Write("diskinfo exit.\n");
  exit(0);
}

以下为收集CPU和内存信息,top命令显示如下,zombie表示僵尸,q退出,cpu用到20%算忙了

在这里插入图片描述

ps -ef |grep htidc,cpuinfo.cpp思路是定义一个数据结构,三个结构体变量,加载cpu信息到结构体里,睡60s,再继续加载cpu信息到结构体里,再将两结构体成员相减,就可以知道一分钟内cpu情况,采用的是一分钟信息。vi /proc/stat如下
在这里插入图片描述

//cpuinfo.cpp
#include "_public.h"
void EXIT(int sig);
CLogFile logfile;
struct st_cpuinfo
{
  double user;
  double sys;
  double wait;
  double nice;
  double idle;
  double irq;
  double softirq;
  double total;
};
struct st_cpuinfo stcpuinfo1,stcpuinfo2,stcpuinfo3;
bool LoadCPUInfo(struct st_cpuinfo &stcpuinfo);

int main(int argc,char *argv[])
{
  if (argc != 4)
  {
    printf("\n");
    printf("Using:./cpuinfo hostname logfilename outputpath\n");
    printf("Example:/htidc/public/bin/cpuinfo 118.89.50.198 /tmp/htidc/log/cpuinfo.log /tmp/htidc/monclient\n\n");

    printf("此程序读取/proc/stat文件,把本服务器的CPU使用率信息写入xml文件。\n");
    printf("hostname是本服务器的主机名,为了方便识别,也可以用IP。\n");
    printf("logfilename是本程序的日志文件名。\n");
    printf("outputpath是输出的xml文件存放的目录。\n");
    printf("此程序运行在需要监控的服务器上(本程序只适用Linux系统),采集后的xml文件由文件传输程序发送给数据处理服务程序入库。\n\n\n");
 
    return -1;
  }
  //memset(strHostName,0,sizeof(strHostName));
  //strncpy(strHostName,argv[2],20);
  // 关闭全部的信号和输入输出
  // 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程
  // 但请不要用 "kill -9 +进程号" 强行终止
  CloseIOAndSignal(); signal(SIGINT,EXIT); signal(SIGTERM,EXIT);
  if (logfile.Open(argv[2],"a+") == false)
  {
    printf("logfile.Open(%s) failed.\n",argv[2]); return -1;
  }
  memset(&stcpuinfo1,0,sizeof(struct st_cpuinfo));
  memset(&stcpuinfo2,0,sizeof(struct st_cpuinfo));
  memset(&stcpuinfo3,0,sizeof(struct st_cpuinfo));

  if (LoadCPUInfo(stcpuinfo1) ==false) return -1;  
  sleep(60);
  if (LoadCPUInfo(stcpuinfo2) ==false) return -1;

  stcpuinfo3.user=stcpuinfo2.user-stcpuinfo1.user;
  stcpuinfo3.sys=stcpuinfo2.sys-stcpuinfo1.sys;
  stcpuinfo3.wait=stcpuinfo2.wait-stcpuinfo1.wait;
  stcpuinfo3.nice=stcpuinfo2.nice-stcpuinfo1.nice;
  stcpuinfo3.idle=stcpuinfo2.idle-stcpuinfo1.idle;
  stcpuinfo3.irq=stcpuinfo2.irq-stcpuinfo1.irq;
  stcpuinfo3.softirq=stcpuinfo2.softirq-stcpuinfo1.softirq;

  stcpuinfo3.total=stcpuinfo3.user+stcpuinfo3.sys+stcpuinfo3.wait+stcpuinfo3.nice+stcpuinfo3.idle+stcpuinfo3.irq+stcpuinfo3.softirq;

  char strLocalTime[21];
  memset(strLocalTime,0,sizeof(strLocalTime));
  LocalTime(strLocalTime,"yyyymmddhh24miss");

  char strXMLFileName[301];
  memset(strXMLFileName,0,sizeof(strXMLFileName));
  snprintf(strXMLFileName,300,"%s/cpuinfo_%s_%s.xml",argv[3],strLocalTime,argv[1]);

  CFile XMLFile;
  if (XMLFile.OpenForRename(strXMLFileName,"w+") == false )
  {
    logfile.Write("XMLFile.OpenForRename(%s) failed.\n",strXMLFileName); return -1;
  }
  XMLFile.Fprintf("<data>\n");

  XMLFile.Fprintf("<nodip>%s</nodip><crttime>%s</crttime><user>%0.02f</user><sys>%0.02f</sys><wait>%0.02f</wait><nice>%0.02f</nice><idle>%0.02f</idle><usep>%0.02f</usep><endl/>\n",argv[1],strLocalTime,stcpuinfo3.user/stcpuinfo3.total*100.0,stcpuinfo3.sys/stcpuinfo3.total*100.0,stcpuinfo3.wait/stcpuinfo3.total*100.0,stcpuinfo3.nice/stcpuinfo3.total*100.0,stcpuinfo3.idle/stcpuinfo3.total*100.0,100.0-stcpuinfo3.nice/stcpuinfo3.total*100.0);

  XMLFile.Fprintf("</data>\n");
  XMLFile.CloseAndRename();
  logfile.Write("create %s ok.\n",strXMLFileName);
  exit(0);
}
void EXIT(int sig)
{
  if (sig > 0) signal(sig,SIG_IGN);
  logfile.Write("catching the signal(%d).\n",sig);
  logfile.Write("cpuinfo exit.\n");
  exit(0);
}

bool LoadCPUInfo(struct st_cpuinfo &stcpuinfo)
{
  CFile CPUFile;
  if (CPUFile.Open("/proc/stat","r") == false )
  {
    logfile.Write("CPUFile.OpenForRead(/proc/stat) failed.\n"); return false;
  }

  CCmdStr CmdStr;
  char strBuffer[1024];
  while (true)
  {
    memset(strBuffer,0,sizeof(strBuffer));
    if (CPUFile.FFGETS(strBuffer,500) == false) break;
    // 删除字符串前后的空格
    DeleteLRChar(strBuffer,' ');
    // 把字符串中间的多个空格全部转换为一个空格
    UpdateStr(strBuffer,"  "," ");
    ToLower(strBuffer);
    CmdStr.SplitToCmd(strBuffer," ");
    if (strcmp(CmdStr.m_vCmdStr[0].c_str(),"cpu")==0) 
    {
      stcpuinfo.user=atof(CmdStr.m_vCmdStr[1].c_str());
      stcpuinfo.sys=atof(CmdStr.m_vCmdStr[2].c_str());
      stcpuinfo.wait=atof(CmdStr.m_vCmdStr[3].c_str());
      stcpuinfo.nice=atof(CmdStr.m_vCmdStr[4].c_str());
      stcpuinfo.idle=atof(CmdStr.m_vCmdStr[5].c_str());
      stcpuinfo.irq=atof(CmdStr.m_vCmdStr[6].c_str());
      stcpuinfo.softirq=atof(CmdStr.m_vCmdStr[7].c_str());
      return true;
    }
  }
  logfile.Write("Read /proc/stat failed.\n"); 
  return false;
}
在这里插入图片描述

vi /tmp/htdic/monclient/cpu*,如下是收集到的信息


在这里插入图片描述

收集内存信息#free -m,和top命令查看的内存是一样的,也在系统文件vi /proc/meminfo


在这里插入图片描述

以下是收集Oracle表空间信息,表空间就像磁盘空间一样,表空间的信息收集要去读取oracle的数据字典,如下sql是查询oracle表空间使用率,取出后写入xml文件里,vi tbspaceinfo.cpp,要dba权限。
select * from (
Select a.tablespace_name,
to_char(a.bytes/1024/1024,'99,999.999') total_bytes,
to_char(b.bytes/1024/1024,'99,999.999') free_bytes,
to_char(a.bytes/1024/1024 - b.bytes/1024/1024,'99,999.999') use_bytes,
to_char((1 - b.bytes/a.bytes)*100,'99.99') || '%' use
from (select tablespace_name,
sum(bytes) bytes
from dba_data_files
group by tablespace_name) a,
(select tablespace_name,
sum(bytes) bytes
from dba_free_space
group by tablespace_name) b
where a.tablespace_name = b.tablespace_name
union all
select c.tablespace_name,
to_char(c.bytes/1024/1024,'99,999.999') total_bytes,
to_char( (c.bytes-d.bytes_used)/1024/1024,'99,999.999') free_bytes,
to_char(d.bytes_used/1024/1024,'99,999.999') use_bytes,
to_char(d.bytes_used*100/c.bytes,'99.99') || '%' use
from
(select tablespace_name,sum(bytes) bytes
from dba_temp_files group by tablespace_name) c,
(select tablespace_name,sum(bytes_cached) bytes_used
from v$temp_extent_pool group by tablespace_name) d
where c.tablespace_name = d.tablespace_name
)
在这里插入图片描述
//tbspaceinfo.cpp
#include "_public.h"
#include "_ooci.h"
void EXIT(int sig);
struct st_TBSPACEINFO
{
  long  taskid;
  char  nodip[31];
  char  tablespace[101];
  double  total;
  double  used;
  double  available;
  double  usep;
  double alarmvalue;
  int  alarmsts;
  char crttime[21];
  int  rsts;
};
struct st_TBSPACEINFO stTBSPACEINFO;
CLogFile logfile;
connection conn;
int main(int argc,char *argv[])
{
  if (argc != 5)
  {
    printf("\n");
    printf("Using:./tbspaceinfo hostname logfilename outputpath username/password@tnsnames\n");

    printf("Example:/htidc/public/bin/tbspaceinfo 10.153.98.13 /tmp/htidc/log/tbspaceinfo_10.153.98.13.log /tmp/htidc/monclient shqx/pwdidc@SZQX_10.153.98.13\n\n");
    printf("此程序连接远程数据库,把远程数据库表空间使用率信息写入xml文件。\n");
    printf("hostname是本服务器的主机名,为了方便识别,也可以用IP。\n");
    printf("logfilename是本程序的日志文件名。\n");
    printf("outputpath是输出的xml文件存放的目录。\n");
    printf("username/password@tnsnames为待监控的远程数据库的用户名/密码@连接名。\n");
    printf("此程序运行在数据中心应用程序的服务器上。\n\n\n");

    return -1;
  }
  // 关闭全部的信号和输入输出
  // 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程
  // 但请不要用 "kill -9 +进程号" 强行终止
  CloseIOAndSignal(); signal(SIGINT,EXIT); signal(SIGTERM,EXIT);
  if (logfile.Open(argv[2],"a+") == false)
  {
    printf("logfile.Open(%s) failed.\n",argv[2]); return -1;
  }
  if (conn.connecttodb(argv[4],"Simplified Chinese_China.ZHS16GBK") != 0)
  {
    logfile.Write("conn.connecttodb(%s) failed.\n",argv[4]); return -1;
  }
  
  sqlstatement stmt;
  stmt.connect(&conn);
  stmt.prepare("\
    select f.tablespace_name,a.total,u.used,f.free,(u.used/a.total)*100  from\
        (select tablespace_name,sum(bytes/(1024*1024*1024)) total from DBA_DATA_FILES\
          group by tablespace_name) a,\
        (select tablespace_name,round(sum(bytes/(1024*1024*1024))) used from DBA_EXTENTS\
          group by tablespace_name) u,\
        (select tablespace_name,round(sum(bytes/(1024*1024*1024))) free from DBA_FREE_SPACE\
          group by tablespace_name) f\
    where a.tablespace_name = f.tablespace_name\
      and a.tablespace_name = u.tablespace_name\
      and f.tablespace_name in (select tablespace_name from DBA_TABLESPACES where contents='PERMANENT')");
  stmt.bindout(1,stTBSPACEINFO.tablespace,100);
  stmt.bindout(2,&stTBSPACEINFO.total);
  stmt.bindout(3,&stTBSPACEINFO.used);
  stmt.bindout(4,&stTBSPACEINFO.available);
  stmt.bindout(5,&stTBSPACEINFO.usep);

  if (stmt.execute() != 0)
  {
    logfile.Write("select DBA_DATA_FILES,DBA_EXTENTS,DBA_FREE_SPACE failed.\n%s\n",stmt.m_cda.message); return -1;
  }

  char strLocalTime[21];
  memset(strLocalTime,0,sizeof(strLocalTime));
  LocalTime(strLocalTime,"yyyymmddhh24miss");

  char strXMLFileName[301];
  memset(strXMLFileName,0,sizeof(strXMLFileName));
  snprintf(strXMLFileName,300,"%s/tbspaceinfo_%s_%s.xml",argv[3],strLocalTime,argv[1]);

  CFile XMLFile;
  if (XMLFile.OpenForRename(strXMLFileName,"w+") == false )
  {
    logfile.Write("XMLFile.OpenForRename(%s) failed.\n",strXMLFileName); return -1;
  }
  XMLFile.Fprintf("<data>\n");

  while (true)
  {
    memset(&stTBSPACEINFO,0,sizeof(stTBSPACEINFO));
    if (stmt.next() != 0) break;
    XMLFile.Fprintf(\
            "<nodip>%s</nodip>"\
            "<crttime>%s</crttime>"\
            "<tablespace>%s</tablespace>"\
            "<total>%0.02f</total>"\
            "<used>%0.02f</used>"\
            "<available>%0.02f</available>"\
            "<usep>%0.02f</usep><endl/>\n",
             argv[1],
             strLocalTime,
             stTBSPACEINFO.tablespace,
             stTBSPACEINFO.total,
             stTBSPACEINFO.used,
             stTBSPACEINFO.available,
             stTBSPACEINFO.usep);
  }
  XMLFile.Fprintf("</data>\n");
  XMLFile.CloseAndRename();
  logfile.Write("生成文件%s.\n",strXMLFileName);
  exit(0);
}
void EXIT(int sig)
{
  if (sig > 0) signal(sig,SIG_IGN);
  logfile.Write("catching the signal(%d).\n",sig);
  logfile.Write("tbspaceinfo exit.\n");
  exit(0);
}

在这里插入图片描述

为表空间增加数据文件有两种方式:固定大小和自动增长(自动增长对监控意义不大,使用率永远98%,99%)
以下是收集Oracle会话信息,我们用客户端通oracle的监听连上oracle数据库,oracle数据库会启动一个进程向会话提供服务,是多进程的服务端,每增加一连接(进程),需要消耗系统资源(内存,socket连接)。如下一共有6个客户端连上,SQL>exit退出一个就剩下5个。ps -ef |grep oracle可查看oracle数据库系统进程。
在这里插入图片描述

在这里插入图片描述

如下两个sql是查询数据字典查到进程和上面一样,第一个sql是查全部包括系统进程。如下表有一个OSUSER表示连上来的客户端操作系统名字,还可以通过SQL_ID查到连上来的客户端sql语句操作。还可以指定oracle用户的会话数
在这里插入图片描述

在这里插入图片描述

// dbsessioninfo.cpp
#include "qxmon.h"
void EXIT(int sig);
struct st_DBSESSIONINFO stDBSESSIONINFO;

CLogFile logfile;
connection conndst;
CProgramActive ProgramActive;

int main(int argc,char *argv[])
{
  if (argc != 5)
  {
    printf("\n");
    printf("Using:./dbsessioninfo hostname logfilename outputpath username/password@tnsnames\n");

    printf("Example:/htidc/htidc/bin/procctl 300 /htidc/qxmon/bin/dbsessioninfo 10.153.98.13 /log/qxmon/dbsessioninfo_10.153.98.13.log /qxdata/qxmon/qxmonclient szidc/pwdidc@SZQX_10.153.98.13\n\n");
    printf("此程序连接远程数据库,把远程数据库会话信息写入XML文件。\n");
    printf("hostname是本服务器的主机名,为了方便识别,也可以用IP。\n");
    printf("logfilename是本程序的日志文件名。\n");
    printf("outputpath是输出的XML文件存放的目录。\n");
    printf("username/password@tnsnames为待监控的远程数据库的用户名/密码@连接名。\n");
    printf("此程序运行在监控平台的服务器上。\n\n\n");

    return -1;
  }

  // 关闭全部的信号和输入输出
  // 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程
  // 但请不要用 "kill -9 +进程号" 强行终止
  CloseIOAndSignal(); signal(SIGINT,EXIT); signal(SIGTERM,EXIT);

  if (logfile.Open(argv[2],"a+") == FALSE)
  {
    printf("logfile.Open(%s) failed.\n",argv[2]); return -1;
  }

  ProgramActive.SetProgramInfo(&logfile,"alarmserver",300);

  if (conndst.connecttodb(argv[4])!=0)
  {
    logfile.Write("connmon.connecttodb(%s) failed.\n",argv[4]); EXIT(-1);
  }

  sqlstatement stmt;
  stmt.connect(&conndst);
  stmt.prepare("select count(*) from V$SESSION where type!='BACKGROUND' and status!='KILLED'");
  stmt.bindout(1,&stDBSESSIONINFO.total);

  if (stmt.execute() != 0)
  {
    logfile.Write("select V$SESSION failed.\n%s\n",stmt.cda.message); EXIT(-1);
  }

  char strLocalTime[21];
  memset(strLocalTime,0,sizeof(strLocalTime));
  LocalTime(strLocalTime,"yyyymmddhh24miss");

  char strXMLFileName[301];
  memset(strXMLFileName,0,sizeof(strXMLFileName));
  snprintf(strXMLFileName,300,"%s/dbsessioninfo_%s_%s.xml",argv[3],strLocalTime,argv[1]);

  CFile XMLFile;
  if (XMLFile.OpenForRename(strXMLFileName,"w+") == FALSE )
  {
    logfile.Write("XMLFile.OpenForRename(%s) failed.\n",strXMLFileName); EXIT(-1);
  }

  XMLFile.Fprintf("<data>\n");

  while (TRUE)
  {
    memset(&stDBSESSIONINFO,0,sizeof(stDBSESSIONINFO));

    if (stmt.next() != 0) break;

    XMLFile.Fprintf(\
            "<nodip>%s</nodip>"\
            "<crttime>%s</crttime>"\
            "<total>%d</total><endl/>\n",
             argv[1],
             strLocalTime,
             stDBSESSIONINFO.total);
  }

  XMLFile.Fprintf("</data>\n");

  XMLFile.CloseAndRename();

  logfile.Write("生成文件%s.\n",strXMLFileName);

  exit(0);
}

void EXIT(int sig)
{
  if (sig > 0) signal(sig,SIG_IGN);
  logfile.Write("catching the signal(%d).\n",sig);
  logfile.Write("dbsessioninfo exit.\n");
  exit(0);
}

在这里插入图片描述

oracle里有一个系统参数processes(最大允许的会话总数)。_ooci.h中connection类连接上一个后进程及processes+1,sqlstatement类实例化并指定数据库连接也会消耗一种资源即open_cursors参数。oracle有缓存机制存sql,下次用到则不进行语法分析,所以oracle需大内存,存的越多,缓存命中率越高。
在这里插入图片描述

收集到了信息放入oracle表,再写一个程序去判断是否告警。告警了的话形成一段文字如哪个服务器ip的cpu使用率超了多少或哪个分区磁盘空间超了多少。告警信息通知维护人员两个方案实现:1.将手机号和短信内容通过接口(数据库表这种接口,http,文件)给短信平台。2.无短信平台,写一个程序分析收到的信息,形成一段文字,调用邮件功能(web服务器:tomcat,java发邮件)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335