天气项目复盘

调度进程和进程心跳检查进程

调度进程

流程

有些进程需要常驻在内存中,一直运行,如果这些进程发生意外被终止了,就要有措施对他进行回收以及重新调度。
fork一个子进程,使用exec函数族替换进程块,从而执行另外一个进程。父进程堵在wait等待子进程回归,所以只要子进程没有宕掉,那么父进程不会创建额外的子进程去执行任务。
但是一旦子进程宕掉,那么就会在以一个时间周期中,产生一个子进程继续执行任务,从而保证该任务流常驻内存中执行。

进程心跳检查进程

进程心跳类

共享内存

申请一个共享内存,用于进程间通信,共享内存的存在使得各个进程将自己的心跳信息记录在共享内存中,从而可以让心跳检查进程对我们的心跳信息进行检查,从而判断我们是不是已经没有心跳,进而结束我们,让调度进程再一次启动我们。

信号量------锁

信号量也是进程间通信的一种手段,共享内存和信号量的申请和获取都是内核共享,所以所有进程都可以拿到它们进行使用。

流程

1.抽象一个进程心跳的类:进程信息的写入(包括日志文件),进程信息的更新,进程退出的信息清除。
2.信号量的包装
信号量是内核提供的给所有进程通信的一种,共享内存,socket,信号量都是进程间通信的手段。
但是这里进行了包装,包装成一个对象,这个对象中有信号量的句柄,可以当做信号量来用,但是并不是信号量的本体,但是可以拥有信号量的一些特性。
对信号量进行封装有什么好处:首先不直接接触信号量本身,而是通过对象操作信号量,分装一些接口,隐藏一些细节。
3.具体的操作:

// 信号量。
class CSEM
{
private:
  union semun  // 用于信号量操作的共同体。
  {
    int val;
    struct semid_ds *buf;
    unsigned short  *arry;
  };

  int   m_semid;         // 信号量描述符。

  // 如果把sem_flg设置为SEM_UNDO,操作系统将跟踪进程对信号量的修改情况,
  // 在全部修改过信号量的进程(正常或异常)终止后,操作系统将把信号量恢
  // 复为初始值(就像撤消了全部进程对信号的操作)。
  // 如果信号量用于表示可用资源的数量(不变的),设置为SEM_UNDO更合适。
  // 如果信号量用于生产消费者模型,设置为0更合适。
  // 注意,网上查到的关于sem_flg的用法基本上是错的,一定要自己动手多测试。
  short m_sem_flg;
public:
  CSEM();
  // 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
  bool init(key_t key,unsigned short value=1,short sem_flg=SEM_UNDO); 
  bool P(short sem_op=-1); // 信号量的P操作。
  bool V(short sem_op=1);  // 信号量的V操作。
  int  value();            // 获取信号量的值,成功返回信号量的值,失败返回-1。
  bool destroy();          // 销毁信号量。
 ~CSEM();
};

// 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
bool CSEM::init(key_t key,unsigned short value,short sem_flg)
{
  if (m_semid!=-1) return false;

  m_sem_flg=sem_flg;

  // 信号量的初始化不能直接用semget(key,1,0666|IPC_CREAT),因为信号量创建后,初始值是0。

  // 信号量的初始化分三个步骤:
  // 1)获取信号量,如果成功,函数返回。
  // 2)如果失败,则创建信号量。
  // 3) 设置信号量的初始值。

  // 获取信号量。
  if ( (m_semid=semget(key,1,0666)) == -1)
  {
    // 如果信号量不存在,创建它。
    if (errno==2)
    {
      // 用IPC_EXCL标志确保只有一个进程创建并初始化信号量,其它进程只能获取。
      if ( (m_semid=semget(key,1,0666|IPC_CREAT|IPC_EXCL)) == -1)
      {
        if (errno!=EEXIST)
        {
          perror("init 1 semget()"); return false;
        }
        if ( (m_semid=semget(key,1,0666)) == -1)
        { perror("init 2 semget()"); return false; }
    
        return true;
      }

      // 信号量创建成功后,还需要把它初始化成value。
      union semun sem_union;
      sem_union.val = value;   // 设置信号量的初始值。
      if (semctl(m_semid,0,SETVAL,sem_union) <  0) { perror("init semctl()"); return false; }
    }
    else
    { perror("init 3 semget()"); return false; }
  }

  return true;
}
bool CSEM::P(short sem_op)
{
  if (m_semid==-1) return false;

  struct sembuf sem_b;
  sem_b.sem_num = 0;      // 信号量编号,0代表第一个信号量。
  sem_b.sem_op = sem_op;  // P操作的sem_op必须小于0。
  sem_b.sem_flg = m_sem_flg;   
  if (semop(m_semid,&sem_b,1) == -1) { perror("p semop()"); return false; }

  return true;
}

bool CSEM::V(short sem_op)
{
  if (m_semid==-1) return false;

  struct sembuf sem_b;
  sem_b.sem_num = 0;      // 信号量编号,0代表第一个信号量。
  sem_b.sem_op = sem_op;  // V操作的sem_op必须大于0。
  sem_b.sem_flg = m_sem_flg;
  if (semop(m_semid,&sem_b,1) == -1) { perror("V semop()"); return false; }

  return true;
}

// 获取信号量的值,成功返回信号量的值,失败返回-1。
int CSEM::value()
{
  return semctl(m_semid,0,GETVAL);
}

bool CSEM::destroy()
{
  if (m_semid==-1) return false;

  if (semctl(m_semid,0,IPC_RMID) == -1) { perror("destroy semctl()"); return false; }

  return true;
}
// 进程心跳操作类。
class CPActive
{
private:
  CSEM m_sem;                 // 用于给共享内存加锁的信号量id。
  int  m_shmid;               // 共享内存的id。
  int  m_pos;                 // 当前进程在共享内存进程组中的位置。
  st_procinfo *m_shm;         // 指向共享内存的地址空间。

public:
  CPActive();  // 初始化成员变量。

  // 把当前进程的心跳信息加入共享内存进程组中。
  bool AddPInfo(const int timeout,const char *pname=0,CLogFile *logfile=0);

  // 更新共享内存进程组中当前进程的心跳时间。
  bool UptATime();

  ~CPActive();  // 从共享内存中删除当前进程的心跳记录。
};

CPActive::CPActive()
{
  m_shmid=0;
  m_pos=-1;
  m_shm=0;
}

// 把当前进程的心跳信息加入共享内存进程组中。
bool CPActive::AddPInfo(const int timeout,const char *pname,CLogFile *logfile)
{
  if (m_pos!=-1) return true;

  if (m_sem.init(SEMKEYP) == false)  // 初始化信号量。
  {
    if (logfile!=0) logfile->Write("创建/获取信号量(%x)失败。\n",SEMKEYP); 
    else printf("创建/获取信号量(%x)失败。\n",SEMKEYP);

    return false;
  }

  // 创建/获取共享内存,键值为SHMKEYP,大小为MAXNUMP个st_procinfo结构体的大小。
  if ( (m_shmid = shmget((key_t)SHMKEYP, MAXNUMP*sizeof(struct st_procinfo), 0666|IPC_CREAT)) == -1)
  { 
    if (logfile!=0) logfile->Write("创建/获取共享内存(%x)失败。\n",SHMKEYP); 
    else printf("创建/获取共享内存(%x)失败。\n",SHMKEYP);

    return false; 
  }

  // 将共享内存连接到当前进程的地址空间。
  m_shm=(struct st_procinfo *)shmat(m_shmid, 0, 0);
  
  struct st_procinfo stprocinfo;    // 当前进程心跳信息的结构体。
  memset(&stprocinfo,0,sizeof(stprocinfo));

  stprocinfo.pid=getpid();            // 当前进程号。
  stprocinfo.timeout=timeout;         // 超时时间。
  stprocinfo.atime=time(0);           // 当前时间。
  STRNCPY(stprocinfo.pname,sizeof(stprocinfo.pname),pname,50); // 进程名。

  // 进程id是循环使用的,如果曾经有一个进程异常退出,没有清理自己的心跳信息,
  // 它的进程信息将残留在共享内存中,不巧的是,当前进程重用了上述进程的id,
  // 这样就会在共享内存中存在两个进程id相同的记录,守护进程检查到残留进程的
  // 心跳时,会向进程id发送退出信号,这个信号将误杀当前进程。

  // 如果共享内存中存在当前进程编号,一定是其它进程残留的数据,当前进程就重用该位置。
  for (int ii=0;ii<MAXNUMP;ii++)
  {
    if ( (m_shm+ii)->pid==stprocinfo.pid ) { m_pos=ii; break; }
  }

  m_sem.P();  // 给共享内存上锁。

  if (m_pos==-1)
  {
    // 如果m_pos==-1,共享内存的进程组中不存在当前进程编号,找一个空位置。
    for (int ii=0;ii<MAXNUMP;ii++)
      if ( (m_shm+ii)->pid==0 ) { m_pos=ii; break; }
  }

  if (m_pos==-1) 
  { 
    if (logfile!=0) logfile->Write("共享内存空间已用完。\n");
    else printf("共享内存空间已用完。\n");

    m_sem.V();  // 解锁。

    return false; 
  }

  // 把当前进程的心跳信息存入共享内存的进程组中。
  memcpy(m_shm+m_pos,&stprocinfo,sizeof(struct st_procinfo)); 

  m_sem.V();   // 解锁。

  return true;
}

// 更新共享内存进程组中当前进程的心跳时间。
bool CPActive::UptATime()
{
  if (m_pos==-1) return false;

  (m_shm+m_pos)->atime=time(0);

  return true;
}

CPActive::~CPActive()
{
  // 把当前进程从共享内存的进程组中移去。
  if (m_pos!=-1) memset(m_shm+m_pos,0,sizeof(struct st_procinfo));

  // 把共享内存从当前进程中分离。
  if (m_shm!=0) shmdt(m_shm);
}

进程检查

1.流程:由调度进程周期性启动,扫描共享内存,对共享内存中的每一个进程的信息进行检查。
2.如果当前没有进程,跳过
3.获取当前进程最后一次心跳,对比是否超时,没有就继续下一个,否则强制终止。

基于ftp协议的文件上传、下载功能模块

下载模块

简介:实现以下功能:1.从服务器增量得到最新的文件,包括是否带有文件的时间戳信息,是否要更新该文件(文件类型可选)2.同时对服务器上已经完成下载的文件进行处理(备份还是删除)。

实现

