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

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

6.1 限速

限速的关键在于睡眠时间,当前传输速度大于限速的时候,就进行睡眠,可以这样想:
传输速度 = 传输Byte / 传输时间 。 传输Byte是定值,只有改变传输时间才可以改变传输速度。

睡眠的时间 = 最大限速传输所需时间 - 当前速度传输所需时间 = (当前传输速度 / 限速传输速度 - 1)* 当前传输时间

所以问题的关键就是求出传输速度,核心在于求出传输x字节数据的时间差,所以要在传输数据之前计时,在数据传输完一轮之后,立即计算速度!!!

每一轮的read之后进行限速计算,在限速计算中判断是否超速,并进行睡眠操作。

在开始传输前,获取当前时间,并存在sess中:

  1. sess->bw_transfer_start_sec = get_time_sec();
  2. sess->bw_transfer_start_usec = get_time_usec();

限速计算中,获取当前时间与上一次时间做差,获得时间差:

  1. void limit_rate(session_t *sess, int bytes_transfered, int is_upload)
  2. {
  3. sess->data_process = 1; //数据传输状态
  4. //睡眠时间 = (当前传输速度/最大传输速度 - 1)*当前传输时间
  5. long curr_sec = get_time_sec();
  6. long curr_usec = get_time_usec();
  7. double elapsed; //当前传输时间
  8. elapsed = curr_sec - sess->bw_transfer_start_sec;
  9. elapsed += (double)(curr_usec - sess->bw_transfer_start_usec) / (double)1000000;
  10. if (elapsed <= (double)0) elapsed = (double)0.01; //浮点数
  11. unsigned int bw_rate = (unsigned int)((double)bytes_transfered / elapsed); //计算当前传输速度
  12. unsigned int max_rate; //最大传输速度
  13. max_rate = (is_upload == 1) ? sess->bw_upload_rate_max : sess->bw_download_rate_max;
  14. double rate_ratio;
  15. if (bw_rate > max_rate) {
  16. rate_ratio = bw_rate / max_rate;
  17. } else {
  18. //即使不需要限速 也要更新
  19. sess->bw_transfer_start_sec = get_time_sec();
  20. sess->bw_transfer_start_usec = get_time_usec();
  21. return ;
  22. }
  23. double pause_time = (rate_ratio - 1) * elapsed; //睡眠时间
  24. //通过封装 nanosleep函数进行睡眠
  25. nano_sleep(pause_time);
  26. //为什么不用sleep?sleep内部可能用时钟信号机制来实现,所以尽量不将sleep和alarm函数一起使用
  27. //alarm用来实现空闲断开
  28. //更新时间
  29. sess->bw_transfer_start_sec = get_time_sec();
  30. sess->bw_transfer_start_usec = get_time_usec();
  31. }

睡眠操作如下:

  1. void nano_sleep(double seconds)
  2. {
  3. time_t secs = (time_t) seconds;
  4. double fractional = seconds - (double) secs;
  5. struct timespec ts;
  6. ts.tv_sec = secs;
  7. ts.tv_nsec = (long) (fractional * (double) 1000000000);
  8. int ret;
  9. do {
  10. ret = nanosleep(&ts, &ts);
  11. } while (ret == -1 && errno == EINTR); //防止中断打断
  12. }

注意这里使用nanosleep进行睡眠操作,不使用sleep函数进行睡眠操作的原因是:Linux中并没有提供系统调用sleep(),sleep()是在库函数中实现的,它是通过调用alarm()来设定报警时间,调用sigsuspend()将进程挂起在信号SIGALARM上,它设置的定时器执行函数是在指定时间向当前进程发送SIGALRM信号。

后面要用alarm来实现空闲断开。

6.2 空闲断开

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

控制连接

安装信号SIGALRM,启动定时闹钟,如果再闹钟来临之前没有收到任何命令,在信号处理程序中关闭闹钟,并给客户端421 timeout的响应,退出会话。

在服务进程的处理逻辑中启动ALARM闹钟,在whil循环中,每一次收到命令后都重新启动闹钟,闹钟启动函数与处理函数如下:

  1. session_t *p_sess; //全局变量,在main中extern
  2. //定时处理函数
  3. void handle_alarm_timeout(int sig)
  4. {
  5. shutdown(p_sess->ctl_fd, SHUT_RD); //关闭读取
  6. ftp_relply(p_sess, FTP_IDLE_TIMEOUT, "Timeout");
  7. shutdown(p_sess->ctl_fd, SHUT_WR); //关闭写入
  8. exit(EXIT_FAILURE);
  9. //服务进程的退出会导致nobody退出,因为nobody会在内部通信是读取到0,代表服务进程退出,随之我们退出nobody进程
  10. }
  11. //启动ALARM闹钟 超时断开用到
  12. void start_cmdio_alarm(void)
  13. {
  14. if (tunable_idle_session_timeout > 0) {
  15. signal(SIGALRM, handle_alarm_timeout); //注册处理函数
  16. alarm(tunable_idle_session_timeout); //启动闹钟
  17. }
  18. }

