《多文件云传输》框架

《多文件云传输》框架

1. 特别声明

本《多文件云传输》框架是作为JavaSE练习而创造的,并不是为了“造方轮子”而制作的!!!

在过去的编程里,我用过许多的相同的”制式代码“,基于这种认知,为了方便之后的编程,产生了制造框架的意图,于是有了本框架的诞生。

2. 《多文件云传输》框架概述

《多文件云传输》:在《资源发现》框架和《MecRmi》框架(详见本博客另外两个框架)的基础上的可以用于多种资源文件、多个ip节点之间进行文件传输的,带有自平衡、断点续传的多文件云传输框架。

3. 框架目的及功能

当下资源发送大多是单一服务器或专有服务器向资源请求者发送资源,随着服务器的使用,资源请求者数量与日俱增,服务器的负载也会越来越重,《多文件云传输》框架基于此产生。

  1. 实现文件“云”传输:当资源请求者请求到相关资源后,变成为资源拥有者,并参与后续资源的发送。
  2. 多个资源发送者向同一资源请求者发送同一资源。
  3. 多个资源发送者向同一资源请求者发送同一资源的不同文件。
  4. 多个资源发送者向同一资源请求者发送同一文件的不同片段(文件较大)。
  5. 将文件分割成片段是为了负载均衡,让多个资源发送者发送量相近;
  6. 当资源发送者数量较多时,该发送者将不再参与这个资源的发送。
  7. 资源接受者在成为某个资源的发送者后,其依然可以是其他资源的资源请求者。

4. 技术点实现

  1. 自平衡(负载均衡):每个资源拥有者有一个“健康值”数据,用来表示其同时进行传输的资源持有者数量

  2. 断点续传:因为特殊情况导致文件传输停止后可以继续完成文件传输工作

    处理方法:将文件分成小片段,记录已传输和未传输的片段,重新传输时调取未传输的片段继续传输。

  3. 与APP无关:本框架可以根据需求在不同的APP上实现不同的功能,如:

    1. 选择资源绝对根获取其下面的所有文件进行注册
    2. 选择资源绝对跟和其下的某个文件进行单一注册
    3. 进度条的实现:根据资源数量获取文件传输的进度并显示在进度条中

5. 技术难点

相对来说拿得出手的技术点(具体实现在下面的第6条):

  1. 未接收文件片段的数据准备

    多个资源发送者可以同时发送同一个文件的不同片段,当发生传输异常时,中断本次传输,在断点续传时希望只接受该文件未接受的那些片段(这些片段有可能非连续)。为此需要准备详细数据,以提供支持。(详见6.1)

  2. 文件控制块池——极大加快了程序运行速度

    以前的处理是在片段传输中存在大量的文件开关操作,极大影响代码执行时间,因而引进文件控制块池子

    避免了因为文件传输频繁打开和关闭文件,加快程序运行速度(详见6.2)

  3. 文件头部信息与文件内容配合使用

    文件大小不一,并且因为文件片段按某一长度分割后最后一个片段的长度不尽相同,引入文件头部信息,记载文件片段信息。(详见6.3)

