目录

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 个大写字母组成,命令后面跟参数,用空格分开。每个命令都以 “\r\n"结束(应答也是用”\r\n"结束),例如发送一个CWD命令,那要要发送数据就是:CWD dirname\r\n

常见的FTP命令有:
https://s1.ax1x.com/2020/05/12/YNtW3F.png

响应结构

FTP响应跟命令结构是类似的。
FTP响应通常是单行的,格式为"响应码+空格+提示信息+\r\n"。如果需要产生一条多行应答,第1行在第3位数字应答码之后包含一个连字符"-",而不是空格,最后一行包含相同的3位数字应答码,后跟一个空格字符。

FTP响应码

客户端发送 FTP 命令后,服务器返回响应码。
响应码用三位数字编码表示:
第一个数字给出了命令状态的一般性指示,比如响应成功、失败或不完整。
第二个数字是响应类型的分类,如 2 代表跟连接有关的响应,3 代表用户认证。
第三个数字提供了更加详细的信息。

第一个数字的含义如下:

  • 1 表示服务器正确接收信息,还未处理。
  • 2 表示服务器已经正确处理信息。
  • 3 表示服务器正确接收信息,正在处理。
  • 4 表示信息暂时错误。
  • 5 表示信息永久错误。

第二个数字的含义如下:

  • 0 表示语法。
  • 1 表示系统状态和信息。
  • 2 表示连接状态。
  • 3 表示与用户认证有关的信息。
  • 4 表示未定义。
  • 5 表示与文件系统有关的信息。

例子

客户端登录 FTP 服务器为例子 https://s1.ax1x.com/2020/05/18/YfCuff.jpg

大致调用函数过称为:

 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;
        }
    }
}

参考: