接上篇【多文件自平衡云传输 二】网络收发文件片段&文件片段读写准备好操作文件片段和文件读写的工具之后。现在需要考虑多文件问题,上篇测试示例中只是处理了单个文件的分片收发,而且是一个发送端对一个接收端。现在需要结合之前准备好的资源发现的一套工具来处理多文件问题,并实现多个发送端同时给一个接收端发送文件。
什么是资源?
在多文件收发的角度来看,“多文件信息”就是一个资源。
如何结合资源发现,各自扮演什么角色?
- 资源持有者:也就是拥有这套文件资源的一端。也即他们负责发送文件。
- 资源请求者:也就是请求获取这套文件资源的一端,也即他们负责接收。
- 资源注册中心:资源注册中心只负责资源的注册注销。而且它保存了所有资源持有者的网络地址。
需要注意的是:资源请求者一个是从注册中心获取资源持有者网络地址的,因为他一开始不可能知道谁有这个资源。当获取到资源持有者网络地址之后,二者才建立连接进行收发
首先上篇中已经存在了表示一个文件片段信息的类。操作多文件,在多的角度,首先应该以单个文件为基本单元。
要表示一个文件的信息,需要声明如下信息:
- 一个文件的路径(相对路径),我所说的多文件是基于一个指定目录(暂且将其成为父级根目录)下的所有文件(其中可以嵌套目录)。所以相对于该目录来说,这里面的文件应该表示为相对路径。
- 这个文件的大小
- 文件的编号(利用文件编号相等与否来区分是否为同一文件)
FileInfo
//注意:区别于FileSectionInfo(表示一个片段的信息),而该类表示一个文件的大小
public class FileInfo {
private String filePath; //该文件路径是相对路径
private long fileSize; //该文件的长度
private int fileNo; //该文件编号
//要根据绝对路径去找,所以此处需要设置绝对路径
public void setFilePath(int fileNo, String absoulteRoot, String filePath) {
this.fileNo = fileNo;
String absoultPath = absoulteRoot + filePath;
File file = new File(absoultPath);
if(!file.exists()) {
throw new RuntimeException("文件[" + absoulteRoot + "]不存在");
}
this.fileNo = fileNo;
this.fileSize = file.length();
this.filePath = filePath;
}
//...省略三个属性的get和set方法
//按照文件编号作为相等条件,生成hashCode和equals方法
@Override
public String toString() {
return filePath + " : " + fileSize + " : " + fileNo;
}
}
在得到一个文件信息的对象之后。还需要一个能表示一个目录(指定的父级目录)下的所有文件结构的类,即SourceFileList。该类的涵盖了一个目录下的所有文件结构,核心方法collectFiles是通过指定的上级目录来收集该目录层级下的所有文件信息而且,此处只是收集信息,包括这个文件的路径(含目录层级结构),这个文件的大小和文件编号。
如果说ResourceInfo是用于标识一个资源的话(暂且将其称为资源基本信息),那么此处的SourceFileList就是这个资源的具体内容,暂且将其称为“资源详细信息”。而且这个“表示多文件信息的类”是发送端和接收端共同需要的。发送端要明确自己要发送哪些文件(一个资源)。所以它就需要按照自己本地文件结构来填充SourceFileList。 相应的接收端要接收这套资源。接收到之后他需要按发过来的结构来写入到自己的磁盘中。
SourceFileList
public class SourceFileList {
private String absoulteRoot; //父级根目录
private List<FileInfo> fileInfoList; //该上级目录下的所有文件信息集合
public SourceFileList() {
fileInfoList = new ArrayList<FileInfo>();
}
public String getAbsoluteRoot() {
return absoulteRoot;
}
public void setAbsoluteRoot(String absoluteRoot) {
this.absoulteRoot = absoluteRoot;
}
public List<FileInfo> getFileInfoList() {
return fileInfoList;
}
public void addFile(String filePath) {
FileInfo fileInfo = new FileInfo();
int fileNo = fileInfoList.size() + 1;
fileInfo.setFilePath(fileNo, absoulteRoot, filePath);
fileInfoList.add(fileInfo);
}
public void collectFiles() {
if(this.absoulteRoot == null) {
return;
}
collectFiles(absoulteRoot);
}
//收集一个指定父级根下的所有文件信息
private void collectFiles(String curPath) {
File curDir = new File(curPath);
File[] files = curDir.listFiles();
for (File fileItem : files) {
if(fileItem.isFile()) { //是文件的情况
String filePath = fileItem.getPath().replace(absoulteRoot, "");
addFile(filePath);
}else{ //是目录的情况,再递归进去
collectFiles(fileItem.getAbsolutePath());
}
}
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer(absoulteRoot + "\n");
for(FileInfo fileInfo : fileInfoList) {
sb.append("\t" + fileInfo + "\n");
}
return sb.toString();
}
}
有了目录结构之后,还需要具体操作磁盘文件的对象。因为一切的收发都是需要建立在能够与本地磁盘文件交互的基础上的,一个文件对应一个FileReadWrite对象。由于一套目录结构有多个文件,所以可以将操作这些真实文件的FileReadWrite对象封装在一个容器中。不论是发送端还是接收端,都必须有一个自己的这个容器。
ResourceFilePool(以HahMap存储数据)
public class ResourceFilePool {
// 键: 文件编号 值:FileReadWrite对象 (一个文件对应一个FileReadWrite对象)
private Map<Integer, FileReadWrite> fileAcceptPool;
private IFileReadWriteIntercepter fileReadWriteIntercepter= new FileReadWriteIntercepterAdapter();
public ResourceFilePool() {
this.fileAcceptPool = new HashMap<Integer, FileReadWrite>();
}
public void setFileReadWriteIntercepter(IFileReadWriteIntercepter fileReadWriteIntercepter) {
this.fileReadWriteIntercepter = fileReadWriteIntercepter;
}
//根据传过来的文件集合来填充fileAcceptPool。主要是创建用于操作每个文件的FileReadWrite对象
public void addFileList(SourceFileList sourceFileList) {
List<FileInfo> fileInfoList = sourceFileList.getFileInfoList();
for (FileInfo fileInfo : fileInfoList) {
//注意这里的FileReadWrite对象里边的filePath是绝对路径,用于定位到磁盘文件
FileReadWrite frw = new FileReadWrite(fileInfo.getFileNo(), sourceFileList.getAbsoluteRoot() + fileInfo.getFilePath());
//为每个文件操作设置拦截器
frw.setIntercepter(fileReadWriteIntercepter);
fileAcceptPool.put(fileInfo.getFileNo(), frw);
}
}
public FileReadWrite getAcceptFrw(int fileNo) {
return fileAcceptPool.get(fileNo);
}
}
接收端的逻辑
接收端即资源请求者。它是用来接收数据的。这里在接收端建立服务器。发送端作为客户端。而且需要注意的是,此处启动发送任务,是通过RMI技术远程调用发送端的“本地方法”。发送时机的控制权在接收端。
还有重要的一点是:接收端接收信息之前,一定是知道这套资源的详细信息的。
接收端服务器:接收端服务器只完成服务器本身的功能;对于接收过程逻辑,尽可能不参与。使用的是BIO通信,故接收端要提前得知自己需要侦听几个连接。
作为服务器的最基本的功能,启动服务器(ip和端口都是必须的)和关闭服务器的功能。此外接收端通过线程来专门处理客户端连接,在获得一个连接之后再通过独立线程负责通信问题。使用BIO就是会造成线程开销大所以为了尽可能做一些优化,可以使用线程池。核心接收数据过程
发送端发来文件片段。主要是通过发送端发送过来“之前约定好的结束标识”来结束通信线程。而且一个大问题是:当接收完毕之后,需要将自己注册为资源持有者,可以再参与到后边的资源发送中。
ReceiveServer
public class ReceiveServer implements Runnable{
public static final int DEFAULT_PORT = 54191;
private ServerSocket server;
private int port;
private String ip;
private volatile boolean goon;
//这个必须给,必须明确发送端数量,这样才能知道我要侦听多少个连接。 否则使用BIO会阻塞
private int senderCount;
private ThreadPoolExecutor threadPool;
private ResourceFilePool receiveFilePool; //用于接收端与本地磁盘文件交互的容器
public ReceiveServer(int port) {
try {
this.ip = InetAddress.getLocalHost().getHostAddress();
this.port = port;
this.threadPool = new ThreadPoolExecutor(5, 20, 500, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
this.receiveFilePool = new ResourceFilePool();
this.goon = false;
this.senderCount = 0;
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
public ReceiveServer() {
this(DEFAULT_PORT);
}
/**
* 要使接收到的数据能够写入到接收端本地磁盘,必须要先填充该池子。即:创造出每个文件对应的FileReadWrite对象
* 当然,此处接收端只使用其“写操作”功能
*/
public void setReceiveFilePool(SourceFileList sourceFileList) {
this.receiveFilePool.addFileList(sourceFileList);
}
//服务器启动方法
public void startUp() {
if(goon) {
return;
}
try {
server = new ServerSocket(port);
this.goon = true;
//此处开启线程侦听连接
new Thread(this,"服务器接收端").start();
} catch (IOException e) {
e.printStackTrace();
}
}
//关闭服务器方法
public void shutDown() {
if(!goon) {
return;
}
this.goon = false;
if(this.server != null && !this.server.isClosed()) {
try {
this.server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void setPort(int port) {
this.port = port;
}
public int getPort() {
return this.port;
}
public void setSenderCount(int senderCount) {
this.senderCount = senderCount;
}
public String getIp() {
return ip;
}
@Override
public void run() {
if(this.senderCount <= 0) {
return;
}
for(int index = 0; index < senderCount; index++) {
try {
Socket socket = server.accept();
System.out.println("侦听到一个连接");
Receiver reveiver = new Receiver(socket);
//对于每一个发送端的连接,也使用线程与之进行独立通信
threadPool.execute(reveiver);
} catch (IOException e) {
e.printStackTrace();
}
}
shutDown();
}
//接收线程,负责接收数据(文件真实内容)
class Receiver implements Runnable{
private FileSectionSendRecevice fssr;
private DataInputStream dis;
public Receiver(Socket socket) {
try {
this.fssr = new FileSectionSendRecevice();
this.dis = new DataInputStream(socket.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
System.out.println("线程启动,准备往磁盘写入数据");
FileSectionInfo sectionInfo = this.fssr.receiveSection(dis);
FileReadWrite frw = null;
while(sectionInfo.getLen() > 0) {
frw = receiveFilePool.getAcceptFrw(sectionInfo.getFileNo());
frw.writtenSection(sectionInfo);
sectionInfo = this.fssr.receiveSection(dis);
}
/**
* 能执行到这里就说明一个发送端已经完成了自己的发送任务。所以此处tmpCount减1,表示已经有一个完成任务了。
* 当tmpCount减为0时,说明所有片段均已经接收到。即整个资源接收完毕
*/
ResourceReceiver.tmpCount.decrementAndGet();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
接收端能建立服务器接收数据是必须的,最基本的。不过基于最初的考虑,接收端需要按照资源的基本信息能够获取到资源持有者网络地址集合。所以他应该是一个资源请求者。
而且为了使发送端负载均衡,可以在获取到资源持有者之后并不是所有资源持有者都需要参与到发送任务中来,此处执行两种负载均衡策略:
- 筛选:筛选出负载较轻的资源持有者做为发送端来执行发送任务。
- 均衡分配任务:在筛选出来发送端之后,我需要尽可能地让每个发送端的任务量均衡,即每个发送端发送片段量差不多。譬如:现在有3个发送端,一个接收端。接收端需要接收A资源(包含a文件)。我可以根据资源的详细信息(包含了该文件的编号,大小)来将每个文件切分成多个片段集合,然后按照发送端数量将这些片段集合组装在一个容器中。一个发送端发送一个小集合分量的数据。
在得到上述数据基础之后就可以进行发送。---所以啊,为什么要将是否发送,谁来发送的控制权交给接收方,因为我从资源注册中心获取到资源持有者集合之后,需要筛选出合适的作为发送端,而且要为每个发送端分配发送任务,等这些东西处理好了,才开始发送,所以控制权应该在接收方。但是发送方法的本体一定是存在于发送端的,因为发送端需要读取需要发送资源详细数据。故此处利用RMI调用远程方法。
ResourceReceiver
/**
*资源接收:此处存在两个远程方法的调用
*1. 资源注册中心的远程方法:为获取资源持有者地址列表,由于注册中心掌握着这个数据。
* 故需要从注册中心获取该信息,而具体的获取方法本体存在于注册中心一端,故需要执行注册中心的远程方法
*
*2. 发送端(资源持有者)的远程方法。发送方法本体存在于发送端。再合理的筛选出资源持有者作为发送端之后
* 为每个发送端均衡地分配需要发送的片段大小,一切准备就绪之后,主动调用发送端的发送方法。
*/
public class ResourceReceiver {
//资源基本信息
private ResourceInfo baseInfo;
//源文件集合, 该需要由外部设置进来。以明确发送端发过来文件的目录结构
private SourceFileList sourceFileList;
//资源请求者(接收端即为资源请求者)
private ResourceRequester requester;
//文件片段均衡分配
private FileDistribution fileDistribution;
//发送端负载均衡策略:通过该策略来筛选出负载较轻的资源持有者作为发送端
private ISenderSelectedStrategy senderSelectedStrategy;
//接收服务器---用于接收数据。并将接收到的数据写入到磁盘中
private ReceiveServer receiveServer;
//为了能执行发送端的“发送方法本体”。故此处应该存在RMIClient。
private RMIClient receiveClient;
//该成员就是为了执行远程方法而定义的接口。
private IResourceSender resourceSender;
//再接受完之后需要将自己注册为资源持有者,故需要知道资源注册中心的网络地址
private String centerIp;
private int centerPort;
//在获取到发送端数量的时候初始化值为发送端数量。当一个端发送完毕之后就进行减一,当为值0时说明接收完毕
static volatile AtomicInteger tmpCount = null;
//双参构造:由于需要向注册中心获取资源持有者地址列表。故需要连接资源注册中心
public ResourceReceiver(String centerIp, int centerPort) {
this.requester = new ResourceRequester();
this.requester.setCenterIp(centerIp);
this.requester.setCenterPort(centerPort);
this.fileDistribution = new FileDistribution();
this.receiveServer = new ReceiveServer();
this.receiveClient = new RMIClient();
this.resourceSender = this.receiveClient.getProxy(IResourceSender.class);
this.centerIp = centerIp;
this.centerPort = centerPort;
}
//设置最大文件片段长度 这个是一次最大接收的片段长度
public void setMaxFileSectionSize(int maxSectionSize) {
this.fileDistribution.setMaxSectionSize(maxSectionSize);
}
//设置发送端筛选策略
public void setSenderSelectedStrategy(ISenderSelectedStrategy senderSelectedStrategy) {
this.senderSelectedStrategy = senderSelectedStrategy;
}
//设置资源发送者 负载均衡的策略
public void setDistributionStrategy(IDistributionStrategy strategy) {
this.fileDistribution.setDistributionStrategy(strategy);
}
//设置资源基本信息
public void setBaseInfo(ResourceInfo baseInfo) {
this.baseInfo = baseInfo;
}
//设置资源文件列表(一个资源的详细信息)
public void setFileList(SourceFileList fileList) {
this.sourceFileList = fileList;
}
//设置接收服务器的Port
public void setReceiveServerPort(int port) {
this.receiveServer.setPort(port);
}
//核心方法
public boolean getResourceFiles() throws ResourceNotExistException{
//1.根据 资源基本信息 获取资源持有者集合
List<DefaultNetNode> senderList = requester.getAddrList(baseInfo);
System.out.println("资源持有者地址列表 : " + senderList);
if(senderList == null || senderList.isEmpty()) {
throw new ResourceNotExistException("资源[" + baseInfo + "]不存在");
}
if(this.senderSelectedStrategy == null) {
this.senderSelectedStrategy = new SenderSelect();
}
/**
* 2.
* 这里考虑到负载均衡策略----可能资源持有者数量很多。
* 这里的目的就是为了在众多的持有者集合中选择出合适的作为资源发送者作为负载均衡策略
*/
senderList = senderSelectedStrategy.selectSender(senderList);
//3.获取发送端个数
int senderCount = senderList.size();
//4.根据资源文件列表 和 发送者个数 利用[均衡发送者任务策略] 来分配出 每一个文件众多片段信息的集合
List<List<FileSectionInfo>> fileSectionInfoListList = fileDistribution.distribution(this.sourceFileList, senderCount);
/**
* 5.告诉接收服务器,他所要接收的文件集合,以及发送端的数量。并启动接收服务器
* 这个方法里面做了一个很重要的事,就是为每个文件都设置了一个用于文件读写的FileReadWrite对象,
* 并以文件编号为键存储于HashMap中。需要注意的是:发送端也存在一个ReceiveFilePool
*/
this.receiveServer.setReceiveFilePool(this.sourceFileList);
this.receiveServer.setSenderCount(senderCount);
this.receiveServer.startUp(); //此处启动接收服务器
System.out.println("开始发送,共有发送端数量:" + senderCount);
ResourceReceiver.tmpCount = new AtomicInteger(senderCount);
for(int index = 0; index < senderCount; index++) {
//获取每一个发送端的网络地址
DefaultNetNode sendNode = senderList.get(index);
//获取到每个小的片段集合
List<FileSectionInfo> sectionList = fileSectionInfoListList.get(index);
//RMI远程连接发送方(资源持有者)
this.receiveClient.setRmiServerIp(sendNode.getIp());
this.receiveClient.setRmiServerPort(sendNode.getPort());
System.out.println("[" + sendNode.getPort() + "]需要发送的文件片段集合 : " + sectionList);
//这里调用发送端的远程方法
this.resourceSender.sendResource(this.receiveServer.getIp(), this.receiveServer.getPort(), baseInfo,sectionList);
}
//这里应该开启一个线程来判断是否能够注册自己,也即是否接收完毕
new Thread(new RegisterMe(), "用于注册自己的线程").start();
return true;
}
//将自己注册成为资源持有者
class RegisterMe implements Runnable{
@Override
public void run() {
while(true) {
final int count = ResourceReceiver.tmpCount.get();
if(count == 0) {
SourceHolderNode rhn = SourceHolderNode.getInstance();
rhn.setCenterIp(centerIp);
rhn.setCenterPort(centerPort);
//这里原本的逻辑应该是提供自己的端口和ip。介于只有一台电脑所以将其ip默认设为127.0.0.1
rhn.setServerPort(receiveServer.getPort());
rhn.registryResource(baseInfo, sourceFileList);
rhn.startUp();
break;
}
}
System.out.println("注册成功,我已成为合格的资源持有者");
}
}
//发送端筛选策略
class SenderSelect implements ISenderSelectedStrategy {
public SenderSelect() {}
@Override
public List<DefaultNetNode> selectSender(List<DefaultNetNode> senderList) {
//执行RMI方法。获取资源持有者当前的发送任务数量。
List<DefaultNetNode> sendList = new ArrayList<DefaultNetNode>();
for (DefaultNetNode defaultNetNode : senderList) {
final int senderPort = defaultNetNode.getPort();
receiveClient.setRmiServerPort(senderPort);
final SendCounter sendCounter = resourceSender.getSendCounter();
//此处做法是如果历史发送次数大于100,或者当前由超过20个发送任务在执行。就排除
if(sendCounter.getSendAccount() > 100 || sendCounter.getSendCount() > 20) {
continue;
}
sendList.add(defaultNetNode);
}
return sendList;
}
}
}
在筛选客户端的策略中写法上要细心一些,最开始是在判断成立后,直接senderList.romve(defaultNetNode),这个是个错误,因为 在用forEach遍历的时候(其实就是用的迭代器),一边遍历一遍删除会报出并发修改异常
对于平衡发送任务的策略:
/**
* 文件分配
* 例如: 父级根:
* 文件1:xxx 2345KB
* 文件2:yyy 12315KB
* ...
* 上面表示的就是一个资源。假如现在有多个上述资源持有者作为发送方。
* 该类的核心目的是:将上述资源(多个文件)“切片”---即切分成多个文件片段集合。目的是为了竟可能让每个发送端负载均衡
*/
public class FileDistribution {
public static final int DEFAULT_SECTION_SIZE = 1 << 23; //默认的片段大小8M
private int maxSectionSize; //最大片段长度
private IDistributionStrategy distributionStrategy; //可供外部自己实现的分配策略
public FileDistribution() {
this.maxSectionSize = DEFAULT_SECTION_SIZE;
this.distributionStrategy = new DefaultDistributionStrategy();
}
//设置最大片段大小
public FileDistribution setMaxSectionSize(int maxSectionSize) {
this.maxSectionSize = maxSectionSize;
return this;
}
public FileDistribution setDistributionStrategy(IDistributionStrategy distributionStrategy) {
this.distributionStrategy = distributionStrategy;
return this;
}
public List<List<FileSectionInfo>> distribution(SourceFileList sourceFileList, int count){
//实例化文件片段集合
List<FileSectionInfo> sectionList = new ArrayList<FileSectionInfo>();
//获取该资源下的所有文件集合
List<FileInfo> fileList = sourceFileList.getFileInfoList();
//这里将每个文件都切分成 众多"片段" 的集合存储进sectionList中
for(FileInfo fileInfo : fileList) {
int fileNo = fileInfo.getFileNo();
long fileSize = fileInfo.getFileSize();
int len;
long offset = 0;
while(fileSize > 0) {
//将该文件分成---最大为maxSectionSize长度的片段,只是分成片段了还没有设置该片段的具体内容。
len = (int) (fileSize > maxSectionSize ? maxSectionSize : fileSize);
FileSectionInfo sectionInfo = new FileSectionInfo(fileNo, offset, len);
sectionList.add(sectionInfo);
fileSize -= len;
offset += len;
}
}
//此处是将上面众多文件小片段组装在一起
List<List<FileSectionInfo>> result = distributionStrategy.distributionFileSection(sectionList, count);
return result;
}
}
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);
//假如有10个section,count个数为5,则会分解成5个片段集合,每个片段集合含两个片段。这样均衡就分配了
index = (index + 1) % count;
}
return result;
}
}
外部可通过覆盖接口或适配器中的方法来制定自己的策略
package com.mec.nettrans.strategy;
public class DefaultDistributionStrategy implements IDistributionStrategy {
public DefaultDistributionStrategy() {}
}
发送端的逻辑
首先发送端得是资源持有者。能向资源注册中心注册资源。
public class SourceHolderNode extends ResourceHolder {
private static volatile SourceHolderNode me;
static {
RMIFactory.scanfPath("/ResourceSender.xml");
}
//资源持有者端口。资源持有者也需要建立RMI服务器,用于资源注册中心的健康检测
private static int port;
//键:资源基本信息 值:资源详细信息
private static Map<ResourceInfo, SourceFileList> resources;
public void setServerPort(int port) {
super.setHolderPort(port);
}
private SourceHolderNode() {
super(port);
resources = new HashMap<ResourceInfo, SourceFileList>();
}
public static SourceHolderNode getInstance() {
if(me == null) {
synchronized (SourceHolderNode.class) {
if(me == null) {
me = new SourceHolderNode();
}
}
}
return me;
}
//资源持有者向注册中心注册新资源
public void registryResource(ResourceInfo resourceInfo, SourceFileList sourceFileList) {
if(!resources.containsKey(resourceInfo)) {
resources.put(resourceInfo, sourceFileList);
me.register(resourceInfo);
}
}
//获取资源的详细信息
public SourceFileList getSourceFileList(ResourceInfo resourceInfo) {
return resources.get(resourceInfo);
}
}
其次发送端作为客户端与接收端进行网络通信。
//负责操作本地磁盘文件并与接收端进行网络通信
public class FileSender implements Runnable{
private String receiverIp;
private int receiverPort;
private ResourceFilePool rscFilePool; //发送端的池子,用于操作本地磁盘读取真实文件内容
private List<FileSectionInfo> sectionList;
private FileSectionSendRecevice fileSectionSend;
public FileSender(String receiveIp, int receivePort, ResourceFilePool rscFilePool,List<FileSectionInfo> sectionList) {
this.receiverIp = receiveIp;
this.receiverPort = receivePort;
this.rscFilePool = rscFilePool;
this.sectionList = sectionList;
this.fileSectionSend = new FileSectionSendRecevice();
}
//开始发送
public void startSend() {
new Thread(this, "文件发送端").start();
}
@Override
public void run() {
Socket socket = null;
DataOutputStream dos = null;
try {
socket = new Socket(receiverIp, receiverPort);
dos = new DataOutputStream(socket.getOutputStream());
System.out.println("需要我发送的片段信息 : " + sectionList);
for(FileSectionInfo sectionInfo : sectionList) {
//先从本地磁盘中读取片段再发送
FileReadWrite frw = rscFilePool.getAcceptFrw(sectionInfo.getFileNo());
sectionInfo = frw.readSection(sectionInfo);
fileSectionSend.sendSection(dos, sectionInfo);
}
//发送结束标志
fileSectionSend.sendEndInfo(dos);
//发送结束之后将当前发送正在发送数量减1。历史发送数量加1
ResourceSender.sendCounter.decSendCount();
ResourceSender.sendCounter.incSendAccount();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
close(socket, dos);
}
}
//释放资源
private void close(Socket socket, DataOutputStream dos) {
if(dos != null) {
try {
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket != null && !socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
另外作为RMI服务器,它存储着接收端需要调用的“远程方法”。具体定义如下:
public interface IResourceSender {
boolean sendResource(String receiveIp, int receivePort, ResourceInfo baseInfo, List<FileSectionInfo> sectionList);
SendCounter getSendCounter();
}
public class ResourceSender implements IResourceSender {
private SourceHolderNode holder;
static final SendCounter sendCounter = new SendCounter();
public ResourceSender() {
holder = SourceHolderNode.getInstance();
}
public static void decSendCount() {
sendCounter.decSendCount();
}
public static void incSendAccount() {
sendCounter.incSendAccount();
}
@Override
public boolean sendResource(String receiveIp, int receivePort, ResourceInfo baseInfo, List<FileSectionInfo> sectionList) {
System.out.println("发现一个发送请求,来自 :[ " + receiveIp + " : " + receivePort + "]");
//注意每一次发送都需要将自己当前正在执行任务次数加1。以便接收端进行负载均衡筛选。
sendCounter.incSendCount();
//这个sourceFileList是发送端设置进来的sourceFileList
SourceFileList sourceFileList = holder.getSourceFileList(baseInfo);
System.out.println("需要发送的资源信息 : " + sourceFileList);
//这是发送端的ResourceFilePool
ResourceFilePool rscFilePool = new ResourceFilePool();
//发送之前必须用资源持有者的文件信息集合填充池子,为接下来的发送数据做准备
rscFilePool.addFileList(sourceFileList);
//创建一个文件发送的对象开始发送数据
FileSender fileSender= new FileSender(receiveIp, receivePort, rscFilePool, sectionList);
fileSender.startSend(); //启动发送线程
return true;
}
@Override
public SendCounter getSendCounter() {
return sendCounter;
}
}
其中涉及到SendCounter这个类,其主要作用是记录发送端历史上执行发送任务次数和保存当前正在执行发送任务的个数。主要是为了在接收端根据这两个数据进行发送端筛选的。这两个数据决定了能不能从“资源持有者”变成“发送端”。
public class SendCounter {
private volatile AtomicInteger sendCount; //表示当前正在并发进行的发送的个数
private volatile AtomicInteger sendAccount; //历史上发送的发送个数
public SendCounter() {
this.sendAccount = new AtomicInteger();
this.sendCount = new AtomicInteger();
}
public void incSendCount() {
this.sendCount.incrementAndGet();
}
public void decSendCount() {
this.sendCount.decrementAndGet();
}
public void incSendAccount() {
this.sendAccount.incrementAndGet();
}
public int getSendCount() {
return this.sendCount.get();
}
public int getSendAccount() {
return this.sendAccount.get();
}
}
测试类:
资源注册中心:
public class TestCenter {
public static void main(String[] args) {
ResourceCenter center = new ResourceCenter(54200); //默认IP 127.0.0.1
center.startUp();
}
}
资源发送端:
public class TestSender {
public static void main(String[] args) {
//定义资源
ResourceInfo resourceInfo = new ResourceInfo();
resourceInfo.setAppName("app");
resourceInfo.setId("20200803");
resourceInfo.setVersion("1");
//设置该资源对应的文件详细信息
SourceFileList sfl = new SourceFileList();
sfl.setAbsoluteRoot("F:\\MecChatClient\\");
try {
sfl.collectFiles();
} catch (Exception e) {
e.printStackTrace();
}
//定义资源持有者 并注册资源
SourceHolderNode rhn = SourceHolderNode.getInstance();
rhn.setCenterIp("127.0.0.1"); //设置注册中心ip
rhn.setCenterPort(54200); //设置注册中心端口
Scanner scanner = new Scanner(System.in);
int holderPort = scanner.nextInt();
rhn.setServerPort(holderPort); //设置资源持有者自己的端口(ip默认为127.0.0.1)
rhn.startUp();
scanner.close();
rhn.registryResource(resourceInfo, sfl); //注册资源
}
}
资源接收端:
public class TestReceive {
public static void main(String[] args) {
ResourceInfo resourceInfo = new ResourceInfo();
resourceInfo.setAppName("app");
resourceInfo.setId("20200803");
resourceInfo.setVersion("1");
//这里的sfl应该是发送端发过来的
SourceFileList sfl = new SourceFileList();
sfl.setAbsoluteRoot("F:\\MecChatClient\\");
try {
sfl.collectFiles();
} catch (Exception e) {
e.printStackTrace();
}
//------------------------------------------上述内容应该是从主服务器获取到的---------------------------------------------------
//这个才是我接收端需要将传输过来的文件写入的位置(在该目录下依照发送端发过来的目录结构原样拷贝)
sfl.setAbsoluteRoot("F:\\tempDirectory\\");
ResourceReceiver rg = new ResourceReceiver("127.0.0.1", 54200);
rg.setBaseInfo(resourceInfo);
rg.setFileList(sfl);
rg.setReceiveServerPort(54195);
try {
rg.getResourceFiles();
} catch (ResourceNotExistException e) {
e.printStackTrace();
}
}
}
结果:
资源发送端显示如下:
资源接收端显示如下:
资源注册中心:
磁盘中结果对比: