多文件自平衡云模式文件传输系统(上)
关于多文件云传输的构思
对于传统的客户端和服务端来说,客户端面对的是单一的服务器, 服务器及网络的带宽决定了网络的性能,每台服务器提供的信息数量,受到自身存储空间的限制,而任意时刻它所能支持的客户端的访问数量,则受到自身处理能力和网络带宽的限制,一旦服务器崩溃,整个网络也随之瘫痪。
对于服务器来说,当拥有大量的客户端进行访问的时候,服务器将承受巨大的压力。
对我们的多文件传输来说,我们的客户端想要请求某一数据资源的时候,我们只能访问单一的服务器,如果存在其他的客户端需要请求的资源和之前某一个客户端请求的资源相同,那么对于服务器来说,就需要再次进行响应相同的请求,那无疑对服务器来说是一种压力。
所以我的多文件自平衡云传输框架,有一个显著的特点,那就是当某个客户端在进行服务器请求的时候,最开始只有服务器有资源,当某个客户端拥有资源后,自己同样可以充当一个服务端,那么当再有客户端请求相同的资源的时候,那就会存在两个服务端可以进行资源的发送,以此类推,在出现了大量客户端的请求是时,对于真正的服务器来说,压力有一定的降低。
对于真正的服务器来说,首先向注册中心服务器进行资源的注册,告知注册资源自己的信息以及自己拥有的资源,当某个客户端向服务器请求资源时,服务端只是响应相关资源的一些基本的信息,而不会直接将真正的资源发送给客户端,此时客户端根据资源的基本信息,去向注册中心请求相关拥有该资源的节点列表,客户端根据返回的节点列表的信息,进行负载均衡挑选合适的能够发送资源的节点,进行链接这些节点,由这些节点服务器进行资源不同片段的发送。客户端根据一定的方案,将分散的资源进行整合。同时在接收到完整的资源之后,自已同样也可以作为一个发送端。
多文件云传输架构
注册中心负责资源的管理,当有新的app服务器时,app服务器需要先向注册中心注册,,客户端每次上线后需要先建立与资源注册中心的连接,建立连接成功后注册中心会返回一个app服务器列表给客户端,客户端根据需要选择相应的app服务器后,注册中心将该app服务器的所有资源拥有者列表发送给客户端,客户端再进行负载均衡功能,选择为其服务的资源持有者列表,并与其逐个建立连接,连接建立完成后,资源持有者向该客户端发送文件。
文件的分片发送与断点续传
在前面的讲述中,我们知道客户端对资源的请求最终是由多个资源持有者共同向客户端发送,那么该怎样给每个资源持有者分配任务?如果有资源持有者在发送过程中有文件片段异常发送那么该怎样处理?
文件分片发送
基于网络的多端发送和单端接受:
对于一个文件,由多个发送端向一个接收端发送该文件;
每一个发送端发送这个文件的部分内容;
接收端必须能清楚的知道所接受的内容和缺少的内容;
发送端知道其要发送的内容,但是,接收端不知道某一个发送算要发送的内容的整体消息。
鉴于上面的描述,我们的结论是:
发送方的一次发送,接收方未必对应一次接受;
要使得发送与接受的信息对等,且,可控,最重要的是:接收端需要知道发送方本次发送的字节数。
则,应该按下面的思考考虑控制发送端和接受方行为:
发送方发送一段逻辑完整的数据时,发送两段数据:
第一段:长度固定,内容类型固定,且,包含第二段数据长度;
第二段:是真正要发送的数据;
接收方接受一段逻辑完整的数据时,接受两段数据:
第一段数据,长度固定,内容类型固定并且包含第二段数据长度;
第二段数据,是真正要接受的数据;
那我们在该项目中要发送和接收的是文件片段,接收端需要知道所接受的文件片段如下的基本信息:
①文件(int型)
②偏移量(long型)
③长度(int型)
在这我们用文件编号的表达文件而不是文件名称的原因是因为在上面我们有讨论过发送端发送和接收端接受时第一段数据时固定长度的,若用文件名称表示,则其长度不定。文件编号与文件名称之间的关系,可以事先编写,并同时让接收方发送方都拥有。
下面是一个 定义文件的类
。
//
public class FileInfo {
private int fileNo;
private String filePath;
private long fileSize;
public FileInfo() {
}
public void setFilePath(int fileNo, String absoluteRoot, String filePath) throws Exception {
this.fileNo = fileNo;
String absoluteFilePath = absoluteRoot + filePath;
File file = new File(absoluteFilePath);
if(!file.exists()) {
throw new Exception("文件[" + absoluteFilePath + "[不存在");
}
this.filePath = filePath;
this.fileSize = file.length();
}
public String getFilePath() {
return filePath;
}
public long getFileSize() {
return fileSize;
}
public int getFileNo() {
return this.fileNo;
}
@Override
public String toString() {
return fileNo + ":" + filePath + ":" + fileSize;
}
}
下面是一个 定义文件资源的类
。
//
public class SourceFileList {
private String absoluteRoot;
private List<FileInfo> fileList;
public SourceFileList() {
fileList = new ArrayList<FileInfo>();
}
public void setAbsoluteRoot(String absoluteRoot) {
this.absoluteRoot = absoluteRoot;
}
public String getAbsoluteRoot() {
return this.absoluteRoot;
}
public List<FileInfo> getFileList(){
return this.fileList;
}
public void addFile(String filePath) throws Exception {
FileInfo fileInfo = new FileInfo();
int fileNo = fileList.size() + 1;
fileInfo.setFilePath(fileNo, absoluteRoot, filePath);
fileList.add(fileInfo);
}
public void collectFiles() {
if(this.absoluteRoot == null) {
return;
}
collectFiles(absoluteRoot);
}
private void collectFiles(String curPath) {
File curDir = new File(curPath);
File[] files = curDir.listFiles();
for(File file : files) {
if(file.isFile()) {
System.out.println(file);
String filePath = file.getPath().replace(absoluteRoot, "");
try {
addFile(filePath);
} catch (Exception e) {
e.printStackTrace();
}
}else if(file.isDirectory()) {
collectFiles(file.getAbsolutePath());
}
}
}
@Override
public String toString() {
StringBuffer result = new StringBuffer();
result.append("资源根:").append(this.absoluteRoot).append('\n');
for(FileInfo fileInfo : fileList) {
result.append("\t").append(fileInfo).append('\n');
}
return result.toString();
}
}
下面是一个 资源池
,以app服务器为键,该服务器所对应的的资源为值形成的一个Map,并且该类时单例工厂模式。
//
public class Resources {
private static Map<ResourceInfo, SourceFileList> resourcePool;
private static volatile Resources me;
private Resources() {
resourcePool = new HashMap<ResourceInfo, SourceFileList>();
}
public static Resources newInstance() {
if(me == null) {
synchronized (Resources.class) {
if(me == null) {
me = new Resources();
}
}
}
return me;
}
public void addResource(ResourceInfo resourceInfo, SourceFileList sourceFileList) {
resourcePool.putIfAbsent(resourceInfo, sourceFileList);
}
public void removeResource(ResourceInfo resourceInfo) {
resourcePool.remove(resourceInfo);
}
public SourceFileList getSourceFileList(ResourceInfo resourceInfo) {
return resourcePool.get(resourceInfo);
}
}
下面的是 关于资源发送端的基础类
。
//
public class ResourceSender extends DefaultNetNode{
public static final int APP_SERVER = 1;
public static final int APP_CLIENT = 2;
private int nodeType;
public ResourceSender() {
super();
}
public ResourceSender(String ip, int port) {
super(ip, port);
}
public ResourceSender(String ip, int port, int nodeType) {
super(ip, port);
setNodeType(nodeType);
}
public int getNodeType() {
return nodeType;
}
public void setNodeType(int nodeType) {
this.nodeType = nodeType;
}
}
断点续传
所谓断点续传,是当接受文件完毕后,若文件接受不完整,需要再次接受,但是只用接受那些不完整的部分。
在这部分主要需要处理的难点是:
如何知道哪些片段未接收?
由上面我们已经知道接收的信息组成为三部分
①文件编号
②偏移量
③片段长度
在发送时,存在不同发送端发送同一文件的不同片段,因此,某一个发送端最后一次发送的内容,不一定是最后一个片段,所以,不可以根据“最后一个片段”来计算哪些没有发送;另外,如果某一个发送端发送一部分内容后,断网,这个发送端应该发送的很多片段都没有发送,需要计算出来。
在这我们有一个断点续传的算法:
手动模拟过程如下:
具体实现过程:
在这,文件片段的切片,是由专门的程序控制,使得切分得到的偏移量和长度不存在“交叉”;
从上述操作过程来看,offset:len的数对必须按顺序排列,又因为存在频繁地插入和删除,因此,这个算法使用的数据结构应该是Linkist。
在这里,因为可能存在同时接收到多个片段,应该注意线程安全问题。
须注意的是,这个操作最终可以得到的是因发送方异常造成传输不完整,可以立刻得到断点续传所需片段;但是,如果是接收方异常,尤其是接收方掉电,这种方法并不支持断点续传。
关于断点续传的代码如下:
下面是一个关于 断点续传文件片段的基础类
。
//
public class FileSectionInfo {
private int fileNo;
private long offset;
private int len;
private byte[] content;
public FileSectionInfo() {
}
public FileSectionInfo(byte[] bytes) {
this.fileNo = MecBinary.bytesToInt(bytes, 0);
this.offset = MecBinary.bytesToLong(bytes, 4);
this.len = MecBinary.bytesToInt(bytes, 12);
}
public FileSectionInfo(int fileNo, long offset, int len) {
this.fileNo = fileNo;
this.offset = offset;
this.len = len;
}
public byte[] toBytes() {
byte[] res = new byte[16];
MecBinary.intToBytes(res, 0, fileNo);
MecBinary.longToBytes(res, 4, offset);
MecBinary.intToBytes(res, 12, len);
return res;
}
public int getFileNo() {
return fileNo;
}
public void setFileNo(int fileNo) {
this.fileNo = fileNo;
}
public long getOffset() {
return offset;
}
public void setOffset(long offset) {
this.offset = offset;
}
public int getLen() {
return len;
}
public void setLen(int len) {
this.len = len;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
@Override
public String toString() {
return fileNo + " : " + offset + ", " + len;
}
}
在这我们定义一个关于文件片段的分发器,并给其默认(可由用户定义)执行方式。
//
public interface IDistributionStrategy {
default List<List<FileSectionInfo>> distributionFileSection(List<FileSectionInfo> sectionList, int count){
List<List<FileSectionInfo>> result = new ArrayList<List<FileSectionInfo>>();
for(int i = 0; i < count; i++) {
result.add(new ArrayList<FileSectionInfo>());
}
int index = 0;
for(FileSectionInfo section : sectionList) {
List<FileSectionInfo> oneSectionList = result.get(index);
oneSectionList.add(section);
index = (index + 1) % count;
}
return result;
}
}
为了后面方便,我们先实现这个接口实现类:
//
public class DefaultDistributionStrategy implements IDistributionStrategy {
public DefaultDistributionStrategy() {
}
}
由于在客户端需要进行一个负载均衡过程,而该过程应该由用户在自己选择实现方式,所以我们在这给一个接口。
//
public interface ISenderSelectedStrategy {
List<DefaultNetNode> selectesSender(List<DefaultNetNode> senderList);
}
我们在这给其一个实现类:
// An highlighted block
public class DefaultSenderSelector implements ISenderSelectedStrategy{
public DefaultSenderSelector() {
}
@Override
public List<DefaultNetNode> selectesSender(List<DefaultNetNode> senderList) {
List<DefaultNetNode> result = null;
if(senderList == null || senderList.isEmpty()) {
return result;
}
result = new ArrayList<DefaultNetNode>();
for(INetNode node : senderList) {
if(node instanceof ResourceSender) {
System.out.println("系统懵逼了");
return null;
}
ResourceSender sender = (ResourceSender)node;
if(sender.getNodeType() == ResourceSender.APP_SERVER) {
continue;
}
result.add((DefaultNetNode)node);
}
return result;
}
}
准备好这些东西后我们开始进行文件的分发和文件的断点续传。
文件的分片与分发
在文件的分片过程中,用户可以自己定义分片的大小,同时在此我们自己给它一个分片默认大小值,其大小为64kb,在将资源分片后,调用分发器的分发功能将该资源分片结果分给每个资源拥有者。
//
public class FileDistribution {
public static final int DEFAULT_SECTION_SIZE = 1 << 15;
private int maxSectionSize;
private IDistributionStrategy distributionStrategy;
public FileDistribution() {
this.maxSectionSize = DEFAULT_SECTION_SIZE;
this.distributionStrategy = new DefaultDistributionStrategy();
}
public void setMaxSectionSize(int maxSectionSize) {
this.maxSectionSize = maxSectionSize;
}
public void setDistributionStrategy(IDistributionStrategy distributionStrategy) {
this.distributionStrategy = distributionStrategy;
}
public List<List<FileSectionInfo>> distribution(SourceFileList sourceFileList, int count){
List<FileSectionInfo> sectionList = new ArrayList<FileSectionInfo>();
List<FileInfo> fileList = sourceFileList.getFileList();
for(FileInfo file : fileList) {
int fileNo = file.getFileNo();
long fileSize = file.getFileSize();
int len;
long offset = 0;
while(fileSize > 0) {
len = (int) (fileSize > maxSectionSize ? maxSectionSize : fileSize);
FileSectionInfo section = new FileSectionInfo(fileNo, offset, len);
sectionList.add(section);
fileSize -= len;
offset += len;
}
}
List<List<FileSectionInfo>> result = distributionStrategy
.distributionFileSection(sectionList, count);
return result;
}
}
文件的断点续传
由前面我们对断点续传的描述,我们首先需要建立一个定义offset和len变量的类:
//
public class SectionInfo {
private long offset;
private long len;
SectionInfo() {
}
SectionInfo(long offset, long len) {
this.offset = offset;
this.len = len;
}
long getOffset() {
return offset;
}
void setOffset(long offset) {
this.offset = offset;
}
long getLen() {
return len;
}
void setLen(long len) {
this.len = len;
}
boolean isRange(long offset) {
return this.offset + this.len > offset;
}
@Override
public String toString() {
return offset + ":" + len;
}
}
关于文件中未发送片段的计算与断点续传:
//
public class UnReceiverSection {
private int fileNo;
private List<SectionInfo> sectionList;
public UnReceiverSection(int fileNo, long fileSize) {
System.out.println("文件编号:" + fileNo + ", 文件长度:" + fileSize);
this.sectionList = new LinkedList<SectionInfo>();
this.fileNo = fileNo;
this.sectionList.add(new SectionInfo(0, fileSize));
}
public UnReceiverSection(long offset, long len) {
this.sectionList.add(new SectionInfo(offset, len));
}
public void addUnreceiveSection(int fileNo) {
this.fileNo = fileNo;
this.sectionList = new LinkedList<SectionInfo>();
}
private int searchSection(Long recOffset)
throws ReceiveSectionOutOfRangeException {
for(int i = 0; i < sectionList.size(); i++) {
SectionInfo section = sectionList.get(i);
if(section.isRange(recOffset)) {
return i;
}
}
throw new ReceiveSectionOutOfRangeException(
"文件号:" + fileNo + "片段偏移量:" + recOffset);
}
public void receiveSection(long recOff, long recLen)
throws ReceiveSectionOutOfRangeException {
int index = searchSection(recOff);
SectionInfo curSection = sectionList.get(index);
long curOff = curSection.getOffset();
long curLen = curSection.getLen();
long lOff = curOff;
long lLen = recOff - curOff;
long rOff = recOff + recLen;
long rLen = curOff + curLen - rOff;
sectionList.remove(index);
if(rLen > 0) {
sectionList.add(index, new SectionInfo(rOff, rLen));
}
if(lLen > 0) {
sectionList.add(index, new SectionInfo(lOff, lLen));
}
}
public boolean isReceived() {
return sectionList.isEmpty();
}
public List<SectionInfo> getSectionList() {
return sectionList;
}
}