FTP 协议简析
FTP(File Transfer Protocol)是 TCP/IP 协议组中的协议之一,FTP 的工作就是完成两台计算机之间的拷贝
在 TCP/IP 协议中, 需要两个端口,一个是数据端口,一个是控制端口
- 控制端口一般为21,而数据端口不一定是20,这和 FTP 的应用模式有关:
- 如果是主动模式,应该为20
- 如果为被动模式,由服务器端和客户端协商而定
- FTP 协议要用到两个 TCP 连接:
- 一个是命令链路,用来在 FTP 客户端与服务器之间传递命令
- 另一个是数据链路,用来上传或下载数据
主动模式与被动模式
FTP 支持两种模式,一种方式叫做 Standard(也就是 PORT 方式,主动方式),一种是 Passive(也就是 PASV,被动方式),下面介绍一个这两种方式的工作原理:
主动 FTP :
- 命令连接:客户端向服务器的 FTP 端口(默认是21)发送连接请求,服务器接受连接,建立一条命令链路
- 客户端 >1024 端口 → 服务器 21 端口
- 数据连接:客户端在命令链路上用 PORT 命令告诉服务器自己开启的端口,于是服务器从自己的 20 端口去连接客户端开启的端口并传输数据
- 客户端 >1024 端口 ← 服务器 20 端口
被动 FTP :
- 命令连接:客户端向服务器的 FTP 端口(默认是21)发送连接请求,服务器接受连接,建立一条命令链路
- 客户端 >1024 端口 → 服务器 21 端口
- 数据连接:客户端在命令链路上用 PASV 命令告诉服务器使用被动模式,于是服务器开启一个端口专门提供给客户端使用,然后客户端主动连接服务器端口来传输数据
- 客户端 >1024 端口 → 服务器 > 1024 端口
FTP 服务器的代码实现
FTP 需要实现的功能如下:
- 读取配置信息
- 与目标主机建立连接(使用 socket)
- 实现 FTP 命令集
这里就以轻量级的 FTP 服务器 LightFTP
为例,来学习一下 FTP 服务器的代码实现
LightFTP 读取配置信息
在 LightFTP
中,使用 config_parse
函数来读取文件信息:
1 | int config_parse( |
该函数的参数如下:
pcfg
:初始化函数config_init
返回的缓冲区(配置文件fftp.conf
中的内容被 Read 入其中)section_name
:配置段名称,在配置文件fftp.conf
中被大括号[]
括起来的内容,用于区分某段配置(初始化配置名称为ftpconfig
)key_name
:关键字名称,记录有在配置文件fftp.conf
中写入的关键信息,程序初始化时会把等号=
后面的内容读入内存value
:用于外传数据的指针(程序会把目标key_name
对应的值写入value
中)value_size_max
:用于表示value
的最大值
为了理解这些参数,我们需要其他代码进行辅助,就从 config_init
函数开始:
1 | char *config_init(char *cfg_filename) |
- 程序默认的
cfg_filename
就是fftp.conf
- 这个函数的功能就是把配置文件读取到本地内存
buffer
中,然后返回buffer
1 | if (argc > 1) |
config_init
的返回值,将会放入config_parse
中(作为第一个参数pcfg
)
接下来看一看配置文件 fftp.conf
的内容:
1 | [ftpconfig] |
- 大括号容就是
section_name
- 等号左边的内容就是
key_name
,右边的内容就是将要被设置的值
使用函数 config_parse
需要指定 section_name
和 key_name
,该函数会把对应的值提取到参数 value
中,然后外传到上层函数中
1 | int config_parse( |
函数 config_parse
总体的思路就是利用2个 while
循环来变量所有的 section_name
和 key_name
:
- 外层的循环会匹配大括号
[]
,用一个while
读取出section_name
然后进行匹配:
1 | if (strcmp(vname, section_name) == 0) |
- 内层的循环会匹配等号
=
,用一个while
读取出key_name
然后进行匹配:
1 | if (strcmp(vname, key_name) == 0) |
- 最后用一个
while
把等号右边的数据存储到value
中:
1 | while ((*p != '\n') &&(*p != 0)) { |
函数 config_parse
最后返回的 value
将被会存储在结构体 FTP_CONFIG
中:
1 | typedef struct _FTP_CONFIG { |
LightFTP 与目标主机建立连接
把服务器中 socket bind listen
的代码提取出来:
1 | ftpsocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); /* 基于IPv4和TCP */ |
setsockopt
函数可以对 socket 进行详细的设置SO_REUSEADDR
参数提供如下四个功能:SO_REUSEADDR
允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在(这通常是重启监听服务器时出现,若不设置此选项,则bind
时将出错)SO_REUSEADDR
允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地 IP 地址即可(对于 TCP,我们根本不可能启动捆绑相同 IP 地址和相同端口号的多个服务器)SO_REUSEADDR
允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定不同的本地 IP 地址即可。这一般不用于 TCP 服务器SO_REUSEADDR
允许完全重复的捆绑:当一个 IP 地址和端口绑定到某个套接口上时,还允许此 IP 地址和端口捆绑到另一个套接口上(一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言,TCP不支持多播)
然后再循环里面 accept
客户端的请求:
1 | while ( socketret == 0 ) { |
g_cfg.MaxUsers
:服务器可以并发处理的客户端最大数目
LightFTP 实现 FTP 命令集
实现 FTP 命令集的函数就是 ftp_client_thread
在此之前需要介绍一下 LightFTP 中的一个结构体数组:
1 | static const FTPROUTINE_ENTRY ftpprocs[MAX_CMDS] = { |
- 乍一看这个结构有点像 Python 中的字典(用于把 “FTP 命令名称” 和 “对应的函数指针” 绑定)
- 但这其实是
FTPROUTINE_ENTRY
类型的数组
1 | typedef struct _FTPROUTINE_ENTRY { |
使用这个结构体数组的代码如下:
1 | while ( ctx.ControlSocket != INVALID_SOCKET ) { |
- 一个简单的遍历匹配
最后单独介绍几个 FTP 命令的实现:
- 匹配密码和登录设置:
1 | int ftpPASS(PFTPCONTEXT context, const char *params) |
- 列出目标目录中所有的文件:
1 | int ftpLIST(PFTPCONTEXT context, const char *params) |
- 从服务器上下载目标文件:
1 | int ftpRETR(PFTPCONTEXT context, const char *params) |
1 | f = open(context->FileName, O_RDONLY); /* 打开目标文件 */ |