多文件传输简介
解释:在编写多文件传输时,首先要明确我们多文件传输的目的,它的功能是什么。
当一个节点请求某一项资源时,不会是只有一个服务器向它发送资源,只要是拥有这个资源的节点就可以发送。每一个节点发送不同的部分。
优点:这样做大大降低了服务器的压力,不用多文件传输的话,常常一个服务器可能会不停的向节点发送资源。多文件的话会有别的节点帮助服务器一起发送,服务器甚至可以选择不参与发送节点。请求资源的节点接收资源耗费的时间也缩短了。
组成:多文件传输有三个角色。
- 资源管理中心 :中心拥有一张表,这张表上面记录的是资源基本信息与拥有这项资源的节点列表。
- 发送端 :拥有资源的节点
- 接收端:请求资源的节点
注意:中心那张表里的资源基本信息并不是资源的具体内容,而是有关这个资源的基本信息。
例如:这个资源的名字,他的绝对路径,他的版本。还有这个资源下所有的文件的文件编号,文件大小,文件的相对路径。还有将文件分割之后的片段信息,片段偏移量,片段大小,片段所属文件的编号。
//这是资源基本信息的构成
private String appName;//资源名字
private String absolutePath;//资源的绝对路径
private String version;//资源的版本
private List<ResourceStructInfo> rsiList;//所有文件的基本信息
private List<SectionInfo> sectionList;//文件分割后的片段信息
//为什么要写两个list呢,其实这两个list不是同时存在的。举个例子,当我一开始从
//某个APP服务器获取它的资源基本信息的时候,这时候接收端得到的应该是含有
//ResourceStructInfo列表的资源基本信息。而并没有sectionList。
//当然发送端注册时和接收端申请节点列表用的资源基本信息都是上述的这个。
//还有一种情况,下面再讲。
//我这样分比较麻烦,后面用的时候也要非常注意,所以也可以用更为简单的方法。
//也可以只用一个list,列表里面放的是,将文件编号,文件相对路径,文件大小,文件偏移量封装起来的类。这样的话,后续操作很多内容就要以他为准。
//所以一开始定好这些很关键,这就决定了你后面编写的时候,是否简单且逻辑清晰。本文用的还是我最开始的。
//这是资源下的文件基本信息
private int fileHandle;//文件编号
private String filePath;//文件的相对路径
private int size;//文件的大小
//这是文件分割后的片段信息
private int fileHandle;
private int offset;//片段的偏移量
private long size;//片段的大小
当然,资源的基本信息大概就是由这些构成,但是具体的实现并不唯一,主要看自己后面的实现怎么用着顺手就好
一个节点他会有两种身份,那就是发送者和接受者。
当他请求某项资源时,他是请求端;当他拥有某项资源后,他就是发送端。所以一个节点既可以是请求端也可以是发送端
疑问:那么应该会有人疑问为什么多文件传输的组成是这三部分?
我们先看一个图,这是他们三个之间的关系
接下来让我来为大家介绍他们三个的主要功能,和这张图中他们的三个的关系。
多文件传输的功能
技术:
在上图中发送端和中心是短连接rmi,接收端和中心也是短连接,接收端向发送端也是短连接,而发送端向接收端是长连接。
资源管理中心的功能:
- 发送端对某一项资源的注册,注销。
- 接收端对某一资源的申请。
//这个注解是短连接调用时需要的注解,这个是自己写的短连接工具
@InterfaceKlass(interfaces = {IProviderMethods.class, IRequestMethods.class})
public class ResourceCenter implements IProviderMethods, IRequestMethods {
//中心的这张表里,以资源的hashcode为键,以拥有这个资源的节点列表为值
private static final Map<String, List<NetNode>> resourceMap =
new HashMap<String, List<NetNode>>();
public ResourceCenter() {
}
@Override
public void registy(ResourceBaseInfo rbi, NetNode node) {
List<NetNode> nodeList = null;
String name = String.valueOf(rbi.toString().hashCode());
synchronized (resourceMap) {
nodeList = resourceMap.get(name);
if(nodeList == null) {
nodeList = new ArrayList<NetNode>();
resourceMap.put(name, nodeList);
}
nodeList.add(node);
System.out.println(node + "该节点已注册");
}
}
@Override
public void logout(ResourceBaseInfo rbi, NetNode node) {
String name = String.valueOf(rbi.toString().hashCode());
List<NetNode> nodeList = resourceMap.get(name);
if(nodeList == null) {
return;
}
if(nodeList.contains(node)) {
nodeList.remove(node);
}
if(nodeList.isEmpty()) {
resourceMap.remove(name);
}
}
@Override
public void removeNode(NetNode node) {
for(List<NetNode> nodeList : resourceMap.values()) {
Iterator<NetNode> listIter = nodeList.iterator();
while(listIter.hasNext()) {
if(listIter.next() == node) {
listIter.remove();
}
}
}
}
@Override
public List<NetNode> getServerList(ResourceBaseInfo rbi){
String name = String.valueOf(rbi.toString().hashCode());
List<NetNode> nodeList = resourceMap.get(name);
return nodeList;
}
}
发送端的功能:
-
发送具体片段值。
这里就是资源基本信息用法的第二种情况,接收端会远程调用发送端这边提供的方法,将含有这个节点分配到的片段列表的资源基本信息(也就是说资源基本信息里只有sectionList没有rsiList)和自己的IP,port传递过来。知道自己要发送的片段是什么之后,然后找到这个片段所在的位置在哪,然后打开文件,根据偏移量和大小,读取出来。这里有两个问题:
- 传递过来的绝对路径,不一定是我本地资源保存的绝对路径
- 我传递过来的片段里面并没有这个文件的相对路径
这两个路径构成了文件的路径,而我打开文件正需要这个路径,那么应该怎么做呢。
解决方法:在发送端这边也放一个表,储存自己本地拥有的资源,以appName来分类。这里存放的资源就是一开始有文件基本信息的资源。根据名字找到资源,根据文件编号找到文件的相对路径(这样做的时间复杂度是NxM,不太好)。
好了现在我们已经找到对应片段的值了,接下来就是将这个值发给接收端。这个发也是有讲究的。当时接收端传递过来的参数不仅有资源还有他的IP和port,我们只要知道这个就可以连接他的服务器,然后一个一个将片段发过去,直到发完接收端给他分配的所有的片段,然后结束会话。每给一个接收端发完所有的片段,发送端本身的发送次数加一,这是为了后面的负载均衡。
这下肯定有人会问了,我把片段的值发过去,可是接收端那边怎么知道这个值是哪一个文件的哪一块。所以之前的片段基本信息至关重要,每找到一块片段,先将他的资本信息发过去再发送值。
private SendSection sendSection;
public SendResource() {
sendSection = new SendSection();
}
//短连接接收端调用的方法
//接收端调用这个方法,接收端会一直等待结果的返回,为了接收端的的效率这边就采用了
//线程的形式。而且你也不可能一直等到发送端将所有的片段找到然后当成结果返回吧。
//所以这里就将接收端当做服务器连接,将片段发过去。
@Override
public void getResource(ResourceBaseInfo rbi, NetNode node) {
sendSection.setNode(node);
sendSection.setRbi(rbi);
new Thread(sendSection).start();
}
@Override
public int getSendCount() {
return sendSection.getSendCount();
}
private LocalResourcePool pool;
private ReadWrite rw;
private SendAndRecieveSection srs;
private Socket socket;
private ResourceBaseInfo rbi;
private NetNode node;
private volatile int count;
private DataOutputStream dos;
public SendSection(ResourceBaseInfo rbi, NetNode node) {
this.node = node;
this.rbi = rbi;
}
public SendSection() {
}
public static int getSendCount() {
return count;
}
@Override
public void run() {
pool = new LocalResourcePool();
rw = new ReadWrite();
srs = new SendAndRecieveSection();
try {
socket = new Socket(node.getIp(), node.getPort());
dos = new DataOutputStream(socket.getOutputStream());
String name = rbi.getAppName();
ResourceBaseInfo localRbi = pool.getResource(name);
List<ResourceStructInfo> rsiList = localRbi.getRsiList();
String localPath = localRbi.getAbsolutePath();
List<SectionInfo> sectionList = rbi.getSectionList();
for(SectionInfo section : sectionList) {
int fileHandle = section.getFileHandle();
for(ResourceStructInfo rsi : rsiList) {
if(rsi.getFileHandle() == fileHandle) {
String filePath = localPath + rsi.getFilePath();
byte[] result = rw.readBytes(filePath,
section.getOffset(), (int)section.getSize());
srs.setSection(section);
srs.setValue(result);
srs.send(dos);
}
}
}
close();//发送完所有的片段之后关闭会话。
System.out.println("请求端服务器已关闭........");
count++;
} catch (IOException e) {
e.printStackTrace();
}
}
接收端的功能:
- 节点选择策略:在得到节点列表之后选出此时压力较小的几个节点。
看到这应该就明白上面发送端为什么会有一个发送次数。接收端向中心请求资源得到的节点列表是所有的节点,而我们只需要选择几个比较“闲”的发送端,所以我们就有了一个节点选择。
接收端得到节点列表之后,先短连接每一个发送端,得到他们的发送次数,进行比较选出前几个发送次数较少的节点。这个比较有很多种方法,我这里介绍我自己用的方法。首先我画个图。
public List<NetNode> getNeededNode(List<NetNode> allNode) {
nodeList = new ArrayList<NodeAndCount>();
newNodeList = new ArrayList<NetNode>();
countArray = new int[nodeCount];
for(NetNode node : allNode) {
RMIClient client = new RMIClient(node.getIp(), node.getPort());
ClientProxy proxy = new ClientProxy(client);
IRequestToProvider requestToProvider = proxy.getProxy(IRequestToProvider.class);
int providerSendCount = requestToProvider.getSendCount();
NodeAndCount nodeCount = new NodeAndCount(node, providerSendCount);
nodeList.add(nodeCount);
countArray[providerSendCount]++;
}
int num = 0;
for(int i = 0; i < countArray.length; i++) {
num += countArray[i];
if(countArray[i] <= 0) {
continue;
}
for(NodeAndCount nodeCount : nodeList) {
if(nodeCount.getCount() == i && newNodeList.size() < this.nodeCount) {
newNodeList.add(nodeCount.getNode());
}
}
if(num >= nodeCount) {
break;
}
}
return newNodeList;
}
- 资源分配策略:节点选择之后,将需求的资源均分给每一个节点。即给每一个节点分配差不多的片段列表。
有了节点之后,就是给每一个节点均分资源。将文件均分给每一个节点,虽然说是均分其实还是会有一些片段不会是均分。
public List<NodeAndResource> getResourceDistributed(List<NetNode> nodeList,
ResourceBaseInfo orgRbi){
nodeResourceList = new ArrayList<NodeAndResource>();
List<SectionInfo> orgSectionList = orgRbi.getSectionList();
for(NetNode node : nodeList) {
NodeAndResource nar = new NodeAndResource();
ResourceBaseInfo rbi = new ResourceBaseInfo(orgRbi);
nar.setNode(node);
nar.setRbi(rbi);
nodeResourceList.add(nar);
}
int index = 0;
int length = nodeList.size();
for(SectionInfo section : orgSectionList) {
int fileLen;
int len = 0;
int restLen = (int) section.getSize();
int fileSize = restLen;
int fileHandle = section.getFileHandle();
while(restLen > 0 || fileSize == 0) {
fileSize = -1;
NodeAndResource nar = nodeResourceList.get(index);
ResourceBaseInfo rbi = nar.getRbi();
List<SectionInfo> sectionList = rbi.getSectionList();
if(sectionList == null) {
sectionList = new ArrayList<SectionInfo>();
rbi.setSectionList(sectionList);
}
fileLen = restLen > fileLength ? fileLength : restLen;
SectionInfo newSection = new SectionInfo(fileHandle, len, fileLen);
sectionList.add(newSection);
len += fileLen;
restLen -= fileLen;
index++;
index %= length;
}
}
return nodeResourceList;
}
- 连接上面选出来的节点,将他自己负责发送的节点,和接收端自己的IP和port通过短连接传递过去。
- 建立自己的服务器,接收发送端传过来的片段。
遇到的问题:
在编写多文件传输的时候遇到一个印象深刻的问题。当发送端发送完之后,关闭信道,但是我的接收端无法捕获这个异常,进而我没有办法关闭接收端的和他的信道。后来才发现是和自己用的read方法有关。
我用的是int java.io.DataInputStream.read(byte[] b, int off, int len) 这个方法,而这个方法在对方断掉以后,信道里没有数据以后只会返回-1,并没有抛出异常,所以我们无法捕获到异常。
但是我们可以自己判断,如果检测到返回值是-1,后续可以自己操作结束信道。
最后:
这是我自己关于多文件的一些想法,做法并不是唯一的。如果有遗漏或者不正确的地方,欢迎指出大家一起讨论。