一种数据推送解决方案

这几天之前做的一个实验室项目终于到了验收阶段,但是甲方突然提出了将爬虫放到我们的机器上,只在他们的机器上部署一套程序,但是不进行采集,我们将采集到的数据每天推送到他们的机器上。没办法,只能够修改程序了。整个解决方案还是比较简单的,但是解决过程中还是遇到了一些坑,这里简单记录一下。

解决方案

问题

  1. 需要导入的内容存在数据库记录和图片文件,图片文件格式很多
  2. 甲方给我们提供的机器没有公网IP

思路

针对这些问题,主要考虑了一下几种思路,这几种思路的不同主要体现在数据传输的方式上面:

  1. 通过甲方单位的邮箱

    在数据采集端,定时将采集到的最新数据发到甲方单位的邮箱中,然后利用部署在甲方单位的客户端程序定时去接收邮件,然后将数据导入到特定的位置。对于这一种方案,没有做出更多的调研。不知道甲方是否能够帮助提供邮箱,也不知道邮箱的限制会有哪些(邮件附件大小等),所以放弃了。

  2. 通过网盘进行数据同步

    将在采集端压缩好的数据放到网盘对应的文件夹中,然后借助网盘提供的同步功能将数据同步到网盘提供商的服务器上。客户端登录同样的网盘帐号,从网盘中下载数据文件,然后解压,导入到特定的地方。这种方案感觉还是挺麻烦的,首先网盘服务感觉不是特别稳定,不知道哪天网盘就挂了;第二,网盘是否允许多个地方同时登录;第三,如果网盘不提供客户端,还要模拟登录什么的。总之,这个方案就是一个深坑。

  3. 通过FTP进行数据同步

    最终还是选择了这种方案,在采集服务器上开启FTP服务,然后数据压缩之后放到FTP对应的目录里面。客户端程序定时访问FTP,然后下载文件,导入到甲方的服务器中。这种方案相对于上面两种方案最大的优点就是服务我们能够控制,避免了其它服务的问题对系统运行的影响。

解决方案详述

整体结构

程序结构图

程序整体结构如上图所示,所有涉及到的工作主要是在FilePush这一块。这一块的功能包括数据库记录的导入导出,图片的移动、压缩、解压等。

对于数据库记录的导入导出采用Java对象的序列化方法。

模块详述

  • 服务器端程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
ExportData exportData = new ExportData();
long initDelay = 1;
long delay = ExportConstants.MINUTES_BETWEEN_EXPORT;
executorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
logger.info(DateUtil.getForDate("yyyy-MM-dd HH:mm:ss", new Date()));
exportData.exp();
}
}, initDelay, delay, TimeUnit.MINUTES);
}

这里采用的就是一个定时任务,定时运行程序,将采集端的数据导出,然后压缩成zip文件,放到FTP对应的目录中。

  • 客户端程序
1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
ImportData importData = new ImportData();
while (true) {
importData.imp();
try {
Thread.sleep(ImportConstants.MINUTES_BETWEEN_IMPORT * 60 * 1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}

这里和上面的导出程序的明显区别在于使用了Thread.sleep()的方法,而没有采用定时执行的方法,因为在采集端导出数据通常是比较快的,而客户端需要下载文件,下载时间受到网络环境的影响,没有具体的时间间隔,因此采用了Thread.sleep()的方法。

遇到的坑

程序写好之后,在我们的所有的机器上测试都没有问题,但是部署到甲方单位之后,就出现了问题,无法将文件下载下来。查了一下,问题主要出现在FTP这一块上面。

服务端和甲方机器的操作系统都是Windows Server 2008。在甲方的机器上,安装Filezilla的客户端版本,通过客户端能够建立连接并且完成数据传输。在甲方机器的cmd里面,通过ftp命令,能够登录到服务器端的FTP上,但是获取目录的时候就出现了问题;而我的程序,一直停留在港打开的样子,没有任何的输出(我SB的只设置了连接超时时间,而没有设置数据传输超时时间),实际上应该也是登录成功了,但是没有成功地完成数据传输。

通过ftp命令看到发生了以下的错误:

1
425 Can't open data connection

这个问题主要是客户端于服务器不能够建立数据传输的连接。这里先补充一点FTP的内容。

FTP基本知识

哈哈,其实,这才应该是重头戏 :P

FTP协议包含两种连接:控制连接和数据连接,控制连接用于服务器于客户端之间的命令传输,主要使用服务器上的21端口。数据连接用于服务器于客户端之间的数据交换,包含目录列表、文件上传下载等,这种连接在需要数据传输时建立,传输结束之后释放。

FTP的数据传输分为主动模式(PORT Mode)和被动模式(Passive Mode),这两种模式的主动和被动主要针对服务器来说,主动模式由服务器主动发起建立数据连接,而被动模式则是由客户端发起建立数据连接。

  • 主动模式

主动模式

对于主动模式,服务器接收到客户端的数据传输相关的命令之后(如ls),由20端口主动与N+1(N为客户端命令端口)建立连接。

  • 被动模式

被动模式

对于被动模式,在客户端于服务器端建立连接之后,客户端对服务器端发送PASV命令,服务器端返回PORT P,其中P(P>1023)为服务器端打开的用于监听数据连接的端口。客户端用N+1端口与P端口建立数据连接,用于数据传输。

问题结论

在我的程序中,使用的是PORT模式,而Windows的ftp.exe不支持passive模式。而使用Filezilla客户端连接是时候,在服务端的内容中看到发送了PASV命令,因此可能的问题是甲方单位的防火墙(注意这里不是部署程序的服务器)的防火墙对于主动建立的连接进行了限制。

在我们自己的机器上测试的时候,能够建立连接则可能是因为没有相应的防火墙规则。

注意:这里服务器本身的防火墙规则也可能对连接造成影响,可能需要配置服务器本身的防火墙规则。

最后,解决问题的方法也就变得简单了,在FTPUtil的工具类中加上下面的代码:

1
2
3
if (ImportConstants.FTP_PASSIVE_MODE) {
ftpClient.enterLocalPassiveMode();
}

总结一下

上面的问题实际上是我的猜测,并没有想到好的方法验证,也没法接触到甲方的网管,没法确认是否有防火墙的关系。

另外,网上其实有很多关于客户端连接FTP使用主动模式还是被动模式的讨论,其实核心的问题就是在于FTP服务器所处的网络环境,如果服务器处于局域网中,可能主动模式较好,如果服务器处于公网中,其实两种模式我感觉都差不多,主要还是要看具体的问题。

参考资料

FTP基础知识博客
MS-DOS ftp.exe不支持passive模式