详解 网络文件传输技术 的基本实现

Youzg LOGO

在本人之前的博文《详解 网络编程》《详解 多线程》两篇博文 中,分别讲解了:

  • 如何进行 网络通信
  • 如何通过 读取、写入 文件

那么,在本篇博文中,本人将运用之前两篇博文所讲解的知识,来实现下:

通过网络,来 发送/接收 文件 的技术

那么,话不多说,现在就开始本篇博文的讲解吧:

首先,本人来讲解下 实现的思路

实现 思路:

  • 在当今的 网络文件发送/接收 的过程中,文件发送端 可能 不止一个
    因此,我们就不能 简单地 实现 整个文件 的 发送/接收
    由上述的思想,我们就需要将 整个目标文件 分为 多个文件片段
  • 有时候我们也会在 文件的上传/接收 过程中,能看到文件的 发送/接收 网速
    那么,在 网络文件发送/接收 的基础上,我们也顺便提供扩展的接口,以便 使用者 的操作

那么,现在我们就来依据 上述的思想,用代码来实现一下:

实现 代码:

首先,本人来给出一个 用于 封装文件片段信息实体类

文件片段 实体类 —— FileSectionInfo:

对于 文件片段,我们需要知道的信息 有四个

  • 文件片段 编号
  • 文件片段所处 偏移量
  • 文件片段 长度
  • 文件片段 内容
package edu.youzg.network_transmission.section;

import edu.youzg.util.ByteConverter;

/**
 * 封装 文件片段的信息
 */
public class FileSectionInfo {
    private int fileNo; // 当前文件片段 编号
    private long offset;    // 当前文件片段所处 偏移量
    private int length; // 当前文件片段长度
    private byte[] content; // 文件片段内容

    public FileSectionInfo() {
    }

    /**
     * 初始化 fileNo、offset、length
     * @param head
     */
    public FileSectionInfo(byte[] head) {
        this.fileNo = ByteConverter.bytesToInt(head);
        this.offset = ByteConverter.bytesToLong(head, 4);
        this.length = ByteConverter.bytesToInt(head, 12);
    }

    public FileSectionInfo(int fileNo, long offset, int length) {
        this.fileNo = fileNo;
        this.offset = offset;
        this.length = length;
    }

    /**
     * 将 fileNo、offset、length 信息转换为 字节数组
     * @return
     */
    public byte[] toBytes() {
        byte[] res = new byte[16];
        ByteConverter.intToBytes(res, 0, fileNo);
        ByteConverter.longToBytes(res, 4, offset);
        ByteConverter.intToBytes(res, 12, length);

        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 getLength() {
        return length;
    }

    public void setLength(int len) {
        this.length = len;
    }

    public byte[] getContent() {
        return content;
    }

    public void setContent(byte[] content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return fileNo + " : " + offset + ", " + length;
    }

}

针对 一个文件片段接收/发送,可能需要在 发送/接收 前后 进行一些 其它操作
(譬如:进度条显示)
那么,本人再来给出一个 针对 文件片段读写拦截器

文件片段读写 拦截器 —— IFileReadWriteIntercepter:

package edu.youzg.network_transmission.section;

/**
 * 文件片段 读/写 拦截器 功能接口
 */
public interface IFileReadWriteIntercepter {
    void beforeRead(FileSectionInfo sectionInfo);
    FileSectionInfo afterRead(FileSectionInfo sectionInfo);
    void beforeWrite(String filePath, FileSectionInfo sectionInfo);
    void afterWritten(FileSectionInfo sectionInfo);
}

为了方便我们的使用,本人再针对上面的接口,来给出一个 适配器

文件片段读写 拦截器适配器 —— FileReadWriteIntercepterAdapter:

package edu.youzg.network_transmission.section;

/**
 * 文件片段读写 拦截器适配器
 */
public class FileReadWriteIntercepterAdapter implements IFileReadWriteIntercepter {

    public FileReadWriteIntercepterAdapter() {
    }

    @Override
    public void beforeRead(FileSectionInfo sectionInfo) {
    }

    @Override
    public FileSectionInfo afterRead(FileSectionInfo sectionInfo) {
        return sectionInfo;
    }

    @Override
    public void beforeWrite(String filePath, FileSectionInfo sectionInfo) {
    }

    @Override
    public void afterWritten(FileSectionInfo sectionInfo) {
    }
    
}

从本人之前的博文《详解 网络编程》中,我们能够了解到:

关于 文件网络发送/接收
在底层都是 字节数据 的交互

那么,本人来提供一个 用于 通过网络 发送/接收 字节数组功能接口

网络接收/发送字节数据 功能接口 —— ISendReceive:

package edu.youzg.network_transmission.net;

import java.io.DataInputStream;
import java.io.DataOutputStream;

/**
 * 收发字节数组 功能接口
 */
public interface ISendReceive {

