广东互联网医院监管对接_前置机FTP文件上传

最近在广东省互联网医院监管对接过程中,有些文件需要同步到前置机上,我这里用到的解决方案是通过ftp上传文件功能实现的,这里记录一下一些遇到的问题。

1.安装,配置ftp服务器软件

首先在前置机上安装filezilla服务器端软件。百度搜索filezilla即可。

广东互联网医院监管对接_前置机FTP文件上传

打开这个软件,连接到服务器。这里是链接管理ftp服务器的。

广东互联网医院监管对接_前置机FTP文件上传
广东互联网医院监管对接_前置机FTP文件上传

然后开始设置服务器信息,包括监听端口,超时时间,被动模式信息等。

广东互联网医院监管对接_前置机FTP文件上传
广东互联网医院监管对接_前置机FTP文件上传

为了方便配置安全组规则,这里的被动模式,端口范围自己配置了一下,然后被动模式的ip也改成了服务器ip(ip固定是因为我修改了之后就能上传成功,背后的原理没去了解)。

广东互联网医院监管对接_前置机FTP文件上传

然后还修改了安全设置里的防护等级,因为我正式使用的时候是通过内网访问,和外网是阻隔的,所以这些安全配置,也没去细究了(总而言之就是尽量减少开发报错,别让这些问题阻碍开发进度)。

广东互联网医院监管对接_前置机FTP文件上传

然后就是添加用户组和用户了,然后添加文件夹授予相关权限。

广东互联网医院监管对接_前置机FTP文件上传

2.代码实现文件上传

这里用的是网上的php Ftp工具类

<?php

class Ftp
{

	private $host = 'ip';//远程服务器地址
	private $user = 'username';//ftp用户名
	private $pass = 'password';//ftp密码

	private $port = 9203;//ftp登录端口
	private $error = '';//最后失败时的错误信息
	protected $conn;//ftp登录资源

	/**
	 * 可以在实例化类的时候配置数据,也可以在下面的connect方法中配置数据
	 * Ftp constructor.
	 * @param array $config
	 */
	public function __construct(array $config = [])
	{
		empty($config) OR $this->initialize($config);
	}

	/**
	 * 初始化数据
	 * @param array $config 配置文件数组
	 */
	public function initialize(array $config = [])
	{
		$this->host = $config['host'];
		$this->user = $config['user'];
		$this->pass = $config['pass'];
		$this->port = isset($config['port']) ?: 21;
	}

	/**
	 * 连接及登录ftp
	 * @param array $config 配置文件数组
	 * @return bool
	 */
	public function connect(array $config = [])
	{
		empty($config) OR $this->initialize($config);
		if (FALSE == ($this->conn = @ftp_connect($this->host,$this->port))) {
			$this->error = "主机连接失败";
			return FALSE;
		}
		if (!$this->_login()) {
			$this->error = "服务器登录失败";
			return FALSE;
		}
		return TRUE;
	}

	/**
	 * 上传文件到ftp服务器--账户权限不够 替换文件 删除文件都会上传不成功
	 * @param string $local_file 本地文件路径
	 * @param string $remote_file 服务器文件地址
	 * @param bool $permissions 文件夹权限
	 * @param string $mode 上传模式(ascii和binary其中之一)
	 */
	public function upload($local_file = '', $remote_file = '', $mode = 'auto', $permissions = NULL, $replace = true)
	{
		if (!file_exists($local_file)) {
			$this->error = "本地文件不存在,您的选择的本地文件路径:".$local_file;
			return FALSE;
		}

		if ($mode == 'auto') {
			$ext = $this->_get_ext($local_file);
			$mode = $this->_set_type($ext);
		}

		/* 不覆盖同名文件 */
		if (!$replace && is_file($remote_file)) {
			$this->error = '存在同名文件' . $remote_file;
			return false;
		}

		//创建文件夹
		$this->_create_remote_dir($remote_file);
        //$mode = FTP_BINARY;
		$mode = ($mode == 'ascii') ? FTP_ASCII : FTP_BINARY;
		ftp_pasv($this->conn, true);//开启被动
		$result = ftp_put($this->conn, $remote_file, $local_file, $mode);//同步上传
		if ($result === FALSE) {
			$this->error = "文件上传失败";
			return FALSE;
		}

		return true;
	}

