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); /* 打开目标文件 */ |