可是这种情况忽略了数据传输的情况。

如果只是以上这种情况,在进行数据传输的时候,如果超时,也是会断开的。

所以!!!当目前处理数据传输的状态时,即使控制连接超时了,也不能退出会话。可以在数据传输前关闭控制连接的闹钟。

数据连接

如果数据连接通道建立了,一定时间没有传输数据,也要将会话断开。

实现方法:在传输数据之前安装SIGALRM信号,启动闹钟。传输数据的过程中如果收到信号,要进行判断,即如果不处于数据传输的时候退出,否则重新安装信号。所以在sess中新建一个变量data_process用于表示当前是否处于数据传输。

注意!!!在数据通道建立之前重启闹钟,另外创建一个信号处理函数,于控制通道的处理函数不同。与此同时还得关闭控制连接通道的闹钟

  1. void handle_siglarm(int sig)
  2. {
  3. if (p_sess->data_process == 0) { //如果不进行数据传输 退出
  4. ftp_relply(p_sess, FTP_DATA_TIMEOUT, "Data timeout. Reconnect.");
  5. exit(EXIT_FAILURE);
  6. }
  7. //当前处于数据传输状态 收到超时信号,将状态置为数据传输状态并重新启动闹钟
  8. p_sess->data_process = 0;
  9. start_data_alarm();
  10. }
  11. void start_data_alarm(void)
  12. {
  13. if (tunable_data_connection_timeout > 0) { //开启数据连接通道 闹钟
  14. signal(SIGALRM, handle_siglarm);
  15. alarm(tunable_data_connection_timeout);
  16. } else if (tunable_idle_session_timeout > 0) { //关闭控制连接通道 闹钟
  17. alarm(0); //关闭先前安装的闹钟
  18. }
  19. }

数据传输状态的确定放在限速函数中去实现!!!上传下载都要用到限速函数。

6.3 连接数限制

参考:github

最大连接数限制

超过最大连接数之后,回复客户端421提示,如下:

将当前连接数保存在全局变量中,当有新连接请求时与配置文件中的配置项进行判断。在创建新会话的时候判断最大连接数,通过check_limits来判断:

  1. void check_limits(session_t *sess)
  2. {
  3. //开启连接数限制 并且当前连接数大于最大连接数 421响应
  4. if (tunable_max_clients > 0 && sess->num_clients > tunable_max_clients) {
  5. ftp_relply(sess, FTP_TOO_MANY_USERS,
  6. "There are too many connected users, please try later.");
  7. exit(EXIT_FAILURE);
  8. }
  9. //检查每IP最大连接数
  10. if (tunable_max_per_ip > 0 && sess->num_this_ip > tunable_max_per_ip) {
  11. ftp_relply(sess, FTP_IP_LIMIT,
  12. "There are too many connections from your internet address.");
  13. exit(EXIT_FAILURE);
  14. }
  15. }

每当有断开连接,nobody进程都会向主进程发出SIGCILD信号,我们就在这个信号的处理函数中将连接数-1:

  1. void signal_handler(int argc)
  2. {
  3. //等待任意,没等到立刻返回0
  4. unsigned int pid;
  5. while (pid = waitpid(-1, NULL, WNOHANG) > 0) {
  6. --s_children; //统计的进程数-1 改变的是父进程的变量
  7. unsigned int *ip = hash_lookup_entry(s_pid_ip_hash, &pid, sizeof(pid));
  8. if (ip == NULL) {
  9. continue;
  10. }
  11. drop_ip_count(ip); //pid->ip->count--
  12. hash_free_entry(s_pid_ip_hash, &pid, sizeof(pid)); //释放空间
  13. }
  14. return ;
  15. }

要注意,在新会话中也创建了一个服务进程,当服务进程结束的时候,服务进程也会向新会话进程发送SIGCILD信号,但是我们之前以及建立了服务进程退出导致nobody进程退出的操作(read返回0),所以这里要在创建会话之前再声明一次信号处理操作。

在创建新会话之前进行如下操作,避免nobody中对服务进程的退出做响应:

  1. signal(SIGCHLD, SIG_IGN); //忽略信号

每个IP最大连接数

比如设置每个IP最大连接数是3,当同一IP第四个连接到达时,有如下回应:

需要维护两个哈希表:

  • IP与连接数的关系 :每IP的当前连接数,主要用于记录连接
  • 进程与IP的关系:在进程退出的时候,根据这个哈希表找到对应的IP,然后根据另外一个哈希表将计数减一

在最大连接数的限制的时候,进程退出时发出的信号,在信号处理函数中将连接数减一。对于每个IP连接数在进程退出时的变化,根据waitpid返回的pid,再通过pid与IP的哈希表找出对应IP的连接数,然后减一。

  1. //根据ip,将连接数+1
  2. unsigned int handle_ip_count(void *ip)
  3. {
  4. unsigned int count;
  5. //查看当前的连接数
  6. unsigned int *p_count = (unsigned int *) hash_lookup_entry(s_ip_count_hash,
  7. ip, sizeof(unsigned int));
  8. if (p_count == NULL) { //新增表项
  9. count = 1;
  10. hash_add_entry(s_ip_count_hash, ip, sizeof(unsigned int),
  11. &count, sizeof(unsigned int));
  12. } else {
  13. count = *p_count;
  14. ++count;
  15. *p_count = count;
  16. }
  17. return count;
  18. }
  19. //根据ip,将连接数-1
  20. void drop_ip_count(void *ip)
  21. {
  22. unsigned int count;
  23. unsigned int *p_count = (unsigned int *) hash_lookup_entry(s_ip_count_hash,
  24. ip, sizeof(unsigned int));
  25. if (p_count == NULL) {
  26. return;
  27. }
  28. count = *p_count;
  29. if (count <= 0) {
  30. return;
  31. }
  32. --count;
  33. *p_count = count;
  34. if (count == 0) {
  35. hash_free_entry(s_ip_count_hash, ip, sizeof(unsigned int));
  36. }
  37. }

miniFTP项目实战六的更多相关文章

  1. miniFTP项目实战五

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  2. miniFTP项目实战三

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  3. miniFTP项目实战四

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  4. miniFTP项目实战二

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  5. miniFTP项目集合

    项目简介 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务进 ...

  6. miniFTP项目实战一

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  7. 【无私分享:ASP.NET CORE 项目实战(第六章)】读取配置文件(一) appsettings.json

    目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 在我们之前的Asp.net mvc 开发中,一提到配置文件,我们不由的想到 web.config 和 app.config,在 ...

  8. SaltStack项目实战(六)

    SaltStack项目实战 系统架构图 一.初始化 1.salt环境配置,定义基础环境.生产环境(base.prod) vim /etc/salt/master 修改file_roots file_r ...

  9. 【.NET Core项目实战-统一认证平台】第十六章 网关篇-Ocelot集成RPC服务

    [.NET Core项目实战-统一认证平台]开篇及目录索引 一.什么是RPC RPC是"远程调用(Remote Procedure Call)"的一个名称的缩写,并不是任何规范化的 ...

随机推荐

  1. netcore3.1 + vue (前后端分离)Excel导入

    1.前端(vue)代码 2.公共类ExcelHelper 3.后端(netcore)代码 思路:导入类似于上传,将excel上传后将流转换为数据 1.前端(Vue)代码 这里使用的是ElementUI ...

  2. NSIS 插件开发引发的思考

    支持NSIS的DLL扩展编程通用语法结构 #include <windows.h> #include <stdio.h> #define FORCE_SWITCH " ...

  3. 脱离OBDeploy工具,手工部署OceanBase方法

    [简介] OBDeploy是OceanBase集群部署的工具,可以通过简单的几行命令,就能快速的进行OceanBase部署.但对于初学者来讲,可能会比较困惑,Deploy到底做了哪些事情?里面的具体步 ...

  4. python3安装pp过程

    并行计算的目的是将所有的核心都运行起来以提高代码的执行速度,在python中由于存在全局解释器锁(GIL)如果使用默认的python多线程进行并行计算可能会发现代码的执行速度并不会加快,甚至会比使用但 ...

  5. 备战- Java虚拟机

    备战- Java虚拟机 试问岭南应不好,却道,此心安处是吾乡. 简介:备战- Java虚拟机 一.运行时数据区域 程序计算器.Java 虚拟机栈.本地方法栈.堆.方法区 在Java 运行环境参考链接: ...

  6. 【洛谷P1795 无穷的序列_NOI导刊2010提高(05)】模拟

    分析 map搞一下 AC代码 #include <bits/stdc++.h> using namespace std; map<int,int> mp; inline int ...

  7. UnitTest + HTMLTestRunner

    #导入HTMLTestRunner类 from unitTest.tools1.HTMLTestRunner import HTMLTestRunner import unittest discove ...

  8. python基础问题

    包安装相关问:如何安装Python三方包?在命令行如何检查一个包是否已安装?答:安装用pip install 卸载用 pip uninstall 直接import 这个包问:环境变量PATH的作用是什 ...

  9. 当vue 页面加载数据时显示 加载loading

    参考:https://www.jianshu.com/p/104bbb01b222 Vue 页面加载数据之前增加 `loading` 动画 创建组件 1.新建 .vue 文件: src -> c ...

  10. 本地图片转base64编码

    通常获取图片的base64编码都是通过input的上传file属性获取转化,但是有时候需要的是本地图片不经过上传操作,直接拿本地图片转成base64编码就不行了,input上传操作需要人为操作一下,没 ...