	/**
	 * 从ftp服务器下载文件到本地
	 * @param string $local_file 本地文件地址
	 * @param string $remote_file 远程文件地址
	 * @param string $mode 上传模式(ascii和binary其中之一)
	 */
	public function download($local_file = '', $remote_file = '', $mode = 'auto')
	{
		if ($mode == 'auto') {
			$ext = $this->_get_ext($remote_file);
			$mode = $this->_set_type($ext);
		}
		$mode = ($mode == 'ascii') ? FTP_ASCII : FTP_BINARY;
		$result = @ftp_get($this->conn, $local_file, $remote_file, $mode);
		if ($result === FALSE) {
			return FALSE;
		}
		return TRUE;
	}

	/**
	 * 删除ftp服务器端文件
	 * @param string $remote_file 文件地址
	 */
	public function delete_file($remote_file = '')
	{
		$result = @ftp_delete($this->conn, $remote_file);
		if ($result === FALSE) {
			return FALSE;
		}
		return TRUE;
	}

	/**
	 * ftp创建多级目录
	 * @param string $remote_file 要上传的远程图片地址
	 */
	private function _create_remote_dir($remote_file = '', $permissions = NULL)
	{
		$remote_dir = dirname($remote_file);
		$path_arr = explode('/', $remote_dir); // 取目录数组
        //dd($path_arr);
//            $file_name = array_pop($path_arr); // 弹出文件名
		$path_div = count($path_arr); // 取层数
		foreach ($path_arr as $val) // 创建目录
		{
			if (@ftp_chdir($this->conn, $val) == FALSE) {
				$tmp = @ftp_mkdir($this->conn, $val);//此处创建目录时不用使用绝对路径(不要使用:2018-02-20/ceshi/ceshi2,这种路径),因为下面ftp_chdir已经已经把目录切换成当前目录
				if ($tmp == FALSE) {
					echo "目录创建失败,请检查权限及路径是否正确!";
					exit;
				}
				if ($permissions !== NULL) {
					//修改目录权限
					$this->_chmod($val, $permissions);
				}
				@ftp_chdir($this->conn, $val);
			}
		}

		for ($i = 0; $i < $path_div; $i++) // 回退到根,因为上面的目录切换导致当前目录不在根目录
		{
			@ftp_cdup($this->conn);
		}
	}

	/**
	 * 递归删除ftp端目录
	 * @param string $remote_dir ftp目录地址
	 */
	public function delete_dir($remote_dir = '')
	{
		$list = $this->list_file($remote_dir);
		if (!empty($list)) {
			$count = count($list);
			for ($i = 0; $i < $count; $i++) {
				if (!preg_match('#\.#', $list[$i]) && !@ftp_delete($this->conn, $list[$i])) {
					//这是一个目录,递归删除
					$this->delete_dir($list[$i]);
				} else {
					$this->delete_file($list[$i]);
				}
			}
		}
		if (@ftp_rmdir($this->conn, $remote_dir) === FALSE) {
			return FALSE;
		}
		return TRUE;
	}

	/**
	 * 更改 FTP 服务器上的文件或目录名
	 * @param string $old_file 旧文件/文件夹名
	 * @param string $new_file 新文件/文件夹名
	 */
	public function remane($old_file = '', $new_file = '')
	{
		$result = @ftp_rename($this->conn, $old_file, $new_file);
		if ($result === FALSE) {
			$this->error = "移动失败";
			return FALSE;
		}
		return TRUE;
	}

	/**
	 * 列出ftp指定目录
	 * @param string $remote_path
	 */
	public function list_file($remote_path = '')
	{
		$contents = @ftp_nlist($this->conn, $remote_path);
		return $contents;
	}

	/**
	 * 获取文件的后缀名
	 * @param string $local_file
	 */
	private function _get_ext($local_file = '')
	{
		return (($dot = strrpos($local_file, '.')) == FALSE) ? 'txt' : substr($local_file, $dot + 1);
	}

