文件片段的定义
多文件收发的过程中,要精准的接收到一个文件片段,需要三个基本标识:该片段属于哪个文件(文件编号)、该片段在源文件中的偏移量、该片段的长度。这三个合起来才可以准确的标识一个文件片段的基本信息。暂且将其称为元信息。除次之外,肯定必须还需要有片段的真实内容。即二进制数据。而且网络传输只能是二进制数据。发送端要发送一个片段总得先让接收端知道这个文件的元信息吧,总不能盲目地接收,要不然就算接收到了都无法确定该写入那个文件或者是这个片段该写入那个片段。所以接受端首先要接收的就是文件片段的元信息。鉴于网络上只能收发二进制字节。故需要有一个将元信息解析转换为二进制字节数组的工具:MecBinary
基于上述分析。就可以确定一个片段的对象了。
文件片段信息(FileSectionInfo)
public class FileSectionInfo {
private int fileNo;
private long offset;
private int len;
private byte[] content;
public FileSectionInfo(int fileNo, long offset, int len) {
this.fileNo = fileNo;
this.offset = offset;
this.len = len;
}
/**
* 将一个字节数组解析还原出元信息
* @param bytes 按照规定解析好的字节数组。int + long + int == 16个字节
*/
public FileSectionInfo(byte[] bytes) {
//从当前偏移量(0)开始,将其后的4个字节转换为int型数据
this.fileNo = MecBinary.bytesToInt(bytes, 0);
//从当前偏移量(4)开始,将其后的8个字节转换为long型数据
this.offset = MecBinary.bytesToLong(bytes, 4);
//从当前偏移量(12)开始,将其后的4个字节转换为int型数据
this.len = MecBinary.bytesToInt(bytes, 12);
}
//将一个文件片段的元信息解析成字节数组
public byte[] toBytes() {
byte[] bytes = new byte[16];
//将int型数据(this.fileNo)转换成四字节,并从偏移量(0开始)存储进bytes
MecBinary.intToBytes(bytes, 0, this.fileNo);
MecBinary.longToBytes(bytes, 4, this.offset);
MecBinary.intToBytes(bytes, 8, this.len);
return bytes;
}
public int getFileNo() {
return fileNo;
}
public FileSectionInfo setFileNo(int fileNo) {
this.fileNo = fileNo;
return this;
}
public long getOffset() {
return offset;
}
public FileSectionInfo setOffset(long offset) {
this.offset = offset;
return this;
}
public int getLen() {
return len;
}
public FileSectionInfo setLen(int len) {
this.len = len;
return this;
}
public byte[] getContent() {
return content;
}
public FileSectionInfo setContent(byte[] content) {
this.content = content;
return this;
}
@Override
public String toString() {
return fileNo + " : " + offset + " : " + len;
}
}
网络传输二进制数据(NetSendReceive)
//为网络上收发文件片段的二进制信息制定规范
public interface ISendReceive {
void send(DataOutputStream dos, byte[] content) throws IOException;
byte[] receive(DataInputStream dis, int len) throws IOException;
}
public class NetSendReceive implements ISendReceive {
//默认一次发送的字节长度。 1 << 10 == 1024(1KB) ==> 1 << 15 == 32KB
private static final int DEFAULT_BUFFER_SIZE = 1 << 15;
//一次发送的长度
private int bufferSize;
public NetSendReceive() {
this(DEFAULT_BUFFER_SIZE);
}
public NetSendReceive(int bufferSize) {
this.bufferSize = bufferSize;
}
public void setBufferSize(int bufferSize) {
this.bufferSize = bufferSize;
}
@Override
public void send(DataOutputStream dos, byte[] content) throws IOException {
int factLen = 0; //每次真实发送的长度
int resetLen = content.length; //剩余片段的长度
int offset = 0; //每次发送的偏移量
while(resetLen > 0) {
factLen = resetLen > bufferSize ? bufferSize : resetLen;
dos.write(content, offset, factLen);
resetLen -= factLen;
offset += factLen;
}
}
@Override
public byte[] receive(DataInputStream dis, int len) throws IOException {
byte[] result = new byte[len];
int factLen = 0; //每次真实读取的长度
int offset = 0; //每次读取的偏移量
int size; //一次读取的字节量
while(len > 0) {
size = len > bufferSize ? bufferSize : len;
factLen = dis.read(result, offset, size);
len -= factLen;
offset += factLen;
}
return result;
}
}
网络收发文件片段(FileSectionSendRecevice)
上面NetSendRecevice只关心二进制信息的收发,不关心什么片段不片段。而该类相当于对NetSendRecevice做了进一步封装,以网络收发二进制信息为基石,将其与文件片段信息结合起来。
public class FileSectionSendRecevice {
private ISendReceive sendReceive;
public FileSectionSendRecevice() {
this.sendReceive = new NetSendReceive();
}
public FileSectionSendRecevice(ISendReceive sendReceive) {
this.sendReceive = sendReceive;
}
/**
* 发送文件片段 。 注意发送的时候首先要将片段信息发送过去(以便对端处理)
* @param dos 输出流
* @param sectionInfo 文件片段信息
*/
public void sendSection(DataOutputStream dos, FileSectionInfo sectionInfo) throws IOException {
if(sectionInfo == null || sectionInfo.getLen() == 0) {
return;
}
//首先发送片段元信息。 这个很重要
this.sendReceive.send(dos, sectionInfo.toBytes());
this.sendReceive.send(dos, sectionInfo.getContent());
}
/**
* 接受文件片段信息。注意:首次先接受文件片段的元信息。
* @param dis 输入流
* @return 返回填充好的FileSectionInfo对象
*/
public FileSectionInfo receiveSection(DataInputStream dis) throws IOException {
//首先接受元信息
byte[] bytes = this.sendReceive.receive(dis, 16);
FileSectionInfo sectionInfo = new FileSectionInfo(bytes);
//大于0的时候再接受真实内容
int len;
if((len = sectionInfo.getLen()) > 0) {
byte[] content = this.sendReceive.receive(dis, len);
sectionInfo.setContent(content);
}
return sectionInfo;
}
//发送结束信息。可以用来标志文件片段已经发送完毕
public void sendEndInfo(DataOutputStream dos) throws IOException {
FileSectionInfo sectionInfo = new FileSectionInfo(-1, -1, -1);
this.sendReceive.send(dos, sectionInfo.toBytes());
}
}
到此最基本的网络传输文件片段的工具算做好了,但是还需要一个用于操作本地磁盘文件的工具,Java为我们提供了一个类:RandomAccessFile此类可以随意操作一个文件的任何一个地方。有最基本的read和write方法,最重要的是它的seek方法,相当于文件位置指针,可以通过移动文件位置指针来操作文件的任意地方。现在需要借助该类来完成一个文件的读和写。
FileReadWrite
/**
* 该类可完成一整个文件的读写操作。该类是与本地磁盘交互的类
* 要想操作本地文件,必须要知道文件的路径。
* 注意:一个文件对应一个FileReadWrite对象,而非一个片段。
* 该类揉和了“读操作”和“写操作”。
* 对应的发送端可利用该对象从发送端本地读取片段。接收端也可以利用它将一个片段写入接收端本地磁盘中。
*/
public class FileReadWrite {
private int fileNo;
private String filePath; //这个filePath是绝对路径
private RandomAccessFile raf;
private IFileReadWriteIntercepter intercepter; //这个接口可以用作读写文件时的前置和后置拦截, 方便以后扩展
public FileReadWrite(int fileNo, String filePath) {
this.fileNo = fileNo;
this.filePath = filePath;
//此处先定义成默认的适配器,什么也不干
this.intercepter = new FileReadWriteIntercepterAdapter();
}
public int getFileNo() {
return fileNo;
}
//设置自定义拦截策略
public void setIntercepter(IFileReadWriteIntercepter intercepter) {
this.intercepter = intercepter;
}
//从目标磁盘文件中读取指定片段
public FileSectionInfo readSection(FileSectionInfo sectionInfo) throws IOException {
//读片段的前置拦截
this.intercepter.beforeRead(sectionInfo);
if(this.raf == null) {
raf = new RandomAccessFile(this.filePath, "r");
}
//移动读文件的指针
raf.seek(sectionInfo.getOffset());
//获取需要读取的长度
int len = sectionInfo.getLen();
byte[] bytes = new byte[len];
raf.read(bytes);
sectionInfo.setContent(bytes);
return this.intercepter.afterRead(sectionInfo);
}
//向目标磁盘文件中写入指定的文件片段
public boolean writtenSection(FileSectionInfo sectionInfo) {
//写片段的前置拦截
this.intercepter.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.intercepter.afterWrite(sectionInfo);
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
//关闭资源
public void close() {
if(this.raf != null) {
try {
this.raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
到此为止,底层的一些工具性质的东西就准备好了。现在可以来测试一下。建立网络客户端(发送端)和服务器(接收端)。利用FileReadWrite来各自操作双端本地磁盘文件。发送端读取到片段之后再利用网络将字节数据传递出去,接收端从网络中接收到字节数据之后再利用FileReadWrite对象写入到自己的相应的磁盘位置处。
测试
接收端
public class ReceiveTest {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(54188);
Socket socket = server.accept();
DataInputStream dis = new DataInputStream(socket.getInputStream());
String filePath = "C:\\Users\\Administrator\\Desktop\\leftCpoy.mp3";
FileReadWrite frw = new FileReadWrite(1, filePath);
FileSectionSendRecevice fileReceive = new FileSectionSendRecevice();
//第一次读取。在发送端未发出消息之前,程序会阻塞在这
FileSectionInfo sectionInfo = fileReceive.receiveSection(dis);
while(sectionInfo.getLen() > 0) {
frw.writtenSection(sectionInfo);
sectionInfo = fileReceive.receiveSection(dis);
}
frw.close();
dis.close();
server.close();
}
}
发送端:
public class SenderTest {
public static int maxSection = 1 << 20; //定义1M片段大小
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 54188);
//这首歌3.51M
String filePath = "C:\\Users\\Administrator\\Desktop\\leftHand.mp3";
File file = new File(filePath);
List<FileSectionInfo> sectionInfoList = new ArrayList<FileSectionInfo>();
//将该文件切片
int restLen = (int) file.length();
int offset = 0;
int curLen = 0;
//分片不能太小,如果太小可能造成堆内存溢出
while(restLen > 0) {
curLen = restLen > maxSection ? maxSection : restLen;
sectionInfoList.add(new FileSectionInfo(1, offset, restLen));
offset += curLen;
restLen -= curLen;
}
FileReadWrite frw = new FileReadWrite(1, filePath);
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
FileSectionSendRecevice fileSend = new FileSectionSendRecevice();
for (FileSectionInfo sectionInfo : sectionInfoList) {
frw.readSection(sectionInfo);
fileSend.sendSection(dos, sectionInfo);
}
//发送所有有效数据之后再发送一条 ”结束标识“
fileSend.sendEndInfo(dos);
frw.close();
dos.close();
socket.close();
}
}