    /**
     * 通过网络,发送字节数组
     * @param dos 操作的流
     * @param content 目标字节数组
     * @throws Exception
     */
    void send(DataOutputStream dos, byte[] content) throws Exception;

    /**
     * 通过网络,接收字节数组
     * @param dis 操作的流
     * @param len 读取的长度
     * @return 读取到的数组
     * @throws Exception
     */
    byte[] receive(DataInputStream dis, int len) throws Exception;

}

那么,针对这个接口,本人也来提供给它的 具体实现类

网络接收/发送字节数据 功能实现类 —— NetSendReceive:

package edu.youzg.network_transmission.net;

import java.io.DataInputStream;
import java.io.DataOutputStream;

/**
 * 通过网络,收发字节数组
 */
public class NetSendReceive implements ISendReceive {
    public static final int DEFAULT_SECTION_LEN = 1 << 15;
    private int bufferSize; // 设置的缓冲区大小
    private INetSendReceiveSpeed speed; // 用于监控网速

    public NetSendReceive() {
        speed = new NetSendReceiveSpeedAdapter();
        bufferSize = DEFAULT_SECTION_LEN;
    }

    public void setBufferSize(int bufferSize) {
        this.bufferSize = bufferSize;
    }

    public void setSpeed(INetSendReceiveSpeed speed) {
        this.speed = speed;
    }

    /**
     * 通过网络,发送字节数组
     * @param dos 操作的流
     * @param content 目标字节数组
     * @throws Exception
     */
    @Override
    public void send(DataOutputStream dos, byte[] content) throws Exception {
        int length = content.length;
        int offset = 0;
        int curLen = 0;

        while (length>0) {
            curLen = length>bufferSize ? bufferSize : length;   // 使得当前长度为两者中最小者(防止每次发送大小超出限制)
            dos.write(content, offset, curLen);
            offset += curLen;
            length -= curLen;
        }
        this.speed.afterSend(length);
    }

    /**
     * 通过网络,接收字节数组,<br/>
     * 并作为返回值 返回
     * @param dis 操作的流
     * @param length 字节数组的大小
     * @return 读取到的 字节数组
     * @throws Exception
     */
    @Override
    public byte[] receive(DataInputStream dis, int length) throws Exception {
        byte[] buffer = new byte[length];

        int curLen = 0;
        int factLen = 0;    // 真实的读取长度
        int offset = 0;
        while (length > 0) {
            curLen = length>bufferSize ? this.bufferSize : length;
            factLen = dis.read(buffer, offset, curLen);
            length -= factLen;
            offset += factLen;
        }
        this.speed.afterReceive(length);

        return buffer;
    }

}

正如本人上文所讲:

我们 可能要针对 网络文件传输 进行 网速检测
因此,当我们 发送/接收 到了 字节数据后,针对 发送/接收 到的 字节数据量 进行一些处理,就可以实现 网速检测功能

那么,现在本人就来实现下 网速检测功能
首先是 网速检测 功能接口

网速检测 功能接口 —— INetSendReceiveSpeed:

package edu.youzg.network_transmission.net;

/**
 * 网络文件片段收发 善后功能 接口
 */
public interface INetSendReceiveSpeed {

    /**
     * 发送后
     * @param sendBytes 要发送的 字节数
     */
    void afterSend(int sendBytes);

    /**
     * 接收后<br/>
     * 在本框架中,主要用于 计算平均/瞬时速率
     * @param receiveBytes 接收的字节数
     */
    void afterReceive(int receiveBytes);

}

既然给出了接口,本人也来针对 上述的接口,提供一个 适配器

网速检测 功能适配器 —— NetSendReceiveSpeedAdapter:

package edu.youzg.network_transmission.net;

/**
 * 网络文件片段收发 善后功能 适配器
 */
public class NetSendReceiveSpeedAdapter implements INetSendReceiveSpeed {

    @Override
    public void afterSend(int sendBytes) {
    }

    /**
     * 在本框架中,主要用于 计算平均/瞬时速率
     * @param receiveBytes 接收的字节数
     */
    @Override
    public void afterReceive(int receiveBytes) {
    }

}

接下来,本人来提供一个 网速检测 功能的具体实现类

网速检测 功能实现类 —— NetSpeed:

package edu.youzg.network_transmission.net;

/**
 * 提供了 单例性、多例性 的构造方式,<br/>
 * 并 提供了接收后 计算接收速率 的方法
 */
public class NetSpeed extends NetSendReceiveSpeedAdapter {
    private volatile static NetSpeed me;    // 用作 “单例性构造”
    private volatile static long startReceiveTime;  // 开始接收时间
    private volatile static long lastReceiveTime;   // 接收结束时间