1.客户端有一个文件记录已经下载下来的文件信息
2.进入服务端获取所有的待下载的文件信息
3.根据参数比较,得到哪些文件要下载,哪些文件不要下载
4.对要下载的文件下载,最终将下载完成的文件写到客户端存储已经现在完成的文件磁盘中。

比较操作还是使用容器比较好。

运行参数
// 程序运行参数的结构体。
struct st_arg
{
  char host[31];           // 远程服务端的IP和端口。
  int  mode;               // 传输模式,1-被动模式,2-主动模式,缺省采用被动模式。
  char username[31];       // 远程服务端ftp的用户名。
  char password[31];       // 远程服务端ftp的密码。
  char remotepath[301];    // 远程服务端存放文件的目录。
  char localpath[301];     // 本地文件存放的目录。
  char matchname[101];     // 待上传文件匹配的规则。
  int  ptype;              // 上传后客户端文件的处理方式:1-什么也不做;2-删除;3-备份。
  char localpathbak[301];  // 上传后客户端文件的备份目录。
  char okfilename[301];    // 已上传成功文件名清单。
  int  timeout;            // 进程心跳的超时时间。
  char pname[51];          // 进程名,建议用"ftpputfiles_后缀"的方式。
} starg;
文件信息结构体
// 文件信息的结构体。
struct st_fileinfo
{
  char filename[301];   // 文件名。
  char mtime[21];       // 文件时间。
};
具体实现流程

1.进入服务器文件夹,读出所有的文件信息

// 进入ftp服务器存放文件的目录。
    if (ftp.chdir(starg.remotepath) == false)
    {
        logfile.Write("ftp.chdir(%s) failed.\n", starg.remotepath);
        return false;
    }

    // 调用ftp.nlist()方法列出服务器目录中的文件,结果存放到本地文件中。
    //这个方法会把服务器文件的信息记录到文件中
    if (ftp.nlist("", starg.listfilename) == false)
    {
        logfile.Write("ftp.nlist(%s) failed.\n", starg.remotepath);
        return false;
    }

2.将所有的文件信息读取到容器中,方便等会进行处理比较,得出,哪些文件要我们更新,包括,新增的,已经修改的文件都是要进行重新下载的。
注意这里是要进行文件匹配的,有些文件并不是我们要下载的,那么我们不用关心那些文件,所以直接跳过就行,我们只关心我们关心的那些文件。
这一步主要完成:1.我们关心的文件的文件信息,包括文件名,文件时间,并且都放在V2中,等待过滤。现在还不是下载的时候,等一会会进行过滤,从而去下载,新增的文件,新修改的文件。

bool LoadListFile()
{
    vlistfile2.clear();

    CFile File;

    if (File.Open(starg.listfilename, "r") == false)
    {
        logfile.Write("File.Open(%s) 失败。\n", starg.listfilename);
        return false;
    }

    struct st_fileinfo stfileinfo;

    while (true)
    {
        memset(&stfileinfo, 0, sizeof(struct st_fileinfo));

        if (File.Fgets(stfileinfo.filename, 300, true) == false)
        {
            break;
        }

        if (MatchStr(stfileinfo.filename, starg.matchname) == false)
        {
            continue;
        }

        if ((starg.ptype == 1) && (starg.checkmtime == true))
        {
            // 获取ftp服务端文件时间。注意,这个的使用,前提是已经定位到服务器的下面了,否则获取实践出错
            if (ftp.mtime(stfileinfo.filename) == false)
            {
                logfile.Write("ftp.mtime(%s) failed.\n", stfileinfo.filename);
                return false;
            }

            strcpy(stfileinfo.mtime, ftp.m_mtime);
        }

        //记录时间
        vlistfile2.push_back(stfileinfo);
    }

    return true;
}
  1. 上一次已经下载完成的文件已经被记录在客户端一个文件中,读出来,并且与当前等待下载的容器进行对比,得出真正要下载的文件的文件信息。
//从一个文件中把上一次服务器有的放进来
bool LoadOKFile()
{
    vlistfile1.clear();

    CFile File;

    // 注意:如果程序是第一次下载,okfilename是不存在的,并不是错误,所以也返回true。
    if ((File.Open(starg.okfilename, "r")) == false)
    {
        return true;
    }

    char strbuffer[501];

    struct st_fileinfo stfileinfo;

    while (true)
    {
        memset(&stfileinfo, 0, sizeof(struct st_fileinfo));

        if (File.Fgets(strbuffer, 300, true) == false)
            break;

        GetXMLBuffer(strbuffer, "filename", stfileinfo.filename);
        GetXMLBuffer(strbuffer, "mtime", stfileinfo.mtime);

        vlistfile1.push_back(stfileinfo);
    }

    return true;
}

4 进行比较,比较已经下载好的文件和当前服务器等待下载的文件,比较得出,那些要下载,哪些不要下载。比较准则包括,文件名,时间戳。只要文件名或者时间戳有不同,那就说明要下载或者重新下载。

bool CompVector()
{
    vlistfile3.clear();
    vlistfile4.clear();

    int ii, jj;

    // 遍历vlistfile2。
    for (ii = 0; ii < vlistfile2.size(); ii++)
    {
        // 在vlistfile1中查找vlistfile2[ii]的记录。
        for (jj = 0; jj < vlistfile1.size(); jj++)
        {
            // 如果找到了,把记录放入vlistfile3。
            if ((strcmp(vlistfile2[ii].filename, vlistfile1[jj].filename) == 0) &&
                (strcmp(vlistfile2[ii].mtime, vlistfile1[jj].mtime) == 0))
            {
                vlistfile3.push_back(vlistfile2[ii]);
                break;
            }
        }

        // 如果没有找到,把记录放入vlistfile4。
        if (jj == vlistfile1.size())
            vlistfile4.push_back(vlistfile2[ii]);
    }

    return true;
}

5 现在,已经过滤了哪些要下载,哪些不要下载。将不要下载的文件的信息重新写回客户端记录已经下载完成的那个文件中。
6.下载那些要下载的文件。同时把下载成功的文件的文件信息,追加到客户端中记录下砸成功的那个文件中。

// 遍历容器vlistfile。
    for (int ii = 0; ii < vlistfile2.size(); ii++)
    {
        SNPRINTF(strremotefilename, sizeof(strremotefilename), 300, "%s/%s", starg.remotepath, vlistfile2[ii].filename);
        SNPRINTF(strlocalfilename, sizeof(strlocalfilename), 300, "%s/%s", starg.localpath, vlistfile2[ii].filename);
        // 调用ftp.get()方法从服务器下载文件。
        logfile.Write("get %s ...", strremotefilename);

        if (ftp.get(strremotefilename, strlocalfilename) == false)
        {
            logfile.WriteEx("failed.\n");
            break;
        }

        logfile.WriteEx("ok.\n");
        PActive.UptATime(); // 更新进程的心跳。

        // 如果ptype==1,把下载成功的文件记录追加到okfilename文件中。
        if (starg.ptype == 1)
        {
            AppendToOKFile(&vlistfile2[ii]);
        }

        //------>如果服务器上的数据是删除
        if (starg.ptype == 2)
        {
            //直接删除改调数据
            if (ftp.ftpdelete(strremotefilename) == false)
            {
                logfile.Write("ftp.ftpdelete(%s) failed.\n", strremotefilename);
                return false;
            }
            logfile.Write("ftp.ftpdelete(%s) successed.\n", strremotefilename);
            PActive.UptATime(); // 更新进程的心跳。
        }

        // 转存到备份目录。
        if (starg.ptype == 3)
        {
            char strremotefilenamebak[301];
            SNPRINTF(strremotefilenamebak, sizeof(strremotefilenamebak), 300, "%s/%s", starg.remotepathbak, vlistfile2[ii].filename);
            if (ftp.ftprename(strremotefilename, strremotefilenamebak) == false)
            {
                logfile.Write("ftp.ftprename(%s,%s) failed.\n", strremotefilename, strremotefilenamebak);
                return false;
            }
            PActive.UptATime(); // 更新进程的心跳。
        }
    }

基于TCP的文件上传、下载模块-----文件快速传输

服务端和客户端应该是协同开发的,因为在某个任务中,如果你不协同开发,任务不匹配的话,就会出现,你说你的,我说我的,这样就无法完成对话,因此应该是,同步的协同的去开发。
在一个任务中,如果有多个“表示不同业务的报文”,无法保证在管道里的是谁。
但是如果AB是连在一起的并且A一定在B之前,C则是独立于两人的业务报文,那么。对于AB和C的顺序是无法保证的,但是对于AB的顺序则是一定有保证的。
所以才需要同步执行,因为会有不同的业务报文,导致不可理解性。
总流程:

  • 客户端:1.打开文件夹,读取文件信息,发送文件信息。2.将各个文件分组发送过去。3.客户端接受回应,并开始下一个文件的传输。
  • 服务端:1.读取来自客户端的文件信息,真被读取各个文件3.对各个文件,分组进行接受,全部接受后,完成改名。3.对于一个文件,全部接受后,发送一个回应。
  • 异步传输:使用I/O复用技术,只有当准备好了读,才去回应,就不浪费时间在等待回应的时间上了。
  • 这可能就是应用层协议的要点:如果没有应用层协议,那么就会要这样同步协同编程。因为没有应用层协议,就不知道,现在管道里到底是什么业务的信息,是什么样类型的信息,容易出现牛头不对马嘴的情况。按照我的理解,在开发一个模块的时候,应该协同规定好这个模块的信息的业务报头,这样就可以分开来进行业务的编写,只需要对报文的内容进行下解析,就知道是什么样的信息,就可以拿去做事。

服务器模块

这些是对客户端那边的登录报文的解析,从而可以协助客户端完成文件上传和下载功能。

struct st_arg
{
  int clienttype;       // 客户端类型,1-上传文件;2-下载文件。
  char ip[31];          // 服务端的IP地址。
  int port;             // 服务端的端口。
  int ptype;            // 文件成功传输后的处理方式:1-删除文件;2-移动到备份目录。
  char clientpath[301]; // 客户端文件存放的根目录。
  bool andchild;        // 是否传输各级子目录的文件,true-是;false-否。
  char matchname[301];  // 待传输文件名的匹配规则,如"*.TXT,*.XML"。
  char srvpath[301];    // 服务端文件存放的根目录。
  char srvpathbak[301]; // 服务端文件存放的根目录。
  int timetvl;          // 扫描目录文件的时间间隔,单位:秒。
  int timeout;          // 进程心跳的超时时间。
  char pname[51];       // 进程名,建议用"tcpgetfiles_后缀"的方式。
} starg;

服务端使用多进程完成多个客户端的通信和处理数据。
客户端和服务端是收发机制。
我有个问题:服务端虽然是收发机制,但是,服务端也不知道,客户端发过来的消息现在是什么,流式传输,对于内容是不了解的,但是现在指定了“协议”,也就是说,现在,每一个报文都是会有长度大小,读取的时候,先读头,然后解析,最后再度内容,然后对客户端进行回复,发送消息给客户端。
我的问题是,要不要对对方的行为了如指掌,也就是说,必须同时编程?客户端写一个功能,服务端就要写一个功能?其实不要。
为啥?
首先,虽然现在是流式传输,但是你可以在读取之前就可以解析到现在要读取多少字节,然后解析,然后根据解析出来的内容按需对客户端进行回复。所以并不要协同。
也就是说,“客户端要你回复的内容已经包含在内容里了,你不需要对着客户端的行为去编程,你只管对内容读取和发送,对端会进行解析和完成它要干的事,而不要管对象的事”。

while (true)
  {
    // 等待客户端的连接请求。
    if (TcpServer.Accept() == false)
    {
      logfile.Write("TcpServer.Accept() failed.\n");
      FathEXIT(-1);
    }

    logfile.Write("客户端(%s)已连接。\n", TcpServer.GetIP());

    if (fork() > 0)
    {
      TcpServer.CloseClient();
      continue;
    } // 父进程继续回到Accept()。

    // 子进程重新设置退出信号。
    signal(SIGINT, ChldEXIT);
    signal(SIGTERM, ChldEXIT);

    TcpServer.CloseListen();

    // 子进程与客户端进行通讯,处理业务。

    // 处理登录客户端的登录报文。
    if (ClientLogin() == false)
      ChldEXIT(-1);

    // 如果clienttype==1,调用上传文件的主函数。
    if (starg.clienttype == 1)
      RecvFilesMain();

    // 如果clienttype==2,调用下载文件的主函数。
    if (starg.clienttype == 2)
      SendFilesMain();

    ChldEXIT(0);
  }

服务器初始化以及绑定类

抽象出一个服务器类:
包括:客户端的地址信息,服务端的地址信息,服务端用于监听的端口号,客户端连接过来的端口号,服务端读取超时等待时间,服务端读取的报文大小。
然后就是将一些面向过程的服务端,客户端网络通信的繁杂代码封装进入其中。

// socket通讯的服务端类
class CTcpServer
{
private:
  int m_socklen;                    // 结构体struct sockaddr_in的大小。
  struct sockaddr_in m_clientaddr;  // 客户端的地址信息。
  struct sockaddr_in m_servaddr;    // 服务端的地址信息。
public:
  int  m_listenfd;   // 服务端用于监听的socket。
  int  m_connfd;     // 客户端连接上来的socket。
  bool m_btimeout;   // 调用Read方法时,失败的原因是否是超时:true-超时,false-未超时。
  int  m_buflen;     // 调用Read方法后,接收到的报文的大小,单位:字节。

  CTcpServer();  // 构造函数。

  // 服务端初始化。
  // port:指定服务端用于监听的端口。
  // 返回值:true-成功;false-失败,一般情况下,只要port设置正确,没有被占用,初始化都会成功。
  bool InitServer(const unsigned int port,const int backlog=5); 

  // 阻塞等待客户端的连接请求。
  // 返回值:true-有新的客户端已连接上来,false-失败,Accept被中断,如果Accept失败,可以重新Accept。
  bool Accept();

  // 获取客户端的ip地址。
  // 返回值:客户端的ip地址,如"192.168.1.100"。
  char *GetIP();

  // 接收客户端发送过来的数据。
  // buffer:接收数据缓冲区的地址,数据的长度存放在m_buflen成员变量中。
  // itimeout:等待数据的超时时间,单位:秒,缺省值是0-无限等待。
  // 返回值:true-成功;false-失败,失败有两种情况:1)等待超时,成员变量m_btimeout的值被设置为true;2)socket连接已不可用。
  bool Read(char *buffer,const int itimeout=0);

  // 向客户端发送数据。
  // buffer:待发送数据缓冲区的地址。
  // ibuflen:待发送数据的大小,单位:字节,缺省值为0,如果发送的是ascii字符串,ibuflen取0,如果是二进制流数据,ibuflen为二进制数据块的大小。
  // 返回值:true-成功;false-失败,如果失败,表示socket连接已不可用。
  bool Write(const char *buffer,const int ibuflen=0);

  // 关闭监听的socket,即m_listenfd,常用于多进程服务程序的子进程代码中。
  void CloseListen();

  // 关闭客户端的socket,即m_connfd,常用于多进程服务程序的父进程代码中。
  void CloseClient();

  ~CTcpServer();  // 析构函数自动关闭socket,释放资源。
};
初始化监听端口和日志文件

进行端口监听和绑定以及listen。

bool CTcpServer::InitServer(const unsigned int port,const int backlog)
{
  // 如果服务端的socket>0,关掉它,这种处理方法没有特别的原因,不要纠结。
  if (m_listenfd > 0) { close(m_listenfd); m_listenfd=-1; }

  if ( (m_listenfd = socket(AF_INET,SOCK_STREAM,0))<=0) return false;

  // 忽略SIGPIPE信号,防止程序异常退出。
  signal(SIGPIPE,SIG_IGN);   

  // 打开SO_REUSEADDR选项,当服务端连接处于TIME_WAIT状态时可以再次启动服务器,
  // 否则bind()可能会不成功,报:Address already in use。
  //char opt = 1; unsigned int len = sizeof(opt);
  int opt = 1; unsigned int len = sizeof(opt);
  setsockopt(m_listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,len);    

  memset(&m_servaddr,0,sizeof(m_servaddr));
  m_servaddr.sin_family = AF_INET;
  m_servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   // 任意ip地址。
  m_servaddr.sin_port = htons(port);
  if (bind(m_listenfd,(struct sockaddr *)&m_servaddr,sizeof(m_servaddr)) != 0 )
  {
    CloseListen(); return false;
  }

  if (listen(m_listenfd,backlog) != 0 )
  {
    CloseListen(); return false;
  }

  return true;
}
服务端准备接受客户端连接
bool CTcpServer::Accept()
{
  if (m_listenfd==-1) return false;

  m_socklen = sizeof(struct sockaddr_in);

  if ((m_connfd=accept(m_listenfd,(struct sockaddr *)&m_clientaddr,(socklen_t*)&m_socklen)) < 0)
      return false;

  return true;
}
TCP读写解决粘包和分包的问题

通过在报文的头部加上一个长度,在每次读取信息的时候,先对报文的头部进行解析,然后在读取相应的字节数,相当于在这个功能的实现中,我们使用的是自己的“协议”,完成了数据的上传预下载,但是在其他的应用中切记,要处理好。

// 接收socket的对端发送过来的数据。
// sockfd:可用的socket连接。
// buffer:接收数据缓冲区的地址。
// ibuflen:本次成功接收数据的字节数。
// itimeout:接收等待超时的时间,单位:秒,-1-不等待;0-无限等待;>0-等待的秒数。
// 返回值:true-成功;false-失败,失败有两种情况:1)等待超时;2)socket连接已不可用。
bool TcpRead(const int sockfd,char *buffer,int *ibuflen,const int itimeout)
{
  if (sockfd==-1) return false;

  // 如果itimeout>0,表示需要等待itimeout秒,如果itimeout秒后还没有数据到达,返回false。
  if (itimeout>0)
  {
    struct pollfd fds;
    fds.fd=sockfd;
    fds.events=POLLIN;
    if ( poll(&fds,1,itimeout*1000) <= 0 ) return false;
  }

  // 如果itimeout==-1,表示不等待,立即判断socket的缓冲区中是否有数据,如果没有,返回false。
  if (itimeout==-1)
  {
    struct pollfd fds;
    fds.fd=sockfd;
    fds.events=POLLIN;
    if ( poll(&fds,1,0) <= 0 ) return false;
  }

  (*ibuflen) = 0;  // 报文长度变量初始化为0。

  // 先读取报文长度,4个字节。
  if (Readn(sockfd,(char*)ibuflen,4) == false) return false;

  (*ibuflen)=ntohl(*ibuflen);  // 把报文长度由网络字节序转换为主机字节序。

  // 再读取报文内容。
  if (Readn(sockfd,buffer,(*ibuflen)) == false) return false;

  return true;
}

// 向socket的对端发送数据。
// sockfd:可用的socket连接。
// buffer:待发送数据缓冲区的地址。
// ibuflen:待发送数据的字节数,如果发送的是ascii字符串,ibuflen填0或字符串的长度,
//          如果是二进制流数据,ibuflen为二进制数据块的大小。
// 返回值:true-成功;false-失败,如果失败,表示socket连接已不可用。
bool TcpWrite(const int sockfd,const char *buffer,const int ibuflen)
{
  if (sockfd==-1) return false;

  int ilen=0;  // 报文长度。

  // 如果ibuflen==0,就认为需要发送的是字符串,报文长度为字符串的长度。
  if (ibuflen==0) ilen=strlen(buffer);
  else ilen=ibuflen;

  int ilenn=htonl(ilen);    // 把报文长度转换为网络字节序。

  char TBuffer[ilen+4];     // 发送缓冲区。
  memset(TBuffer,0,sizeof(TBuffer));  // 清区发送缓冲区。
  memcpy(TBuffer,&ilenn,4);           // 把报文长度拷贝到缓冲区。
  memcpy(TBuffer+4,buffer,ilen);      // 把报文内容拷贝到缓冲区。
  
  // 发送缓冲区中的数据。
  if (Writen(sockfd,TBuffer,ilen+4) == false) return false;

  return true;
}

// 从已经准备好的socket中读取数据。
// sockfd:已经准备好的socket连接。
// buffer:接收数据缓冲区的地址。
// n:本次接收数据的字节数。
// 返回值:成功接收到n字节的数据后返回true,socket连接不可用返回false。
bool Readn(const int sockfd,char *buffer,const size_t n)
{
  int nLeft=n;  // 剩余需要读取的字节数。
  int idx=0;    // 已成功读取的字节数。
  int nread;    // 每次调用recv()函数读到的字节数。

  while(nLeft > 0)
  {
    if ( (nread=recv(sockfd,buffer+idx,nLeft,0)) <= 0) return false;

    idx=idx+nread;
    nLeft=nLeft-nread;
  }

  return true;
}

