Bootstrap

用 ftp 获取文件,保证文件数据的完整性

1.生产观测数据函数的漏洞

  之前生成观测数据的程序中的将全国气象站点观测数据写入文件的函数,有一个漏洞。这个漏洞就是当将气象观测数据写入文件的时,数据还没写完就被取走了(客户端 ftp取走),客户端取到的文件就是不完整的。

  就相当于我在写信的时候还没写完就被拿去寄了,里面的信息是不完整的。

2.代码分析

2.1 生产观测数据函数的代码

  将全国气象站点观测数据写入文件的函数如下

bool CrtSurfFile(const char *outpath)
{
  CFile File;

  char strFileName[301];
  memset(strFileName, 0, sizeof(strFileName));

  //获取时间
  char strLocalTime[21];
  memset(strLocalTime, 0, sizeof(strLocalTime));
  LocalTime(strLocalTime,"yyyymmddhh24miss");

  //为了不让文件名冲突,所以在文件名上加上目录,时间,进程id
  snprintf(strFileName, 300, "%s/SURF_ZH_%s_%d.txt",outpath,strLocalTime,getpid());

  if (File.Open(strFileName,"w") == false)
   {
     logfile.Write("File.Open(%s) 失败!\n",strFileName);  return false;
   }

 for (int ii=0; ii<vsurfdata.size(); ii++)
   {
     //站点代码,数据时间,气温,气压,相对湿度,风向,风速,降雨量,能见度
     File.Fprintf("%s,%s,%.1f,%.1f,%d,%d,%.1f,%.1f,%.1f\n",vsurfdata[ii].obtid,\
          vsurfdata[ii].ddatetime,vsurfdata[ii].t/10.0,vsurfdata[ii].p/10.0,vsurfdata[ii].u,\
          vsurfdata[ii].wd,vsurfdata[ii].wf/10.0,vsurfdata[ii].r/10.0,vsurfdata[ii].vis/10.0);
   }

  logfile.Write("生成数据文件(%s)成功!数据时间=%s,记录数=%d\n",strFileName,vsurfdata[0].ddatetime,vsurfdata.size());
   
  File.Close();    //关闭文件
  return true;
}

2.2 代码段分析

  分析一下这段代码:

if (File.Open(strFileName,"w") == false)
   {
     logfile.Write("File.Open(%s) 失败!\n",strFileName);  return false;
   }

  当使用文件 Open 函数时,会在磁盘空间创建或打开一个文件,这里暂时叫做 aaa.txt 。

  接着调用 Fprintf 函数往 aaa.txt 里面写入数据

 for (int ii=0; ii<vsurfdata.size(); ii++)
   {
     //站点代码,数据时间,气温,气压,相对湿度,风向,风速,降雨量,能见度
     File.Fprintf("%s,%s,%.1f,%.1f,%d,%d,%.1f,%.1f,%.1f\n",vsurfdata[ii].obtid,\
          vsurfdata[ii].ddatetime,vsurfdata[ii].t/10.0,vsurfdata[ii].p/10.0,vsurfdata[ii].u,\
          vsurfdata[ii].wd,vsurfdata[ii].wf/10.0,vsurfdata[ii].r/10.0,vsurfdata[ii].vis/10.0);
   }

  这里就可能会出现问题,当要写入的数据很大,还没有写完进去,其他的进程就读取了,这样读取的数据就不完整了。

2.3 漏洞模拟

  接下来就利用 sleep() 来模拟一下写入的数据很大,也就是要花在写入的时间很久的情况。

  1.我们先来看看调用了 Open 函数会在磁盘空间打开或创建一个文件,现在文件还有创建,所以会先创建这函数。
在这里插入图片描述
  在这里让程序停留60秒,还没有启动程序之前,在这个存放观测数据的目录中( /home/xza/qxidc/data/ftp/surfdata )是没有文件的.
在这里插入图片描述
  启动程序后创建了一个文件(SURF_ZH_20210403231023_230102.txt)

在这里插入图片描述

  为了模拟写入的时间很长,我在写入了一条数据后停留 60s,如下图
在这里插入图片描述

  接着在客户端用ftp脚本去获取(下载)这个文件 ,客户端就会存在这个文件(SURF_ZH_20210403231023_230102.txt)

  再接着在客户端中查看(SURF_ZH_20210403231023_230102.txt)的内容。
在这里插入图片描述
  会发现这个文件只有73行,但是完整的文件是839行。
在这里插入图片描述

3. 解决漏洞

3.1 解决技巧

  那么我们该如何解决这个读取文件可能不完整的问题呢,也就是如何保证文件的完整性。这里有一个小技巧,就是没写完的文件就在文件末尾加上没有写完的标识。比如说aaa.txt,在没有写完之前命名为 aaa.txt.tmp,写完了之后重命名为 aaa.txt。客户端在获取文件的时候只获取那些完整的文件,不完整的就不获取。

  给文件重命名,在末尾加上 .tmp 未写完标识。如下图
在这里插入图片描述

  文件写完之后,再将aaa.txt.tmp,重命名为 aaa.txt。如下图

在这里插入图片描述

3.2 封装的解决漏洞函数

  一般我们可以把重命名的步骤分别封装到一个函数里面。

3.2.1打开文件重命名函数

// 专为改名而打开文件,参数与fopen相同,打开成功true,失败返回false
bool CFile::OpenForRename(const char *filename,const char *openmode,bool bEnBuffer)
{
  Close();

  memset(m_filename,0,sizeof(m_filename));
  strncpy(m_filename,filename,300);

  memset(m_filenametmp,0,sizeof(m_filenametmp));
  SNPRINTF(m_filenametmp,sizeof(m_filenametmp),300,"%s.tmp",m_filename);

  if ( (m_fp=FOPEN(m_filenametmp,openmode)) == 0 ) return false;

  m_bEnBuffer=bEnBuffer;
  
  return true;
}

3.2.2 关闭文件重命名函数

// 关闭文件并改名
bool CFile::CloseAndRename()
{
  if (m_fp==0) return false;

  fclose(m_fp);  // 关闭文件指针

  m_fp=0;

  if (rename(m_filenametmp,m_filename) != 0)
  {
    remove(m_filenametmp);
    memset(m_filename,0,sizeof(m_filename));
    memset(m_filenametmp,0,sizeof(m_filenametmp));
    return false;
  }
   memset(m_filename,0,sizeof(m_filename));
  memset(m_filenametmp,0,sizeof(m_filenametmp));

  return true;
}

3.3 再次测试

  所以就可以将这个程序的 Open 和 Close 函数分别改为 OpenForRename 和 CloseAndRename
在这里插入图片描述

在这里插入图片描述

  将程序改好了,再次启动程序,然后查看生成的观测数据文件,没有写完的文件会有一个 .tmp 的标识
在这里插入图片描述

4.补充解决技巧

  凡是涉及到文件传输的操作,都要保证传输后文件的完整性。如从服务端下载文件,从客户端上传文件到服务端。这两个操作在封装的Cftp类中,分别是get() 函数和 put() 函数。

  为了保证在传输过程中文件的完整性,在这个两个函数的参数中加了一个参数,get()函数加了核对传输前后时间,put() 函数加了核对传输前后文件大小。

  这个两个函数的声明如下:

4.1 get()

  为什么要加第三个参数呢?

  在文件传输之前,获取文件内容的最后修改时间,传输了之后再获取文件的最后的修改时间。对比这两个时间如果不相同,就说明源文件传输完之后被修改了,与传输的目标文件已经不同了,那么我得到这个被修改过后的文件后要丢弃。

  为什么不核对大小呢?文件内容改变了大小不一定变,但文件内容的最后修改时间一定变。

// 从ftp服务器上获取文件。
  // remotefilename:待获取ftp服务器上的文件名。
  // localfilename:保存到本地的文件名。
  // bCheckMTime:文件传输完成后,是否核对远程文件传输前后的时间,保证文件的完整性。
  // 返回值:true-成功;false-失败。
  // 注意:文件在传输的过程中,采用临时文件命名的方法,即在localfilename后加".tmp",在传输
  // 完成后才正式改为localfilename。
  bool get(const char *remotefilename,const char *localfilename,const bool bCheckMTime=true);

4.2 put()

  这个不用核对时间,因为客户端传输文件给另一端之前都会保证文件是正确的完整的,不会是中间状态的文件,传输的是准备好的文件。所以核对传输前后的文件大小就可以了。

 // 向ftp服务器发送文件。
  // localfilename:本地待发送的文件名。
  // remotefilename:发送到ftp服务器上的文件名。
  // bCheckSize:文件传输完成后,是否核对本地文件和远程文件的大小,保证文件的完整性。
  // 返回值:true-成功;false-失败。
  // 注意:文件在传输的过程中,采用临时文件命名的方法,即在remotefilename后加".tmp",在传输
  // 完成后才正式改为remotefilename。
  bool put(const char *localfilename,const char *remotefilename,const bool bCheckSize=true);
;