一、背景
业务中对接第三方平台、或过网闸将图片向内网摆渡时,经常需要把大量图片(业务数据)上传FTP服务器,以往的FTP上传方式,是先把图片下载存储到本地,然后再把本地图片上传至FTP,图片会在本地磁盘进行中转,FTP下载文件时也是如此。因为FTP上传文件的接口都是需要以文件在本地磁盘的路径作为输入的,这样通过磁盘中转文件,会使传输速度大打折扣。此外FTP传输图片文件的性能一直不高,之前也没有人在这方面做过深入探索。
基于以上原因,在项目实践中对FTP的性能和传输方法做了深入研究,解决掉需要磁盘中转文件的缺陷,提升传输性能,并对FTP传输性能做了统计测试,找出最佳上传方案。
二、以往FTP上传方式
所采用的方法: CFtpConnection::PutFile/FtpPutFile由windows MFC中提供的FTP网络通讯类方法
a、BOOL CFtpConnection::PutFile(LPCTSTR pstrLocalFile,LPCTSTR pstrRemoteFile,DWORD dwFlags =FTP_TRANSFER_TYPE_BINARY,DWORD_PTR dwContext = 1);
b、BOOLAPI FtpPutFile(__in HINTERNET hConnect,__in LPCSTR lpszLocalFile,__in LPCSTR lpszNewRemoteFile,__in DWORD dwFlags,__in_opt DWORD_PTR dwContext);
可以看到红色参数,都是需要输入一个本地的文件路径,才能上传,而无法直接将内存中的数据输入,所以以往的设计才迫不得已使用了磁盘中转。
三、改进后的FTP上传方式
改进后,图片不再经过磁盘中转,而是在FTP会话上创建CinternetFile文件操作(由MFC提供,允许对使用Internet协议的远端系统文件的访问,派生于CFile类——MFC文件操作的基类),通过文件读写方法Write、Close,将内存中的图片数据写入FTP。
方法virtual void Write(const void*lpBuf,UINT nCount)的第一个参数类型为void*,能够直接输入图片在内存中的地址,从而使内存中的图片数据不需经过磁盘中转,就能写到FTP上。
源码:
CInternetFile*intePicFile =m_pFtpConnection->OpenFile(chPath,GENERIC_WRITE);// pFtpConnection为ftp会话类
if(NULL!=intePicFile)
{
intePicFile->Write(pData,Len);
intePicFile->Close();
}
同理,通过Read方法下载文件源码如下:(方法virtual UINT Read(void*lpBuf,UINT nCount)的第一个参数输入预分配的内存地址,使图片从ftp上读取到内存中。)
CInternetFile*intePicFile =m_pFtpConnection->OpenFile(pFtpFilePath.c_str(),GENERIC_READ);
if(NULL!=intePicFile)
{
Len =intePicFile->Read(pData,MAXPICLEN);
intePicFile->Close();
}
如此就解决了FTP下载上传图片需通过磁盘中转的问题,不仅提升了传输速度,并且精简了代码逻辑。
四、FTP类常用方法的封装
解决上述问题后,为了便于日后的开发,这里重新封装了FTP常用方法,提高自动化操作,包括如下内容(源码已包含在demo中,类CFtpOper):
a. 构造函数:能初始化FTP连接,并在析构中自动断开连接,传输期间若发生异常断开,会自动重连,无需额外干预。
CFtpOper::CFtpOper(string strFtpIP,int nFtpPort,string strUserName,string strPassword);
参数1:strFtpIP, FTP服务器的IP地址
参数2:nFtpPort, FTP服务器的端口号
参数3:strUserName, FTP服务器的用户名
参数4:strPassword, FTP服务器的密码
返回值:构造函数无返回值
b. 文件上传函数:采用优化后的上传方法,图片无需通过磁盘中转。支持多级目录创建,通过结构体UpPath将各级目录名称、目录总数输入,该方法会自动在FTP上检索对应目录,若无此目录,或目录不完整,会自动创建,创建完成后,会将要上传的文件传输到此目录下面。最多支持10级目录(可通过修改宏定义扩展级数上限),
int UpLoadImage(const char*pData,int Len,const char*pFtpPicName,UpPath uPath);
参数1:pData,待上传的图片数据指针
参数2:Len,待上传的图片数据长度
参数3:pFtpPicName,图片数据在到FTP上的文件名,含后缀,如xx.jpg。
参数4:uPath,存储目录信息的结构体,uPath. m_nLev 为目录级数(总共几级目录),uPath. m_strPath[i] 为第i级目录的名称,i从0开始,支持10级目录。
返回值: 1 成功;0 连接不可用; 2 失败
c. 文件下载函数:采用优化后的上传方法,图片无需通过磁盘中转。
下载前需修改代码中MAXPICLEN的值为预分配内存块pData的长度,该长度要大于需读取的文件大小。
int DownLoadImage(char*pData,int&Len,const string pFtpFilePath);
参数1:pData 预分配的内存块,下载的图片将存储到此内存块中
参数2:Len 将返回下载图片的实际长度。
参数3:文件在FTP上的完整路径,如/05/photo/[20160422]/1143/aaa.jpg
返回值:1 成功;0 连接不可用; 2 失败
d. 多级目录创建函数:通过结构体UpPath将各级目录名称、目录总数输入,该方法会自动在FTP上检索改目录,目录若不存在或不完整,将创建对应目录,最多支持10级目录(可通过修改宏定义扩展级数上限)
bool CreateDir(UpPath UploadPath);
参数: UploadPath, 同 UpLoadImage方法中的参数4。
返回值:true 成功;false 失败
c. 文件的查找、删除:可通过已有的CFtpConnection::Remove方法和CFtpFileFind类实现,此处不再扩展,demo中也做了示例程序以供参考。
五、FTP上传性能测试统计
实践中发现,FTP单会话上传数据速度很慢(已确定FTP上没有对传输速率做限制),270KB的图片文件,一秒钟只能上传1.5-2张,且无法通过在一个会话中启用多线程来加快传输,反而会因此产生异常崩溃,这种方法是不可行的。只能通过开启多个FTP会话来提升传输速度,每个会话使用一个线程来实现,即一个线程对应一个会话。
但是开多少个线程(会话)能使传输速度达到最优呢,理论上线程并非越多越好,随着线程数的上涨,线程调度上耗费的时间也会上涨,以及其他软件和硬件因素限制,反而会使性能下降,或者速率随着线程数的增长而提高甚微。
这个数值对实际应用有很大参考价值的,所以此处做了测试统计。
a. 测试程序:基于优化封装后的代码实现。
关键代码如下(详见demo程序):
b.测试过程:反复上传一张270KB大小的图片到FTP上,当然名称保证各不相同,测试不同线程数量下图片的传输速度。
c.硬件环境:
客户端主机:台式机,cpu E3-1230 v3 3.3GHZ八核,硬盘7200转
FTP服务器主机:笔记本,cpu i5 2.7GHZ 四核,硬盘7200转
e. 测试统计结果基于以下事实:测试中网络带宽,磁盘写入速度远未达到瓶颈值,硬件环境在测试过程中始终保持一致。
程序运行截图:
结果统计:
线程数(每个线程开启一个单独的ftp会话) | 1 | 5 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 50 |
20秒传输数量 | 48 | 231 | 411 | 488 | 507 | 535 | 542 | 544 | 547 | 559 |
平均每秒传输数量 | 2.4 | 11.55 | 20.55 | 24.4 | 25.35 | 26.75 | 27.1 | 27.2 | 27.35 | 27.95 |
生成图表如下:
比对图表曲线可以得出结论:(因硬件资源条件有限,测试结果仅供参考)
随着线程数量(此处等同FTP会话数量)的增加,图片传输数量呈对数曲线增长。建议选择15个左右会话线程数,以获得最佳传输速度——270k大小图片,25张/每秒。30个会话线程基本已达到最高传输速度。
其中限制传输性能的因素包括:
1、CPU:限制多会话的处理速度。
2、硬盘:限制文件的写入速度。
3、网络带宽:限制文件网络传输速度。
Demo 附件:http://pan.baidu.com/s/1jImVyaa
含测试数据记录