上一篇文章中我们主要讲述了FTP服务器的整体框架以及FTP数据连接建立的方式。
本篇中,主要实现文件的上传,下载,限速等功能。
文件的上传和下载功能
文件上传功能主要是将本地的文件存储在服务器中。下载是将服务器中的文件传输到本地。
上传功能:
- 建立数据连接,如果数据连接无法建立,直接返回。
- 在服务器当前文件夹下创建一个和windows上一样的文件-使用open函数:不存在的话则创建。
- 使用recv函数,通过数据连接套接字描述符,将sockfd中接收缓冲区中的数据拷贝到buf中。(while循环,通过recv函数的返回值ret来判断,如果ret=0,代表数据传输完成,跳出循环)
- 将buf中的数据写入到之前打开的文件中。
对应的代码如下:
static void do_stor(session_t *sess)
{
// 1.建立数据连接
if(get_transfer_fd(sess)==-1)
{
ERR_EXIT("data store connection faliure");
return;
}
// 2.在本地以create方式||可写的方式的打开文件
// 本地需要新建文件
int fd;
if((fd=open(sess->arg, O_CREAT|O_WRONLY, 0755))<0)
{
ftp_reply(sess, FTP_FILEFAIL, "Failed to open file.");
return;
}
// 回复150
ftp_reply(sess, FTP_DATACONN, "Ok to send data.");
char buf[MAX_BUFFER_SIZE]={0};
while(1)
{
memset(buf, 0, MAX_BUFFER_SIZE);
// 接收数据
// 客户端发送数据:数据刚开始被存放在sess->data_fd中的接受缓冲区中(socket是系统函数,即是在系统内核中完成的)。
// recv函数的作用是将sess->data_fd接收缓冲区中的数据放入buf数组中。
int ret =recv(sess->data_fd,buf,MAX_BUFFER_SIZE,0);
printf("the ret =%d\n",ret);
if(ret ==-1)
{
ftp_reply(sess, FTP_BADSENDNET, "Failure writting to network stream.");
break;
}
// 如果返回值为0的话,则代表接收完毕,退出循环。
if(ret == 0)
{
ftp_reply(sess, FTP_TRANSFEROK, "Transfer complete.");
break;
}
write(fd, buf, ret);
}
close(fd);
if(sess->data_fd != -1)
{
close(sess->data_fd);
sess->data_fd = -1;
}
}
下载功能功能和上传功能是类似的,不同之处在于文件下载的时候,需要知道传输文件的大小,因此需要调用fstat函数来获取该文件的总大小。下载的步骤如下:
- 建立数据连接
- 获取文件的总大小,使用fstat函数
- 向客户端回复相关的代码(包含传输的方式和传输文件的大小)
- 定义缓冲区buf的大小为1024字节,通过read函数读取数据到buf中。通过send函数将buf中的数据发送出去。
- 每次发送之后文件的总大小需要减去已经发送的数据大小,直到read函数的返回值为0时,代表已经读取完毕,服务器会向客户端回复相关代码,跳出while循环。
代码如下所示:
static void do_retr(session_t *sess)
{
// 1.建立数据连接
if(get_transfer_fd(sess)==-1)
{
ERR_EXIT("data retrive connection faliure");
return;
}
int fd= -1 ;
if((fd=open(sess->arg,O_RDONLY))<0)
{
ftp_reply(sess, FTP_FILEFAIL, "Failed to open file.");
return;
}
char buf[MAX_BUFFER_SIZE] = {0};
// 描述文件的信息的结构体
struct stat sbuf;
// 调用该函数之后,sbuf:中将会存储该文件的信息
fstat(fd,&sbuf);
// 文件的总大小
// 将文件的字节和传输方式发送给客户端,其实这里并没设置以ASII还是binary:只是这样回复一下嗷!
if(sess->is_ascii)
sprintf(buf, "Opening ASCII mode data connection for %s (%llu bytes).", sess->arg, (unsigned long long int)sbuf.st_size);
else
sprintf(buf, "Opening BINARY mode data connection for %s (%llu bytes).",sess->arg, (unsigned long long int)sbuf.st_size);
ftp_reply(sess, FTP_DATACONN, buf);
unsigned long long int total_size =sbuf.st_size;
int read_count=0;
while(1)
{
//每次传输的时候:都将发送缓冲区的buf清空。
memset(buf,0,MAX_BUFFER_SIZE);
read_count=total_size>MAX_BUFFER_SIZE?MAX_BUFFER_SIZE:total_size;
// ret =0代表,成功读取到的字节数为0个,代表文件读取完成
int ret =read(fd,buf,read_count);
if(ret== -1 || ret!=read_count)
{
ftp_reply(sess, FTP_BADSENDNET, "Failure writting to network stream.");
break;
}
if(ret == 0)
{
ftp_reply(sess, FTP_TRANSFEROK, "Transfer complete.");
break;
}
// 0是默认发送方式,不需要管
send(sess->data_fd,buf,ret,0);
total_size-=read_count;
}
}
文件的断点续传
断点续传指的是在下载或者上传文件的过程中,由于某种原因文件被中止传输,那么下一次传输文件的时候不需要从头开始,而可以从上一次中断的地方来下载和上传文件。断点续传的步骤如下所示:
- 客户端会向服务端发送REST命令,统计已经传输的数据的多少,在session会话结构体中建立数据成员变量restart_pos来保存命令行的参数值。
- 在do_stor模块中,保存sess->restart_pos的值,使用lseek函数,设置offset的值为当前已经传输的文件大小,whence设置为SEEK_SET,文件指针会相对于起始位置,向后偏移offset大小。
- 后续的步骤和之前文件的上传是一样的。
注意:下载的断点续传:文件的总大小需要减去之前的偏移量。
文件的限速
限速的关键就是让进程睡眠,当前文件的传输速度如果超过配置文件中的给定的传输速度,就需要让进程睡眠。
推导的过程如下所示:
由于总的字节数是不变的,假设当前文件的传输速度为
v
1
v1
v1,当前文件的传输时间为
t
1
t1
t1,最大传输速度为
v
2
v2
v2,对应的传输时间为
t
2
t2
t2。那么有以下三个公式:
v
1
v
2
=
t
2
t
1
(
1
)
\frac {v1}{v2}=\frac{t2}{t1}\quad(1)
v2v1=t1t2(1)
v
1
v
2
−
1
=
t
2
t
1
−
1
=
t
2
−
t
1
t
1
(
2
)
\frac {v1}{v2}-1=\frac{t2}{t1}-1=\frac{t2-t1}{t1}\quad(2)
v2v1−1=t1t2−1=t1t2−t1(2)
t
2
−
t
1
=
(
v
1
v
2
−
1
)
t
1
(
3
)
t2-t1=(\frac {v1}{v2}-1)t1\quad(3)
t2−t1=(v2v1−1)t1(3)
因
此
睡
眠
时
间
等
于
=
[
(
当
前
的
传
速
速
度
/
最
大
的
传
输
速
度
)
−
1
]
∗
当
前
的
传
输
时
间
因此睡眠时间等于=[(当前的传速速度/最大的传输速度)-1]*当前的传输时间
因此睡眠时间等于=[(当前的传速速度/最大的传输速度)−1]∗当前的传输时间
限速的过程,流程图如下所示:
- 对于下载的限速,在读取数据之前登记传输的开始时间,在send发送数据之前进行限速。如果在send发送之后限速,就已经没有意义了,已经发送完毕了。
- 对于上传的限速,在接收时间之前登记开始时间,将数据通过recv函数进行接收到缓冲区中,之后写入到服务器中对应的本地文件中,因此在write函数写入之前进行限速。
代码如下所示,注意休眠的函数使用到了nanosleep函数。
void limit_rate(session_t *sess, unsigned long bytes_transfer, int is_upload)
{
//登记结束时间
unsigned long long cur_sec = get_time_sec();
unsigned long long cur_usec = get_time_usec();
// 秒的时间+微妙的时间
double pass_time = (double)(cur_sec - sess->transfer_start_sec);
pass_time +=(double)(cur_usec-sess->transfer_start_usec)/(double)1000000;
printf("pass_time =%f\n",pass_time);
//当前的传输速度
double cur_rate = (double)bytes_transfer / pass_time;
printf("cur_rate =%lld\n",(long long int)cur_rate);
printf("max_upload_rate=%lld\n",(long long int)tunable_upload_max_rate);
double rate_ratio; //速率
if(is_upload ==1)
{
if(tunable_upload_max_rate==0 || cur_rate<=(double)tunable_upload_max_rate)
{
//不限速,返回数据传输之后的时间即可。
sess->transfer_start_sec = get_time_sec();
sess->transfer_start_usec = get_time_usec();
printf("upload:enter no limit rate\n");
return;
}
rate_ratio = cur_rate / tunable_upload_max_rate;
}
else
{
//下载的时候is_upload为0
if(tunable_download_max_rate==0 || cur_rate <= tunable_download_max_rate)
{
//不限速
sess->transfer_start_sec = get_time_sec();
sess->transfer_start_usec = get_time_usec();
printf("xiazai:enter no limit rate\n");
return;
}
rate_ratio = cur_rate / tunable_download_max_rate;
}
double sleep_time =(rate_ratio-1)*pass_time;
//休眠
nano_sleep(sleep_time);
//重新登记开始时间
sess->transfer_start_sec = get_time_sec();
sess->transfer_start_usec = get_time_usec();
}
FTP空闲断开
空闲断开包括控制连接断开和数据连接断开
控制连接断开
用户连接上服务端之后,如果长时间没有操作的话,可以关闭控制连接。
思路也是比较简单的:
- 设置SIGALRM信号,通过alarm函数进行定时,定时的时间是由配置文件中读取的。
- 假设定时的时间为10s,那么如果10s内客户端没有向服务器发送任何的命令,那么将关闭控制连接文件描述符的读端和写端。
- 向客户端发送421:timeout的响应,退出进程。
代码如下:
// 设置控制连接断开
void handle_ctrl_timeout(int sig_num)
{
shutdown(p_sess->ctrl_fd,SHUT_RD);
ftp_reply(p_sess, FTP_IDLE_TIMEOUT, "Timeout.");
shutdown(p_sess->ctrl_fd, SHUT_WR);
exit(EXIT_SUCCESS);
}
void start_cmdio_alarm()
{
// 等于0的话代表没有设置时间
if(tunable_idle_session_timeout>0)
{
signal(SIGALRM,handle_ctrl_timeout);
alarm(tunable_idle_session_timeout);
}
}
数据连接断开
数据连接的断开指的是,数据连接建立之后,在一段时间内没有进行数据传输的话,可以考虑关闭会话。相比较于控制连接的断开,有以下几点需要注意:
- 数据在传输的过程中,不能发生数据连接的断开。
- 数据在传输的过程中,即使控制连接的时间到了,也不能断开客户端和服务器的连接。
整体的过程如下所示:
1定义数据连接闹钟函数,达到数据连接的超时时间后,SIGALRM信号会调用我们自定义的处理数据连接的函数如下所示:
void handle_data_timeout()
{
if(!p_sess->data_process)
{
ftp_reply(p_sess,FTP_DATA_TIMEOUT,"Data timeout ,REconnect Sorry");
exit(EXIT_FAILURE);
}
p_sess->data_process=0;
start_data_alarm();
}
void start_data_alarm()
{
if(tunable_data_connection_timeout >0)
{
signal(SIGALRM,handle_data_timeout);
alarm(tunable_data_connection_timeout);
}
}
会话结构体中定义数据成员data_process,假设数据处于上传和下载的过程中,那么对应的data_process的值为1,此时就不会进入if分支语句中,此时进程也就不会退出。
由于alarm函数在senconds之后会发送SIGALRM信号给当前进程,那么发送完毕之后就会失效,因此需要重新开启闹钟函数。需要注意的是,再下一次开启之前,对应的data_process重新变为0,重新进行统计。
其中start_data_alarm函数被调用在get_transfer_fd函数中,这样写的话会减少代码量,因为无论是上传,下载还是显示列表,都需要建立数据连接。
老师讲课的时候:说到下载和传输结束之后需要重新开启控制连接的闹钟函数,其实是不需要的,因为客户端给服务端发送新的指令,就会进入handle_child函数中,在while循环中已经设置了控制连接的闹钟函数。
handle_child函数
while(1)
{
memset(sess->cmdline, 0, MAX_COMMOND_LINE_SIZE);
memset(sess->cmd, 0, MAX_CMD_SIZE);
memset(sess->arg, 0, MAX_ARG_SIZE);
//控制空闲断开
start_cmdio_alarm();
int ret =recv(sess->ctrl_fd,sess->cmdline,MAX_CMD_SIZE,0);
if(ret==0)
exit(EXIT_SUCCESS);
if(ret<0)
ERR_EXIT("recv");
str_trim_crlf(sess->cmdline);
str_split(sess->cmdline, sess->cmd, sess->arg, ' ');
// printf("cmdline=%s\n",sess->cmdline);
// printf("cmd=%s,arg=%s\n",sess->cmd,sess->arg);
//命令映射
int table_size = sizeof(ctrl_cmds) / sizeof(ctrl_cmds[0]);
int i;
for(i=0; i<table_size; ++i)
{
if(strcmp(sess->cmd, ctrl_cmds[i].cmd) == 0)
{
if(ctrl_cmds[i].cmd_handler)
ctrl_cmds[i].cmd_handler(sess);
else
ftp_reply(sess, FTP_COMMANDNOTIMPL, "Unimplement command.");
break;
}
}
if(i >= table_size)
ftp_reply(sess, FTP_BADCMD, "Unknown command.");
}
static int get_transfer_fd(session_t *sess)
{
if(port_active(sess)== -1 &&pasv_active(sess)== -1)
{
//425 Use PORT or PASV first.
ftp_reply(sess, FTP_BADSENDCONN, "Use PORT or PASV first.");
return -1;
}
// 0代表成功,-1代表不成功
if(port_active(sess)==0&&get_port_fd(sess)==0)
{
if(sess->port_addr)
{
free(sess->port_addr);
sess->port_addr = NULL;
}
// 主动模式的开启
start_data_alarm();
return 0;
}
if(pasv_active(sess)==0&&get_pasv_fd(sess)==0)
{
//被动模式的开启
start_data_alarm();
return 0;
}
else
return -1;
}