多文件自平衡云传输(二)资源接收端篇
资源接收端从请求到接收的过程:
1.资源接收端启动时先与拥有资源的服务器(可以是APP服务器,也可以是拥有资源的服务器)进行短连接进行资源资本信息的获取。
2.根据获取的资源的信息中的资源的名称进行短连接资源注册中心,请求拥有对应资源的节点列表。
3.根据资源基本信息中的资源的多个文件的基本信息进行本地资源的查询,若本地拥有该资源(之前可能进行过请求保存到本地)则进行缺省部分资源文件的请求,若本地无该资源,则进行全部资源的全部文件的请求。
4.在请求的过程中,资源接收端根据资源分配策略进行分配请求的资源的多个文件,将每个文件分成长度相等的多个片段。
5.接收端进行采用负载均衡策略进行可发送资源节点的选取,选取合适发送的节点,根据节点的个数将多个文件片段分成与节点个数相同的片段组,将每一组分派给节点。
6.将分好的资源片段组通过短连接的方式进行链接发送给该组对应的节点。
7.接收端开启临时的长连接服务器,进行接收发送端的链接与资源片段的发送
8.直到接收完整,当发送端出现异常导致接收端无法接收完整的时候,接收端启用断点续传的功能,再次进行缺省部分资源的请求。直到接收完整接收端拥有该资源。可以选择接收端此时作为一个发送端进行资源的注册,随意。
资源接收
Consumer
此类功能也即消费者的功能
1.进行资源发送节点的获取
2.选择负载均衡的策略,进行节点的选择。
public class Consumer {
private static int PORT = 54200;
private static String IP = "127.0.0.1";
private rmiCilent rc;
private CilentProxy cp;
private int RegisterPort = PORT;
private String RegisterIp = IP;
private NetNode self;
//节点选择策略
private INodeSelect nodeSelect;
//资源分配策略
private IResourceSelect resourceSelect;
private String resourceName;
public Consumer() {
this.cp = new CilentProxy();
rc = new rmiCilent();
rc.setPort(RegisterPort);
rc.setId(RegisterIp);
cp.setRmicilent(rc);
defaultInit();
}
public List<NetNode> getServerNode(String resorceName){
//通过名称得到拥有资源的节点列表
//通过链接注册中心得到节点列表
this.resourceName = resorceName;
IRegister ir = this.cp.getProxy(IRegister.class);
return ir.getServerList(resorceName);
}
private void defaultInit() {
//采用默认节点选择和分配策略
nodeSelect = new NodeSelect();
resourceSelect = new ResourceSelect();
//这里可以采用配置文件的方式进行节点策略的设定
}
public INodeSelect getNodeSelect() {
return nodeSelect;
}
public IResourceSelect getResourceSelect() {
return resourceSelect;
}
/*
*与注册中心进行短连接时的异常处理
*/
class RequstNodeException implements ICilentException{
@Override
public void peerAbnormalDrop() {
getServerNode(resourceName);
}
}
}
ReceiveServer
接收端的入口类,与Consumer不同的是,Consumer不同之处在于,Consumer的主要功能是获取节点和节点的负载均衡的选择。
ReceiveServer给外部一个可以请求资源的方法,对外部而言不用关心内部的请求过程,只需要知道调用了该方法可以请求资源。
真正的资源请求过程由内部去完成,在这里ReceiveServer这个类中关键的作用请求一个资源的基本信息,剩下的工作交给了ReceiveDeal 和Consumer来做,ReceiveDeal 类中拥有Consumer作为成员。
public class ReceiveServer{
private rmiCilent serverRc;
private CilentProxy cp;
private boolean processBar;
private IRequstAction Ire;
private ICilentException Ice;
public ReceiveServer(int connectServerPort, String connectServerIp) {
serverRc = new rmiCilent();
serverRc.setPort(connectServerPort);
serverRc.setId(connectServerIp);
cp = new CilentProxy();
cp.setRmicilent(serverRc);
}
/*
*通过链接资源服务器进行资源基本信息的获取
*并创建ReceiveDeal对象进行真正的节点列表的获取和资源的分配
*/
public void requstResource(String ResourceName) {
//先链接服务器,然后得到对应的资源的基本的信息
//先通过服务器得到资源的一个基本的信息。
Iserver server = cp.getProxy(Iserver.class);
//根据服务器得到资源的基本的信息
ResourceInfo ri = server.getResourceInfo(ResourceName);
ResourceInfo temp = ri.getResoureFromXml();
if(temp == null) {
//表明没有该资源,用户第一次请求资源的时候需要设置本地的存储路径
//可以给外边进行路径的设置,
String localRoot = Ire.notfoundLocalResource();
ri.setRoot(localRoot);
ri.saveObject();
ri.getAllFiles(ri.getFileInfoMap());
//根据请求的资源的左右构造请求的条件
}else {
//TODO
//根据本地的已经存储的路径,判断该路径下的文件是否完整
//如果完整则不用请求
//此时应该根据本地的记录,判断是否进行获取。
return;
}
//创建一个线程立即返回,这样对于客户端来说请求资源可以快速的响应
new ThreadPool().Execute(new ReceiveDeal(ri, processBar));
}
/*
* 是否设置使用接收文件进度条
* 同样也可以进行设置进度条的格式,采用set方法进行设置,
* 我这里给出的是默认的进度条,进度条不是必选项。
*/
public void IFuseProcessBar(boolean choose) {
this.processBar = choose;
}
/*
*设置请求资源过程中的异常和问题
*例如 本地无对应资源表明第一次申请,则需要设置存储路径
*/
public void setRequstServerException(IRequstAction Ire) {
this.Ire = Ire;
}
/*
*设置连接服务器的异常
*/
public void setConnectServerException(ICilentException Ice){
this.Ice = Ice;
}
}
ReceiveDeal
此类的作用就是进行需要请求资源的分配和合适节点的选择(节点列表的获取,资源的分片和节点的选择由Consumer来完成),ReceiveDeal主要进行将分片资源根据节点的个数分组,然后分配给相应个数的节点,与其节点进行短连接告知节点应该发送的片段,并开启线程等待发送节点进行链接发送资源,进行资源的接收。
public class ReceiveDeal implements Runnable{
private Consumer consumer;
private ResourceInfo ri;
private CilentProxy cp;
private rmiCilent rc;
private static NetNode self = new NetNode();
private progressView view;
private boolean showProcess;//是否设置进度条
private List<NetNode> nodelist;
private List<List<FileHead>> headList;
static {
self.setIp(getSelfIp());
self.setPort(PortPool.getPort());
}
public ReceiveDeal(ResourceInfo ri, boolean choose) {
this.ri = ri;
cp = new CilentProxy();
rc = new rmiCilent();
this.showProcess = choose;
consumer = new Consumer();
List<NetNode> templist = consumer.getServerNode(ri.getResourceName());
nodelist = consumer.getNodeSelect().getProperNodeList(templist);
headList = consumer.getResourceSelect().getSendcountList(ri.getFileHeadList(), nodelist.size());
}
@Override
public void run() {
NetNode node = new NetNode();
node.setIp(self.getIp());
node.setPort(self.getPort());
int sendCount = nodelist.size();
new Receive(sendCount).start();//开启线程进行accept发送端的链接并且进行资源的接收
//进行短连接可发送资源的节点,告知节点其需要发送的资源片段。
for(int index = 0; index < nodelist.size(); index++) {//
NetNode sender = nodelist.get(index);
ResourceInfo requst = new ResourceInfo(ri);
requst.setFileHeadList(headList.get(index));
rc.setPort(sender.getPort());
rc.setId(sender.getIp());
cp.setRmicilent(rc);
Isender peer = cp.getProxy(Isender.class);
peer.send(node, requst);
}
}
public ReceiveDeal getReceiveDeal() {
return this;
}
/*
*进行资源响应响应链接,并且为每一个接收端开启线程进行资源接收
*
*/
class Receive implements Runnable{
private int sendCount;
private ServerSocket server;
private int count = 0;
private ContinueTransfer ct;
private boolean goon;
public Receive(int count) {
this.sendCount = count;
ct = new ContinueTransfer();
ct.initMap(ri.getFileHeadList(), ri.getFileInfoMap());
ct.setRd(getReceiveDeal());
initProcessBar();
}
public void start() {
if(goon == true) {
return;
}
goon = true;
try {
server = new ServerSocket(self.getPort());
} catch (IOException e) {
e.printStackTrace();
}
new Thread(new barView()).start();
new Thread(this).start();
}
/*
*当用户设置显示进度条,则在开始进行接收线程的时候,开启另一个线程进行展示进度条
*/
class barView implements Runnable{
@Override
public void run() {
if(showProcess) {
view.showView();
}
}
}
@Override
public void run() {
if(showProcess) {
view.showView();
}
while(goon) {
try {
Socket socket = server.accept();
//开启处理接收资源的线程,由ReceiveDetail进行处理
new ThreadPool().Execute(new ReceiveDetail(socket, ri, ct, view));
} catch (IOException e) {
e.printStackTrace();
}
++count;
if(count >= sendCount) {
while(goon) {
if(ct.IfRecived(sendCount)) {
if(ct.Ifcomplete()) {
goon = false;
}
count = 0;
break;
}
}
}
}
close();
}
private void close() {
if(server != null) {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
server = null;
}
}
}
}
/*
*初始化进度条对象
*/
public void initProcessBar() {
Map<String, Object> nameList = new HashMap<String, Object>();
List<FileHead> headList = ri.getFileHeadList();
Map<Integer, FileInfo> fileInfoMap = ri.getFileInfoMap();
for(int i = 0; i < headList.size(); i++) {
int handle = headList.get(i).getFileHandle();
String name = fileInfoMap.get(handle).getFileRoot();
nameList.put(name, null);
}
List<String> temp = new ArrayList<String>();
temp.addAll(nameList.keySet());
view = new progressView();
view.showProgressView(temp, ri.getResourceName());
}
public static String getSelfIp() {
try {
InetAddress address = InetAddress.getLocalHost();
String ip = address.getHostAddress();
return ip;
} catch (UnknownHostException e) {
e.printStackTrace();
}
return null;
}
}
ReceiveDetail
此类为真正进行侦听发送端的链接,并且接收资源写入本地。此处有一个需要注意的点就是,对于不同的发送端来说,开启了不同的接受线程,但对于不同的发送端来说,可能发送同一个文件的不同片段,我们知道在进行本地文件写操作时,需要打开和关闭文件。
问题就在于打开和关闭文件的操作,频繁的进行IO操作,消耗系统,CPU,还有内存的资源。
若不进行频繁的打开关闭,当开始写某一文件片段的时候,打开该文件,等资源接收完全之后再进行个文件的关闭操作,明显有一个大问题就是,多线程带来的不安全性,当某一线程本来写到某一个文件片段的一半时,时间片段到了,刚好有另外一个线程同样是写相同文件的另外一个片段,但是由于公用的一个文件读写指针,那么第二个线程开始写的时候,会在上次读写指针停留的地方接着写,这样就会造成片段写的位置错误,整个文件看似完整,但是里边的内容早已错乱。
所以我们采用RandAccessFilePool 类来进行文件指针的保存,每一个接收线程产生一个RandAccessFilePool ,只保存该线程在写的时候对应文件的读写指针,不同线程对同一文件进行写操作的时候,用的是各自的文件指针,所以保证了文件写入的准确性,而对于同一线程在不同时刻进行写同一文件的不同片段,同样是准确的,因为同一线程执行事件的本来就是顺序执行。
public class ReceiveDetail implements Runnable{
private Socket socket;
private DataInputStream dis;
private ResourceInfo ri;
private RandAccessFilePool rfp;
private boolean goon;
private String root;
private ContinueTransfer ct;
//进度条的设置
private progressView view;
public ReceiveDetail(Socket socket, ResourceInfo ri
, ContinueTransfer ct, progressView view) {
this.socket = socket;
rfp = new RandAccessFilePool();
this.ri = ri;
this.view = view;
this.ct = ct;
root = ri.getRoot();
goon = true;
try {
dis = new DataInputStream(socket.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(goon) {
TransferDetail fs = new TransferDetail();
FileHead fh;
try {
fh = new FileHead(fs.readSignBytes(dis));
} catch (IOException e) {
goon = false;
break;
}
//当通过读取标志信息字节流,将字节流转化为fileHead对象,需要读取的sectionLength为0是表明某个发送端已经结束发送或者是对方掉线
//对应接收端该线程线程停止接收。
if(fh.getSectionLength() == 0) {//表示接收完全
goon = false;
close();
closeFile();
//设置已经该线程已经接收完成,为后期断点续传做准备,因为可能是因为发送端发送完毕
//或者是发送端半途中断,ct.setOk()设置接收端是否接收完毕,
//在Ct中会进行判断如果接收完毕,是否所有的片段都已接收,通过所有片段是否接收完整
//来进行判断需不需要进行断点续传
ct.setOk();
break;
}
byte[] content = null;
int handle = fh.getFileHandle();
FileInfo fi = ri.getFileInfoMap().get(handle);
String absolut=fi.getFileRoot();
String completeRoot = root + absolut;
fileCreat.judgeFile(completeRoot);
RandomAccessFile raf = rfp.getWriteRaf(completeRoot);
try {
content = fs.readContentBytes(dis, fh.getSectionLength());
raf.seek(fh.getOffset());
raf.write(content);
//将接受到的片段描述信息对象放入到对应文件所对应的判断文件是否完整的类FileIFComplete类中的list中,
//每个文件对应一个FileIFComplete类,类中拥有一个list存储一个文件的多个片段
//最后通过文件的完整性决顶是否需要断点续传
ct.getMap().get(handle).afterInsertion(fh);
//当用户设置了显示进度条的操作才会进行初始化一个进度条
//view == null 表明用户没有进行响应的设置
if(view != null) {
new ThreadPool().Execute(new paint(absolut, fh.getSectionLength(), (int)fi.getFileLength()));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class Paint implements Runnable{
private String handle;
private int per;
private int perAll;
public Paint(String handle, int per, int perAll) {
this.handle = handle;
this.per = per;
this.perAll = perAll;
}
@Override
public void run() {
view.changeProgress(handle, per, perAll);
}
}
private void closeFile() {
for (RandomAccessFile raf : rfp.getRandomFiles()) {
try {
raf.close();
} catch (IOException e) {
}
}
}
private void close() {
try {
if (dis != null) {
dis.close();
}
} catch (IOException e) {
} finally {
dis = null;
}
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
} finally {
socket = null;
}
}
}
断点续传
ContinueTransfer
断点续传的管理类
public class ContinueTransfer {
//map的键为文件的句柄,值为FileIFComplete对象,FileIFComplete中拥有一个list成员,用来存储一个文件的多个fileHead信息,相当于存储文件的片段信息
private static final Map<Integer,FileIFComplete> completeMap = new ConcurrentHashMap<Integer, FileIFComplete>();
private int senderCount;//用来记录接收端接收完毕的个数
//因为多个线程进行接收,一个线程接受完需要给SenderCount + 1
//牵扯到多线程安全的问题,所以需要进行同步增加的操作。
//采用lock锁保证线程安全
private ReentrantLock lock;
private ReceiveDeal rd;
public ContinueTransfer() {
lock = new ReentrantLock();
}
/*
*初始化话completeMap,先根据headList中的文件片段信息,对照map的内容
*初始化每个FileIFComplete,在一开始的时候FileIFComplete只存在一个fileHead,偏移量为0,长度为文件的总长度。
*/
public void initMap(List<FileHead> headList, Map<Integer, FileInfo> fileInfo) {
Set<Integer> keys = fileInfo.keySet();
for(FileHead one : headList) {
int handle = one.getFileHandle();
FileInfo fi = fileInfo.get(handle);
FileIFComplete tempffc = new FileIFComplete((int) fi.getFileLength()
, fi.getFileHandle());
completeMap.put(handle, tempffc);
}
}
public void setRd(ReceiveDeal rd) {
this.rd = rd;
}
public void setOk() {
lock.lock();
try {
++senderCount;
}finally {
lock.unlock();
}
}
public Map<Integer,FileIFComplete> getMap(){
return this.completeMap;
}
public boolean IfRecived(int count) {
lock.lock();
try {
return count == senderCount;
}finally {
lock.unlock();
}
}
public boolean Ifcomplete() {
//判断是否每个文件都完整
Collection<FileIFComplete> value = completeMap.values();
Iterator<FileIFComplete> itr = value.iterator();
List<FileHead> hlist = new ArrayList<FileHead>();
while(itr.hasNext()) {
FileIFComplete one = itr.next();
if(!one.ifComplete()) {//如果不完全,将剩余片段记录下来
hlist.addAll(one.getHeadList());
}
}
if(hlist.isEmpty()) {
//当每个文件都完整的话,就不会往hlist进行添加片段,则hlist为空
//说明文件接受完整
return true;
}else {
new Thread(rd).start();
//表明没有接受完全,用该重新将这些个文件分片,进行重新的请求
return false;
}
}
}
FileIFComplete
public class FileIFComplete {
//通过检查每一个文件的完整性
//因为对于每个文件来说,可能存在被分片的概念,所以,当前这个类的作用就是
//将每个文件的片段组装进行判断,每一个文件是否完整
private int handle;
private List<FileHead> fileHeadList;
private ReentrantLock lock;
public FileIFComplete(int size, int handle) {
lock = new ReentrantLock();
fileHeadList = new CopyOnWriteArrayList<FileHead>();
this.handle = handle;
FileHead fh = new FileHead();
fh.setFileHandle(this.handle);
fh.setOffset(0L);
fh.setSectionLength(size);
fileHeadList.add(fh);
}
public List<FileHead> getHeadList(){
return fileHeadList;
}
public boolean ifComplete() {
return fileHeadList.isEmpty();
}
public FileHead getRightHead(FileHead fh) throws Exception {
for(int i = 0; i < fileHeadList.size(); i++) {
FileHead listOfHead = fileHeadList.get(i);
if(listOfHead.ifRightHead(fh)) {
return fileHeadList.get(i);
}
}
throw new Exception("片段错误" + fh);
}
/*
*当添加一个fileHead对象,表明添加一个已经接受的文件片段
*进行加入片段信息之后的计算,
*/
public void afterInsertion(FileHead fh) {
//先判断是否是合格的片段
if(fh == null) {
return;
}
FileHead replaceHead;
try {
lock.lock();
replaceHead = getRightHead(fh);
long orgOffset = replaceHead.getOffset();
int orgSize = replaceHead.getSectionLength();
long currentOffset = fh.getOffset();
int currentSize = fh.getSectionLength();
long leftOffset = orgOffset;
int leftSize = (int) (currentOffset - orgOffset);
long rightOffset = currentOffset + currentSize;
int rightSize = (int) (orgOffset + orgSize - rightOffset);
fileHeadList.remove(replaceHead);
if(leftSize > 0) {
fileHeadList.add(new FileHead(fh.getFileHandle(), leftOffset, leftSize));
}
if(rightSize > 0) {
fileHeadList.add(new FileHead(fh.getFileHandle(), rightOffset, rightSize));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}//得到替换的head,将一个head根据传入的偏移量和长度
//进行合理的拆分
}
}
下面所说的给FileIFComplete添加filehead ,事实上FileIFComplete中有一个list成员,给list进行添加。
这里阐述一下上面的检查文件完整性的一个算法技巧。
在我们初始化一个文件对应的FileIFComplete 对象的时候,我们会在初始化的时候会给FileIFComplete对象中添加一个filehead来表示该文件的信息。
例如,一个文件的总长度为811字节。
那我们初始化一个FileIFComplete 的时候会添加一个fileHead对象,对象的三个成员值分别为 handle = 该文件handle, offset = 0, length = 811.
当我们接收端接受收一个片段时,因为是先接接收标记信息同样也是fileHead对象的字节流,我们将提取fileHead中的handle,通过handle找到此片段对应的文件的FileIFComplete对象,然后将接收到的fileHead添加进FileIFComplete中。
假设我们接收到的fileHead的偏移量为8, 片段长度为64.
那么FileIFComplete会进行添加后的计算,同样是该类中afterInsertion(FileHead fh)方法代码所描述,下面简单说说计算过程。
因为我们初始化的时候该文件对应的FileIFComplete中已经有了一个filhead
offset = 0 length = 811;
现在添加进来一个filehead offset = 8 length = 64;
那么首先判断新添加的filehead是否是正确的文件片段,判断方法就是在FileIFComplete中已有的filehead中寻找,是否有片段信息包含添加的片段信息,像本例来说,因为初始化中已有开始片段信息,表明文件的0位置到810位置为空,那么此时加进来filehead 写的位置为文件的8位置开始往后64个字节,而此时FileIFComplete中原有的filehead满足新添加片段写的要求,因为整个文件都是空的,当然新添加的这个写哪个位置都是可以的,
如果FileIFComplete此时之后一个filehead为 offset = 10 length = 32;那此时如果新添加的filehead offset = 8 length = 64;此时的片段就是错误的,因为FileIFComplete中拥有的filehead表明文件还有哪些片段没有写,此时表明文件从10位置开始往后32字节的地方还没有,而新添加的明显想写的部分不符合没有写的片段内容。
所以FileIFComplete的afterInsertion(FileHead fh)通过进行传入的fileHead正确新的判断,并且根据加入新的filehead计算出文件中还没有被写的片段信息。当所有片段都写完,则FileIFComplete为空,同时通过空来判断文件的完整性。