// 向已经准备好的socket中写入数据。
// sockfd:已经准备好的socket连接。
// buffer:待发送数据缓冲区的地址。
// n:待发送数据的字节数。
// 返回值:成功发送完n字节的数据后返回true,socket连接不可用返回false。
bool Writen(const int sockfd,const char *buffer,const size_t n)
{
  int nLeft=n;  // 剩余需要写入的字节数。
  int idx=0;    // 已成功写入的字节数。
  int nwritten; // 每次调用send()函数写入的字节数。
  
  while(nLeft > 0 )
  {    
    if ( (nwritten=send(sockfd,buffer+idx,nLeft,0)) <= 0) return false;      
    
    nLeft=nLeft-nwritten;
    idx=idx+nwritten;
  }

  return true;
}

上传模块

简介:从客户端上传文件到服务端,包含文件上传的类型,名字,时间戳检查,基于TCP文件流的上传功能。
使用多进程编程,可以同时接受多个客户端的消息并发处理。

服务端的响应

我觉得每一个业务的完成其实应该都是客户端和服务端的协同完成,先暂时不去想是否正确,先以为是这样的,每一个业务,包括其实是客户端和服务端的共同推进和开发。
因为可能存在这样一种情况,就是在一个业务中你客户端发出了很多数据过去,服务端如果不协同处理的话,就也不知道到底是那些东西,最终无法进行通话。

// 处理登录客户端的登录报文。
    if (ClientLogin() == false)
      ChldEXIT(-1);

    // 如果clienttype==1,调用上传文件的主函数。
    if (starg.clienttype == 1)
      RecvFilesMain();

    // 如果clienttype==2,调用下载文件的主函数。
    if (starg.clienttype == 2)
      SendFilesMain();

上传模块开发

服务端客户端协同开发

1.首先是登录,进行参数传递。
客户端连接至服务端,进行信息传递,以及参数传递。

// 登录。
bool ClientLogin()
{
  memset(strrecvbuffer, 0, sizeof(strrecvbuffer));
  memset(strsendbuffer, 0, sizeof(strsendbuffer));

  if (TcpServer.Read(strrecvbuffer, 20) == false)
  {
    logfile.Write("TcpServer.Read() failed.\n");
    return false;
  }
  logfile.Write("strrecvbuffer=%s\n", strrecvbuffer);

  // 解析客户端登录报文。
  _xmltoarg(strrecvbuffer);

  if ((starg.clienttype != 1) && (starg.clienttype != 2))
    strcpy(strsendbuffer, "failed");
  else
    strcpy(strsendbuffer, "ok");

  if (TcpServer.Write(strsendbuffer) == false)
  {
    logfile.Write("TcpServer.Write() failed.\n");
    return false;
  }

  logfile.Write("%s login %s.\n", TcpServer.GetIP(), strsendbuffer);

  return true;
}

// 把xml解析到参数starg结构中
bool _xmltoarg(char *strxmlbuffer)
{
  memset(&starg, 0, sizeof(struct st_arg));

  // 不需要对参数做合法性判断,客户端已经判断过了。
  GetXMLBuffer(strxmlbuffer, "clienttype", &starg.clienttype);
  GetXMLBuffer(strxmlbuffer, "ptype", &starg.ptype);
  GetXMLBuffer(strxmlbuffer, "clientpath", starg.clientpath);
  GetXMLBuffer(strxmlbuffer, "andchild", &starg.andchild);
  GetXMLBuffer(strxmlbuffer, "matchname", starg.matchname);
  GetXMLBuffer(strxmlbuffer, "srvpath", starg.srvpath);
  GetXMLBuffer(strxmlbuffer, "srvpathbak", starg.srvpathbak);

  GetXMLBuffer(strxmlbuffer, "timetvl", &starg.timetvl);
  if (starg.timetvl > 30)
    starg.timetvl = 30;

  GetXMLBuffer(strxmlbuffer, "timeout", &starg.timeout);
  if (starg.timeout < 50)
    starg.timeout = 50;

  GetXMLBuffer(strxmlbuffer, "pname", starg.pname, 50);
  strcat(starg.pname, "_srv");

  return true;
}

2.这里就是客户端的登录报文传送

    // 登录业务。
    if (Login(argv[2]) == false)
    {
        logfile.Write("Login() failed.\n");
        EXIT(-1);
    }

正式协同开发

先来想一下主要的逻辑:
1.客户端每隔一段时间就调用一次发送文件的流程

服务端接受文件业务

1.将进程写入到共享内存中。
2.客户端上传业务开始传送文件
每隔循环完成上传文件的业务。

while (true)
    {
        // 调用文件上传的主函数,执行一次文件上传的任务。

        printf("44444\n");

        if (_tcpputfiles() == false)
        {
            logfile.Write("_tcpputfiles() failed.\n");
            EXIT(-1);
        }

        if (bcontinue == false)
        {
            sleep(starg.timetvl);

            if (ActiveTest() == false)
                break;
        }

        PActive.UptATime();
    }

2.读取来自客户端的文件流。

// 上传文件的主函数。
void RecvFilesMain()
{
  PActive.AddPInfo(starg.timeout, starg.pname);

  while (true)
  {
    memset(strsendbuffer, 0, sizeof(strsendbuffer));
    memset(strrecvbuffer, 0, sizeof(strrecvbuffer));

    PActive.UptATime();

    // 接收客户端的报文。
    // 第二个参数的取值必须大于starg.timetvl,小于starg.timeout。
    if (TcpServer.Read(strrecvbuffer, starg.timetvl + 10) == false)
    {
      logfile.Write("TcpServer.Read() failed.\n");
      return;
    }
    // logfile.Write("strrecvbuffer=%s\n",strrecvbuffer);

    // 处理心跳报文。
    if (strcmp(strrecvbuffer, "<activetest>ok</activetest>") == 0)
    {
      strcpy(strsendbuffer, "ok");
      // logfile.Write("strsendbuffer=%s\n",strsendbuffer);
      if (TcpServer.Write(strsendbuffer) == false)
      {
        logfile.Write("TcpServer.Write() failed.\n");
        return;
      }
    }

    // 处理上传文件的请求报文。
    if (strncmp(strrecvbuffer, "<filename>", 10) == 0)
    {
      // 解析上传文件请求报文的xml。
      char clientfilename[301];
      memset(clientfilename, 0, sizeof(clientfilename));
      char mtime[21];
      memset(mtime, 0, sizeof(mtime));
      int filesize = 0;
      GetXMLBuffer(strrecvbuffer, "filename", clientfilename, 300);
      GetXMLBuffer(strrecvbuffer, "mtime", mtime, 19);
      GetXMLBuffer(strrecvbuffer, "size", &filesize);

      // 客户端和服务端文件的目录是不一样的,以下代码生成服务端的文件名。
      // 把文件名中的clientpath替换成srvpath,要小心第三个参数
      char serverfilename[301];
      memset(serverfilename, 0, sizeof(serverfilename));
      strcpy(serverfilename, clientfilename);
      UpdateStr(serverfilename, starg.clientpath, starg.srvpath, false);

      // 接收文件的内容。
      logfile.Write("recv %s(%d) ...", serverfilename, filesize);
      if (RecvFile(TcpServer.m_connfd, serverfilename, mtime, filesize) == true)
      {
        logfile.WriteEx("ok.\n");
        SNPRINTF(strsendbuffer, sizeof(strsendbuffer), 1000, "<filename>%s</filename><result>ok</result>", clientfilename);
      }
      else
      {
        logfile.WriteEx("failed.\n");
        SNPRINTF(strsendbuffer, sizeof(strsendbuffer), 1000, "<filename>%s</filename><result>failed</result>", clientfilename);
      }

      // 把接收结果返回给对端。
      // logfile.Write("strsendbuffer=%s\n",strsendbuffer);
      if (TcpServer.Write(strsendbuffer) == false)
      {
        logfile.Write("TcpServer.Write() failed.\n");
        return;
      }
    }
  }
}

MySQL开发

加粗样式数据库连接类和句柄类:
通过这些类,可以对数据库加以控制。

// MySQL登录环境
struct LOGINENV
{
  char ip[32];       // MySQL数据库的ip地址。
  int  port;         // MySQL数据库的通信端口。
  char user[32];     // 登录MySQL数据库的用户名。
  char pass[32];     // 登录MySQL数据库的密码。
  char dbname[51];   // 登录后,缺省打开的数据库。
};
struct CDA_DEF         // 每次调用MySQL接口函数返回的结果。
{
  int      rc;         // 返回值:0-成功;其它是失败,存放了MySQL的错误代码。
  unsigned long rpc;   // 如果是insert、update和delete,存放影响记录的行数,如果是select,存放结果集的行数。
  char     message[2048]; // 如果返回失败,存放错误描述信息。
};
// MySQL数据库连接类。
class connection
{
private:
  // 从connstr中解析ip,username,password,dbname,port。
  void setdbopt(const char *connstr);

  // 设置字符集,要与数据库的一致,否则中文会出现乱码。
  void character(const char *charset);

  LOGINENV m_env;      // 服务器环境句柄。

  char m_dbtype[21];   // 数据库种类,固定取值为"mysql"。
public:
  int m_state;         // 与数据库的连接状态,0-未连接,1-已连接。

  CDA_DEF m_cda;       // 数据库操作的结果或最后一次执行SQL语句的结果。

  char m_sql[10241];   // SQL语句的文本,最长不能超过10240字节。

  connection();        // 构造函数。
 ~connection();        // 析构函数。

  // 登录数据库。
  // connstr:数据库的登录参数,格式:"ip,username,password,dbname,port",
  // 例如:"172.16.0.15,qxidc,qxidcpwd,qxidcdb,3306"。
  // charset:数据库的字符集,如"utf8"、"gbk",必须与数据库保持一致,否则会出现中文乱码的情况。
  // autocommitopt:是否启用自动提交,0-不启用,1-启用,缺省是不启用。
  // 返回值:0-成功,其它失败,失败的代码在m_cda.rc中,失败的描述在m_cda.message中。
  int connecttodb(const char *connstr,const char *charset,unsigned int autocommitopt=0);

  // 提交事务。
  // 返回值:0-成功,其它失败,程序中一般不必关心返回值。
  int commit();

  // 回滚事务。
  // 返回值:0-成功,其它失败,程序中一般不必关心返回值。
  int  rollback();

  // 断开与数据库的连接。
  // 注意,断开与数据库的连接时,全部未提交的事务自动回滚。
  // 返回值:0-成功,其它失败,程序中一般不必关心返回值。
  int disconnect();

  // 执行SQL语句。
  // 如果SQL语句不需要绑定输入和输出变量(无绑定变量、非查询语句),可以直接用此方法执行。
  // 参数说明:这是一个可变参数,用法与printf函数相同。
  // 返回值:0-成功,其它失败,失败的代码在m_cda.rc中,失败的描述在m_cda.message中,
  // 如果成功的执行了非查询语句,在m_cda.rpc中保存了本次执行SQL影响记录的行数。
  // 程序中必须检查execute方法的返回值。
  // 在connection类中提供了execute方法,是为了方便程序员,在该方法中,也是用sqlstatement类来完成功能。
  int execute(const char *fmt,...);

  
  // 以下成员变量和函数,除了sqlstatement类,在类的外部不需要调用它。
  MYSQL     *m_conn;   // MySQL数据库连接句柄。
  int m_autocommitopt; // 自动提交标志,0-关闭自动提交;1-开启自动提交。
  void err_report();   // 获取错误信息。
  
};

// 执行SQL语句前绑定输入或输出变量个数的最大值,256是很大的了,可以根据实际情况调整。
#define MAXPARAMS  256

// 操作SQL语句类。
class sqlstatement
{
private:
  MYSQL_STMT *m_handle; // SQL语句句柄。
  
  MYSQL_BIND params_in[MAXPARAMS];            // 输入参数。
  unsigned long params_in_length[MAXPARAMS];  // 输入参数的实际长度。
  my_bool params_in_is_null[MAXPARAMS];       // 输入参数是否为空。
  unsigned maxbindin;                         // 输入参数最大的编号。

  MYSQL_BIND params_out[MAXPARAMS]; // 输出参数。

  CDA_DEF m_cda1;      // prepare() SQL语句的结果。
  
  connection *m_conn;  // 数据库连接指针。
  int m_sqltype;       // SQL语句的类型,0-查询语句;1-非查询语句。
  int m_autocommitopt; // 自动提交标志,0-关闭;1-开启。
  void err_report();   // 错误报告。
  void initial();      // 初始化成员变量。
public:
  int m_state;         // 与数据库连接的绑定状态,0-未绑定,1-已绑定。

  char m_sql[10241];   // SQL语句的文本,最长不能超过10240字节。

  CDA_DEF m_cda;       // 执行SQL语句的结果。

  sqlstatement();      // 构造函数。
  sqlstatement(connection *conn);    // 构造函数,同时绑定数据库连接。

 ~sqlstatement();      // 析构函数。

  // 绑定数据库连接。
  // conn:数据库连接connection对象的地址。
  // 返回值:0-成功,其它失败,只要conn参数是有效的,并且数据库的游标资源足够,connect方法不会返回失败。
  // 程序中一般不必关心connect方法的返回值。
  // 注意,每个sqlstatement只需要绑定一次,在绑定新的connection前,必须先调用disconnect方法。
  int connect(connection *conn);

  // 取消与数据库连接的绑定。
  // 返回值:0-成功,其它失败,程序中一般不必关心返回值。
  int disconnect();

  // 准备SQL语句。
  // 参数说明:这是一个可变参数,用法与printf函数相同。
  // 返回值:0-成功,其它失败,程序中一般不必关心返回值。
  // 注意:如果SQL语句没有改变,只需要prepare一次就可以了。
  int prepare(const char *fmt,...);

  // 绑定输入变量的地址。
  // position:字段的顺序,从1开始,必须与prepare方法中的SQL的序号一一对应。
  // value:输入变量的地址,如果是字符串,内存大小应该是表对应的字段长度加1。
  // len:如果输入变量的数据类型是字符串,用len指定它的最大长度,建议采用表对应的字段长度。
  // 返回值:0-成功,其它失败,程序中一般不必关心返回值。
  // 注意:1)如果SQL语句没有改变,只需要bindin一次就可以了,2)绑定输入变量的总数不能超过MAXPARAMS个。
  int bindin(unsigned int position,int    *value);
  int bindin(unsigned int position,long   *value);
  int bindin(unsigned int position,unsigned int  *value);
  int bindin(unsigned int position,unsigned long *value);
  int bindin(unsigned int position,float *value);
  int bindin(unsigned int position,double *value);
  int bindin(unsigned int position,char   *value,unsigned int len);
  // 绑定BLOB字段,buffer为BLOB字段的内容,size为BLOB字段的大小。
  int bindinlob(unsigned int position,void *buffer,unsigned long *size);

  // 把结果集的字段与变量的地址绑定。
  // position:字段的顺序,从1开始,与SQL的结果集字段一一对应。
  // value:输出变量的地址,如果是字符串,内存大小应该是表对应的字段长度加1。
  // len:如果输出变量的数据类型是字符串,用len指定它的最大长度,建议采用表对应的字段长度。
  // 返回值:0-成功,其它失败,程序中一般不必关心返回值。
  // 注意:1)如果SQL语句没有改变,只需要bindout一次就可以了,2)绑定输出变量的总数不能超过MAXPARAMS个。
  int bindout(unsigned int position,int    *value);
  int bindout(unsigned int position,long   *value);
  int bindout(unsigned int position,unsigned int  *value);
  int bindout(unsigned int position,unsigned long *value);
  int bindout(unsigned int position,float *value);
  int bindout(unsigned int position,double *value);
  int bindout(unsigned int position,char   *value,unsigned int len);
  // 绑定BLOB字段,buffer用于存放BLOB字段的内容,buffersize为buffer占用内存的大小,
  // size为结果集中BLOB字段实际的大小,注意,一定要保证buffer足够大,防止内存溢出。
  int bindoutlob(unsigned int position,void *buffer,unsigned long buffersize,unsigned long *size);

  // 执行SQL语句。
  // 返回值:0-成功,其它失败,失败的代码在m_cda.rc中,失败的描述在m_cda.message中。
  // 如果成功的执行了insert、update和delete语句,在m_cda.rpc中保存了本次执行SQL影响记录的行数。
  // 程序中必须检查execute方法的返回值。
  int execute();

  // 执行SQL语句。
  // 如果SQL语句不需要绑定输入和输出变量(无绑定变量、非查询语句),可以直接用此方法执行。
  // 参数说明:这是一个可变参数,用法与printf函数相同。
  // 返回值:0-成功,其它失败,失败的代码在m_cda.rc中,失败的描述在m_cda.message中,
  // 如果成功的执行了非查询语句,在m_cda.rpc中保存了本次执行SQL影响记录的行数。
  // 程序中必须检查execute方法的返回值。
  int execute(const char *fmt,...);

  // 从结果集中获取一条记录。
  // 如果执行的SQL语句是查询语句,调用execute方法后,会产生一个结果集(存放在数据库的缓冲区中)。
  // next方法从结果集中获取一条记录,把字段的值放入已绑定的输出变量中。
  // 返回值:0-成功,1403-结果集已无记录,其它-失败,失败的代码在m_cda.rc中,失败的描述在m_cda.message中。
  // 返回失败的原因主要有两种:1)与数据库的连接已断开;2)绑定输出变量的内存太小。
  // 每执行一次next方法,m_cda.rpc的值加1。
  // 程序中必须检查next方法的返回值。
  int next();
};

站点参数文件入库

这是站点参数的表,由于mysql会自动类型转换,所以这里的obtid都是采用的varchar类型。有一个自增字段keyid.作为唯一键。

drop table if exists T_ZHOBTCODE;

/*==============================================================*/
/* Table: T_ZHOBTCODE                                           */
/*==============================================================*/
create table T_ZHOBTCODE
(
   obtid                varchar(10) not null comment '站点代码',
   cityname             varchar(30) not null comment '城市名称',
   provname             varchar(30) not null comment '省名称',
   lat                  int not null comment '纬度,单位:0.01度。',
   lon                  int not null comment '经度,单位:0.01度。',
   height               int comment '海拔高度,单位:0.1米。',
   upttime              timestamp not null comment '更新时间,数据被插入或更新的时间。',
   keyid                int not null auto_increment comment '记录编号,自动增长列。',
   primary key (obtid),
   unique key ZHOBTCODE_KEYID (keyid)
);

alter table T_ZHOBTCODE comment '这是一个参数表,存放了全国的站点参数,约800条记录,本表的数据极少变更。
应用程序对本表有inser和up';

这是每一条数据的存储结构体,将每一条数据存储到数据库中,采用这样的内存对象。

// 全国气象站点参数结构体。
struct st_stcode
{
  char provname[31]; // 省
  char obtid[11];    // 站号
  char cityname[31];  // 站名
  char lat[11];      // 纬度
  char lon[11];      // 经度
  char height[11];   // 海拔高度
};

由于是站点参数数据,记录的是各个气象站的那种地点信息,并不是气象数据,所以变更很少,但是依然要有变换的可能
为什么要这样呢?
因为插入一条数据的主体结构是一样的,都是"insert into T_ZHOBTCODE(obtid,cityname,provname,lat,lon,height,upttime) values(:1,:2,:3,:4100,:5100,:6*10,now())",变化的只有后面具体的值,但是这个主题是没有变化的,所以只要对值进行更新就可以了。
然后由于每条数据存在高度统一性,干脆直接用一个数据对象不断地变化,然后将那个数据对象绑定到这条语句,然后只要不断地变化那个数据对象,就可以往里面插入不同的数据了。

/**
 * @brief 
 * 
 * ----------------SQL语句(高度相似),需要数据----------------------
 *                     |
 *                     |依赖于(绑定)
 *                     |
 * -------------------数据对象---------------------
 *                     |
 *                     |操作数据对象内容
 *                     |
 * ----------------从而形成不一样的SQL语句,减少SQL编写
 * 
 */

以上的过程懂了吧,因此有一个过程:1.准备SQL句柄对象。2.prepareSQL语句,也就是形成带有依赖对象的SQL语句,现在就差将数据对象绑定过去。3.绑定数据对象,变化数据对象内容,从而实现不同的SQL语句,但是高度相似。
prepare 就是准备这样的一条string,带有依赖对象的sql源语,绑定,就是为那些经常变的值绑定数据对象。

// 准备插入表的SQL语句。
  sqlstatement stmtins(&conn);
  stmtins.prepare("insert into T_ZHOBTCODE(obtid,cityname,provname,lat,lon,height,upttime) values(:1,:2,:3,:4*100,:5*100,:6*10,now())");
  stmtins.bindin(1,stcode.obtid,10);
  stmtins.bindin(2,stcode.cityname,30);
  stmtins.bindin(3,stcode.provname,30);
  stmtins.bindin(4,stcode.lat,10);
  stmtins.bindin(5,stcode.lon,10);
  stmtins.bindin(6,stcode.height,10);

