miniFTP项目实战五

项目简介:
在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务进程来为客户提供服务。同时每个ftp服务进程配套了nobody进程(内部私有进程),主要是为了做权限提升和控制。
实现功能:
除了基本的文件上传和下载功能,还实现模式选择、断点续传、限制连接数、空闲断开、限速等功能。
用到的技术:
socket、I/O复用、进程间通信、HashTable
欢迎技术交流q:2723808286
项目开源!!!

miniFTP项目实战一
miniFTP项目实战二
miniFTP项目实战三
miniFTP项目实战四
miniFTP项目实战五
miniFTP项目实战六

在这里插入图片描述

5.1 下载文件 断点续载

RETR命令是从服务器下载文件,命令流程如下:
在这里插入图片描述

通过RETR 命令来指定下载文件,当传输中断的时候客户端保存已下载文件的偏移量,后面续传的时候从REST的位置继续传输,从而达到断点续载的效果。

加锁读取文件

在获取数据传输通道之后,服务器要打开文件,以只读的方式打开文件,并加读锁:

//打开文件 只读
int  fd = open(sess->arg, O_RDONLY);
if (fd == -1) {
    ftp_relply(sess, FTP_FILEFAIL, "Failed to open file.");
}
//加读锁
int ret = lock_file_read(fd);
if (ret == -1) {
    ftp_relply(sess, FTP_FILEFAIL, "Failed to open file.");
    return ;
}

加锁的具体操作如下:

int lock_file_read(int fd)
{
  int ret;
  struct flock the_lock;
  memset(&the_lock, 0, sizeof(the_lock));
  the_lock.l_type = F_RDLCK;  //加锁类型为:读锁
  the_lock.l_whence = SEEK_SET;  //文件头部开始加锁
  the_lock.l_start = 0;        //文件头开始的偏移地址开始加锁
  the_lock.l_len = 0;         //加锁的范围 0表示将整个文件加锁

  do {
    ret = fcntl(fd, F_SETLKW, &the_lock);  //文件描述符  加锁 锁相关的结构体
  } while (ret < 0 && errno == EINTR);  //排除信号中断

  return ret;
}

定位到断点

如果是之前传输中断之后的,会通过REST来设置断点,在do_rest函数中,保存断点位置到sess中:

static void do_rest(session_t *sess)
{
    //字符串转换为long long
    sess->restart_pos = str_to_longlong(sess->arg); 
    char text[1024] = {0};
    sprintf(text, "Restart position accepted (%lld)", sess->restart_pos);
    ftp_relply(sess, FTP_RESTOK, text);
}

然后通过lseek函数定位到断点处:

//定位到断点
if (offset != 0) {
    ret = lseek(fd, offset, SEEK_SET);  //从头开始
    if (ret == -1) {
        ftp_relply(sess, FTP_FILEFAIL, "Failed to open file.");
        return ;
    }
}

传输文件

文件本身是放在磁盘中存放的,从磁盘到内核是通过DMA取得,这里我们从内核再读取文件,通过read系统调用来读取文件,然后通过writen写入内核,通过socket发送出去:

char buf[4096];
int flag = 0;
while (1) {
    ret = read(fd, buf, sizeof(buf));
    if (ret == -1) {  //被中断打断 继续执行  其他情况退出
        if (errno == EINTR) continue;
        else {
            flag = 1;
            break;
        }
    } else if (ret == 0) {  //成功读完
        flag = 0;
        break;
    }

    if (writen(sess->data_fd, buf, sizeof(buf)) != ret) {  //写入失败
        flag = 2;
        break;
    }
}

但是这样传输文件得话,从内核到用户空间,再从用户空间到内核,开销太大了。

下面采用直接在内核中完成拷贝得方式:

    long long bytes_to_send = sbuf.st_size;
    if (offset > bytes_to_send) {
        bytes_to_send = 0;
    } else {
        bytes_to_send -= offset;
    }

    sess->bw_transfer_start_sec = get_time_sec();
    sess->bw_transfer_start_usec = get_time_usec();
    while (bytes_to_send) {
        int num_this_time = bytes_to_send > 4096 ? 4096 : bytes_to_send;  //决定当此发送的数据字节数
        ret = sendfile(sess->data_fd, fd, NULL, num_this_time); 
        if (ret == -1) {
            flag = 2;
            break;
        }

        limit_rate(sess, bytes_to_send, 0);  //限速
        if (sess->abor_received) {
            flag = 2;
            break;
        }
        bytes_to_send -= ret;  //更新要发送的Byte
    }
    if (bytes_to_send == 0) {
        flag = 0;
    } else {
        flag = 2;
    }

