文件传输协议FTP(File Transfer Protocol,由RFC 959描述)。linux
FTP工做在TCP/IP协议族的应用层,其传输层使用的是TCP协议,它是基于客户/服务器模式工做的。git
①、ASCII码文件,这是FTP默认的文本格式【经常使用】web
②、EBCDIC码文件,它也是一种文本类型文件,用8位代码表示一个字符,该文本文件在传输时要求两端都使用EBCDIC码【不经常使用】windows
③、图象(Image)文件,也称二进制文件类型,发送的数据为连续的比特流,一般用于传输二进制文件【经常使用】bash
④、本地文件,字节的大小有本地主机定义,也就是说每一字节的比特数由发送方规定【不经常使用】服务器
因此咱们要实现的FTP也只支持ASCII码文件和图像文件类型,对于这两种文件类型,到底有什么区别呢?下面作一个简要的介绍:session
对于文本文件和二进制文件,最直观的区别就是文本文件是能够查看,而二进制打开看到的是乱码,实际上,这二者在物理结构(或存储结构)上都是由一系列的比特位构成的,它们之间的区别仅仅是在逻辑上:ASCII码文件是由7个比特位构成,最高位老是0(由于一个字节=8位),因此ASCII码文件最多能表示的字符数为2^7=128个,经过man帮助也能看到:数据结构
而若是最高位为1则打开就会是乱码,也就是二进制文件最高位应该就是1,这是一个区别。多线程
另一个区别就是\r\n换行符,在不一样平台上是不同的:windows上换行是用\r\n表示;linux上换行是用\n表示;mac上换行是用\r表示。若是在传输文件的时候,以这两种文件类型传输实际上对\r\n的解析方式是不一样的,至于有什么不一样,这里经过FTP客户端链接FTP服务端来作一个演示,首先启动FTP服务器,这里用vsftpd服务器:socket
接下来进行ftp文件配置:
配置好以后接下来从新启动vsftpd服务:
接下来用一个ftp客户端来进行链接,链接ftp服务器的客户端有不少工具,这里用“LeapFtp”:
接下来新建一个文件进行上传:
能够用十六进制的文本编辑器来查看一下内容:
接下来开始上传它至FTP服务器:
上传以后的大小也是8个字节,来用命令查看一下:
那若是是用二进制文件上传又会是怎么样呢?
那这两种类型难道没有差异么,实际上在个人机器上是没差异,在有些机器上是有区别的,区别以下:
若是以ASCII方式来传输文件,而且从windows->linux会将\r\n转换成\n,而从linux->windows会将\n转换成\r\n;而若是以二进制文件来传输文件,那么不作任何转换。
在C语言阶段其实咱们也学过了打开文件能够以ASCII和二进制两种方式打开,这二者的区别也就只是换行符的不一样,跟上面同样。
文件结构,这是FTP默认的方式,文件被认为是一个连续的字节流,文件内部没有表示结构的信息。
记录结构,该结构只适用于文本文件(ASCII码或EBCDIC码文件)。记录结构文件是由连续的记录构成的。
页结构,在FTP中,文件的一个部分被称为页。当文件是由非连续的多个部分组成时,使用页结构,这种文件称为随机访问文件。每页都带有页号发送,以便收方能随机地存储各页。
流方式,这是支持文件传输的默认方式,文件以字节流的形式传输。【主流FTP也仅仅实现了这种方式】
块方式,文件以一系列块来传输,每块前面都带有本身的头部。头部包含描述子代码域(8位)和计数域(16位),描述子代码域定义数据块的结束标志登内容,计数域说明了数据块的字节数。
压缩方式,用来对连续出现的相同字节进行压缩,如今已不多使用。
在客户端,经过交互式的用户界面,客户从终端上输入启动FTP的用户交互式命令
客户端TCP协议层根据用户命令给出的服务器IP地址,向服务器提供FTP服务的21端口(该端口是TCP协议层用来传输FTP命令的端口)发出主动创建链接的请求。服务器收到请求后,经过3次握手,就在进行FTP命令处理的用户协议解释器进程和服务器协议解释器进程之间创建了一条TCP链接。
之后全部用户输入的FTP命令和服务器的应答都由该链接进行传输,所以把它叫作控制链接。
当客户经过交互式的用户界面,向FTP服务器发出要下载服务器上某一文件的命令时,该命令被送到用户协议解释器。
其中用户的动做会解析成相对应的一些FTP命令,如看到的:
其实也能够用windows的命令来进行FTP链接,也能很清晰地看出用户的每一个动做都会解析成对应的FTP命令:
服务器经过控制链接发送给客户的FTP应答,由ASCII码形式的3位数字和一行文本提示信息组成,它们之间用一个空格分割。应答信息的每行文本以回车<CR>
和换行<LF>
对结尾。
若是须要产生一条多行的应答,第一行在3位数字应答代码以后包含一个连字符“-”,而不是空格符;最后一行包含相同的3位数字应答代码,后跟一个空格符,关于这个能够实际查看下:
确保在文件传输过程当中的请求和正在执行的动做保持一致
保证用户程序老是能够获得服务器的状态信息,用户能够根据收到的状态信息对服务器是否正常执行了有关操做进行断定。
第一位数字标识了响应是好,坏或者未完成
第二位数响应大概是发生了什么错误(好比,文件系统错误,语法错误)
第三位为第二位数字更详细的说明
如:
500 Syntax error, command unrecognized. (语法错误,命令不能被识别)可能包含由于命令行太长的错误。
501 Syntax error in parameters or arguments. (参数语法错误)
502 Command not implemented. (命令没有实现)
503 Bad sequence of commands. (命令顺序错误)
504 Command not implemented for that parameter. (没有实现这个命令参数)
#define FTP_DATACONN 150
#define FTP_NOOPOK 200
#define FTP_TYPEOK 200
#define FTP_PORTOK 200
#define FTP_EPRTOK 200
#define FTP_UMASKOK 200
#define FTP_CHMODOK 200
#define FTP_EPSVALLOK 200
#define FTP_STRUOK 200
#define FTP_MODEOK 200
#define FTP_PBSZOK 200
#define FTP_PROTOK 200
#define FTP_OPTSOK 200
#define FTP_ALLOOK 202
#define FTP_FEAT 211
#define FTP_STATOK 211
#define FTP_SIZEOK 213
#define FTP_MDTMOK 213
#define FTP_STATFILE_OK 213
#define FTP_SITEHELP 214
#define FTP_HELP 214
#define FTP_SYSTOK 215
#define FTP_GREET 220
#define FTP_GOODBYE 221
#define FTP_ABOR_NOCONN 225
#define FTP_TRANSFEROK 226
#define FTP_ABOROK 226
#define FTP_PASVOK 227
#define FTP_EPSVOK 229
#define FTP_LOGINOK 230
#define FTP_AUTHOK 234
#define FTP_CWDOK 250
#define FTP_RMDIROK 250
#define FTP_DELEOK 250
#define FTP_RENAMEOK 250
#define FTP_PWDOK 257
#define FTP_MKDIROK 257
#define FTP_GIVEPWORD 331
#define FTP_RESTOK 350
#define FTP_RNFROK 350
#define FTP_IDLE_TIMEOUT 421
#define FTP_DATA_TIMEOUT 421
#define FTP_TOO_MANY_USERS 421
#define FTP_IP_LIMIT 421
#define FTP_IP_DENY 421
#define FTP_TLS_FAIL 421
#define FTP_BADSENDCONN 425
#define FTP_BADSENDNET 426
#define FTP_BADSENDFILE 451
#define FTP_BADCMD 500
#define FTP_BADOPTS 501
#define FTP_COMMANDNOTIMPL 502
#define FTP_NEEDUSER 503
#define FTP_NEEDRNFR 503
#define FTP_BADPBSZ 503
#define FTP_BADPROT 503
#define FTP_BADSTRU 504
#define FTP_BADMODE 504
#define FTP_BADAUTH 504
#define FTP_NOSUCHPROT 504
#define FTP_NEEDENCRYPT 522
#define FTP_EPSVBAD 522
#define FTP_DATATLSBAD 522
#define FTP_LOGINERR 530
#define FTP_NOHANDLEPROT 536
#define FTP_FILEFAIL 550
#define FTP_NOPERM 550
#define FTP_UPLOADFAIL 553
上次咱们说过,FTP是由两种类型的链接构成的,一种是控制链接【主要是接收FTP客户端发来的命令请求,而且对这些命令进行应答】,一种是数据链接【双方之间进行数据的传输,包括目录列表的传输以及文件的传输】,其中控制链接老是由客户端向服务器发起,而数据链接则不一样了,它有两种工做模式:主动模式【由服务器向客户端发起链接而创建数据链接通道】和被动模式【由客户端向服务器发起链接而创建数据链接通道】。下面来看一下这两个工做模式的工做过程:
FTP客户端首先向服务器端的21端口发起链接,通过三次握手建设立控制链接通道,客户端本地也会选择一个动态的端口号AA,一旦控制链接通道创建以后,双方就能够交换信息了:客户端能够经过控制链接通道发起命令请求,服务器也能够经过它向客户端对这些命令请求进行应答。
接下来,若是要涉及到数据的传输,势必要建立一个数据链接:
在建立数据链接以前,要选择工做模式,若是是PORT模式,客户端会上服务器端发送PORT命令,这也是经过控制链接通道完成的,向服务器的21端口传送一个PORT命令,而且告知客户端的一个端口号BB,由于这个信息服务器端才知道要链接客户端的哪一个端口号,服务器端获得了这个信息,最后就向BB端口号发起了一个请求,创建了一个数据链接通道,数据链接通道一旦创建完毕,就能够进行数据的传输了,包含目录列表、文件的传输,一旦数据传输完毕,数据链接通道就会关闭掉,它是临时的。
这里须要注意一点:
接下来用实验来讲明一下双方创建的详细命令,这边经过登陆一个客户端来看一下双方之间所交换的命令:
接下来进行数据传输,假设要传输一个列表,刷新一下。在得到列表以前须要建立一个数据链接,而在建立数据链接时须要根据模式来建立数据链接,这里面采用的是PORT模式:
其整个的工做过程以下:
在了解了主动模式以后,被动模式就比较好理解了,以下:
从中能够发现,主被动模式只是链接创建的方向不一样而已,一样的,也经过实验来查看一下PASV模式所要交换的FTP命令:
这时一样请求列表:
其整个的工做过程以下:
以上就是FTP的两种工做模式,那为何要有这两种模式呢?这其实是跟NAT或防火墙对主被动模式有关系,下面就来了解下:
NAT的全称是(Network Address Translation),经过NAT能够将内网私有IP地址转换为公网IP地址。必定程度上解决了公网地址不足的问题。
其地址映射关系能够以下:
192.168.1.100:5678【内网IP】 -> 120.35.3.193:5678【NAT转换IP】 -> 50.118.99.200:80【外网IP】
从而就创建了一个链接,而链接的创建是经过NAT服务器进行地址转换完成的。
创建控制链接通道
由于NAT会主动记录由内部发送外部[相反则没法记录]的链接信息,而控制链接通道的创建是由客户向服务器端链接的,所以这一条接能够顺利地创建起来。
复制代码
客户端与服务器端数据链接创建时的通知
客户端先启用PORT BB端口,并经过命令通道告知FTP服务器,且等待服务器端的主动链接。
复制代码
服务器主动链接客户端
因为经过NAT转换以后,服务器只能得知NAT的地址并不知道客户端的IP地址,所以FTP服务器会以20端口主动地向NAT的PORT BB端口发送主动链接请求,但NAT并无启用PORT BB端口,于是链接被拒绝。
复制代码
咱们要将程序中的开关作成可配置的,这里能够看一下VSFTP的配置文件:
保存并重启VSFTP服务:
可见过了5秒空闲链接就断开了,这时进程也结束了:
也就是上传跟下载文件的限速功能,下面也来演示一下,默认状况下是没有限速的:
其速度传输过程序中会慢慢降到100K的样子。
这里包含两个方面的限制:总链接数的限制,针对全部IP来讲的、同一个IP链接数的限制,下面来进行配置:
接下来配置同一个IP的链接数的限制:
当成功链接一个客户端时,这时能够看到建立了两个进程:
可见该FTP服务器是采用多进程的方式来实现的,为何不用多线程的方式呢?
对于FTP服务器来说,多线程的方式是绝对不可取的,由于:
那为何链接一个客户端要建立两个进程呢?先看一下系统逻辑结构:
从中能够发现,服务进程是直接跟客户端进行通信,而nobody进程并无,它仅仅是跟服务进程通讯,来协助服务进程来创建数据链接通道,以及须要一些特珠权限的控制,好比服务进程创建了链接以后,假设是PORT模式,因为是服务器端主动链接客户端,服务器端须要绑定20端口来链接客户端,而服务进程是没有权限来绑定20端口的,也就意味着没办法正常创建数据链接通道,因此须要加入nobody进程。而nobody和服务进程是采用内部通讯的协议,这个协议对外是不可见的,彻底能够由咱们本身来定义,因此能够用UNIX域协议来进行通信,而不用TCP/IP协议了。
#ifndef LINUX_FTP_COMMON_H
#define LINUX_FTP_COMMON_H
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} \
while (0)
#endif //LINUX_FTP_COMMON_H
复制代码
#ifndef LINUX_FTP_SYSUTIL_H
#define LINUX_FTP_SYSUTIL_H
#include "common.h"
int tcp_server(const char *host, unsigned short port);
int getlocalip(char *ip);
void activate_nonblock(int fd);
void deactivate_nonblock(int fd);
int read_timeout(int fd, unsigned int wait_seconds);
int write_timeout(int fd, unsigned int wait_seconds);
int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds);
int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds);
ssize_t readn(int fd, void *buf, size_t count);
ssize_t writen(int fd, const void *buf, size_t count);
ssize_t recv_peek(int sockfd, void *buf, size_t len);
ssize_t readline(int sockfd, void *buf, size_t maxline);
void send_fd(int sock_fd, int fd);
int recv_fd(const int sock_fd);
#endif //LINUX_FTP_SYSUTIL_H
复制代码
//
// Created by zpw on 2019-06-08.
//
#include "sysutil.h"
/**
* tcp_server - 启动TCP服务器
* @param host 服务器IP地址或者服务器主机名
* @param port 服务器端口
* @return 成功返回监听套接字
*/
int tcp_server(const char *host, unsigned short port) {
//建立套接字
int listenfd;
if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
ERR_EXIT("socket");
}
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
if (host != NULL) {
if (inet_aton(host, &servaddr.sin_addr) == 0) {
//证实传过来的是主机名而不是点分十进制的IP地址,接下来要进行转换
struct hostent *hp;
hp = gethostbyname(host);
if (hp == NULL) {
ERR_EXIT("gethostbyname");
}
servaddr.sin_addr = *(struct in_addr *) hp->h_addr;
}
} else {
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
}
servaddr.sin_port = htons(port);//端口号
//设置地址重复利用
int on = 1;
if ((setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const char *) &on, sizeof(on))) < 0) {
ERR_EXIT("gethostbyname");
}
//绑定
if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
ERR_EXIT("bind");
}
//监听
if (listen(listenfd, SOMAXCONN) < 0) {
ERR_EXIT("listen");
}
return listenfd;
}
int getlocalip(char *ip) {
char host[100] = {0};
if (gethostname(host, sizeof(host)) < 0) {
return -1;
}
struct hostent *hp;
if ((hp = gethostbyname(host)) == NULL) {
return -1;
}
strcpy(ip, inet_ntoa(*(struct in_addr *) hp->h_addr));
return 0;
}
/**
* activate_noblock - 设置I/O为非阻塞模式
* @fd: 文件描符符
*/
void activate_nonblock(int fd) {
int ret;
int flags = fcntl(fd, F_GETFL);
if (flags == -1) {
ERR_EXIT("fcntl");
}
flags |= O_NONBLOCK;
ret = fcntl(fd, F_SETFL, flags);
if (ret == -1) {
ERR_EXIT("fcntl");
}
}
/**
* deactivate_nonblock - 设置I/O为阻塞模式
* @fd: 文件描符符
*/
void deactivate_nonblock(int fd) {
int ret;
int flags = fcntl(fd, F_GETFL);
if (flags == -1) {
ERR_EXIT("fcntl");
}
flags &= ~O_NONBLOCK;
ret = fcntl(fd, F_SETFL, flags);
if (ret == -1) {
ERR_EXIT("fcntl");
}
}
/**
* read_timeout - 读超时检测函数,不含读操做
* @fd: 文件描述符
* @wait_seconds: 等待超时秒数,若是为0表示不检测超时
* 成功(未超时)返回0,失败返回-1,超时返回-1而且errno = ETIMEDOUT
*/
int read_timeout(int fd, unsigned int wait_seconds) {
int ret;
if (wait_seconds > 0) {
fd_set read_fdset;
struct timeval timeout;
FD_ZERO(&read_fdset);
FD_SET(fd, &read_fdset);
timeout.tv_sec = wait_seconds;
timeout.tv_usec = 0;
do {
ret = select(fd + 1, &read_fdset, NULL, NULL, &timeout);
} while (ret < 0 && errno == EINTR);
if (ret == 0) {
ret = -1;
errno = ETIMEDOUT;
} else if (ret == 1) {
ret = 0;
}
}
return ret;
}
/**
* write_timeout - 读超时检测函数,不含写操做
* @fd: 文件描述符
* @wait_seconds: 等待超时秒数,若是为0表示不检测超时
* 成功(未超时)返回0,失败返回-1,超时返回-1而且errno = ETIMEDOUT
*/
int write_timeout(int fd, unsigned int wait_seconds) {
int ret;
if (wait_seconds > 0) {
fd_set write_fdset;
struct timeval timeout;
FD_ZERO(&write_fdset);
FD_SET(fd, &write_fdset);
timeout.tv_sec = wait_seconds;
timeout.tv_usec = 0;
do {
ret = select(fd + 1, NULL, NULL, &write_fdset, &timeout);
} while (ret < 0 && errno == EINTR);
if (ret == 0) {
ret = -1;
errno = ETIMEDOUT;
} else if (ret == 1) {
ret = 0;
}
}
return ret;
}
/**
* accept_timeout - 带超时的accept
* @fd: 套接字
* @addr: 输出参数,返回对方地址
* @wait_seconds: 等待超时秒数,若是为0表示正常模式
* 成功(未超时)返回已链接套接字,超时返回-1而且errno = ETIMEDOUT
*/
int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) {
int ret;
socklen_t addrlen = sizeof(struct sockaddr_in);
if (wait_seconds > 0) {
fd_set accept_fdset;
struct timeval timeout;
FD_ZERO(&accept_fdset);
FD_SET(fd, &accept_fdset);
timeout.tv_sec = wait_seconds;
timeout.tv_usec = 0;
do {
ret = select(fd + 1, &accept_fdset, NULL, NULL, &timeout);
} while (ret < 0 && errno == EINTR);
if (ret == -1) {
return -1;
} else if (ret == 0) {
errno = ETIMEDOUT;
return -1;
}
}
if (addr != NULL) {
ret = accept(fd, (struct sockaddr *) addr, &addrlen);
} else {
ret = accept(fd, NULL, NULL);
}
return ret;
}
/**
* connect_timeout - connect
* @fd: 套接字
* @addr: 要链接的对方地址
* @wait_seconds: 等待超时秒数,若是为0表示正常模式
* 成功(未超时)返回0,失败返回-1,超时返回-1而且errno = ETIMEDOUT
*/
int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) {
int ret;
socklen_t addrlen = sizeof(struct sockaddr_in);
if (wait_seconds > 0) {
activate_nonblock(fd);
}
ret = connect(fd, (struct sockaddr *) addr, addrlen);
if (ret < 0 && errno == EINPROGRESS) {
fd_set connect_fdset;
struct timeval timeout;
FD_ZERO(&connect_fdset);
FD_SET(fd, &connect_fdset);
timeout.tv_sec = wait_seconds;
timeout.tv_usec = 0;
do {
ret = select(fd + 1, NULL, &connect_fdset, NULL, &timeout);
} while (ret < 0 && errno == EINTR);
if (ret == 0) {
ret = -1;
errno = ETIMEDOUT;
} else if (ret < 0) {
return -1;
} else if (ret == 1) {
/* ret返回为1,可能有两种状况,一种是链接创建成功,一种是套接字产生错误,*/
/* 此时错误信息不会保存至errno变量中,所以,须要调用getsockopt来获取。 */
int err;
socklen_t socklen = sizeof(err);
int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen);
if (sockoptret == -1) {
return -1;
}
if (err == 0) {
ret = 0;
} else {
errno = err;
ret = -1;
}
}
}
if (wait_seconds > 0) {
deactivate_nonblock(fd);
}
return ret;
}
/**
* readn - 读取固定字节数
* @fd: 文件描述符
* @buf: 接收缓冲区
* @count: 要读取的字节数
* 成功返回count,失败返回-1,读到EOF返回<count
*/
ssize_t readn(int fd, void *buf, size_t count) {
size_t nleft = count;
ssize_t nread;
char *bufp = (char *) buf;
while (nleft > 0) {
if ((nread = read(fd, bufp, nleft)) < 0) {
if (errno == EINTR)
continue;
return -1;
} else if (nread == 0)
return count - nleft;
bufp += nread;
nleft -= nread;
}
return count;
}
/**
* writen - 发送固定字节数
* @fd: 文件描述符
* @buf: 发送缓冲区
* @count: 要读取的字节数
* 成功返回count,失败返回-1
*/
ssize_t writen(int fd, const void *buf, size_t count) {
size_t nleft = count;
ssize_t nwritten;
char *bufp = (char *) buf;
while (nleft > 0) {
if ((nwritten = write(fd, bufp, nleft)) < 0) {
if (errno == EINTR)
continue;
return -1;
} else if (nwritten == 0)
continue;
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
/**
* recv_peek - 仅仅查看套接字缓冲区数据,但不移除数据
* @sockfd: 套接字
* @buf: 接收缓冲区
* @len: 长度
* 成功返回>=0,失败返回-1
*/
ssize_t recv_peek(int sockfd, void *buf, size_t len) {
while (1) {
int ret = recv(sockfd, buf, len, MSG_PEEK);
if (ret == -1 && errno == EINTR)
continue;
return ret;
}
}
/**
* readline - 按行读取数据
* @sockfd: 套接字
* @buf: 接收缓冲区
* @maxline: 每行最大长度
* 成功返回>=0,失败返回-1
*/
ssize_t readline(int sockfd, void *buf, size_t maxline) {
int ret;
int nread;
char *bufp = buf;
int nleft = maxline;
while (1) {
ret = recv_peek(sockfd, bufp, nleft);
if (ret < 0) {
return ret;
} else if (ret == 0) {
return ret;
}
nread = ret;
int i;
for (i = 0; i < nread; i++) {
if (bufp[i] == '\n') {
ret = readn(sockfd, bufp, i + 1);
if (ret != i + 1)
exit(EXIT_FAILURE);
return ret;
}
}
if (nread > nleft) {
exit(EXIT_FAILURE);
}
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if (ret != nread) {
exit(EXIT_FAILURE);
}
bufp += nread;
}
return -1;
}
void send_fd(int sock_fd, int fd) {
int ret;
struct msghdr msg;
struct cmsghdr *p_cmsg;
struct iovec vec;
char cmsgbuf[CMSG_SPACE(sizeof(fd))];
int *p_fds;
char sendchar = 0;
msg.msg_control = cmsgbuf;
msg.msg_controllen = sizeof(cmsgbuf);
p_cmsg = CMSG_FIRSTHDR(&msg);
p_cmsg->cmsg_level = SOL_SOCKET;
p_cmsg->cmsg_type = SCM_RIGHTS;
p_cmsg->cmsg_len = CMSG_LEN(sizeof(fd));
p_fds = (int *) CMSG_DATA(p_cmsg);
*p_fds = fd;
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &vec;
msg.msg_iovlen = 1;
msg.msg_flags = 0;
vec.iov_base = &sendchar;
vec.iov_len = sizeof(sendchar);
ret = sendmsg(sock_fd, &msg, 0);
if (ret != 1)
ERR_EXIT("sendmsg");
}
int recv_fd(const int sock_fd) {
int ret;
struct msghdr msg;
char recvchar;
struct iovec vec;
int recv_fd;
char cmsgbuf[CMSG_SPACE(sizeof(recv_fd))];
struct cmsghdr *p_cmsg;
int *p_fd;
vec.iov_base = &recvchar;
vec.iov_len = sizeof(recvchar);
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &vec;
msg.msg_iovlen = 1;
msg.msg_control = cmsgbuf;
msg.msg_controllen = sizeof(cmsgbuf);
msg.msg_flags = 0;
p_fd = (int *) CMSG_DATA(CMSG_FIRSTHDR(&msg));
*p_fd = -1;
ret = recvmsg(sock_fd, &msg, 0);
if (ret != 1)
ERR_EXIT("recvmsg");
p_cmsg = CMSG_FIRSTHDR(&msg);
if (p_cmsg == NULL)
ERR_EXIT("no passed fd");
p_fd = (int *) CMSG_DATA(p_cmsg);
recv_fd = *p_fd;
if (recv_fd == -1)
ERR_EXIT("no passed fd");
return recv_fd;
}
复制代码
编写好这个函数以后,则在main函数中去调用一下:
接着则要编写接受客户端的链接:
#ifndef _SESSION_H_
#define _SESSION_H_
#include "common.h"
void begin_session(int conn);
#endif /* _SESSION_H_ */
复制代码
#include "common.h"
#include "session.h"
void begin_session(int conn)
{
}
复制代码
而根据上次介绍的逻辑结构来看:
因此须要建立两个进程:
而后再把这两个进程作的事也模块化,FTP服务进程主要是处理FTP协议相关的一些细节,模块能够叫ftpproto,而nobody进程主要是协助FTP服务进程,只对内,模块能够叫privparent。
因此这里须要创建一个通道来让两进程之间能够相互通讯,这里采用socketpair来进行通讯:
另外能够定义一个session结构体来表明一个会话,里面包含多个信息:
#ifndef _SESSION_H_
#define _SESSION_H_
#include "common.h"
typedef struct session
{
// 控制链接
int ctrl_fd;
char cmdline[MAX_COMMAND_LINE];
char cmd[MAX_COMMAND];
char arg[MAX_ARG];
// 父子进程通道
int parent_fd;
int child_fd;
} session_t;
void begin_session(session_t *sess);
#endif /* _SESSION_H_ */
复制代码
上面用到了三个宏,也须要在common.h中进行定义:
这时在main中就得声明一下该session,并将其传递:
这时再回到begin_session方法中,进一步带到父子进程中去处理:
下面则在session的父子进程中进行函数的声明:
ftpproto.h:
#ifndef _FTP_PROTO_H_
#define _FTP_PROTO_H_
#include "session.h"
void handle_child(session_t *sess);
#endif /* _FTP_PROTO_H_ */
复制代码
ftpproto.c:
#include "ftpproto.h"
#include "sysutil.h"
void handle_child(session_t *sess)
{
}
复制代码
privparent.h:
#ifndef _PRIV_PARENT_H_
#define _PRIV_PARENT_H_
#include "session.h"
void handle_parent(session_t *sess);
#endif /* _PRIV_PARENT_H_ */
复制代码
privparent.c:
#include "privparent.h"
void handle_parent(session_t *sess)
{
}
复制代码
在session.c中须要包含这两个头文件:
接下来咱们将注意力集中在begin_session函数中,首先咱们须要将父进程改为nobody进程,怎么来改呢?这里须要用到一个函数:
下面来编写handle_child()和handle_parent():
另外在链接时,会给客户端一句这样的提示语:
主要仍是将经历投射到handle_child()服务进程上来,其它的先不用关心:
而它主要是完成FTP协议相关的功能,因此它的实现放在了ftpproto.c,目前链接成功以后效果是:
其中"USER webor2006"后面是包含"\r\n"的,FTP的协议规定每条指令后面都要包含它,这时handle_child()函数就会收到这个命令并处理,再进行客户端的一些应答,客户端才可以进行下一步的动做,因为目前尚未处理该命令,因此客户端阻塞了,接下来读取该指令来打印一下:
接下来命令中的\r\n,接下来的操做会涉及到一些字符串的处理,因此先来对其进行封装一下,具体字符串的处理函数以下:
str.h:
#ifndef _STR_H_
#define _STR_H_
void str_trim_crlf(char *str);
void str_split(const char *str , char *left, char *right, char c);
int str_all_space(const char *str);
void str_upper(char *str);
long long str_to_longlong(const char *str);
unsigned int str_octal_to_uint(const char *str);
#endif /* _STR_H_ */
复制代码
str.c:
#include "str.h"
#include "common.h"
void str_trim_crlf(char *str)
{
}
void str_split(const char *str , char *left, char *right, char c)
{
}
int str_all_space(const char *str)
{
return 1;
}
void str_upper(char *str)
{
}
long long str_to_longlong(const char *str)
{
return 0;
}
unsigned int str_octal_to_uint(const char *str)
{
unsigned int result = 0;
return 0;
}
复制代码
①:去除字符串\r\n:rhstr_trim_crlf()
实现思路:
void str_trim_crlf(char *str)
{
char *p = &str[strlen(str)-1];
while (*p == '\r' || *p == '\n')
*p-- = '\0';
}
复制代码
②:解析FTP命令与参数:str_split()
接下来将命令进行分割:
void str_split(const char *str , char *left, char *right, char c)
{
//首先查找要分割字符串中首次出现字符的位置
char *p = strchr(str, c);
if (p == NULL)
strcpy(left, str);//表示没有找到,该命令没有参数,则将一整串拷贝到left中
else
{//表示找到了,该命令有参数
strncpy(left, str, p-str);
strcpy(right, p+1);
}
}
复制代码
③:判断全部的字符是否为空白字符:str_all_space()
④:将字符串转换成大写:str_upper()
其实这个错误是一个很好检验C语言基本功的,修改程序以下:
⑤:将字符串转换为长长整型:str_to_longlong()
可能会想到atoi系统函数能够实现,可是它返回的是一个整型:
可是也有一个现成的函数能够作到:atoll:
long long str_to_longlong(const char *str)
{
return atoll(str);
}
复制代码
可是不是全部的系统都支持它,所以这里咱们本身来实现,其实现思路也比较简单,规则以下:
12345678=8*(10^0) + 7*(10^1) + 6*(10^2) + ..... + 1*(10^7)
因此实现以下:
⑥:将八进制的整形字符串转换成无符号整型str_octal_to_uint()
其实现原理跟上面的差很少:
123456745=5*(8^0) + 4*(8^1) + 7*(8^2) + .... + 1*(8^8)
代码编写也跟上面函数同样,这里采用另一种方式来实现,从高位算起:
先拿10进制来进行说明,好理解:
123456745能够通过下面这个换算获得:
0*10+1=1
1*10+2=12
12*10+3=123
123*10+4=1234
....
因此换算成八进制,其原理就是这样:
0*8+1=1
1*8+2=12
12*8+3=123
123*8+4=1234
....
因此依照这个原理就能够进行实现了,因为八进制可能前面为0,如:0123450,因此须要把第一位0给过滤掉,以下:
而公式里面应该是result8+digit来进行计算,这里用位操做来改写,也就是result8=result <<= 3,移位操做效率更加高效,因此最终代码以下:
上一次对字符串工具模块进行了封装,此次主要是对"参数配置模块"的封装,FTP中有不少配置相关的选项,不可能硬编码到代码中,而应该将它们配置到配置文件当中,像vsftpd的配置文件以下:
而对于miniftpd全部的参数配置项以下:
对于上面这些变量应该是与对应的配置项进行一一对应的,因此须要定义三张表格来进行一一对应:
下面定义两个操做配置文件的函数:
下面则开始进行编码,首先先新建配置文件模块文: tunable.h:对其变量进行声明:
#ifndef _TUNABLE_H_
#define _TUNABLE_H_
extern int tunable_pasv_enable;
extern int tunable_port_enable;
extern unsigned int tunable_listen_port;
extern unsigned int tunable_max_clients;
extern unsigned int tunable_max_per_ip;
extern unsigned int tunable_accept_timeout;
extern unsigned int tunable_connect_timeout;
extern unsigned int tunable_idle_session_timeout;
extern unsigned int tunable_data_connection_timeout;
extern unsigned int tunable_local_umask;
extern unsigned int tunable_upload_max_rate;
extern unsigned int tunable_download_max_rate;
extern const char *tunable_listen_address;
#endif /* _TUNABLE_H_ */
复制代码
另外新建一个配置文件:
接下来还要暴露两个接口出来,对文件和配置项的解析:
parseconf.h:
#ifndef _PARSE_CONF_H_
#define _PARSE_CONF_H_
void parseconf_load_file(const char *path);
void parseconf_load_setting(const char *setting);
#endif /* _PARSE_CONF_H_ */
复制代码
parseconf.c:
#include "parseconf.h"
#include "common.h"
#include "tunable.h"
void parseconf_load_file(const char *path){
}
void parseconf_load_setting(const char *setting){
}
复制代码
另外,因为fgets函数读取的一行字符包含'\n',因此须要将其去掉,能够用咱们以前封装的现成方法:
接下来实现命令行的解析函数,在正式解析以前,须要将配置文件中的配置项与配置项变量对应关系表用代码定义出来,以下:
#include "parseconf.h"
#include "common.h"
#include "tunable.h"
static struct parseconf_bool_setting
{
const char *p_setting_name;
int *p_variable;
}
parseconf_bool_array[] =
{
{ "pasv_enable", &tunable_pasv_enable },
{ "port_enable", &tunable_port_enable },
{ NULL, NULL }
};
static struct parseconf_uint_setting
{
const char *p_setting_name;
unsigned int *p_variable;
}
parseconf_uint_array[] =
{
{ "listen_port", &tunable_listen_port },
{ "max_clients", &tunable_max_clients },
{ "max_per_ip", &tunable_max_per_ip },
{ "accept_timeout", &tunable_accept_timeout },
{ "connect_timeout", &tunable_connect_timeout },
{ "idle_session_timeout", &tunable_idle_session_timeout },
{ "data_connection_timeout", &tunable_data_connection_timeout },
{ "local_umask", &tunable_local_umask },
{ "upload_max_rate", &tunable_upload_max_rate },
{ "download_max_rate", &tunable_download_max_rate },
{ NULL, NULL }
};
static struct parseconf_str_setting
{
const char *p_setting_name;
const char **p_variable;
}
parseconf_str_array[] =
{
{ "listen_address", &tunable_listen_address },
{ NULL, NULL }
};
void parseconf_load_file(const char *path){
FILE *fp = fopen(path, "r");
if (fp == NULL)
ERR_EXIT("fopen");
char setting_line[1024] = {0};
while (fgets(setting_line, sizeof(setting_line), fp) != NULL)
{
if (strlen(setting_line) == 0
|| setting_line[0] == '#'
|| str_all_space(setting_line))
continue;
str_trim_crlf(setting_line);
parseconf_load_setting(setting_line);
memset(setting_line, 0, sizeof(setting_line));
}
fclose(fp);
}
void parseconf_load_setting(const char *setting){
}
复制代码
可见有三种类型的参数,下面一个个来进行解析,对于"pasv_enable=YES"一个配置,可能会写成“ pasv_enable=YES”,因此先去掉左控格:
而后须要将key=pasv_enable;value=YES分隔开,这里能够用以前封装的现成的命令:
但也有可能用户没有配置value,如“pasv_enable=”,因此这是不合法的,也应该作下判断:
接下来,就须要拿这个key在上面的配置表格变量中进行搜索,若是找到了,则将其值赋值给该配置变量,以下:
若是说没有找到话,也就说明当前的配置项不是字符串类型的,这时,还得继续去其它类型的配置项中进行搜寻,以下:
而对于布尔类型,能够有如下几种形式:
AA=YES
AA=yes
AA=TRUE
AA=1
因此,首先将value统一成大写:
当遍历boolean类型配置项中也没有找到时,则须要在无符号整形中进行查找,其中无符号整形有两种形式:一种八进制,以0开头,好比"local_umask=077";另外一种是十进制,如:"listen_port=21",因此须要作下判断,代码基本相似:
接下来能够应用某些配置项了:
可见这样代码就变成可配置的了,另外配置文件的文件名能够作成宏:
这节来实现用户登陆的验证,首先用客户端来登陆vsftpd来演示登陆的过程:
接下来实现它,与协议相关的模块都是在ftpproto.c中完成的,目前的代码以下:
#include "ftpproto.h"
#include "sysutil.h"
#include "str.h"
void do_user(session_t *sess);
void do_pass(session_t *sess);
void handle_child(session_t *sess)
{
writen(sess->ctrl_fd, "220 (miniftpd 0.1)\r\n", strlen("220 (miniftpd 0.1)\r\n"));
int ret;
while (1)
{
memset(sess->cmdline, 0, sizeof(sess->cmdline));
memset(sess->cmd, 0, sizeof(sess->cmd));
memset(sess->arg, 0, sizeof(sess->arg));
ret = readline(sess->ctrl_fd, sess->cmdline, MAX_COMMAND_LINE);
if (ret == -1)
ERR_EXIT("readline");
else if (ret == 0)
exit(EXIT_SUCCESS);
printf("cmdline=[%s]\n", sess->cmdline);
// 去除\r\n
str_trim_crlf(sess->cmdline);
printf("cmdline=[%s]\n", sess->cmdline);
// 解析FTP命令与参数
str_split(sess->cmdline, sess->cmd, sess->arg, ' ');
printf("cmd=[%s] arg=[%s]\n", sess->cmd, sess->arg);
// 将命令转换为大写
str_upper(sess->cmd);
// 处理FTP命令
if (strcmp("USER", sess->cmd) == 0)
{
do_user(sess);
}
else if (strcmp("PASS", sess->cmd) == 0)
{
do_pass(sess);
}
}
}
void do_user(session_t *sess)
{
//USER jjl
}
void do_pass(session_t *sess)
{
// PASS 123456
}
复制代码