ftplib源码分析
FTP协议
相比其他协议,如 HTTP 协议,FTP 协议要复杂一些。与一般的 C/S 应用不同点在于一般的C/S 应用程序一般只会建立一个 Socket 连接,这个连接同时处理服务器端和客户端的连接命令和数据传输。而FTP协议中将命令与数据分开传送的方法提高了效率。
本文环境: * OS:Ubuntu 18.04.4 LTS 还有 Windows 10专业版 * ftplib:V4.0-1 * gcc: 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)
命令结构
FTP 每个命令都有 3 到 4 个大写字母组成,命令后面跟参数,用空格分开。每个命令都以 ""结束(应答也是用""结束),例如发送一个CWD
命令,那要要发送数据就是:CWD dirname\r\n
。
常见的FTP命令有:
响应结构
FTP响应跟命令结构是类似的。
FTP响应通常是单行的,格式为"响应码+空格+提示信息+"。如果需要产生一条多行应答,第1行在第3位数字应答码之后包含一个连字符"-",而不是空格,最后一行包含相同的3位数字应答码,后跟一个空格字符。
FTP响应码
客户端发送 FTP 命令后,服务器返回响应码。
响应码用三位数字编码表示:
第一个数字给出了命令状态的一般性指示,比如响应成功、失败或不完整。
第二个数字是响应类型的分类,如 2 代表跟连接有关的响应,3 代表用户认证。
第三个数字提供了更加详细的信息。
第一个数字的含义如下:
- 1 表示服务器正确接收信息,还未处理。
- 2 表示服务器已经正确处理信息。
- 3 表示服务器正确接收信息,正在处理。
- 4 表示信息暂时错误。
- 5 表示信息永久错误。
第二个数字的含义如下:
* 0 表示语法。 * 1 表示系统状态和信息。 * 2 表示连接状态。 * 3 表示与用户认证有关的信息。 * 4 表示未定义。 * 5 表示与文件系统有关的信息。
例子
用客户端登录 FTP 服务器
为例子
大致调用函数过称为: 1
2
3
4
5
6
7
8
9
10
11
12
13/* 命令 ”USER username\r\n” */
sprintf(send_buf,"USER %s\r\n",username);
/*客户端发送用户名到服务器端 */
write(control_sock, send_buf, strlen(send_buf));
/* 客户端接收服务器的响应码和信息,正常为 ”331 User name okay, need password.” */
read(control_sock, read_buf, read_len);
/* 命令 ”PASS password\r\n” */
sprintf(send_buf,"PASS %s\r\n",password);
/* 客户端发送密码到服务器端 */
write(control_sock, send_buf, strlen(send_buf));
/* 客户端接收服务器的响应码和信息,正常为 ”230 User logged in, proceed.” */
read(control_sock, read_buf, read_len);
源码分析
ftplib在这里下载。
登录了FTP服务器后,肯定需要一个句柄的量,在这个ftplib
中是netbuf
: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19typedef struct NetBuf netbuf;
struct NetBuf {
char *cput,*cget;
int handle;
int cavail,cleft;
char *buf;
int dir;
netbuf *ctrl;
netbuf *data;
int cmode;
struct timeval idletime;
FtpCallback idlecb;
void *idlearg;
unsigned long int xfered;
unsigned long int cbbytes;
unsigned long int xfered1;
char response[RESPONSE_BUFSIZ];
};handle
字段其实就存了tcp握手成功后的socket
。
FtpSendCmd函数
首先来看FtpSendCmd
函数,这个函数顾名思义,就是用来发送FTP命令,你在FtpPwd
、FtpNlst
、FtpDir
、FtpGet
等函数中都可以看到它: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18static int FtpSendCmd(const char *cmd, char expresp, netbuf *nControl)
{
char buf[TMP_BUFSIZ];
if (nControl->dir != FTPLIB_CONTROL)
return 0;
if (ftplib_debug > 2)
fprintf(stderr,"%s\n",cmd);
if ((strlen(cmd) + 3) > sizeof(buf))
return 0;
sprintf(buf,"%s\r\n",cmd);
if (net_write(nControl->handle, buf, strlen(buf)) <= 0)
{
if (ftplib_debug)
perror("write");
return 0;
}
return readresp(expresp, nControl);
}net_write
函数操作socket发送数据 2. readresp
函数读取服务器响应
net_write函数
我们查看一下net_write
,其实它就是封装了一下write
函数,因为TCP通信对于应用程序来说是完全异步,你调用write
写入5个字节,返回不一定是5个字节,可能是3个,4个,所以net_write
多次调用了write
(在《UNIX网络编程 卷1》中,作者也有类似的封装)。另外,write
返回成功了也只代表buf中的数据被复制到了kernel中的TCP发送缓冲区,至于数据什么时候被发往网络,什么时候被对方主机接收,什么时候被对方进程读取,系统调用层面不会给予任何保证和通知。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24int net_write(int fd, const char *buf, size_t len)
{
int done = 0;
while ( len > 0 )
{
int c = write( fd, buf, len );
if ( c == -1 )
{
if ( errno != EINTR && errno != EAGAIN )
return -1;
}
else if ( c == 0 )
{
return done;
}
else
{
buf += c;
done += c;
len -= c;
}
}
return done;
}
readresp函数
发送了FTP的命令数据后,就需要用socket接受响应数据了,切记TCP是流式传输的,所以你需要自己做应用层的解析。 FTP的消息块的分割符是\r\n
,看readline
函数名应该是读取一行数据 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33static int readresp(char c, netbuf *nControl)
{
char match[5];
if (readline(nControl->response, RESPONSE_BUFSIZ, nControl) == -1)
{
if (ftplib_debug)
perror("Control socket read failed");
return 0;
}
if (ftplib_debug > 1)
fprintf(stderr,"%s",nControl->response);
if (nControl->response[3] == '-')
{
strncpy(match,nControl->response,3);
match[3] = ' ';
match[4] = '\0';
do
{
if (readline(nControl->response, RESPONSE_BUFSIZ, nControl) == -1)
{
if (ftplib_debug)
perror("Control socket read failed");
return 0;
}
if (ftplib_debug > 1)
fprintf(stderr,"%s",nControl->response);
}
while (strncmp(nControl->response, match, 4));
}
if (nControl->response[0] == c)
return 1;
return 0;
}
readline函数
1 | static int readline(char *buf, int max, netbuf *ctl) |
socket_wait()函数
这个函数用了select
系统函数,用来检测ctl->handle
是否可读(这里可读的时候就是服务端发过来响应数据了)。select函数的返回值:返回-1表示调用select函数时有错误发生,具体的错误在Linux可通过errno输出来查看;返回0,表示select函数超时;返回正数即调用select函数成功,表示集合中文件描述符的数量,集合也会被修改以显示哪一个文件描述符已准备就绪。
不过在用来发送命令的socket
上(也就是调用FtpConnect
函数得到的那个socket
),因为ctl->dir
是FTPLIB_CONTROL
(ctl->idlecb
也是NULL),所以直接返回了1。
1 | /* |
net_read函数
这个函数简单,用了read
函数读到了数据,就立马返回,但也要注意,你读10个字节,也不一定能读取10个字节,可能会比10个字节小,因为read
总是在接收缓冲区有数据时立即返回,而不是等到给定的read buffer填满时返回。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int net_read(int fd, char *buf, size_t len)
{
while ( 1 )
{
int c = read(fd, buf, len);
if ( c == -1 )
{
if ( errno != EINTR && errno != EAGAIN )
return -1;
}
else
{
return c;
}
}
}