主要是通过sendfile函数实现,其函数原型如下:

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

sendfile 函数在两个文件描写叙述符之间直接传递数据(全然在内核中操作,传送),从而避免了内核缓冲区数据和用户缓冲区数据之间的拷贝,操作效率非常高,被称之为零拷贝。

参考:https://blog.csdn.net/u010649766/article/details/80339988

几种零拷贝技术的对比:https://mp.weixin.qq.com/s/eHhhW8j3vs8puMkC5zoIpQ

综合起来看一下RETR的操作函数:

static void do_retr(session_t *sess)
{
    //获取数据传输通道的fd
    if (get_transfer_fd(sess) == 0) {
        return ;
    }

    long long offset = sess->restart_pos;
    sess->restart_pos = 0;

    //打开文件 只读
    int  fd = open(sess->arg, O_RDONLY);
    if (fd == -1) {
        ftp_relply(sess, FTP_FILEFAIL, "Failed to open file.");
    }
    //加读锁
    int ret = lock_file_read(fd);
    if (ret == -1) {
        ftp_relply(sess, FTP_FILEFAIL, "Failed to open file.");
        return ;
    }

    //判断文件类型 是否是普通文件
    struct stat sbuf;
    ret = fstat(fd, &sbuf);
    if (!S_ISREG(sbuf.st_mode)) {  //不是普通文件
        ftp_relply(sess, FTP_FILEFAIL, "Failed to open file.");
        return ;
    }

    //定位到断点
    if (offset != 0) {
        ret = lseek(fd, offset, SEEK_SET);  //从头开始
        if (ret == -1) {
            ftp_relply(sess, FTP_FILEFAIL, "Failed to open file.");
            return ;
        }
    }

    //回应150  ASCII与二进制传输唯一的区别就是 :是否对\r\n处理
    char tmp[1024] = {0};
    if (sess->is_ascii) {  //ASCII模式
        sprintf(tmp, "Opening ASCII mode data connection for %s (%lld bytes).", 
                    sess->arg, (long long)sbuf.st_size);
    } else {      //二进制模式
        sprintf(tmp, "Opening BINARY mode data connection for %s (%lld bytes).", 
                    sess->arg, (long long)sbuf.st_size);
    }
    ftp_relply(sess, FTP_DATACONN, tmp);

    //下载文件
    //从内核到用户空间 再到内核
    char buf[4096];
    int flag = 0;
    // while (1) {
    //     ret = read(fd, buf, sizeof(buf));
    //     if (ret == -1) {  //被中断打断 继续执行  其他情况退出
    //         if (errno == EINTR) continue;
    //         else {
    //             flag = 1;
    //             break;
    //         }
    //     } else if (ret == 0) {  //成功读完
    //         flag = 0;
    //         break;
    //     }

    //     if (writen(sess->data_fd, buf, sizeof(buf)) != ret) {  //写入失败
    //         flag = 2;
    //         break;
    //     }
    // }

    //直接在内核中完成拷贝
    long long bytes_to_send = sbuf.st_size;
    if (offset > bytes_to_send) {
        bytes_to_send = 0;
    } else {
        bytes_to_send -= offset;
    }

    sess->bw_transfer_start_sec = get_time_sec();
    sess->bw_transfer_start_usec = get_time_usec();
    while (bytes_to_send) {
        int num_this_time = bytes_to_send > 4096 ? 4096 : bytes_to_send;  //决定当此发送的数据字节数
        ret = sendfile(sess->data_fd, fd, NULL, num_this_time); 
        if (ret == -1) {
            flag = 2;
            break;
        }

        limit_rate(sess, bytes_to_send, 0);  //限速
        if (sess->abor_received) {
            flag = 2;
            break;
        }
        bytes_to_send -= ret;  //更新要发送的Byte
    }
    if (bytes_to_send == 0) {
        flag = 0;
    } else {
        flag = 2;
    }

    close(sess->data_fd);
    sess->data_fd = -1;
    close(fd);

    if (flag == 0) {
        ftp_relply(sess, FTP_TRANSFEROK, "Transfer complete.");
    } else if (flag == 1) {  //读取失败
        ftp_relply(sess, FTP_BADSENDFILE, "Failure reading from local file.");
    } else if (flag == 2) {  //发送失败
        ftp_relply(sess, FTP_BADSENDNET, "Failure writting to networks stream.");
    }

    check_abor(sess);
    start_cmdio_alarm();  //数据传输完毕之后 重新启动控制通道时钟
}

5.2 上传文件 断点续传

上传文件有三种方式:

  • STOR + REST:断点续传
  • APPE:追加
  • STOR:覆盖

