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命令有:

响应结构
FTP响应跟命令结构是类似的。
FTP响应通常是单行的,格式为”响应码+空格+提示信息+\r\n”。如果需要产生一条多行应答,第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命令,你在FtpPwd
、FtpNlst
、FtpDir
、FtpGet
等函数中都可以看到它:
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); }
|
这个函数的过程:
- 用
net_write
函数操作socket发送数据
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->dir
是FTPLIB_CONTROL
(ctl->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
|
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; } } }
|
参考: