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
19
typedef 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命令,你在FtpPwdFtpNlstFtpDirFtpGet等函数中都可以看到它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static 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);
}
这个函数的过程: 1. 用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
24
int 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
33
static 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
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
static int readline(char *buf, int max, netbuf *ctl)
{
int x, retval = 0;
char *end,*bp=buf;
int eof = 0;

if ((ctl->dir != FTPLIB_CONTROL) && (ctl->dir != FTPLIB_READ))
return -1;
if (max == 0)
return 0;
do
{
if (ctl->cavail > 0)
{
x = (max >= ctl->cavail) ? ctl->cavail : max-1;
end = memccpy(bp, ctl->cget, '\n',x);
if (end != NULL)
x = end - bp;
retval += x;
bp += x;
*bp = '\0';
max -= x;
ctl->cget += x;
ctl->cavail -= x;
if (end != NULL)
{
bp -= 2;
if (strcmp(bp,"\r\n") == 0)
{
*bp++ = '\n';
*bp++ = '\0';
--retval;
}
break;
}
}
if (max == 1)
{
*buf = '\0';
break;
}
if (ctl->cput == ctl->cget)
{
ctl->cput = ctl->cget = ctl->buf;
ctl->cavail = 0;
ctl->cleft = FTPLIB_BUFSIZ;
}
if (eof)
{
if (retval == 0)
retval = -1;
break;
}
if (!socket_wait(ctl))
return retval;
if ((x = net_read(ctl->handle, ctl->cput, ctl->cleft)) == -1)
{
if (ftplib_debug)
perror("read");
retval = -1;
break;
}
if (x == 0)
eof = 1;
ctl->cleft -= x;
ctl->cavail += x;
ctl->cput += x;
}
while (1);
return retval;
}

socket_wait()函数

这个函数用了select系统函数,用来检测ctl->handle是否可读(这里可读的时候就是服务端发过来响应数据了)。select函数的返回值:返回-1表示调用select函数时有错误发生,具体的错误在Linux可通过errno输出来查看;返回0,表示select函数超时;返回正数即调用select函数成功,表示集合中文件描述符的数量,集合也会被修改以显示哪一个文件描述符已准备就绪。
不过在用来发送命令的socket上(也就是调用FtpConnect函数得到的那个socket),因为ctl->dirFTPLIB_CONTROLctl->idlecb也是NULL),所以直接返回了1。

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
33
34
35
36
37
38
39
/*
* socket_wait - wait for socket to receive or flush data
*
* return 1 if no user callback, otherwise, return value returned by
* user callback
*/
static int socket_wait(netbuf *ctl)
{
fd_set fd,*rfd = NULL,*wfd = NULL;
struct timeval tv;
int rv = 0;
if ((ctl->dir == FTPLIB_CONTROL) || (ctl->idlecb == NULL))
return 1;
if (ctl->dir == FTPLIB_WRITE)
wfd = &fd;
else
rfd = &fd;
FD_ZERO(&fd);
do
{
FD_SET(ctl->handle,&fd);
tv = ctl->idletime;
rv = select(ctl->handle+1, rfd, wfd, NULL, &tv);
if (rv == -1)
{
rv = 0;
strncpy(ctl->ctrl->response, strerror(errno),
sizeof(ctl->ctrl->response));
break;
}
else if (rv > 0)
{
rv = 1;
break;
}
}
while ((rv = ctl->idlecb(ctl, ctl->xfered, ctl->idlearg)));
return rv;
}

net_read函数

这个函数简单,用了read函数读到了数据,就立马返回,但也要注意,你读10个字节,也不一定能读取10个字节,可能会比10个字节小,因为read总是在接收缓冲区有数据时立即返回,而不是等到给定的read buffer填满时返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int 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;
}
}
}

参考: * 使用 Socket 通信实现 FTP 客户端程序 * 网络编程中的read,write函数