6. 技术难点的实现

  1. 未接收文件的处理

    数据推导过程:

    假设文件长度为12个单位(也许11.3个单位),写成如下图:
    
    _ _ _ _ _ _ _ _ _ _ _ _
    
    0 1 2 3 4 5 6 7 8 9 0 1
                          1
    又假设,当前接收到的片段信息为:
    接收片段:
    4 : 3
    而初始未接收片段信息是:
    0 : 12
    可以按照4 : 3将0 : 12分成左右两部分:
    _ _ _ _ x x x _ _ _ _ _
    0 1 2 3 4 5 6 7 8 9 0 1
                          1
    0 : 4
    7 : 5
    
    再假设,现在又接收到片段:
    1 : 3
    _ y y y x x x _ _ _ _ _
    0 1 2 3 4 5 6 7 8 9 0 1
                          1
    分成的未接收片段信息是:
    0 : 1
    7 : 5
    
    再假设现在又接收到:
    8 : 3
    _ y y y x x x _ z z z _
    0 1 2 3 4 5 6 7 8 9 0 1
                          1
    未接收片段信息是:
    0 : 1
    7 : 1
    11 : 1
    
    上面的处理过程是“视觉”过程,需要找出“数学”过程!
    class SegmentInfo {
    	offset
    	length
    }
    class UnreceivedSegment {
    	id
    	List<SegmentInfo> segmentList;
    }
    UnreceivedSegment对象unreceivedSegment初值为:
    id : X
    List<?> segmentList只有一个元素:
    os.offset	:	os.len
    0			:	12
    
    当前接收到的片段信息是:rs
    rs.offset	:	rs.len
    3			:	3
    _ _ _ x x x _ _ _ _ _ _
    0 1 2 3 4 5 6 7 8 9 0 1
                          1
    
    os.leftOffset	:	os.leftLen
    os.offset		:	rs.offset - os.offset	(0 : 3)
    
    os.rightOffset		:	os.rightLen
    rs.offset + rs.len	:	os.offset + os.len - (rs.offset + rs.len)	(6 : 6)
    
    当前接收到的片段信息是:rs
    rs.offset	:	rs.len
    8			:	3
    当前未接收文件片段如下:
    0 : 3
    6 : 6
    _ _ _ x x x _ _ y y y _
    0 1 2 3 4 5 6 7 8 9 0 1
                          1
    第一步应该确定对哪个未接收片段进行分解(计算):
    第一项:0 : 3不符合要求;
    第二项:6 : 6符合分解要求!
    那么,符合分解要求的条件是:
    rs.offset + rs.len <= os.offet + os.len
    
    os.leftOffset	:	os.leftLen
    os.offset		:	rs.offset - os.offset	(6 : 2)
    
    os.rightOffset		:	os.rightLen
    rs.offset + rs.len	:	os.offset + os.len - (rs.offset + rs.len)	(11 : 1)
    
    当前接收到的片段信息是:rs
    rs.offset	:	rs.len
    3			:	3
    _ _ _ x x x _ _ _ _ _ _
    0 1 2 3 4 5 6 7 8 9 0 1
                          1
    所形成的未接收片段信息:
    0 : 3
    6 : 6
    再接收到的片段信息:
    rs.offset	:	rs.len
    6			:	3
    
    _ _ _ x x x x x x _ _ _
    0 1 2 3 4 5 6 7 8 9 0 1
                          1
    
    os.leftOffset	:	os.leftLen
    os.offset		:	rs.offset - os.offset	(6 : 0)
    
    os.rightOffset		:	os.rightLen
    rs.offset + rs.len	:	os.offset + os.len - (rs.offset + rs.len)	(9 : 3)
    当前未接收片段信息应该是:
    0 : 3
    9 : 3
    

    package com.mec.cloud.core;
    
    import java.util.LinkedList;
    import java.util.List;
    /*
    *未接收文件片段信息计算基本思路:首先对于每一个将要接收的文件,是存在如下基本信息的:
    *文件编号;
    *文件长度。
    *可以有上述基本信息构建一个“未接收文件片段信息列表”,这个列表初始只有一个元素:
    *偏移量:0
    *长度:文件长度
    *
    *以后,每完成接收一段文件片段内容,就将所接收到的片段信息,从上述信息中删除,其实是:
    *根据当前所接收到的片段信息,将未接收片段分成两部分:左侧和右侧。
    *计算公式:
    *os.leftOffset	:	os.leftLen
    *os.offset		:	rs.offset - os.offset	(6 : 0)
    *
    *os.rightOffset		:	os.rightLen
    *rs.offset + rs.len	:	os.offset + os.len - (rs.offset + rs.len)	(9 : 3)
    *
    *这样,在多线程安全处理后,对于每一个需要接收的文件来说,未接收文件片段信息总是准确的
    */
    public class UnreceivedSection {
    	private List<FileSection> sectionList;
    	//...部分代码已省略
        
    	void receiveSection(FileSection section) {
    		synchronized (this.sectionList) {
                //获取section在列表中哪个部分的下标
    			int index = locationSection(section);
    			if (index == -1) {
    				throw new RuntimeException("片段出错:("
    						+ section.getFileId() + " : "
    						+ section.getOffset() + ":"
    						+ section.getLength() + ")");
    			}
    			
    			FileSection os = this.sectionList.remove(index);
    			//根据剩余右侧的长度判断右片段是否存在
    			int len = (int) (os.getOffset() + os.getLength() - section.getOffset() - section.getLength());
    			if (len > 0) {
    				FileSection right = new FileSection();
    				right.setOffset(section.getOffset() + section.getLength());
    				right.setLength(len);
    				this.sectionList.add(index, right);
    			}
    			//根据剩余左侧的长度判断左片段是否存在
    			len = (int) (section.getOffset() - os.getOffset());
    			if (len > 0) {
    				FileSection left = new FileSection();
    				left.setOffset(os.getOffset());
    				left.setLength((int) (section.getOffset() - os.getOffset()));
    				this.sectionList.add(index, left);
    			}
    		}
    		
    	}
        //定位section属于列表中哪个片段
    	private int locationSection(FileSection section) {
    		int index;
    		
    		for (index = 0; index < this.sectionList.size(); index++) {
    			FileSection os = this.sectionList.get(index);
    			if (section.getOffset() + section.getLength() <= os.getOffset() + os.getLength()) {
    				return index;
    			}
    		}
    		
    		return -1;
    	}
    	
    }
    
    
  2. 文件控制块

    package com.mec.cloud.core;
    
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 资源文件的随机访问控制指针键集。<br>
     * 作用:相当于RandomAccessFile池,以避免对同一个文件的频繁打开和关闭,以提高
     * 文件访问效率。
     * 该类既用于资源发送者在发送文件片段前,读指定文件的指定片段;
     * 也用于资源接受者在接收文件片段后,写入指定文件的指定片段。
     * 但是,这个类只关心:提供RandomAccessFile,不关心具体的文件读写操作。
     * 另外,本类提供关闭指定文件RandomAccessFile的操作。
     * 本类中的成员:SourceFileList sourceFileList是指定资源的原始文件列表。
     * @author ZHO
     *
     */
    public class RandomAccessFileMap {
    	// TODO 特殊情况未处理
    	public static final int READ_ONLY = 1;
    	public static final int READ_WRITE = 2;
    	
    	private SourceFileInfoList sourceFileInfoList;
    	/**
    	 * 以文件编号为键,以文件随机访问控制块为值。
    	 */
    	private Map<Integer, RandomAccessFile> rafPool;
    	private int mode;
    	
    	RandomAccessFileMap(SourceFileInfoList sourceFileInfoList) {
    		this.sourceFileInfoList = sourceFileInfoList;
    		
    		this.rafPool = new HashMap<Integer, RandomAccessFile>();
    	}
    	
    	void setMode(int mode) {
    		this.mode = mode;
    	}
    
    	/**
    	 * 多线程安全的、单例的,获取fileId对应的文件的RandomAccessFile对象。
    	 * @param fileId
    	 * @return
    	 * @throws FileNotFoundException
    	 */
    	RandomAccessFile getRandomAccessFile(int fileId) throws FileNotFoundException {
    		RandomAccessFile raf = this.rafPool.get(fileId);
    
    		if (raf == null) {
    			synchronized (this.rafPool) {
    				raf = this.rafPool.get(fileId);
    				if (raf == null) {
    					raf = openFile(fileId);
    					this.rafPool.put(fileId, raf);
    				}
    			}
    		}
    		
    		return raf;
    	}
    	
    	private RandomAccessFile openFile(int fileId) throws FileNotFoundException {
    		String filePath = this.sourceFileInfoList.getFileName(fileId);
    		
    		String mode = this.mode == RandomAccessFileMap.READ_ONLY ? "r" : "rw";
    		return new RandomAccessFile(filePath, mode);
    	}
    	
    	/**
    	 * 关闭RandomAccessFile后,只是将Map的值改为null,并没有从Map中删除这个键值对!
    	 * @param fileId
    	 * @throws IOException
    	 */
    	void closeRandomAccessFile(int fileId) throws IOException {
    		synchronized (this.rafPool) {
    			RandomAccessFile randomAccessFile = this.rafPool.get(fileId);
    			if (randomAccessFile != null) {
    				randomAccessFile.close();
    				randomAccessFile = null;
    			}
    		}
    	}
    	
    }
    
    
    1. 文件头部信息的创建

      经过试验得出,文件片段传输过程可能会出错,向byte数组中读取数据流length长度的数据会出问题,于是用一下的方式解决

      public static byte[] receive(InputStream dis, int length) throws IOException {
      		byte[] receiveData = new byte[length];
      		
      		int offset = 0;
      		int len;
      		
      		while (length > 0) {
      			len = dis.read(receiveData, offset, length);
      			offset += len;
      			length -= len;
      		}
      		
      		return receiveData;
      	}
      
      • 为了保证文件片段传输完整性,需要知道文件片段的长度(length)

      • 需要知道片段所属的文件,就是文件句柄(fileId)

      • 需要知道片段在文件中的位置,就是偏移量(offset)

      • 保证片段传输的准确性:校验和(sum)

    public class FileSection {
    	public static final int FILE_SECTION_HEAD_LEN = 24;
    	
    	public static final int EOF = -1;
    	public static final int ACCEPT_ERROR = -2;
    	
    	private int fileId;
    	private long offset;
    	private long length;
    	private int sum;
        //...其它代码已省略
    }
        
    