注意,一条语句是会依赖多个可变的数据对象,这里将他们合成为一个结构,是为了清晰,后面就不会将他们合成一个结构,而是分离为不同的数据对象,但是可能聚合在一个二维数组中。
不论增么样:SQL语句的高度相似,所以,不必每次都去编写SQL,而是将不同的地方抽离出来,所用数据对象进行绑定,每次之变换变化的而地方,从而减少操作成本。

这里执行入库,取出一条数据,覆盖SQL语句绑定的对象空间,然后执行。

// 遍历vstcode容器。
  for (int ii=0;ii<vstcode.size();ii++)
  {
    // 从容器中取出一条记录到结构体stcode中。
    memcpy(&stcode,&vstcode[ii],sizeof(struct st_stcode));

    // 执行插入的SQL语句。
    if (stmtins.execute()!=0)
    {
      if (stmtins.m_cda.rc==1062)
      {
        // 如果记录已存在,执行更新的SQL语句。
        if (stmtupt.execute()!=0) 
        {
          logfile.Write("stmtupt.execute() failed.\n%s\n%s\n",stmtupt.m_sql,stmtupt.m_cda.message); return -1;
        }
        else
          uptcount++;
      }
      else
      {
        // 抄这行代码的时候也要小心,经常有人在这里栽跟斗。
        logfile.Write("stmtins.execute() failed.\n%s\n%s\n",stmtins.m_sql,stmtins.m_cda.message); return -1;
      }
    }
    else
      inscount++;
  }

站点数据入库

此时是站点数据入库,这个就很有意思了。
主键:联合主键,obtid和的datetime作为联合主键,即可以有相同地点,不一样时间的数据,但是不可以有两者同时的数据,这是合情合理的。
索引1:唯一索引:联合索引:obtid和的datetime
索引2:时间作为索引,有多条数据
索引3:点代码作为索引,与多个不同时间的同一地点的数据
外键约束:即受制于站点参数表中obtid的外键约束。

这里不像上面的站点参数文件,只有一个文件,这里的文件有很多个,不同时间段的文件,所以对于每一个文件要上传到数据库中。
因此是有,统一地名的,但是是不同时间段的数据记录也都是放在一起。

drop index IDX_ZHOBTMIND_3 on T_ZHOBTMIND;

drop index IDX_ZHOBTMIND_2 on T_ZHOBTMIND;

drop index IDX_ZHOBTMIND_1 on T_ZHOBTMIND;

drop table if exists T_ZHOBTMIND;

/*==============================================================*/
/* Table: T_ZHOBTMIND                                           */
/*==============================================================*/
create table T_ZHOBTMIND
(
   obtid                varchar(10) not null comment '站点代码。',
   ddatetime            datetime not null comment '数据时间,精确到分钟。',
   t                    int comment '湿度,单位:0.1摄氏度。',
   p                    int comment '气压,单位:0.1百帕。',
   u                    int comment '相对湿度,0-100之间的值。',
   wd                   int comment '风向,0-360之间的值。',
   wf                   int comment '风速:单位0.1m/s。',
   r                    int comment '降雨量:0.1mm。',
   vis                  int comment '能见度:0.1米。',
   upttime              timestamp not null comment '更新时间。',
   keyid                bigint not null auto_increment comment '记录编号,自动增长列。',
   primary key (obtid, ddatetime),
   unique key ZHOBTMIND_KEYID (keyid)
);

alter table T_ZHOBTMIND comment '本表存放了全国站点分钟数据,站点数约840个,数据周期为1分钟。
应用程序对本表只有insert操作,没有u';

/*==============================================================*/
/* Index: IDX_ZHOBTMIND_1                                       */
/*==============================================================*/
create unique index IDX_ZHOBTMIND_1 on T_ZHOBTMIND
(
   ddatetime,
   obtid
);

/*==============================================================*/
/* Index: IDX_ZHOBTMIND_2                                       */
/*==============================================================*/
create index IDX_ZHOBTMIND_2 on T_ZHOBTMIND
(
   ddatetime
);

/*==============================================================*/
/* Index: IDX_ZHOBTMIND_3                                       */
/*==============================================================*/
create index IDX_ZHOBTMIND_3 on T_ZHOBTMIND
(
   obtid
);

alter table T_ZHOBTMIND add constraint FK_ZHOBTCODE_ZHOBTMIND foreign key (obtid)
      references T_ZHOBTCODE (obtid) on delete cascade on update cascade;

写一个专门用于上传站点数据上传的类,这个类不具备普适性,只适用于上传站点数据代码,如果有其他类型的要自己写。
你数据库的那个类型不一定要与现在这里的一致,因为是可以实现自动转换的,Mysql是可以自动从字符数据转换成自己的类型的值,你不用担心,只要将数据插入进去就可以,插入是字符串,也会被自动转换。
专门用于站点数据的一条数据的上传,你的逻辑是,读取文件的一条数据,传给这个类,这个类会自动对你的这条数据进行解析,并将当前的数据更改到其中用于SQL的绑定的对象空间中,从而实现一条数据的上传。

#ifndef IDCAPP_H
#define IDCAPP_H

#include "_public.h"
#include "_mysql.h"

struct st_zhobtmind
{
  char obtid[11];      // 站点代码。
  char ddatetime[21];  // 数据时间,精确到分钟。
  char t[11];          // 温度,单位:0.1摄氏度。
  char p[11];          // 气压,单位:0.1百帕。
  char u[11];          // 相对湿度,0-100之间的值。
  char wd[11];         // 风向,0-360之间的值。
  char wf[11];         // 风速:单位0.1m/s。
  char r[11];          // 降雨量:0.1mm。
  char vis[11];        // 能见度:0.1米。
};

// 全国站点分钟观测数据操作类。
class CZHOBTMIND
{
public:
  connection  *m_conn;     // 数据库连接。
  CLogFile    *m_logfile;  // 日志。

  sqlstatement m_stmt;     // 插入表操作的sql。

  char m_buffer[1024];   // 从文件中读到的一行。
  struct st_zhobtmind m_zhobtmind; // 全国站点分钟观测数据结构。

  CZHOBTMIND();
  CZHOBTMIND(connection *conn,CLogFile *logfile);

 ~CZHOBTMIND();

  void BindConnLog(connection *conn,CLogFile *logfile);  // 把connection和CLogFile的传进去。
  bool SplitBuffer(char *strBuffer,bool bisxml);  // 把从文件读到的一行数据拆分到m_zhobtmind结构体中。
  bool InsertTable();  // 把m_zhobtmind结构体中的数据插入到T_ZHOBTMIND表中。
};

#endif
#include "idcapp.h"


CZHOBTMIND::CZHOBTMIND()
{
  m_conn=0;  m_logfile=0;
}

CZHOBTMIND::CZHOBTMIND(connection *conn,CLogFile *logfile)
{
  m_conn=conn;
  m_logfile=logfile;
}

CZHOBTMIND::~CZHOBTMIND()
{
}

void CZHOBTMIND::BindConnLog(connection *conn,CLogFile *logfile)
{
  m_conn=conn;
  m_logfile=logfile;
}

// 把从文件读到的一行数据拆分到m_zhobtmind结构体中。
bool CZHOBTMIND::SplitBuffer(char *strBuffer,bool bisxml)
{
  memset(&m_zhobtmind,0,sizeof(struct st_zhobtmind));
 
  if (bisxml==true)
  {
    GetXMLBuffer(strBuffer,"obtid",m_zhobtmind.obtid,10);
    GetXMLBuffer(strBuffer,"ddatetime",m_zhobtmind.ddatetime,14);
    char tmp[11];
    GetXMLBuffer(strBuffer,"t",tmp,10);   if (strlen(tmp)>0) snprintf(m_zhobtmind.t,10,"%d",(int)(atof(tmp)*10));
    GetXMLBuffer(strBuffer,"p",tmp,10);   if (strlen(tmp)>0) snprintf(m_zhobtmind.p,10,"%d",(int)(atof(tmp)*10));
    GetXMLBuffer(strBuffer,"u",m_zhobtmind.u,10);
    GetXMLBuffer(strBuffer,"wd",m_zhobtmind.wd,10);
    GetXMLBuffer(strBuffer,"wf",tmp,10);  if (strlen(tmp)>0) snprintf(m_zhobtmind.wf,10,"%d",(int)(atof(tmp)*10));
    GetXMLBuffer(strBuffer,"r",tmp,10);   if (strlen(tmp)>0) snprintf(m_zhobtmind.r,10,"%d",(int)(atof(tmp)*10));
    GetXMLBuffer(strBuffer,"vis",tmp,10); if (strlen(tmp)>0) snprintf(m_zhobtmind.vis,10,"%d",(int)(atof(tmp)*10));
  }
  else
  {
    CCmdStr CmdStr;
    CmdStr.SplitToCmd(strBuffer,",");
    CmdStr.GetValue(0,m_zhobtmind.obtid,10);
    CmdStr.GetValue(1,m_zhobtmind.ddatetime,14);
    char tmp[11];
    CmdStr.GetValue(2,tmp,10); if (strlen(tmp)>0) snprintf(m_zhobtmind.t,10,"%d",(int)(atof(tmp)*10));
    CmdStr.GetValue(3,tmp,10); if (strlen(tmp)>0) snprintf(m_zhobtmind.p,10,"%d",(int)(atof(tmp)*10));
    CmdStr.GetValue(4,m_zhobtmind.u,10);
    CmdStr.GetValue(5,m_zhobtmind.wd,10);
    CmdStr.GetValue(6,tmp,10); if (strlen(tmp)>0) snprintf(m_zhobtmind.wf,10,"%d",(int)(atof(tmp)*10));
    CmdStr.GetValue(7,tmp,10); if (strlen(tmp)>0) snprintf(m_zhobtmind.r,10,"%d",(int)(atof(tmp)*10));
    CmdStr.GetValue(8,tmp,10); if (strlen(tmp)>0) snprintf(m_zhobtmind.vis,10,"%d",(int)(atof(tmp)*10));
  }

  STRCPY(m_buffer,sizeof(m_buffer),strBuffer);

  return true;
}

// 把m_zhobtmind结构体中的数据插入到T_ZHOBTMIND表中。
bool CZHOBTMIND::InsertTable()
{
  if (m_stmt.m_state==0)
  {
    m_stmt.connect(m_conn);
    m_stmt.prepare("insert into T_ZHOBTMIND(obtid,ddatetime,t,p,u,wd,wf,r,vis) values(:1,str_to_date(:2,'%%Y%%m%%d%%H%%i%%s'),:3,:4,:5,:6,:7,:8,:9)");
    m_stmt.bindin(1,m_zhobtmind.obtid,10);
    m_stmt.bindin(2,m_zhobtmind.ddatetime,14);
    m_stmt.bindin(3,m_zhobtmind.t,10);
    m_stmt.bindin(4,m_zhobtmind.p,10);
    m_stmt.bindin(5,m_zhobtmind.u,10);
    m_stmt.bindin(6,m_zhobtmind.wd,10);
    m_stmt.bindin(7,m_zhobtmind.wf,10);
    m_stmt.bindin(8,m_zhobtmind.r,10);
    m_stmt.bindin(9,m_zhobtmind.vis,10);
  }

  // 把结构体中的数据插入表中。
  if (m_stmt.execute()!=0)
  {
    // 1、失败的情况有哪些?是否全部的失败都要写日志?
    // 答:失败的原因主要有二:一是记录重复,二是数据内容非法。
    // 2、如果失败了怎么办?程序是否需要继续?是否rollback?是否返回false?
    // 答:如果失败的原因是数据内容非法,记录日志后继续;如果是记录重复,不必记录日志,且继续。
    if (m_stmt.m_cda.rc!=1062)
    {
      m_logfile->Write("Buffer=%s\n",m_buffer);
      m_logfile->Write("m_stmt.execute() failed.\n%s\n%s\n",m_stmt.m_sql,m_stmt.m_cda.message);
    }

    return false;
  }

  return true;
}

打开每一个文件进行数据上传,打开每一个文件,在每一个文件中,循环读取一条数据,将数据给专门用于上传站点数据的类对象,让他进行上传,那个类会对你的数据解析,将数据更改到SQL绑定的对象空间中,最终通过他上传到数据库中。

while (true)
  {
    // 读取目录,得到一个数据文件名。
    if (Dir.ReadDir() == false)
      break;

    if (MatchStr(Dir.m_FullFileName, "*.xml") == true)
      bisxml = true;
    else
      bisxml = false;

    // 连接数据库。
    if (conn.m_state == 0)
    {
      if (conn.connecttodb(connstr, charset) != 0)
      {
        logfile.Write("connect database(%s) failed.\n%s\n", connstr, conn.m_cda.message);
        return -1;
      }

      logfile.Write("connect database(%s) ok.\n", connstr);
    }

    totalcount = insertcount = 0;

    // 打开文件。
    if (File.Open(Dir.m_FullFileName, "r") == false)
    {
      logfile.Write("File.Open(%s) failed.\n", Dir.m_FullFileName);
      return false;
    }

    char strBuffer[1001]; // 存放从文件中读取的一行。

    while (true)
    {
      if (bisxml == true)
      {
        if (File.FFGETS(strBuffer, 1000, "<endl/>") == false)
          break;
      }
      else
      {
        if (File.Fgets(strBuffer, 1000, true) == false)
          break;
        if (strstr(strBuffer, "站点") != 0)
          continue; // 把csv文件中的第一行扔掉。
      }

      // 处理文件中的每一行。
      totalcount++;

      ZHOBTMIND.SplitBuffer(strBuffer, bisxml);

      if (ZHOBTMIND.InsertTable() == true)
        insertcount++;
    }

    // 删除文件、提交事务。
    File.CloseAndRemove();

    conn.commit();

    logfile.Write("已处理文件%s(totalcount=%d,insertcount=%d),耗时%.2f秒。\n", Dir.m_FullFileName, totalcount, insertcount, Timer.Elapsed());
  }

执行SQL脚本文件,进行数据清理

进入脚本文件,读取一个SQL脚本,执行。循环,知道执行完所有的脚本文件,这里主要包括是清理数据库太久远的数据。

while (true)
  {
    memset(strsql, 0, sizeof(strsql));

    // 从SQL文件中读取以分号结束的一行。
    if (File.FFGETS(strsql, 1000, ";") == false)
      break;

    // 如果第一个字符是#,注释,不执行。
    if (strsql[0] == '#')
      continue;

    // 删除掉SQL语句最后的分号。
    char *pp = strstr(strsql, ";");
    if (pp == 0)
      continue;
    pp[0] = 0;

    logfile.Write("%s\n", strsql);

    int iret = conn.execute(strsql); // 执行SQL语句。


    // 把SQL语句执行结果写日志。
    if (iret == 0)
    {
      logfile.Write("exec ok(rpc=%d).\n", conn.m_cda.rpc);
    }
    else
    {
      printf("Hello2\n");
      logfile.Write("exec failed(%s).\n", conn.m_cda.message);
    }
    PActive.UptATime(); // 进程的心跳。
  }

数据抽取子系统

现在所有的数据都已经加入到主库中了,那么现在要干啥,其实就是对主库中的数据进行抽取,抽取成为固定的CSV文件还是Json文件还是xml文件都是可以的。
本质上是:查询结果,将结果存放到文件中,这叫数据抽取。
有两个参数不是很懂:fieldstr,fieldlen.
这两个参数,是写的是结果集字段名以及结果集字段的长度,为什么要这样?首先每个数据库的都是不一样的,我们要尽可能的去提高系统的代码复用性。所以与业务相关的东西应该是越早提出来越好,所以,这里直接将与业务相关的字段名以及字段长度直接提出来。
这样在里面写的时候就不要去担心具体的业务,而是只关心逻辑,提高程序复用性。

// 程序运行参数的结构体。
struct st_arg
{
  char connstr[101];     // 数据库的连接参数。
  char charset[51];      // 数据库的字符集。
  char selectsql[1024];  // 从数据源数据库抽取数据的SQL语句。
  char fieldstr[501];    // 抽取数据的SQL语句输出结果集字段名,字段名之间用逗号分隔。
  char fieldlen[501];    // 抽取数据的SQL语句输出结果集字段的长度,用逗号分隔。
  char bfilename[31];    // 输出xml文件的前缀。
  char efilename[31];    // 输出xml文件的后缀。
  char outpath[301];     // 输出xml文件存放的目录。
  int maxcount;          // 输出xml文件最大记录数,0表示无限制。
  char starttime[52];    // 程序运行的时间区间
  char incfield[31];     // 递增字段名。
  char incfilename[301]; // 已抽取数据的递增字段最大值存放的文件。
  char connstr1[101];    // 已抽取数据的递增字段最大值存放的数据库的连接参数。
  int timeout;           // 进程心跳的超时时间。
  char pname[51];        // 进程名,建议用"dminingmysql_后缀"的方式。
} starg;

以上参数是针对于一个“数据抽取子系统所需要的东西”,提供结果集字段名,是因为要形成具体的文件,而这部风是业务相关的,直接变成参数。

解析参数,由抽象业务变成具体业务

这部分主要是是对参数进行解析,并将参数解析到参数结构体中,供以下逻辑使用。我们已经尽最大可能的去将与业务相关的参数提取出来。
在这里也是要判断是不是增量型抽取业务
通过以下几个步骤就可以将数据从数据库中抽离出来形成文件。
1.首先是不是全量抽取数据,是由我们传进去的参数里有没有自增字段,如果有,代表增量抽取,如果没有,那就全量抽取。
2.读取增量的最大值,进行绑定。
3.执行语句
4.从结果集中把每条语句拿出来存储,形成指定文件名,并进行存储。
5.更新最大的自增字段,如果是增量抽取。

数据抽取模块

记住这里绑定输出参数对象结果集,并没有使用结构的形式,而是使用数组的形式来对结果集各个字段的值进行存储。
1.从文件或者数据库中得到自增字段的值。绑定输出结果对象还有输入字段对象

/**
 * -----------------------------------------
 *      这是一个数组,是一个二维数组
 *      每一个维度是一个字段的存储对象空间
 *      并不是通过结构来绑定空间,但时候也是可以通过结构来绑定
 *      使用结构要提前知道你有多少的字段,当然也是可以的
 *      使用数组直接绑定对象空间也是可以的,实质上就是一块内存罢了。
 *      i=0
 *      ---------------
 *      |--------------|      字段1
 *      i=1
 *      ---------------
 *      |--------------|      字段2
 *      i=2
 *      ---------------
 *      |--------------|      字段3
 *      i=3
 *      ---------------
 *      |--------------|      字段4
 * 
 *
 *       i=ifieldcount
 *      ---------------
 *      |--------------|      字段4
 * 
 * -----------------------------------------
 */
  sqlstatement stmt(&conn);
  stmt.prepare(starg.selectsql);

  //存放查询结果集数据结果的对象集合
  char strfieldvalue[ifieldcount][MAXFIELDLEN + 1]; // 抽取数据的SQL执行后,存放结果集字段值的数组。
  for (int ii = 1; ii <= ifieldcount; ii++)
  {
    stmt.bindout(ii, strfieldvalue[ii - 1], ifieldlen[ii - 1]);
  }

  // 如果是增量抽取,绑定输入参数(已抽取数据的最大id)。
  if (strlen(starg.incfield) != 0)
    stmt.bindin(1, &imaxincvalue);

  if (stmt.execute() != 0)
  {
    logfile.Write("stmt.execute() failed.\n%s\n%s\n", stmt.m_sql, stmt.m_cda.message);
    return false;
  }

2.将结果集每一个结果都给读取写入到文件中
这里注意统计当前存放的数据有没有超过一个文件能够存储的最大值,如果超过了,那就关闭文件,然后重新开启一个文件,然后继续存放接下来的东西。

/**
 * -----------------------------------------
 *      这是一个数组,是一个二维数组
 *      每一个维度是一个字段的存储对象空间
 *      并不是通过结构来绑定空间,但时候也是可以通过结构来绑定
 *      使用结构要提前知道你有多少的字段,当然也是可以的
 *      使用数组直接绑定对象空间也是可以的,实质上就是一块内存罢了。
 * 
 *      i=0
 *      ---------------
 *      |--------------|      字段1
 *      i=1
 *      ---------------
 *      |--------------|      字段2
 *      i=2
 *      ---------------
 *      |--------------|      字段3
 *      i=3
 *      ---------------
 *      |--------------|      字段4
 * 
 * 
 * -----------------------------------------
 */
  while (true)
  {
    memset(strfieldvalue, 0, sizeof(strfieldvalue));

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

    if (File.IsOpened() == false)
    {
      crtxmlfilename(); // 生成xml文件名。

      if (File.OpenForRename(strxmlfilename, "w+") == false)
      {
        logfile.Write("File.OpenForRename(%s) failed.\n", strxmlfilename);
        return false;
      }

      File.Fprintf("<data>\n");
    }

    for (int ii = 1; ii <= ifieldcount; ii++)
      File.Fprintf("<%s>%s</%s>", strfieldname[ii - 1], strfieldvalue[ii - 1], strfieldname[ii - 1]);

    File.Fprintf("<endl/>\n");

    // 如果记录数达到starg.maxcount行就切换一个xml文件。
    if ((starg.maxcount > 0) && (stmt.m_cda.rpc % starg.maxcount == 0))
    {
      File.Fprintf("</data>\n");

      if (File.CloseAndRename() == false)
      {
        logfile.Write("File.CloseAndRename(%s) failed.\n", strxmlfilename);
        return false;
      }

      logfile.Write("生成文件%s(%d)。\n", strxmlfilename, starg.maxcount);

      PActive.UptATime();
    }

    // 更新自增字段的最大值。
    if ((strlen(starg.incfield) != 0) && (imaxincvalue < atol(strfieldvalue[incfieldpos])))
      imaxincvalue = atol(strfieldvalue[incfieldpos]);
  }

数据入库子系统

存在许多的CSV,JSON,xml文件,要是为了每一个不同业务的文件编写不同业务的入库程序,那代价就很大,有没有一种办法,通过配置文件,能够将文件夹中的不同业务的文件入库到对应的表中?这就是我们的目的,设计能够入库不同业务的文件到指定的数据库和表中的程序。
这个比较有意思,要用到存储引擎。
配置文件写什么?
什么样的文件入库到什么样的表中。是否在此之前要执行什么样的语句,如下所示:

<?xml version='1.0' encoding='utf-8'?>

<!--该参数文件存放了数据中心入库参数。-->
<xmltodb>
  <filename>ZHOBTCODE_*.XML</filename><tname>T_ZHOBTCODE1</tname><uptbz>1</uptbz><execsql>delete from T_ZHOBTCODE1</execsql><endl/>
  <filename>ZHOBTMIND_*.XML</filename><tname>T_ZHOBTMIND1</tname><uptbz>2</uptbz><endl/>
</xmltodb>

没有具体的说明具体的文件中有哪些数据,那个表又是有什么样的字段,那些字段是什么样的顺序,是什么样的结果,也就是说,我对他们一无所知,但是我知道它们的衣舍关系,就是哪个文件应该入库到哪个表中。

数据字典

记录了这个表的字段信息,以及表的一切信息,不是只表的数据,而是表的结构信息。

使用这个类可以知道,各个字段的各种信息,就可以根据表的字段的信息,拼接数组自己的insert语句,从而将数据插入到数据表中。

// 表的列(字段)信息的结构体。
struct st_columns
{
  char  colname[31];  // 列名。
  char  datatype[31]; // 列的数据类型,分为number、date和char三大类。
  int   collen;       // 列的长度,number固定20,date固定19,char的长度由表结构决定。
  int   pkseq;        // 如果列是主键的字段,存放主键字段的顺序,从1开始,不是主键取值0。
};

// 获取表全部的列和主键列信息的类。
class CTABCOLS
{
public:
  CTABCOLS();

  int m_allcount;   // 全部字段的个数。
  int m_pkcount;    // 主键字段的个数。
  int m_maxcollen;  // 全部列中最大的长度,这个成员是后来增加的,课程中并未提及。

  vector<struct st_columns> m_vallcols;  // 存放全部字段信息的容器。
  vector<struct st_columns> m_vpkcols;   // 存放主键字段信息的容器。

  char m_allcols[3001];  // 全部的字段名列表,以字符串存放,中间用半角的逗号分隔。
  char m_pkcols[301];    // 主键字段名列表,以字符串存放,中间用半角的逗号分隔。

  void initdata();  // 成员变量初始化。

  // 获取指定表的全部字段信息。
  bool allcols(connection *conn,char *tablename);

  // 获取指定表的主键字段信息。
  bool pkcols(connection *conn,char *tablename);
};
// 获取表全部的列和主键列信息的类。
CTABCOLS::CTABCOLS()
{
  initdata();  // 调用成员变量初始化函数。
}

void CTABCOLS::initdata()  // 成员变量初始化。
{
  m_allcount=m_pkcount=0;
  m_maxcollen=0;
  m_vallcols.clear();
  m_vpkcols.clear();
  memset(m_allcols,0,sizeof(m_allcols));
  memset(m_pkcols,0,sizeof(m_pkcols));
}

// 获取指定表的全部字段信息。
bool CTABCOLS::allcols(connection *conn,char *tablename)
{
  m_allcount=0;
  m_maxcollen=0;
  m_vallcols.clear();
  memset(m_allcols,0,sizeof(m_allcols));

  struct st_columns stcolumns;

  sqlstatement stmt;
  stmt.connect(conn);
  stmt.prepare("select lower(column_name),lower(data_type),character_maximum_length from information_schema.COLUMNS where table_name=:1");
  stmt.bindin(1,tablename,30);
  stmt.bindout(1, stcolumns.colname,30);
  stmt.bindout(2, stcolumns.datatype,30);
  stmt.bindout(3,&stcolumns.collen);

  if (stmt.execute()!=0) return false;

  while (true)
  {
    memset(&stcolumns,0,sizeof(struct st_columns));
  
    if (stmt.next()!=0) break;

    // 列的数据类型,分为number、date和char三大类。
    if (strcmp(stcolumns.datatype,"char")==0)    strcpy(stcolumns.datatype,"char");
    if (strcmp(stcolumns.datatype,"varchar")==0) strcpy(stcolumns.datatype,"char");

    if (strcmp(stcolumns.datatype,"datetime")==0)  strcpy(stcolumns.datatype,"date");
    if (strcmp(stcolumns.datatype,"timestamp")==0) strcpy(stcolumns.datatype,"date");
    
    if (strcmp(stcolumns.datatype,"tinyint")==0)   strcpy(stcolumns.datatype,"number");
    if (strcmp(stcolumns.datatype,"smallint")==0)  strcpy(stcolumns.datatype,"number");
    if (strcmp(stcolumns.datatype,"mediumint")==0) strcpy(stcolumns.datatype,"number");
    if (strcmp(stcolumns.datatype,"int")==0)       strcpy(stcolumns.datatype,"number");
    if (strcmp(stcolumns.datatype,"integer")==0)   strcpy(stcolumns.datatype,"number");
    if (strcmp(stcolumns.datatype,"bigint")==0)    strcpy(stcolumns.datatype,"number");
    if (strcmp(stcolumns.datatype,"numeric")==0)   strcpy(stcolumns.datatype,"number");
    if (strcmp(stcolumns.datatype,"decimal")==0)   strcpy(stcolumns.datatype,"number");
    if (strcmp(stcolumns.datatype,"float")==0)     strcpy(stcolumns.datatype,"number");
    if (strcmp(stcolumns.datatype,"double")==0)    strcpy(stcolumns.datatype,"number");

    // 如果业务有需要,可以修改上面的代码,增加对更多数据类型的支持。
    // 如果字段的数据类型不在上面列出来的中,忽略它。
    if ( (strcmp(stcolumns.datatype,"char")!=0) &&
         (strcmp(stcolumns.datatype,"date")!=0) &&
         (strcmp(stcolumns.datatype,"number")!=0) ) continue;

    // 如果字段类型是date,把长度设置为19。yyyy-mm-dd hh:mi:ss
    if (strcmp(stcolumns.datatype,"date")==0) stcolumns.collen=19;

    // 如果字段类型是number,把长度设置为20。
    if (strcmp(stcolumns.datatype,"number")==0) stcolumns.collen=20;

    strcat(m_allcols,stcolumns.colname); strcat(m_allcols,",");

    m_vallcols.push_back(stcolumns);

    if (m_maxcollen<stcolumns.collen) m_maxcollen=stcolumns.collen;

    m_allcount++;
  }

  // 删除m_allcols最后一个多余的逗号。
  if (m_allcount>0) m_allcols[strlen(m_allcols)-1]=0;

  return true;
}

// 获取指定表的主键字段信息。
bool CTABCOLS::pkcols(connection *conn,char *tablename)
{
  m_pkcount=0;
  memset(m_pkcols,0,sizeof(m_pkcols));
  m_vpkcols.clear();

  struct st_columns stcolumns;

  sqlstatement stmt;
  stmt.connect(conn);
  stmt.prepare("select lower(column_name),seq_in_index from information_schema.STATISTICS where table_name=:1 and index_name='primary' order by seq_in_index");
  stmt.bindin(1,tablename,30);
  stmt.bindout(1, stcolumns.colname,30);
  stmt.bindout(2,&stcolumns.pkseq);

  if (stmt.execute() != 0) return false;

  while (true)
  {
    memset(&stcolumns,0,sizeof(struct st_columns));

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

    strcat(m_pkcols,stcolumns.colname); strcat(m_pkcols,",");

    m_vpkcols.push_back(stcolumns);

    m_pkcount++;
  }

  if (m_pkcount>0) m_pkcols[strlen(m_pkcols)-1]=0;    // 删除m_pkcols最后一个多余的逗号。

  return true;
}

主要逻辑

获取到表的所有的信息,生成SQL操作句柄,解析一条文件的数据,将数据存放到指定内存中,最终执行插入一条数据。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
IT项目复盘会议模板用于总结和评估已完成的IT项目,以确定项目的成功与短板,并为未来的项目提供改进和学习的机会。以下是一个常见的IT项目复盘会议模板的简要内容: 1. 会议目的: - 确定项目成功的关键因素和挑战 - 分享项目经验和教训 - 提供改进建议和措施 2. 项目概述: - 提供项目背景和目标 - 明确项目范围和时间表 3. 成功因素: - 总结项目的成功因素,例如团队合作、领导支持、资源管理等 - 强调成功因素的积极影响和实施方法 4. 挑战和教训: - 讨论项目中的挑战和困难 - 反思潜在的教训和应对方法,包括项目管理、沟通、风险管理等方面 5. 项目绩效评估: - 评估项目的时间表、预算和质量目标的达成情况 - 分析任何超出或不足的情况,并确定其中的原因 6. 改进建议: - 提供项目改进的建议和措施 - 确定未来项目中需要避免的问题,并提出解决方案 7. 团队反馈: - 鼓励团队成员分享对项目的观点和经验 - 倾听并考虑团队成员提出的意见和建议 8. 行动计划: - 制定行动计划,明确改进措施和责任人 - 确定实施时间表和监控方法 9. 会议总结: - 概括会议讨论的要点和重点 - 强调重要的改进措施和下一步行动 IT项目复盘会议模板的内容可以根据实际项目的需要进行调整和添加,以确保全面而有针对性的评估和总结。该模板有助于促进项目范围、进度和质量的改进,提高项目的成功率和效果。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值