	/**
	 * 根据文件后缀获取上传编码
	 * @param string $ext
	 */
	private function _set_type($ext = '')
	{
		//如果传输的文件是文本文件,可以使用ASCII模式,如果不是文本文件,最好使用BINARY模式传输。
		return in_array($ext, ['txt', 'text', 'php', 'phps', 'php4', 'js', 'css', 'htm', 'html', 'phtml', 'shtml', 'log', 'xml'], TRUE) ? 'ascii' : 'binary';
	}

	/**
	 * 修改目录权限
	 * @param $path 目录路径
	 * @param int $mode 权限值
	 */
	private function _chmod($path, $mode = 0755)
	{
		if (FALSE == @ftp_chmod($this->conn, $path, $mode)) {
			return FALSE;
		}
		return TRUE;
	}

	/**
	 * 登录Ftp服务器
	 */
	private function _login()
	{
		return @ftp_login($this->conn, $this->user, $this->pass);
	}

	/**
	 * 获取上传错误信息
	 */
	public function get_error_msg()
	{
		return $this->error;
	}

	/**
	 * 关闭ftp连接
	 * @return bool
	 */
	public function close()
	{
		return $this->conn ? @ftp_close($this->conn_id) : FALSE;
	}
}

调用上传方法,这里本地文件路径我用的是绝对路径,而上传路径则是相对ftp根目录的相对路径。

$ftp = new Ftp();
if($ftp->connect()){
$ftpRes = $ftp->upload(WWW_ROOT.$list['avatar'],$list['avatar']);
if (!$ftpRes){
dd($ftp->get_error_msg());
}
dd($ftpRes);
}else{
dd($ftp->get_error_msg());
}

3.开发调试说明

开发调试时可以通过filezilla终端查看链接和上传情况,这点很好用,开始没有发现这个,全靠代码中打断点,真的很繁琐,而且信息有时还不全。

广东互联网医院监管对接_前置机FTP文件上传

4.相关知识整理

FTP文件传输协议两种模式-主动模式和被动模式(转)

TCP/IP协议中,FTP标准命令TCP端口号为21,Port方式数据端口为20。FTP协议的任务是从一台计算机将文件传送到另一台计算机,它与这两台计算机所处的位置、联接的方式、甚至是是否使用相同的操作系统无关。假设两台计算机通过ftp协议对话,并且能访问Internet, 你可以用ftp命令来传输文件。每种操作系统使用上有某一些细微差别,但是每种协议基本的命令结构是相同的。 

  FTP的传输有两种方式:ASCII传输模式和二进制数据传输模式。

  1.ASCII传输方式:假定用户正在拷贝的文件包含的简单ASCII码文本,如果在远程机器上运行的不是UNIX,当文件传输时ftp通常会自动地调整文件的内容以便于把文件解释成另外那台计算机存储文本文件的格式。

  但是常常有这样的情况,用户正在传输的文件包含的不是文本文件,它们可能是程序,数据库,字处理文件或者压缩文件(尽管字处理文件包含的大部分是文本,其中也包含有指示页尺寸,字库等信息的非打印字符)。在拷贝任何非文本文件之前,用binary 命令告诉ftp逐字拷贝,不要对这些文件进行处理,这也是下面要讲的二进制传输。

  2.二进制传输模式:在二进制传输中,保存文件的位序,以便原始和拷贝的是逐位一一对应的。即使目的地机器上包含位序列的文件是没意义的。例如,macintosh以二进制方式传送可执行文件到Windows系统,在对方系统上,此文件不能执行。

  如果你在ASCII方式下传输二进制文件,即使不需要也仍会转译。这会使传输稍微变慢 ,也会损坏数据,使文件变得不能用。(在大多数计算机上,ASCII方式一般假设每一字符的第一有效位无意义,因为ASCII字符组合不使用它。如果你传输二进制文件,所有的位都是重要的。)如果你知道这两台机器是同样的,则二进制方式对文本文件和数据文件都是有效的。

  5. FTP的工作方式

  FTP支持两种模式,一种方式叫做Standard (也就是 PORT方式,主动方式),一种是 Passive (也就是PASV,被动方式)。 Standard模式 FTP的客户端发送 PORT 命令到FTP服务器。Passive模式FTP的客户端发送 PASV命令到 FTP Server。

  下面介绍一个这两种方式的工作原理:

  Port模式FTP 客户端首先和FTP服务器的TCP 21端口建立连接,通过这个通道发送命令,客户端需要接收数据的时候在这个通道上发送PORT命令。 PORT命令包含了客户端用什么端口接收数据。在传送数据的时候,服务器端通过自己的TCP 20端口连接至客户端的指定端口发送数据。 FTP server必须和客户端建立一个新的连接用来传送数据。

  Passive模式在建立控制通道的时候和Standard模式类似,但建立连接后发送的不是Port命令,而是Pasv命令。FTP服务器收到Pasv命令后,随机打开一个临时端口(也叫自由端口,端口号大于1023小于65535)并且通知客户端在这个端口上传送数据的请求,客户端连接FTP服务器此端口,然后FTP服务器将通过这个端口进行数据的传送,这个时候FTP server不再需要建立一个新的和客户端之间的连接。

  很多防火墙在设置的时候都是不允许接受外部发起的连接的,所以许多位于防火墙后或内网的FTP服务器不支持PASV模式,因为客户端无法穿过防火墙打开FTP服务器的高端端口;而许多内网的客户端不能用PORT模式登陆FTP服务器,因为从服务器的TCP 20无法和内部网络的客户端建立一个新的连接,造成无法工作。

  FTP软件可以更好的帮助你管理FTP目录 提供更系统的工具

  FTP工具推荐使用 cuteftp

  主动和被动模式FTP有两种使用模式:主动和被动。主动模式要求客户端和服务器端同时打开并且监听一个端口以建立连接。在这种情况下,客户端由于安装了防火墙会产生一些问题。所以,创立了被动模式。被动模式只要求服务器端产生一个监听相应端口的进程,这样就可以绕过客户端安装了防火墙的问题。

  一个主动模式的FTP连接建立要遵循以下步骤:

  客户端打开一个随机的端口(端口号大于1024,在这里,我们称它为x),同时一个FTP进程连接至服务器的21号命令端口。此时,源端口为随机端口x,在客户端,远程端口为21,在服务器。

  客户端开始监听端口(x+1),同时向服务器发送一个端口命令(通过服务器的21号命令端口),此命令告诉服务器客户端正在监听的端口号并且已准备好从此端口接收数据。这个端口就是我们所知的数据端口。

  服务器打开20号源端口并且建立和客户端数据端口的连接。此时,源端口为20,远程数据端口为(x+1)。

  客户端通过本地的数据端口建立一个和服务器20号端口的连接,然后向服务器发送一个应答,告诉服务器它已经建立好了一个连接。

FTP有两种使用模式:主动和被动。主动模式要求客户端和服务器端同时打开并且监听一个端口以建立连接。在这种情况下,客户端由于安装了防火墙会产生一些问题。所以,创立了被动模式。被动模式只要求服务器端产生一个监听相应端口的进程,这样就可以绕过客户端安装了防火墙的问题。[4]

一个主动模式的FTP连接建立要遵循以下步骤:
1.客户端打开一个随机的端口(端口号大于1024,在这里,我们称它为x),同时一个FTP进程连接至服务器的21号命令端口。此时,源端口为随机端口x,在客户端,远程端口为21,在服务器。

2.客户端开始监听端口(x+1),同时向服务器发送一个端口命令(通过服务器的21号命令端口),此命令告诉服务器客户端正在监听的端口号并且已准备好从此端口接收数据。这个端口就是我们所知的数据端口。

3.服务器打开20号源端口并且建立和客户端数据端口的连接。此时,源端口为20,远程数据端口为(x+1)。