    private volatile static long allReceiveBytes;   // 总共接收的字节数
    private volatile static long curSpeed;  // 当前接收速率
    private volatile static long averSpeed; // 平均接收速率

    /**
     * 获取一个NetSpeed实例
     * (保证了“多例性”)
     */
    public NetSpeed() {
    }

    /**
     * 获取一个NetSpeed实例
     * @return 一个NetSpeed实例(保证了“单例性”)
     */
    public synchronized static NetSpeed newInstance() {
        if (me==null) {
            startReceiveTime = lastReceiveTime
                    = System.currentTimeMillis();
            allReceiveBytes = 0;
            curSpeed = averSpeed = 0;
            me = new NetSpeed();
        }
        return me;
    }

    /**
     * 清空当前属性的值<br/>
     * 便于第二次使用不受上一次使用的影响
     */
    public static void clear() {
        me = null;
        allReceiveBytes = 0;
        curSpeed = averSpeed = 0;
    }

    /**
     * 计算 瞬时/平均速率,并更新各种时间
     * @param receiveBytes 接收到的字节数
     */
    @Override
    public void afterReceive(int receiveBytes) {
        long curTime = System.currentTimeMillis();
        long deltaTime = curTime - lastReceiveTime;
        long allTime = curTime - startReceiveTime;

        curSpeed = (long) ((double)receiveBytes*1000.0 / deltaTime);    // 计算当前瞬时速率
        allReceiveBytes += receiveBytes;    // 计算当前总收到字节数
        averSpeed = (long) ((double) allReceiveBytes * 1000.0 / allTime); // 计算平均速率

        lastReceiveTime = curTime;
    }

    public static long getCurSpeed() {
        return curSpeed;
    }

    public static long getAverSpeed() {
        return averSpeed;
    }

}

那么,有了上面所有类的铺垫,本人最后来提供两个 核心类

首先是 用于 将文件片段 读取/写入 本地机 的 功能类:

[核心]本地文件片段 读写器 —— FileReadWrite:

package edu.youzg.network_transmission.core;

import edu.youzg.network_transmission.section.FileReadWriteIntercepterAdapter;
import edu.youzg.network_transmission.section.FileSectionInfo;
import edu.youzg.network_transmission.section.IFileReadWriteIntercepter;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * 1. 操作单个文件<br/>
 * 2. FileSectionInfo的 读取、写入 操作(仅针对同一台主机而言)<br/>
 * 3. section层对外提供的类
 */
public class FileReadWrite {
    private int fileNo;     // 文件片段的序号
    private String filePath;    // 源文件 所在路径
    private RandomAccessFile raf;   // 随机访问流,用于 读写指定“偏移量”与“长度” 的文件片段
    private IFileReadWriteIntercepter fileReadWriteIntercepter;

    public FileReadWrite(int fileNo, String filePath) {
        this.fileNo = fileNo;
        this.filePath = filePath;
        this.fileReadWriteIntercepter = new FileReadWriteIntercepterAdapter();
    }

    public void setFileReadWriteIntercepter(IFileReadWriteIntercepter fileReadWriteIntercepter) {
        this.fileReadWriteIntercepter = fileReadWriteIntercepter;
    }

    public int getFileNo() {
        return fileNo;
    }

    /**
     * 根据所传sectionInfo参数 和 filePath属性,<br/>
     * 将指定文件的指定片段 读取并封装入sectionInfo中
     * @param sectionInfo 用于获知目标文件片段的 偏移量、长度,以及封装最终的读取结果
     * @return 目标 sectionInfo
     * @throws IOException
     */
    public FileSectionInfo readSection(FileSectionInfo sectionInfo) throws IOException {
        this.fileReadWriteIntercepter.beforeRead(sectionInfo);
        if (raf==null) {
            // 创建 只读形式 随机访问流
            raf = new RandomAccessFile(filePath, "r");
        }
        raf.seek(sectionInfo.getOffset());  // 定位
        int length = sectionInfo.getLength();
        byte[] buffer = new byte[length];   // 构建缓冲区
        raf.read(buffer);   // 读取数据至缓冲区中
        sectionInfo.setContent(buffer); // 将数据 设置进 sectionInfo中

        return fileReadWriteIntercepter.afterRead(sectionInfo);
    }

