本项目开发基于 Red Hat Enterprise Linux(RHEL) 6.3 平台 ,通过本项目大家会深入理解下述内容:
本项目使用 C 语言作为主要开发语言,Shell作为辅助开发语言,如果你想深入学习 Linux C 网络编程技术,那就快来加入吧!!!
这是我的第一篇 Chat 分享,之前都是在 我的CSDN博客:ZYZMZM 上更新 C/C++/Linux 等基础知识和一些底层实现原理之类的博文,想尝试一种新的方式,并且自己开发本项目也比较用心,本项目由我一个人独立开发,真诚地想和大家分享一下自己的感悟、项目的整题设计流程和遇到的各种坑,没有想到发布两天时间就有 120 多位朋友的预定,在这里再次感谢大家的支持。
完成本项目的预备知识我在文件的后记部分给大家分享了我 CSDN 博文的链接。
本场 Chat 主要围绕下述内容展开介绍:
文章最后给出项目的 GitHub 源码地址,供大家分享。
系统基于 TCP 协议提供局域网 LAN 内文件传输(含大文件传输)服务,首先要有基本的命令交互功能,比如客户端要查看服务端文件信息、root 管理员用户可以删除服务端文件信息等基本命令交互,核心是实现文件的上传和下载功能。
这些功能本身是非常容易实现的,是基本的 TCP 协议通信传输过程。再从功能上进行丰富,需要实现文件传输过程中的断点续传功能、秒传功能。进一步丰富功能,应该实现简单的用户管理功能:新用户可以通过注册进入系统,已注册的用户可以直接登录进入系统,而且系统中应该有一个管理员用户拥有更高级别的权限,比如删除服务端文件、关闭服务器等。
因为添加了用户管理模块,因此需要在文件的上传和下载部分做一些改变进行正确的控制,之后会详述。
更为重要的是需要实现多客户对服务器的并发访问,在这里我们简单地使用多线程来实现。因为服务器提供局域网 LAN 内文件传输,因此多线程实现是基本满足要求的。那么在大型复杂的环境中,我们应该使用分布式集群技术,可以采用负载均衡技术实现分布式集群的高并发问题,我们在文章最后会讨论其原理。
那么首先,先忽视一些细枝末节,我们分析系统应该如何实现文件上传和下载主要功能。
由于是进行文件传输,因此我们需要使用面向连接的、可靠的 TCP 协议来作为系统的网络传输协议。我们使用 Socket 编程,虽然难度较高,但对于理解协议的执行过程是很有效的学习方式,使用两个重要的接口 send() 和 recv() 函数来实现数据的收发功能。我们也自定义通信协议进行客户端与服务器的通信过程,例如,下载文件时,客户端向服务器发送“begin”,服务器收到后便开始向客户端传输文件,服务器传输完毕后向客户端发送“##close##”,客户端收到后便停止数据接收,文件传输完毕。
那么在这里我们需要考虑一个问题,当服务器与客户端进行文件传输的时候,文件未传输完毕,客户端因为各种原因退出了,那么服务器便会崩溃,导致了不稳定性的产生。根本原因是服务器会接收到一个 SIGPIPE 信号,我们可以使用三种方式来解决。
本系统为了服务器的高可用和稳定性,使用了第三种方式来解决客端异常断开连接的情况。我们在后面会进行讲解具体实现。
上面解决了系统的基本问题:上传和下载。接下来我们谈一下系统的用户管理设计、文件管理设计。
我们采用 MySQL 作为服务器的后台数据库,建立了三个数据库:
loginUser
用户信息数据库。数据表为 user,主要存储用户名和密码,用于登录验证的匹配。
md5
文件信息数据库。数据表为 md5table,主要存储文件的主要信息:文件名、文件 MD5 值、文件属主、文件完整标志位。
serverLog
服务器启动信息数据库。数据表为 serverStartInfo,主要记录服务器启动次数、启动时间。
上面的数据库其实并不是一开始就设计好的,在开发过程中遇到了新的需求,便建库或者新建字段。
首先我们来谈一下用户管理的问题。很简单,注册和登录,新用户注册成功后会将其用户名和密码信息存入数据库,进行登录时便可进行匹配,系统默认有一个 root 管理员用户,具有高级权限,那我们如何知道当前用户是否是管理员呢?我们这只需要设置标志位即可,根据登录的用户名设置标志位,便可在以后的操作中执行管理员权限了。
接下来我们谈一下文件管理设计。由于要进行文件传输服务,我们提供了断点续传和秒传的功能(具体的实现我们之后会详解),首先断点续传肯定是文件不完整,不能直接通过文件大小来确定。我们试想这样一种情况:客户端向服务器上传文件,没有上传完毕便终止,下次再次上传该文件时应该重传,但是在上传前客户端又修改了该文件,因此我们不能以文件大小来判断是否重传。为此我们在数据库中设置了文件完整标志位,若文件不完整并且属主是该客户,便可进行断点续传。若文件完整,当客户再次上传该文件时,MD5 值不相同便重传,MD5 值相同便直接秒传。
最后我们讲一下 serverLog 数据库的作用,由于我们要实现秒传功能,因此,我们应该在服务器第一次启动时初始化文件信息数据库,把服务端所有文件的文件名、文件 MD5 值、用户名、文件完整标志位都存入数据库。请注意,我们说的是第一次启动数据库时才进行初始化,之后启动都不用重新初始化。那么我们如何知道是否是第一次启动呢,我们便创建了 serverLog 数据库,记录服务器启动次数,我们查询该表,若返回的记录条数大于 1,便不是首次启动,就不用重新初始化了。
对于下载文件的过程,我们在服务器端的文件信息数据库中保存了每个文件的完整标志位信息,因此客端要下载时,服务器根据要下载的文件名去数据库中查询该文件的完整标志位的值,若文件完整,才允许客端下载。为什么会有文件不完整的情况产生呢?是因为有可能有些用户正在上传文件,但未上传完毕网络就中断了。在该用户继续向服务器续传该文件前,该文件存储在服务器时不完整的,因此当有其他用户请求下载该文件时,我们直接向客端发送错误码,即不允许下载。若文件标志位是完整的,服务器便向客户端发送文件数据进行传输,客户端进行文件的接收,若此时网络中断了,即文件没有传输完毕。那么在下次下载该文件时,不必再重传该文件,而是从断点出继续下载。这就是断点续传功能,我们之后会讲解断点续传的原理及实现。
对于上传文件的过程,相对来说就要复杂一些了,因为涉及到多个数据库的操作和修改。客户端要向服务器上传文件,客户端首先将文件名与文件的 MD5 值一起发送给服务器,服务器首先在数据库中进行匹配检查,文件名与 MD5 值是否相同以及文件完整标志位是否有效,若满足上面的条件,那么直接秒传,关于秒传的原理和实现我们之后会详解。否则,服务器将文件信息加入到文件信息数据库,包括文件名、文件 MD5 值、文件所属者、文件完整标志位。那么在客端上传前,我们将文件完整标志位置为 0,表示文件不完整。然后客端开始上传,若客端上传完毕,就将文件完整标志位置为 1,表示文件完整。文件完整其他用户便可以下载该文件。否则,客端没有上传完毕,那么文件信息数据库中的文件完整标志位仍然为 0,那么其他用户便不可以下载该文件。该文件标志位的作用还有很多,比如秒传时的判断、断点续传时的判断等等。
文件传输系统较为重要的功能便是断点续传了,使用断点续传可以有效的提高传输效率,提升系统性能。有时候可能下载的文件过大,一次性传输遇到网络问题就很可能传输失败而需要全部重新下载。另外,断点续传还能够提供并发的下载提高下载速率。
我们知道在网络状况不好的情况下,对于文件的传输,我们希望能够支持可以每次传部分数据。下次传输时不用全部重传,而是从上次断开的地方继续重传或者接收即可。
断点续传就是在下载/上传的断开点继续开始传输,不用再重新传输。其关键就在于对传输中断点的把握,下面是一个简单的示意图:
我们系统中断点续传的实现分为下载时的断点续传和上传时的断点续传。接下来我们分别介绍系统断点续传的实现。
先来看一下下载时的断点续传是如何设计的,首先我们保证服务器是不会修改客端上传的文件的,因此我们对于下载时的断点续传直接将客端本地已下载的文件大小发送给服务器端,服务器端将该文件直接偏移相应的大小,开始发送数据,客端开始从文件末尾开始写入,这样就实现了下载的断点续传功能。
转入正题,接下来我们讨论上传时的断点续传的设计与实现。上传时的断点续传就比下载要复杂一些,因为涉及到多个数据库的修改操作。情景是当客端上传文件由于网络中断未上传完毕,那么下次继续上传该文件时便需要从断点处续传了。由于我们加入了用户管理,因此我们必须判断上传该同名文件的用户是否是服务器数据库中存储的该文件的属主,若不是,那么就是其他用户在上传该文件了,就需要全部重传。若是,才能进行续传,续传的实现基本和下载文件的续传是相同的,续传完毕后,我们需要将该文件在文件信息数据库中存储的文件完整标志位从 0 置为 1,表示该文件是完整的。
高性能的文件传输服务器也应该具有秒传的模块,我们试想这样一种情景:用户 A 向服务器上传了一部电影,过段时间用户 B 又向服务器上传了一部电影,随着用户规模的增大,越来越多的用户都向服务器上传这部相同的电影。因此,我们提出了秒传的概念。
秒传是一种常见的“忽略式”上传方式,上传到服务器的每个文件,服务器都会校验 MD5 码。如果上传的该文件 MD5 码与已经存在于服务器里的文件的 MD5 码相同的话,服务器将会判断成为重复文件,只需要复制副本保存在服务器上即可,无需重新保存,因为有过这个文件,于是很快完成上传任务,并在有人需要下载的时候将原有的该文件的下载地址放出。这样实现了服务器的高效运作。
下图是本系统所设计的秒传模块。
我们创建了文件信息数据库来存储文件的信息,其中就包括文件的 MD5 值,在服务器首次启动时,我们通过调用脚本文件将服务器端所有文件的信息都存入数据库中,使用数据库的好处有以下几点:
在我们初始化文件信息数据库完毕后,若有用户需要上传文件,那么服务器就把该文件及其相应信息存入数据库中,当 root 用户删除某文件时,就将该文件从数据库中删除。
当有用户请求上传文件时,客端会将文件名和文件的 MD5 值一起发送给服务器,服务器收到后,便根据用户名在数据库中进行查询匹配 MD5 值,查到后边和用户发送来的 MD5 值进行匹配,若相同,则忽略上传,向客端发送秒传成功信号(前提是该文件完整标志位有效)。
本系统我们使用了多个数据库,因此我们实现了多个 API 接口,实现各类功能,例如登录验证、注册验证、初始化文件信息数据库、插入信息到文件信息数据库、在文件信息数据库中删除某个文件记录、MD5 值秒传匹配、判断文件属主是否为当前用户等 9 个 API 接口。
这些 API 接口与 Shell 脚本结合使用,提高程序开发效率,我们编写的 Shell 脚本包括计算文件 MD5 值、遍历当前目录将所有文件信息加入数据库(服务器初始化)、清空数据库、记录启动日志并插入到启动日志数据库、获取服务器发来了文件交互命令结果等多个 Shell 脚本。
Filename | Function |
---|---|
clear.sh | 清空文件信息数据库和登录日志数据库 |
com.sh | 将客端收到的命令存入文件中 |
fileMd5.sh | 服务器在首次启动时初始化文件信息数据库(遍历当前目录并计算 MD5 值存入) |
main.sh | 计算客户端将待上传文件的 MD5 值 |
sermain.sh | v9.0 版本使用,匹配 MD5 值(12.0 版本使用数据库 API 匹配) |
serverstartinfo.sh | 服务器每次启动都启动次数和启动时间信息存入登录日志数据库 |
我们知道,Linux 系统下的 open() 系统调用最多只能打开 2G 的文件,若大于 2G 则 open() 会执行失败,返回 -1。
我们要实现大文件的传输,首先在 open() 函数这里,我们需要使用到标志 O_LARGEFILE,另外我们使用的各种文件操作函数都需要进行修改,例如原来的 lseek() 函数应改为 lseek64() 函数,原来的 ftruncate() 清空文件的函数应改为 ftruncate64() 函数,原来的 atoi() 函数要改为 atoll() 函数,还有原来获取文件大小 int 类型变量全部都要改为 long long 类型,获取文件结构体的类型要改为 stat64,函数也应改为 stat64()。
接下来我们再来谈一下大文件分片传输的原理:多个上传请求均为分片的请求,把大文件分成多个小份一次一次向服务器传递分片完成后,即 upload 完成后,需要向服务器传递一个 merge 请求,让服务器将多个分片文件合成一个文件。即为分片重组的过程。
首先我们来看看传统的 read/write 方式进行 socket 的传输。
流程代码如下:
read(file, tmp_buf, len);write(socket, tmp_buf, len);
当需要对一个文件进行传输的时候,具体流程细节如下:
即 read/write 方式进行 Socket 的传输发生了下述调用:
在这个过程中发生了四次 copy 操作:硬盘 -> 内核 -> 用户 ->socket 缓冲区(内核)-> 协议引擎。
sendfile 系统调用在内核版本 2.1 中被引入,目的是简化通过网络在两个本地文件之间进行的数据传输过程。sendfile 系统调用的引入,不仅减少了数据复制,还减少了上下文切换的次数。
我们可以使用 sendfile 的实现零拷贝,工作原理如下:
sendfile 运行流程如下:
相较传统 read/write 方式,2.1 版本内核引进的 sendfile 已经减少了内核缓冲区到 user 缓冲区,再由 user 缓冲区到 socket 相关缓冲区的文件 copy。而在内核版本 2.4 之后,文件描述符结果被改变,sendfile 实现了更简单的方式,系统调用方式仍然一样,细节与 2.1 版本的 不同之处在于,当文件数据被复制到内核缓冲区时,不再将所有数据 copy 到 socket 相关的缓冲区,而是仅仅将记录数据位置和长度相关的数据保存到 socket 相关的缓存,而实际数据将由 DMA 模块直接发送到协议引擎,再次减少了一次 copy 操作。
目前我们只有一台主服务器,这是由于我们所设计的是局域网内的文件传输系统,一台服务器是足够我们使用的。
但是,抛开局域网的限制,在正常的流量请求中,一台服务器很多时候是完全不可以应对高并发的客端请求的,甚至有可能会造成服务器崩溃,直至系统瘫痪。因此当一台服务器的性能达到极限时,我们可以使用服务器集群来提高系统的整体性能。那么,在服务器集群中,需要有一台服务器充当调度者的角色,用户的所有请求都会首先由它接收,调度者再根据每台服务器的负载情况将请求分配给某一台后端服务器去处理。
那么在这个过程中,调度者如何合理分配任务,保证所有后端服务器都将性能充分发挥,从而保持服务器集群的整体性能最优,这就是负载均衡问题。
负载均衡是高可用网络基础架构的的一个关键组成部分,有了负载均衡,我们通常可以将我们的应用服务器部署多台,然后通过负载均衡将用户的请求分发到不同的服务器用来提高网站、应用、数据库或其他服务的性能以及可靠性。
它可以解决服务器单点故障和响应速度慢等问题,通常情况下,所有的后端服务器会保证提供相同的内容,以便用户无论哪个服务器响应,都能收到一致的内容。
那么负载均衡如何选择要转发的后端服务器呢?负载均衡器一般根据两个因素来决定要将请求转发到哪个服务器。
负载均衡算法决定了后端的哪些健康服务器会被选中。下面是几个常用的算法。
最后,想要解决负载均衡器的单点故障问题,可以将第二个负载均衡器连接到第一个上,从而形成一个集群。如下图所示:
当主负载均衡器发生了故障,就需要将用户请求转到第二个负载均衡器。由于 DNS 更改通常会在较长的时间才能生效,因此需要有一种能灵活解决 IP 地址重新映射的方法,比如浮动 IP(floating IP)。这样域名可以保持和相同的 IP 相关联,而 IP 本身则能在服务器之间移动。下面就是一个使用浮动 IP 的负载均衡架构动态示意图:
我们首先启动客户端,以 root 用户身份登录(已经注册),然后进入系统主页面,可以通过输入 help 来查看命令帮助,然后我们可以通过 get 命令下载文件,put 命令上传文件,还可以使用各种文件操作命令,当服务器端已存储该文件信息后,再次 put 相同文件时,可以直接秒传。如下图:
我们以普通用户登录,同样可以通过输入 help 来查看命令帮助,然后我们可以通过 get 命令下载文件,put 命令上传文件等,当服务器端已存储该文件信息后,再次 put 相同文件时,可以直接秒传。但是普通用户不可以删除文件,也不可以查看服务器的进程信息,如下图:
接下来我们看一下系统的断点续传功能和下载条件控制功能演示,首先 zy 用户向服务器上传 master.mp4 视频文件,上传一半后客户端终止,因此服务器保存的是不完整的文件信息。
接着 yb 用户登录,它要下载该文件系统显示文件不完整,不允许下载。
之后 zy 用户再次登录系统,然后继续上传该文件,我们通过下图可以看到,之前的文件并没有重新传输,而是从断点处续传了。
接下来我们看一下用户管理的注册部分,注册时若用户名和已存在的用户名相同,则系统会给出提示要求重新注册,注册成功后,系统给出登录界面,用户可以进行登录操作进入系统。
接下来我们演示一下大文件的传输和断点续传的过程,我们下面演示的示例传输文件 Ubuntu 是一个 2.3G 的大文件,为了演示大文件断点续传,我们在传输过程中故意切断客户端,如下图所示:
接下来我们重新登录并请求下载该文件,可以看到正在续传,如下图所示:
经过一段时间的传输,文件传输完毕,我们可以通过 ls 命令查看服务器端的该文件的大小,与我们在客端打印的下载到本地的文件大小相同,说明我们成功将大文件进行了断点续传。
最后我们看一种情景,zy 用户上传 master.mp4 视频文件,没有上传完毕便退出了,如下图:
此时另一个用于 yb 也上传相同文件,那么由于他们不是同一用户,因此该文件必须重传而不能续传,如下图所示:
本项目的完整源码地址为:
https://github.com/ZYZMZM/File-Transport-System/tree/master/v12.0
脚本文件的源码地址为:
https://github.com/ZYZMZM/File-Transport-System/tree/master/shell script
MySQL 数据库 API 的源码地址为:
https://github.com/ZYZMZM/File-Transport-System/tree/master/mysql
使用 sendfile 的源码地址为:
https://github.com/ZYZMZM/File-Transport-System/tree/master/v9.0
完成此项目的预备知识,包括 Socket 网络编程、TCP 编程、IO 复用函数、数据库基础——MySQL 编程、Shell 脚本编程。这些基础知识在我 CSDN 的博文中都有讲解。
本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。
阅读全文: http://gitbook.cn/gitchat/activity/5cecf4db71f35173c060c4c7
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。