4.客户端通过本地的数据端口建立一个和服务器20号端口的连接,然后向服务器发送一个应答,告诉服务器它已经建立好了一个连接。
被动模式FTP:
为了解决服务器发起到客户的连接的问题,人们开发了一种不同的FTP连接方式。这就是所谓的被动方式,或者叫做PASV,当客户端通知服务器它处于被动模式时才启用。
在被动方式FTP中,命令连接和数据连接都由客户端发起,这样就可以解决从服务器到客户端的数据端口的入方向连接被防火墙过滤掉的问题。
当开启一个 FTP连接时,客户端打开两个任意的非特权本地端口(N > 1024和N+1)。第一个端口连接服务器的21端口,但与主动方式的FTP不同,客户端不会提交PORT命令并允许服务器来回连它的数据端口,而是提交 PASV命令。这样做的结果是服务器会开启一个任意的非特权端口(P > 1024),并发送PORT P命令给客户端。然后客户端发起从本地端口N+1到服务器的端口P的连接用来传送数据。
对于服务器端的防火墙来说,必须允许下面的通讯才能支持被动方式的FTP:
1. 从任何大于1024的端口到服务器的21端口 (客户端的初始化连接)
2.服务器的21端口到任何大于1024的端口 (服务器响应到客户端的控制端口的连接)
3.从任何大于1024端口到服务器的大于1024端口 (客户端初始化数据连接到服务器指定的任意端口)
4.服务器的大于1024端口到远程的大于1024的端口(服务器发送ACK响应和数据到客户端的数据端口)
编辑本段协议结构
命令描述
ABOR中断数据连接程序
ACCT <account>系统特权帐号
ALLO <bytes>为服务器上的文件存储器分配字节
APPE <filename>添加文件到服务器同名文件
CDUP <dir path>改变服务器上的父目录
CWD <dir path>改变服务器上的工作目录
DELE <filename>删除服务器上的指定文件
HELP <command>返回指定命令信息
LIST <name>如果是文件名列出文件信息,如果是目录则列出文件列表
MODE <mode>传输模式(S=流模式,B=块模式,C=压缩模式)
MKD <directory>在服务器上建立指定目录
NLST <directory>列出指定目录内容
NOOP无动作,除了来自服务器上的承认
PASS <password>系统登录密码
PASV请求服务器等待数据连接
PORT <address>IP 地址和两字节的端口 ID
PWD显示当前工作目录
QUIT从 FTP 服务器上退出登录
REIN重新初始化登录状态连接
REST <offset>由特定偏移量重启文件传递
RETR <filename>从服务器上找回(复制)文件
RMD <directory>在服务器上删除指定目录
RNFR <old path>对旧路径重命名
RNTO <new path>对新路径重命名
SITE <params>由服务器提供的站点特殊参数
SMNT <pathname>挂载指定文件结构
STAT <directory>在当前程序或目录上返回信息
STOR <filename>储存(复制)文件到服务器上
STOU <filename>储存文件到服务器名称上
STRU <type>数据结构(F=文件,R=记录,P=页面)
SYST返回服务器使用的操作系统
TYPE <data type>数据类型(A=ASCII,E=EBCDIC,I=binary)
USER <username>>系统登录的用户名

标准 FTP 信息如下

响应代码解释说明
110新文件指示器上的重启标记
120服务器准备就绪的时间(分钟数)
125打开数据连接,开始传输
150打开连接
200成功
202命令没有执行
211系统状态回复
212目录状态回复
213文件状态回复
214帮助信息回复
215系统类型回复
220服务就绪
221退出网络
225打开数据连接
226结束数据连接
227进入被动模式(IP 地址、ID 端口)
230登录因特网
250文件行为完成
257路径名建立
331要求密码
332要求帐号
350文件行为暂停
421服务关闭
425无法打开数据连接
426结束连接
450文件不可用
451遇到本地错误
452磁盘空间不足
500无效命令
501错误参数
502命令没有执行
503错误指令序列
504无效命令参数
530未登录网络
532存储文件需要帐号
550文件不可用
551不知道的页类型
552超过存储分配
553文件名不允许

原创文章,作者:Zeyu,如若转载,请注明出处:https://jinzhijun.cn/develop/466

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注