当前位置: 首页 > >

Java:Socket断点传输大文件

发布时间:



文章目录
0x00. 解决思路0x01. Server端实现0x02. Client端实现0x03. 测试0x04. 过程感言


0x00. 解决思路
Sever端负责接收指令(文件路径、第几块、每块大小),读取相应的文件中的块数据,返回给Client(顺便附上有效数据长度、MD5)。Client端负责控制断点,通过断点向Server发送指令,接收数据后判断数据完整性(有效数据长度、MD5),再将数据写入目标文件。由于Java流处理类实在繁多,本次实验就统一采用 In/OutputStreamFileIn/FileOutputStream 字节流传输类来传输数据。多线程的Server还没摸清楚,这里就只用单线程了。Client发给Server的指令为UTF-8编码的字符串,格式为文件路径|第几块|每块大小。Server发送给Client的数据为字节数组,格式为:
数据长度,4字节int数据MD5,16字节数组文件块数据,长度由数据长度指定

0x01. Server端实现

监听端口,等待Client连接:

ServerSocket server = new ServerSocket(port); // 监听localhost:port
Socket handle = server.accept(); // 等待Client接入

OutputStream out = handle.getOutputStream();
InputStream in = handle.getInputStream();

设定指令缓冲区:

final int orderBufSize = 1024;
byte[] orderBuf = new byte[orderBufSize];

一直监听指令,直到Client结束:

while (true) {
// 接收并解析Client发过来的指令

// 通过指令读取指定文件中的指定块

// 数据附上有效数据长度和MD5值

// 将数据发送给Client
}
// 关闭连接

接收并解析Client发过来的指令:

// 接收指令
int orderLen = in.read(orderBuf, 0, orderBufSize);
if (orderLen <= 0) {
break;
}
String orderStr = new String(orderBuf, 0, orderLen, "UTF-8");
// 解析指令
String[] orders = orderStr.trim().split("\|"); // 正则表达式,用竖线分割字符串
final String filePath = orders[0]; // 目标文件路径
final int pieceIndex = Integer.parseInt(orders[1]); // 目标数据块
final int pieceBufSize = Integer.parseInt(orders[2]); // 每块大小

读取指定文件中的指定块:

byte[] pieceBuf = new byte[pieceBufSize]; // 文件内容缓冲区
FileInputStream file = new FileInputStream(filePath);
file.skip(pieceIndex * (long) pieceBufSize); // 跳转到目标块
int pieceLen = file.read(pieceBuf); // 读取目标块
file.close();

数据附上有文件内容长度和MD5值。长度是int,要传输就要先转换成byte[]:

// 获取md5值
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(pieceBuf);
byte[] md5 = md.digest();
// 将要返回给Client的数据合并,包括文件内容长度、内容MD5值、文件内容
byte[] lenArr = ByteBuffer.allocate(4).putInt(pieceLen).array();
ByteBuffer byteBuffer = ByteBuffer.allocate(lenArr.length + md5.length + pieceBuf.length);
byte[] res = byteBuffer.put(lenArr).put(md5).put(pieceBuf).array();

这里用到了 java.nio.ByteBuffer 这个类,byte[]操作很方便,比如byte[]->int,int->byte[]。

将数据发送给Client:

out.write(res);

0x02. Client端实现

先做三个断点操作函数:

// 断点不存在或出现错误则返回backup
private static int GetBreakPoint(String bpFile, int backup) {
FileReader fr = null;
try {
fr = new FileReader(bpFile);
BufferedReader br = new BufferedReader(fr);
String numStr = br.readLine();
return Integer.parseInt(numStr);
} catch (NumberFormatException | IOException e) {
return backup;
} finally {
try {
if (fr != null) {
fr.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

private static void SetBreakPoint(String bpFile, int num) throws FileNotFoundException {
PrintStream f = new PrintStream(bpFile);
f.println(num);
f.close();
}

private static void DelBreakPoint(String bpFile) {
File f = new File(bpFile);
if (f.exists()) {
f.delete();
}
}

设定srcFile和dstFile为源文件和目标文件。

读取断点,断点文件为BreakPoint.index

String bpFile = "BreakPoint.index";
int begin = GetBreakPoint(bpFile, 0);

连接到Server:

Socket client = new Socket("localhost", port);
OutputStream out = client.getOutputStream();
InputStream in = client.getInputStream();

设置缓冲区:

final int bufLen = 8 * 1000 * 1000; // 8M
byte[] pieceBuf = new byte[4 + 16 + bufSize]; // 文件内容长度4字节,md5值16字节

Client主体:

for (int pieceIndex = begin; ; ++pieceIndex) {
SetBreakPoint(bpFile, pieceIndex);

// 向Server发送指令

// 接收数据

// 分析数据,判断文件是否结束,校验MD5

// 将数据写入文件
}
DelBreakPoint(bpFile);
// 关闭连接

向Server发送指令:

String order = String.format("%s|%d|%d", srcFile, pieceIndex, bufSize);
out.write(order.getBytes("UTF-8"));

接收数据,直到数据读完或缓冲区满:

int sum = 0, len;
while ((len = in.read(pieceBuf, sum, 20 + bufSize - sum)) > 0 && sum < 20 + bufSize) {
sum += len;
}

分析数据,判断文件是否结束,校验md5:

// 解析Server返回的数据
byte[] temp = new byte[4];
System.arraycopy(pieceBuf, 0, temp, 0, 4);
int pieceLen = ByteBuffer.wrap(temp).getInt();
if (pieceLen <= 0) {
// 文件读取结束
break;
}
// MD5校验
byte[] md5 = new byte[16];
System.arraycopy(pieceBuf, 4, md5, 0, 16);
if (!CheckMD5(pieceBuf, 20, md5)) {
// MD5校验失败,重新请求该文件块
pieceIndex--;
continue;
}

这里也用到了Java自带类 java.nio.ByteBuffer 来做byte[]->int。
CheckMD5是对java.security.MessageDigest的封装:

/**
* MD5校验函数,判断data数组中start位置之后数据的MD5值是否和md5参数一致
*
* @param data 待校验字节数组
* @param start 数据起始下标
* @param md5 md5对照字节数组
* @return 校验结果
*/
private static Boolean CheckMD5(byte[] data, int start, byte[] md5) {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
if (start == 0) {
md.update(data);
} else {
byte[] part = new byte[data.length - start];
System.arraycopy(data, start, part, 0, part.length);
md.update(part);
}

} catch (NoSuchAlgorithmException e) {
return false;
}
byte[] result = md.digest();
for (int i = 0; i < result.length; ++i) {
if (md5[i] != result[i]) {
return false;
}
}
return true;
}

将数据写入文件:

FileOutputStream fw = new FileOutputStream(dstFile, true); // 追加到文件末尾
fw.write(pieceBuf, 20, pieceLen);
fw.close();

0x03. 测试

在本地运行Server和Client,使用Socket传输一个8G的文件(机械硬盘->固态硬盘)用时180秒;而使用操作系统的复制操作用时75秒。为什么会有这么大的差距呢?我觉得原因主要有以下几点:


文件内容需要先被Server读取至内存,再使用Socket通过localhost发送给Client的Socket,再由Client写入至硬盘。这个过程,远没有操作系统“读取+写入”来得高效。由于采用了文件分块机制,所以每传输一次大文件,Socket和Client实际上进行了很多次操作。假如说大文件分成了100块依次进行传输,那么Client就需要发送100次指令、执行100次文件写入操作,Server需要执行100次文件读取操作,所以缓存大小在一定程度上也会影响Socket传输的效率。
0x04. 过程感言
Java的流处理类实在太多,有处理字节的,有处理字符的,有处理一行字符的,有处理各种数据类型的,实在令人头晕。这里就直接统一用最底层的类,省去了很多麻烦。由于TCP数据包的最大长度是65535,所以Client接收64M数据时不能一次接收完,需要循环接收。由于刚开始不太了解,所以一直不能接收到完整的数据,说明了基础的重要性。TCP协议底层使用了CRC做校验,上层再做一次MD5校验更保险(虽然这次传输8G文件100+次传输都没有错错误,汗……)。



友情链接: