一、背景
现需要通过抓取的FTP, tftp网络数据包,进行分析(解析),并拿到里面对应的上传下载文件,将文件还原到本地,同时要考虑高并发(上万条网络流同时进行)的情况。
二、分析FTP, TFTP数据
首先要想还原文件,必须要知道以下几个条件:对应的传输文件流的TCP流信息(srcip,srcport, dstip, dstport),上传还是下载的文件名,主被动模式,传输方式。
1、FTP
详细的ftp协议解析,这里不谈(网上多的是),这里只说重要的地方。
FTP分信号流和文件流,信号流可以根据端口21确定,且21端口的一端为服务器,找到信号流。
(1) 如果收到“PORT”命令,那么就是主动模式,此时记录client的ip, port结合serverip和端口20得到传输文件流的TCP流信息,如果收到“PASV”(或者“EPSV”rfc2428, “LPSV”rfc1639),那么可以确定为被动模式,那么server会response 对应的ip,port,结合client的ip,(这里还拿不到client的文件流port, 这个port会主动连接server的ip, port)得到传输文件流的TCP流信息。
(2) 接下来,如果收到“RETR”命令,说明是要下载文件,如果收到“STOR”命令,就是上传文件,解析命令后面的数据得到文件名。
(3)传输方式,根据命令“TYPE”来判断。 (我个人的做法是,如果是 “A”, 那么就是文本的方式写文件fopen(filename, “w”), 如果是其他,那么就是二进制的方式写文件fopen(filename, “wb”));
(4) 文件结束。收到server端发送的226命令表示文件结束,或者过程中其他的错误码(写好日志)结束写文件操作。
那么根据上面的信息,找到TCP文件流,解析出payloadfwrite进文件就可以了。
这里要注意,信号流与文件流的协同问题,以及文件流端口复用问题。
2、TFTP
TFTP非常简单,就两种模式RRQ,WRQ,server使用端口69,传输使用UDP。
这个过程client 的ip port都不会变,而server发信令的端口使用69,文件流端口是不一样的。
(1) RRQ
如上图,通过命令解析,opcode = 1 表示RRQ (读请求,就是下载),得到文件名 rfc1350.txt,以及文件传输方式octet, 马上下一个数据包,就是server给client发的文件数据包(此时也知道了server段的文件流port),之后是client的ack, server要收到了client的ack后才会继续发下一个data packet。
(2) WRQ
如上图,解析命令 opcode = 2表示WRQ (写请求,就是上传), 得到文件名和传输方式,马上server会发一个ack过来,此时知道了server段的文件流port,接下来就是文件传输了,同上面的RRQ.
文件结束标志:每个文件数据包是定长的(512Byte),如果收到一个数据包payload小于512,那么说明文件结束了
三、异步存储文件设计(解决高并发)
根据上面的描述已经可以得到了文件流,可以直接创建文件,写文件了。但是如果拿到一个数据包就写一个数据包到文件中,会有三个问题, 第一:效率较低。因为写磁盘的速度比较慢,所以会影响整个的效率。第二:产生很多的文件碎片(因为间隔的写小文件块,会被存到磁盘的各个地方,这个具体的请查看相应文档)。第三:因为是持续写,一个文件流就要对应一个文件句柄(如果不对应一个文件句柄,而且每次都fopen, fclose,那么效率实在是太低了),在win6 64位电脑上一个进程能同时打开的文件句柄数为500个左右(我自己测试的是510个),怎么处理同时上万条文件流的写问题。
所以,根据上面的问题情况,下面给出异步写文件的解决方案。
定义
StructDATA_BLOCK // 文件内容块
{
Int nPos; // 在buffer内的偏移量
Int nDatalen;
}
FILE_BUFFER_INTO // 文件缓存buffer
{
Char* pBuffer; // buffer 内存
Int nTotalLen; //buffer 总长度
Int nCurrentPos; // buffer当前使用位置
Map<int,vector<DATA_BLOCK>> mapFIleTaskData; // key: 文件任务句柄
}
StructFILE_TASK_INFO
{
Int nTaskHandle;
String sFileName;
FILE_MODE fm; // 这个是fopen的第二个参数 “w”或者“wb”
}
定义成员:
List<FILE_BUFFER_INFO*>m_listFileWriteBuffer; // 用于写入的buffer
List<FILE_BUFFER_INTO*>m_listFileReadBuffer;// 用于读取buffer数据写文件
Map<int, FILE_TASK_INFO>m_mapFIleTaskInfo; // 存储文件任务
从上面的定义其实可以看出基本思路了, 申请批量(我个人是申请了10个2M大小的buffer)的FILE_BUUFER_INFO用于缓存文件流数据,存入m_listFileWriteBuffer中,收到文件流数据块的时候,写入pBuffer中,并且记录好是哪个文件的(记录在mapFIleTaskData中),当第一个FILE_BUFFER_INFO的buffer块写满了(或定时器到了,见说明1),那么就把这个bufferpush到m_listFileReadBuffer中去,同时继续写第二个buffer块,于此同时,读线程会从m_listFileReadBuffer中读取buffer,将里面的数据(根据mapFIleTaskData的记录)读出来,写入文件(见说明2),读完了一个buffer,那么立刻将此buffer又重新push到m_listFileWriteBuffer中使用,就这样交替异步的缓存机制处理。
这里有几点说明:
第一:m_listFileWriteBuffer的pop条件。当front的buffer满了,肯定要弹出,同时如果定时器时间到了,也要弹出。这里使用定时器的原因是,处理最后数据不能让buffer满,而不能让读线程读到文件最后的数据。
第二:写文件,这里我个人也是开辟了一个写文件缓存,4k大小,因为读线程是每次会把某个文件(任务句柄)在一个buffer中的所有数据都出来,所以我把读到的数据都缓存到4k的缓存里面,当满了,或者结束的时候才调用fwrite,这样效率会更高。
第三:文件句柄,因为整个过程都使用的是任务句柄,文件句柄(fopen产生)只有一个,那就是在读buffer写文件线程里面,一次把buffer里面的同一个文件数据全部读出来写入文件,写完了,就fclose,再fopen下一个 (详 请分析mapFIleTaskDatakey为文件任务句柄)。
第四:文件结束,因为是异步的,当收到的文件结束命令的时候,未必它对应的文件数据块都写到了文件中,如果此时再遍历所有buffer,把这个文件数据都写入文件,再close,效率太低了。其实很简单,当文件结束的时候就不用发什么停止命令了,直接写一个DATA_BLOCK nDatalen = 0就可以了,那么读线程读到这个block的时候自然就知道文件结束了。
下面没了。
Author: min