直接通过upload_common来实现上传文件操作

/* 上传命令      下载命令
 *  STOR          RETR
 * 
 * 断点续传      断点续载
 *  REST          REST
 *  STOR          RETR
 * 
 *  APPE
 * 用upload_common() 来区别APPE断点续传与REST+STOR断点续传
 *  */
static void do_stor(session_t *sess)
{
    upload_common(sess, 0);
}

static void do_appe(session_t *sess)
{
    upload_common(sess, 1);
}

具体操作如下:

void upload_common(session_t *sess, int is_append)
{
    int flag = 0;
    //获取数据传输通道的fd
    if (get_transfer_fd(sess) == 0) {
        return ;
    }

    //保存断点ian
    long long offset = sess->restart_pos;
    sess->restart_pos = 0;

    //以写入的方式创建文件
    int  fd = open(sess->arg, O_CREAT | O_WRONLY, 0666);
    if (fd == -1) {
        ftp_relply(sess, FTP_UPLOADFAIL, "Could not create the file.");
        return ;
    }
    //加写锁
    int ret = lock_file_write(fd);
    if (ret == -1) {
        ftp_relply(sess, FTP_UPLOADFAIL, "Could not create the file.");
        return ;
    }

    //三种上传方式:STOR、STOR+REST、APPE
    if (!is_append && offset == 0) {   //覆盖文件
        ftruncate(fd, 0); 
        if (lseek(fd, 0, SEEK_SET) < 0) {
            ftp_relply(sess, FTP_UPLOADFAIL, "Could not create the file.");
            return ;
        }
    } else if (!is_append && offset != 0) {  //REST+STOR  断点续传
        if (lseek(fd, offset, SEEK_SET) < 0) {
            ftp_relply(sess, FTP_UPLOADFAIL, "Could not create the file.");
            return ;
        }
    } else if (is_append) {//APPE  追加到末尾
        if (lseek(fd, 0, SEEK_END) < 0) {
            ftp_relply(sess, FTP_UPLOADFAIL, "Could not create the file.");
            return ;
        }          
    }

    //获取文件状态
    struct stat sbuf;
    ret = fstat(fd, &sbuf);
    if (!S_ISREG(sbuf.st_mode)) {  //不是普通文件
        ftp_relply(sess, FTP_UPLOADFAIL, "Could not create the file.");
        return ;
    }

    //回应150  ASCII与二进制传输唯一的区别就是 :是否对\r\n处理
    char tmp[1024] = {0};
    if (sess->is_ascii) {  //ASCII模式
        sprintf(tmp, "Opening ASCII mode data connection for %s (%lld bytes).", 
                    sess->arg, (long long)sbuf.st_size);
    } else {      //二进制模式
        sprintf(tmp, "Opening BINARY mode data connection for %s (%lld bytes).", 
                    sess->arg, (long long)sbuf.st_size);
    }
    ftp_relply(sess, FTP_DATACONN, tmp);

    //接收文件  从data_fd中接收数据,放在buf中,然后写入文件
    char buf[1024];
    int data_fd = sess->data_fd;

    sess->bw_transfer_start_sec = get_time_sec();
    sess->bw_transfer_start_usec = get_time_usec();

    while (1) {
        ret = read(data_fd, buf, sizeof(buf));
        if (ret == -1) {  //被中断打断 继续执行  其他情况退出
            if (errno == EINTR) continue;
            else {
                flag = 2;         //从数据socket读取失败
                break;
            }
        } else if (ret == 0) {  //成功读完
            flag = 0;
            break;
        }

        limit_rate(sess, ret, 1);  //判断上传限速
        if (sess->abor_received) {  //数据传输过程中的ABOR处理 给426回复
            flag = 2;
            break;
        }

        if (writen(fd, buf, sizeof(buf)) != ret) {  //写入到本地文件失败
            flag = 1;
            break;
        }
    }
    close(sess->data_fd);
    sess->data_fd = -1;
    close(fd);

    if (flag == 0) {
        ftp_relply(sess, FTP_TRANSFEROK, "Transfer complete.");
    } else if (flag == 1) {  //写入本地失败
        ftp_relply(sess, FTP_BADSENDFILE, "Failure writting to local file.");
    } else if (flag == 2) {  //读取网络失败
        ftp_relply(sess, FTP_BADSENDNET, "Failure reading from networks stream.");
    }

    check_abor(sess);  //传输完成后检查ABOR
    start_cmdio_alarm();  //数据传输完毕之后 重新启动控制通道时钟
}
posted @ 2021-02-16 22:08  Aspirant-GQ  阅读(67)  评论(0编辑  收藏  举报