7.技巧实现

  1. 面向接口编程

    面向未来:接口的方法在某个需要他的类中被调用,但是接口的实现是在未来产生的某个类中

    接口的定义:

    package com.mec.cloud.core;
    
    public interface ISendServerSourceRegistryCenterAction {
    	void reportHealth(int health);
    }
    
    

    接口的使用:

    public class SendServer implements Runnable {
        ...
    void setSendServerSourceRegistryCenterAction(
    			ISendServerSourceRegistryCenterAction sendServerSourceRegistryCenterAction) {
    		this.sendServerSourceRegistryCenterAction = sendServerSourceRegistryCenterAction;
    	}
        ...
    }
    

    接口的实现:

    public class SourceSender {	
        ...
        private SourceSender() throws IOException {
                this.sendServer = SendServer.getInstance();
                this.sendServer.setSendServerSourceRegistryCenterAction(
                        new ISendServerSourceRegistryCenterAction() {
                    @Override
                    public void reportHealth(int health) {
                        SourceSender.this.sourceHolder.setHealth(health);
                    }
                });
    
                this.sourceHolder = new SourceHolder();
                this.sourceHolder.setAddress(sendServer.getAddress());
    
                this.sourcePool = SourcePool.getInstance();
            }
        ...
    }
    
  2. 多线程安全

  • 单例模式:

    private volatile static SourcePool me;
    
    	static SourcePool getInstance() {
    		if (SourcePool.me == null) {
    			synchronized (SourcePool.class) {
    				if (SourcePool.me == null) {
    					SourcePool.me = new SourcePool();
    				}
    			}
    		}
    		
    		return SourcePool.me;
    	}
    
  • 防止逻辑单一操作重复执行

    		RandomAccessFile raf = this.rafPool.get(fileId);
    
    		//这个if是为了不用每一次都进入锁(因为只有第一次需要下面的锁)
    		if (raf == null) {
                //通过这个锁可以保证即使多个线程同时进入这个if语句中也只有一个线程可以执行put操作
    			synchronized (this.rafPool) {
    				raf = this.rafPool.get(fileId);
    				if (raf == null) {
    					raf = openFile(fileId);
    					this.rafPool.put(fileId, raf);
    				}
    			}
                
    		}
    		
    		return raf;
    
  1. 使用链式列表处理片段
  • 文件接收者将分割的小片段放入列表中,方便文件接受者对片段的拿取(避免下标问题)

  • 当文件接受者请求某个片段时,通过类似堆栈的pop方法取出第一个片段

  • 当某个片段因为校验和不符等未完成接收时,通过push方法将其放回列表第一个位置

private List<FileSection> requestSections = = new LinkedList<FileSection>();;

void push(FileSection fileSection) {
    synchronized (this.requestSections) {
        this.requestSections.add(0, fileSection);
    }
}

//如果请求的片段是空的,maker求返回一个“完成”片段来结束片段发送
FileSection pop() {
    synchronized (this.requestSections) {
        if (!this.requestSections.isEmpty()) {
            return this.requestSections.remove(0);
        }

        return FileSection.makeEof();
    }
}
  1. 工具思想的使用

将“公用的”方法放在一个工具类中,在其他类中方便调用这里面的静态方法

package com.mec.util.io;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class MecIo {

	public MecIo() {
	}
	
	public static void send(OutputStream dos, byte[] stream) throws IOException {
		dos.write(stream);
	}
	
	public static byte[] receive(InputStream dis, int length) throws IOException {
		byte[] receiveData = new byte[length];
		
		int offset = 0;
		int len;
		
		while (length > 0) {
			len = dis.read(receiveData, offset, length);
			offset += len;
			length -= len;
		}
		
		return receiveData;
	}
	
	public static byte[] read(RandomAccessFile file, long offset, int len) throws IOException {
		byte[] context = new byte[len];
		
		synchronized (file) {
			file.seek(offset);
			file.read(context);
		}
		
		return context;
	}
	
	public static void write(RandomAccessFile file, long offset, byte[] context) throws IOException {
		synchronized (file) {
			file.seek(offset);
			file.write(context);
		}
	}
	
	public static String getIp() throws UnknownHostException {
		return InetAddress.getLocalHost().getHostAddress();
	}

}

  1. 用空间换时间

    为了保证文件信息可以按顺序存储以及方便内部快速查找文件信息,我们用两个存储数据的类型:ArrayList和HashMap

    ArrayList对于数据的增删时间复杂度接近O(1),方便数据的增删,而且可以保证存储的顺序

    HashMap在数据存储后查询时间复杂度接近O(1),方便查询数据,用于快速查找数据

    	private List<FileInfo> fileList = new ArrayList<FileInfo>();
    	private Map<Integer, FileInfo> fileMap = new HashMap<>();;
    
posted @ 2023-05-31 13:19  Geek李  阅读(33)  评论(0)    收藏  举报