FTP服务器第二部分(文件的上传,下载,断点续传,限速,空闲断开等功能)

上一篇文章中我们主要讲述了FTP服务器的整体框架以及FTP数据连接建立的方式。
本篇中,主要实现文件的上传,下载,限速等功能。

文件的上传和下载功能

文件上传功能主要是将本地的文件存储在服务器中。下载是将服务器中的文件传输到本地。
上传功能:

  1. 建立数据连接,如果数据连接无法建立,直接返回。
  2. 在服务器当前文件夹下创建一个和windows上一样的文件-使用open函数:不存在的话则创建。
  3. 使用recv函数,通过数据连接套接字描述符,将sockfd中接收缓冲区中的数据拷贝到buf中。(while循环,通过recv函数的返回值ret来判断,如果ret=0,代表数据传输完成,跳出循环)
  4. 将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函数来获取该文件的总大小。下载的步骤如下:

  1. 建立数据连接
  2. 获取文件的总大小,使用fstat函数
  3. 向客户端回复相关的代码(包含传输的方式和传输文件的大小)
  4. 定义缓冲区buf的大小为1024字节,通过read函数读取数据到buf中。通过send函数将buf中的数据发送出去。
  5. 每次发送之后文件的总大小需要减去已经发送的数据大小,直到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;
	}
}

文件的断点续传

断点续传指的是在下载或者上传文件的过程中,由于某种原因文件被中止传输,那么下一次传输文件的时候不需要从头开始,而可以从上一次中断的地方来下载和上传文件。断点续传的步骤如下所示:

  1. 客户端会向服务端发送REST命令,统计已经传输的数据的多少,在session会话结构体中建立数据成员变量restart_pos来保存命令行的参数值。
    在这里插入图片描述
  2. 在do_stor模块中,保存sess->restart_pos的值,使用lseek函数,设置offset的值为当前已经传输的文件大小,whence设置为SEEK_SET,文件指针会相对于起始位置,向后偏移offset大小。
  3. 后续的步骤和之前文件的上传是一样的。
    注意:下载的断点续传:文件的总大小需要减去之前的偏移量。

文件的限速

限速的关键就是让进程睡眠,当前文件的传输速度如果超过配置文件中的给定的传输速度,就需要让进程睡眠。
推导的过程如下所示:
由于总的字节数是不变的,假设当前文件的传输速度为 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) v2v11=t1t21=t1t2t1(2)
t 2 − t 1 = ( v 1 v 2 − 1 ) t 1 ( 3 ) t2-t1=(\frac {v1}{v2}-1)t1\quad(3) t2t1=(v2v11)t1(3)
因 此 睡 眠 时 间 等 于 = [ ( 当 前 的 传 速 速 度 / 最 大 的 传 输 速 度 ) − 1 ] ∗ 当 前 的 传 输 时 间 因此睡眠时间等于=[(当前的传速速度/最大的传输速度)-1]*当前的传输时间 =[(/)1]

限速的过程,流程图如下所示:

  1. 对于下载的限速,在读取数据之前登记传输的开始时间,在send发送数据之前进行限速。如果在send发送之后限速,就已经没有意义了,已经发送完毕了。
  2. 对于上传的限速,在接收时间之前登记开始时间,将数据通过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空闲断开

空闲断开包括控制连接断开和数据连接断开

控制连接断开

用户连接上服务端之后,如果长时间没有操作的话,可以关闭控制连接。
思路也是比较简单的:

  1. 设置SIGALRM信号,通过alarm函数进行定时,定时的时间是由配置文件中读取的。
  2. 假设定时的时间为10s,那么如果10s内客户端没有向服务器发送任何的命令,那么将关闭控制连接文件描述符的读端和写端。
  3. 向客户端发送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. 数据在传输的过程中,不能发生数据连接的断开。
  2. 数据在传输的过程中,即使控制连接的时间到了,也不能断开客户端和服务器的连接。

整体的过程如下所示:
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;	
}
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值