详解 网络文件传输技术 的基本实现
在本人之前的博文《详解 网络编程》和《详解 多线程》两篇博文 中,分别讲解了:
- 如何进行 网络通信
- 如何通过
流
去 读取、写入 文件
那么,在本篇博文中,本人将运用之前两篇博文所讲解的知识,来实现下:
通过网络,来 发送/接收 文件 的技术
那么,话不多说,现在就开始本篇博文的讲解吧:
首先,本人来讲解下 实现的思路:
实现 思路:
- 在当今的 网络文件发送/接收 的过程中,文件发送端 可能 不止一个
因此,我们就不能 简单地 实现 整个文件 的 发送/接收
由上述的思想,我们就需要将 整个目标文件 分为 多个文件片段- 有时候我们也会在 文件的上传/接收 过程中,能看到文件的 发送/接收 网速
那么,在 网络文件发送/接收 的基础上,我们也顺便提供扩展的接口,以便 使用者 的操作
那么,现在我们就来依据 上述的思想,用代码来实现一下:
实现 代码:
首先,本人来给出一个 用于 封装文件片段信息 的 实体类:
文件片段 实体类 —— 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
心得体会:
那么,到这里,网络文件传输 技术
就基本实现了
我们在使用时,只需要将 目标文件 分割成 多个 连续且不重复 的 文件片段,再进行 逐一收发 即可
至于使用,将在本人之后的博文《【多文件自平衡云传输】专栏总集篇》中 进行巧妙地运用,
并在最后会有视频展示,有兴趣的同学请前往围观哦!
(最后,附上 本人《多文件自平衡云传输框架》专栏 展示视频的封面,希望大家多多支持哦!)