    /**
     * 根据所传sectionInfo参数 和 filePath属性,<br/>
     * 将指定文件的指定片段 写入到 当前程序运行主机 的 指定位置
     * @param sectionInfo 目标文件片段信息
     * @return 是否写入成功
     */
    public boolean writeSection(FileSectionInfo sectionInfo) {
        fileReadWriteIntercepter.beforeWrite(filePath, sectionInfo);
        if (this.raf == null) {
            synchronized (filePath) {
                try {
                    File file = new File(filePath);
                    if (!file.getParentFile().exists()) {
                        file.getParentFile().mkdirs();
                    }
                    file.createNewFile();
                    this.raf = new RandomAccessFile(filePath, "rw");
                } catch (Exception e) {
                    e.printStackTrace();
                    return false;
                }
            }
        }

        try {
            synchronized (filePath) {
                this.raf.seek(sectionInfo.getOffset());
                this.raf.write(sectionInfo.getContent());
                this.fileReadWriteIntercepter.afterWritten(sectionInfo);
            }
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 关闭当前 随机访问流
     */
    public void close() {
        try {
            raf.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

最后是 通过网络传输/接收 文件片段 的功能类:

[核心]网络文件片段 传输器 —— FileSectionSendReceive:

package edu.youzg.network_transmission.core;

import edu.youzg.network_transmission.net.*;
import edu.youzg.network_transmission.section.FileSectionInfo;

import java.io.DataInputStream;
import java.io.DataOutputStream;

/**
 * 通过 网络 传输 文件片段<br/>
 * net层向外提供的类<br/>
 * 主要提供的方法有:<br/>
 * sendSection、sendLastSection、<br/>
 * receiveSection
 */
public class FileSectionSendReceive {
    private ISendReceive sendReceive;
    private INetSendReceiveSpeed speed;

    public FileSectionSendReceive() {
        this.sendReceive = new NetSendReceive();
        this.speed = new NetSpeed();
    }

    public FileSectionSendReceive(ISendReceive sendReceive) {
        this.sendReceive = sendReceive;
    }

    public void setSendReceive(ISendReceive sendReceive) {
        this.sendReceive = sendReceive;
    }

    public void setSpeed(INetSendReceiveSpeed speed) {
        this.speed = speed;
    }

    /**
     * 发送 “整个文件发送完毕”信号
     * @param dos 操作的流
     * @throws Exception
     */
    public void sendLastSection(DataOutputStream dos) throws Exception {
        FileSectionInfo sectionInfo = new FileSectionInfo(0, 0, 0);
        sectionInfo.setContent(new byte[0]);
        sendReceive.send(dos, sectionInfo.toBytes());   // 发送一个空信息(全为0和null),标记发送结束
    }

    /**
     * 发送文件片段
     * @param dos 操作的流
     * @param sectionInfo 要发送的文件片段
     * @throws Exception
     */
    public void sendSection(DataOutputStream dos, FileSectionInfo sectionInfo) throws Exception {
        if (sectionInfo == null || sectionInfo.getLength() <= 0) {
            return;
        }
        // 发送 “文件片段头部”
        sendReceive.send(dos, sectionInfo.toBytes());
        // 发送 “文件片段 全部内容”
        sendReceive.send(dos, sectionInfo.getContent());

        this.speed.afterSend(sectionInfo.getLength());
    }

    /**
     * 接收文件片段:<br/>
     * 1. 接收头部<br/>
     * 2. 接收文件片段内容
     * @param dis 操作的流
     * @return 读取到的文件片段信息
     * @throws Exception
     */
    public FileSectionInfo receiveSection(DataInputStream dis) throws Exception {
        // 文件片段头组成:fileNo(int) + offset(long) + length(int)
        byte[] head = sendReceive.receive(dis, 16);
        FileSectionInfo sectionInfo = new FileSectionInfo(head);

        int sectionLength = sectionInfo.getLength();
        if (sectionLength > 0) {
            byte[] receive = this.sendReceive.receive(dis, sectionLength);
            sectionInfo.setContent(receive);    // 再次收取 对端发来的 指定长度的 信息
            this.speed.afterReceive(sectionLength);
        }
        return sectionInfo;
    }

}

若有需要上述源码的同学,本人已将本文所讲解到的代码打成了Jar包:

工具 Jar包:

如有需要,请点击下方链接:
Network-Section-Transmitter


心得体会:

那么,到这里,网络文件传输 技术 就基本实现了
我们在使用时,只需要将 目标文件 分割成 多个 连续且不重复文件片段,再进行 逐一收发 即可

至于使用,将在本人之后的博文《【多文件自平衡云传输】专栏总集篇》中 进行巧妙地运用,
并在最后会有视频展示,有兴趣的同学请前往围观哦!

(最后,附上 本人《多文件自平衡云传输框架》专栏 展示视频的封面,希望大家多多支持哦!)
视频展示

posted @ 2020-08-20 18:56  在下右转,有何贵干  阅读(302)  评论(0编辑  收藏  举报