小傅哥 | https://bugstack.cn
沉淀、分享、成长,让自己和他人都能有所收获。专注于原创专题案例编写,目前已完成的专题有;Netty4.x实战专题案例、用Java实现JVM、基于JavaAgent的全链路监控、手写RPC框架、架构设计专题案例、源码分析等。你用剑 、我用刀 ,好的代码都很烧,望你不吝出招!
一、前言介绍在实际应用中我们经常使用到网盘服务,他们可以高效的上传下载较大文件。那么这些高性能文件传输服务,都需要实现的分片发送、断点续传功能。
在Java文件操作中有RandomAccessFile类,他可以支持文件的定位读取和写入,这样就满足了我们对文件分片的最基础需求。
Netty服务端启动后,可以向客户端发送文件传输指令;允许接收文件、控制读取位点、记录传输标记、文件接收完成。
为了保证传输性能我们采用protostuff二进制流进行传输。
读取文件的时候需要注意,我们设定byte1024为默认读取范围,但当读取到最后的时候可能不足1024个字节,就会出现空字节。这个时候需要去掉空字节,否则我们的文件写入会多额外信息,导致文件不能打开{zip、war、exe、jar等}。
二、开发环境jdk1.8【jdk1.7以下只能部分支持netty】
Netty4.1.36.Final【netty3.x 4.x 5每次的变化较大,接口类名也随着变化】
三、代码示例
1itstack-demo-netty-2-04
2└── src
3 ├── main
4 │ └── java
5 │ └── org.itstack.demo.netty
6 │ ├── client
7 │ │ ├── MyChannelInitializer.java
8 │ │ ├── MyClientHandler.java
9 │ │ └── NettyClient.java
10 │ ├── codec
11 │ │ ├── ObjDecoder.java
12 │ │ └── ObjEncoder.java
13 │ ├── domain
14 │ │ ├── Constants.java
15 │ │ ├── FileBurstData.java
16 │ │ ├── FileBurstInstruct.java
17 │ │ ├── FileDescInfo.java
18 │ │ └── FileTransferProtocol.java
19 │ ├── server
20 │ │ ├── MyChannelInitializer.java
21 │ │ ├── MyServerHandler.java
22 │ │ └── NettyServer.java
23 │ └── util
24 │ ├── CacheUtil.java
25 │ ├── FileUtil.java
26 │ ├── MsgUtil.java
27 │ └── SerializationUtil.java
28 │
29 └── test
30 └── java
31 └── org.itstack.demo.test
32 ├── ApiTest.java
33 ├── NettyClientTest.java
34 └── NettyServerTest.java
演示部分重点代码块,完整代码下载关注公众号;bugstack虫洞栈client/MyClientHandler.java *文件客户端;channelRead处理文件协议,其中模拟传输过程中断,场景测试可以注释掉
1@Override
2public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
3 //数据格式验证
4 if (!(msg instanceof FileTransferProtocol)) return;
5
6 FileTransferProtocol fileTransferProtocol = (FileTransferProtocol) msg;
7 //0传输文件'请求'、1文件传输'指令'、2文件传输'数据'
8 switch (fileTransferProtocol.getTransferType()) {
9 case 1:
10 FileBurstInstruct fileBurstInstruct = (FileBurstInstruct) fileTransferProtocol.getTransferObj();
11 //Constants.FileStatus {0开始、1中间、2结尾、3完成}
12 if (Constants.FileStatus.COMPLETE == fileBurstInstruct.getStatus()) {
13 ctx.flush();
14 ctx.close();
15 System.exit(-1);
16 return;
17 }
18 FileBurstData fileBurstData = FileUtil.readFile(fileBurstInstruct.getClientFileUrl(), fileBurstInstruct.getReadPosition());
19 ctx.writeAndFlush(MsgUtil.buildTransferData(fileBurstData));
20 System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " bugstack虫洞栈客户端传输文件信息。 FILE:" + fileBurstData.getFileName() + " SIZE(byte):" + (fileBurstData.getEndPos() - fileBurstData.getBeginPos()));
21 break;
22 default:
23 break;
24 }
25
26 /**模拟传输过程中断,场景测试可以注释掉
27 *
28 */
29 System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " bugstack虫洞栈客户端传输文件信息[主动断开链接,模拟断点续传]");
30 ctx.flush();
31 ctx.close();
32 System.exit(-1);
33
34}domain/FileBurstData.java *文件分片数据块
1/**
2 * 文件分片数据
3 * 虫洞栈:https://bugstack.cn
4 * 公众号:bugstack虫洞栈 {获取学习源码}
5 * 虫洞群:①群5398358 ②群5360692
6 * Create by fuzhengwei on 2019
7 */
8public class FileBurstData {
9
10 private String fileUrl; //客户端文件地址
11 private String fileName; //文件名称
12 private Integer beginPos; //开始位置
13 private Integer endPos; //结束位置
14 private byte[] bytes; //文件字节;再实际应用中可以使用非对称加密,以保证传输信息安全
15 private Integer status; //Constants.FileStatus {0开始、1中间、2结尾、3完成}
16
17 ... get/set
18}domain/FileBurstInstruct.java *文件分片指令
1/**
2 * 文件分片指令
3 * 虫洞栈:https://bugstack.cn
4 * 公众号:bugstack虫洞栈 {获取学习源码}
5 * 虫洞群:①群5398358 ②群5360692
6 * Create by fuzhengwei on @2019
7 */
8public class FileBurstInstruct {
9
10 private Integer status; //Constants.FileStatus {0开始、1中间、2结尾、3完成}
11 private String clientFileUrl; //客户端文件URL
12 private Integer readPosition; //读取位置
13
14 ... get/set
15}domain/FileDescInfo.java *文件传输信息
1/**
2 * 文件描述信息
3 * 虫洞栈:https://bugstack.cn
4 * 公众号:bugstack虫洞栈 {获取学习源码}
5 * 虫洞群:①群5398358 ②群5360692
6 * Create by fuzhengwei on @2019
7 */
8public class FileDescInfo {
9
10 private String fileUrl;
11 private String fileName;
12 private Long fileSize;
13
14 ... get/set
15}domain/FileTransferProtocol.java *文件传输协议
1/**
2 * 文件传输协议
3 * 虫洞栈:https://bugstack.cn
4 * 公众号:bugstack虫洞栈 {获取学习源码}
5 * 虫洞群:5360692
6 * Create by fuzhengwei on @2019
7 */
8public class FileTransferProtocol {
9
10 private Integer transferType; //0请求传输文件、1文件传输指令、2文件传输数据
11 private Object transferObj; //数据对象;(0)FileDescInfo、(1)FileBurstInstruct、(2)FileBurstData
12
13 ... get/set
14}serverMyServerHandler.java *文件服务端;channelRead处理文件协议,并包含了保存续传信息,用于断点续传
1@Override
2public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
3 //数据格式验证
4 if (!(msg instanceof FileTransferProtocol)) return;
5
6 FileTransferProtocol fileTransferProtocol = (FileTransferProtocol) msg;
7 //0传输文件'请求'、1文件传输'指令'、2文件传输'数据'
8 switch (fileTransferProtocol.getTransferType()) {
9 case 0:
10 FileDescInfo fileDescInfo = (FileDescInfo) fileTransferProtocol.getTransferObj();
11
12 //断点续传信息,实际应用中需要将断点续传信息保存到数据库中
13 FileBurstInstruct fileBurstInstructOld = CacheUtil.burstDataMap.get(fileDescInfo.getFileName());
14 if (null != fileBurstInstructOld) {
15 if (fileBurstInstructOld.getStatus() == Constants.FileStatus.COMPLETE) {
16 CacheUtil.burstDataMap.remove(fileDescInfo.getFileName());
17 }
18 //传输完成删除断点信息
19 System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " bugstack虫洞栈服务端,接收客户端传输文件请求[断点续传]。" + JSON.toJSONString(fileBurstInstructOld));
20 ctx.writeAndFlush(MsgUtil.buildTransferInstruct(fileBurstInstructOld));
21 return;
22 }
23
24 //发送信息
25 FileTransferProtocol sendFileTransferProtocol = MsgUtil.buildTransferInstruct(Constants.FileStatus.BEGIN, fileDescInfo.getFileUrl(), 0);
26 ctx.writeAndFlush(sendFileTransferProtocol);
27 System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " bugstack虫洞栈服务端,接收客户端传输文件请求。" + JSON.toJSONString(fileDescInfo));
28 break;
29 case 2:
30 FileBurstData fileBurstData = (FileBurstData) fileTransferProtocol.getTransferObj();
31 FileBurstInstruct fileBurstInstruct = FileUtil.writeFile("E://", fileBurstData);
32
33 //保存断点续传信息
34 CacheUtil.burstDataMap.put(fileBurstData.getFileName(), fileBurstInstruct);
35
36 ctx.writeAndFlush(MsgUtil.buildTransferInstruct(fileBurstInstruct));
37 System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " bugstack虫洞栈服务端,接收客户端传输文件数据。" + JSON.toJSONString(fileBurstData));
38
39 //传输完成删除断点信息
40 if (fileBurstInstruct.getStatus() == Constants.FileStatus.COMPLETE) {
41 CacheUtil.burstDataMap.remove(fileBurstData.getFileName());
42 }
43 break;
44 default:
45 break;
46 }
47
48}util/FileUtil.java *文件读写工具,分片读取写入处理类
1/**
2 * 文件读写工具
3 * 虫洞栈:https://bugstack.cn
4 * 公众号:bugstack虫洞栈 {获取学习源码}
5 * 虫洞群:5360692
6 * Create by fuzhengwei on @2019
7 */
8public class FileUtil {
9
10 public static FileBurstData readFile(String fileUrl, Integer readPosition) throws IOException {
11 File file = new File(fileUrl);
12 RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");//r: 只读模式 rw:读写模式
13 randomAccessFile.seek(readPosition);
14 byte[] bytes = new byte[1024];
15 int readSize = randomAccessFile.read(bytes);
16 if (readSize <= 0) {
17 randomAccessFile.close();
18 return new FileBurstData(Constants.FileStatus.COMPLETE);//Constants.FileStatus {0开始、1中间、2结尾、3完成}
19 }
20 FileBurstData fileInfo = new FileBurstData();
21 fileInfo.setFileUrl(fileUrl);
22 fileInfo.setFileName(file.getName());
23 fileInfo.setBeginPos(readPosition);
24 fileInfo.setEndPos(readPosition + readSize);
25 //不足1024需要拷贝去掉空字节
26 if (readSize < 1024) {
27 byte[] copy = new byte[readSize];
28 System.arraycopy(bytes, 0, copy, 0, readSize);
29 fileInfo.setBytes(copy);
30 fileInfo.setStatus(Constants.FileStatus.END);
31 } else {
32 fileInfo.setBytes(bytes);
33 fileInfo.setStatus(Constants.FileStatus.CENTER);
34 }
35 randomAccessFile.close();
36 return fileInfo;
37 }
38
39 public static FileBurstInstruct writeFile(String baseUrl, FileBurstData fileBurstData) throws IOException {
40
41 if (Constants.FileStatus.COMPLETE == fileBurstData.getStatus()) {
42 return new FileBurstInstruct(Constants.FileStatus.COMPLETE); //Constants.FileStatus {0开始、1中间、2结尾、3完成}
43 }
44
45 File file = new File(baseUrl + "/" + fileBurstData.getFileName());
46 RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");//r: 只读模式 rw:读写模式
47 randomAccessFile.seek(fileBurstData.getBeginPos()); //移动文件记录指针的位置,
48 randomAccessFile.write(fileBurstData.getBytes()); //调用了seek(start)方法,是指把文件的记录指针定位到start字节的位置。也就是说程序将从start字节开始写数据
49 randomAccessFile.close();
50
51 if (Constants.FileStatus.END == fileBurstData.getStatus()) {
52 return new FileBurstInstruct(Constants.FileStatus.COMPLETE); //Constants.FileStatus {0开始、1中间、2结尾、3完成}
53 }
54
55 //文件分片传输指令
56 FileBurstInstruct fileBurstInstruct = new FileBurstInstruct();
57 fileBurstInstruct.setStatus(Constants.FileStatus.CENTER); //Constants.FileStatus {0开始、1中间、2结尾、3完成}
58 fileBurstInstruct.setClientFileUrl(fileBurstData.getFileUrl()); //客户端文件URL
59 fileBurstInstruct.setReadPosition(fileBurstData.getEndPos() + 1); //读取位置
60
61 return fileBurstInstruct;
62 }
63
64}util/MsgUtil.java *传输消息体构建工具类
1/**
2 * 消息构建工具
3 * 虫洞栈:https://bugstack.cn
4 * 公众号:bugstack虫洞栈 {获取学习源码}
5 * 虫洞群:5360692
6 * Create by fuzhengwei on @2019
7 */
8public class MsgUtil {
9
10 /**
11 * 构建对象;请求传输文件(客户端)
12 *
13 * @param fileUrl 客户端文件地址
14 * @param fileName 文件名称
15 * @param fileSize 文件大小
16 * @return 传输协议
17 */
18 public static FileTransferProtocol buildRequestTransferFile(String fileUrl, String fileName, Long fileSize) {
19
20 FileDescInfo fileDescInfo = new FileDescInfo();
21 fileDescInfo.setFileUrl(fileUrl);
22 fileDescInfo.setFileName(fileName);
23 fileDescInfo.setFileSize(fileSize);
24
25 FileTransferProtocol fileTransferProtocol = new FileTransferProtocol();
26 fileTransferProtocol.setTransferType(0);//0请求传输文件、1文件传输指令、2文件传输数据
27 fileTransferProtocol.setTransferObj(fileDescInfo);
28
29 return fileTransferProtocol;
30
31 }
32
33 /**
34 * 构建对象;文件传输指令(服务端)
35 * @param status 0请求传输文件、1文件传输指令、2文件传输数据
36 * @param clientFileUrl 客户端文件地址
37 * @param readPosition 读取位置
38 * @return 传输协议
39 */
40 public static FileTransferProtocol buildTransferInstruct(Integer status, String clientFileUrl, Integer readPosition) {
41
42 FileBurstInstruct fileBurstInstruct = new FileBurstInstruct();
43 fileBurstInstruct.setStatus(status);
44 fileBurstInstruct.setClientFileUrl(clientFileUrl);
45 fileBurstInstruct.setReadPosition(readPosition);
46
47 FileTransferProtocol fileTransferProtocol = new FileTransferProtocol();
48 fileTransferProtocol.setTransferType(Constants.TransferType.INSTRUCT); //0传输文件'请求'、1文件传输'指令'、2文件传输'数据'
49 fileTransferProtocol.setTransferObj(fileBurstInstruct);
50
51 return fileTransferProtocol;
52 }
53
54 /**
55 * 构建对象;文件传输指令(服务端)
56 *
57 * @return 传输协议
58 */
59 public static FileTransferProtocol buildTransferInstruct(FileBurstInstruct fileBurstInstruct) {
60 FileTransferProtocol fileTransferProtocol = new FileTransferProtocol();
61 fileTransferProtocol.setTransferType(Constants.TransferType.INSTRUCT); //0传输文件'请求'、1文件传输'指令'、2文件传输'数据'
62 fileTransferProtocol.setTransferObj(fileBurstInstruct);
63 return fileTransferProtocol;
64 }
65
66 /**
67 * 构建对象;文件传输数据(客户端)
68 *
69 * @return 传输协议
70 */
71 public static FileTransferProtocol buildTransferData(FileBurstData fileBurstData) {
72 FileTransferProtocol fileTransferProtocol = new FileTransferProtocol();
73 fileTransferProtocol.setTransferType(Constants.TransferType.DATA); //0传输文件'请求'、1文件传输'指令'、2文件传输'数据'
74 fileTransferProtocol.setTransferObj(fileBurstData);
75 return fileTransferProtocol;
76 }
77
78}test/NettyServerTest.java *服务端启动器
1/**
2 * 虫洞栈:https://bugstack.cn
3 * 公众号:bugstack虫洞栈 {获取学习源码}
4 * 虫洞群:①群5398358 ②群5360692
5 * Create by fuzhengwei on 2019
6 */
7public class NettyServerTest {
8
9 public static void main(String[] args) {
10 //启动服务
11 new NettyServer().bing(7397);
12 }
13
14}test/NettyClientTest.java *客户端启动器
1/**
2 * 虫洞栈:https://bugstack.cn
3 * 公众号:bugstack虫洞栈 {获取学习源码}
4 * 虫洞群:①群5398358 ②群5360692
5 * Create by fuzhengwei on 2019
6 */
7public class NettyClientTest {
8
9 public static void main(String[] args) {
10
11 //启动客户端
12 ChannelFuture channelFuture = new NettyClient().connect("127.0.0.1", 7397);
13
14 //文件信息{文件大于1024kb方便测试断点续传}
15 File file = new File("C:\\Users\\fuzhengwei\\Desktop\\测试传输文件.rar");
16 FileTransferProtocol fileTransferProtocol = MsgUtil.buildRequestTransferFile(file.getAbsolutePath(), file.getName(), file.length());
17
18 //发送信息;请求传输文件
19 channelFuture.channel().writeAndFlush(fileTransferProtocol);
20
21 }
22
23}
四、测试结果启动NettyServerTest *默认接收地址为E盘根目录启动NettyClientTest *设置传输文件文件传输结果服务端执行结果
1itstack-demo-netty server start done. {关注公众号:bugstack虫洞栈,获取源码}
2链接报告开始
3链接报告信息:有一客户端链接到本服务端。channelId:3a1df8c1
4链接报告IP:127.0.0.1
5链接报告Port:7397
6链接报告完毕
72019-08-04 19:46:46 bugstack虫洞栈服务端,接收客户端传输文件请求。{"fileName":"测试传输文件.rar","fileSize":1400,"fileUrl":"C:\\Users\\fuzhengwei1\\Desktop\\测试传输文件.rar"}
82019-08-04 19:46:46 bugstack虫洞栈服务端,接收客户端传输文件数据。{"beginPos":0,"bytes":"UmFyIRoHAM+QcwAADQAAAAAAAAC4C3SgkkkAFAUAAIjDEQACJRsHe0WECE8dMyQAIAAAALLiytS0q8rkzsS8/i50eHQAbWpL1YsgT5OPoIdl9k4udAB4dACwS2heCZgVEQzPzUEXfAnhs2R75rhNbCQhNE3uMY4EBkBqQJ45izS4lFGujEk08xLGuhp4sSUSbzEscRICakGyOdARhIE6GEPucySJpY5kQ/Cq28ur4XdfH/j1V8UVoo5X5V+B3dl2f8qvvoxd3t6GPv8HZ7dXs+98XT6uJ0Oj8GZ4c6tvV6vzV865ka375utod+9i+pX/O1Uu1tT76tT38TE+Hq+tzud6Of9Xo9T0/S/xytLm12v4NWztfhnda3lbevs7dnXsWL1vT3Kte91triqYuHW/9bf3WPnjq5r5savbHd67V8Nu6r5+lmZtrP0eO63Ba+upVuWtf7bvByg2/0w+5hz8ru14ND5ex/Odw4F7uRWrYedwU2tXmw5m1u+S7lWdjvTq5e/Kv+1apZxrdjT2dHizdHDrlrH1cvkbrWe5k97u6WRnXdviw6zkvc3cD9TOt7+7W9z2/Ys+Sx9VTPwGeYLmrz+h8fQt5u5v8/3fZ5sXKnc4MOT1+n0upicmOVDT86GfY4bPf5vN7XSxMT5Mnsdry8e84///+quHp9l19fRz8vkds+O7qb+9pWe1WvXdb7NWza3vNO3V3cZZ2rPDr1svHwO3Nq14sBuZu3P1zOvuWP++s8ex9O95e5U/vW9/F7Jtb+p+PGeRtzlg8VfG5t5TyAAAAAAAAAAAAAAAARu1PHU9QX3wAAAAAAAAAAAAAAAiiXyXwAAAAAAAAAAAAAAAiiXyXwAAAAAAAAAAAAAAAiiXyXwAAAAAAAAAAAAAAAiiXyXwAEbDTyAAAAAAAAAAAAAEctTx1PUF98AAAAAAAAAAAAAAAIol8l8AAAAAAAAAAAAAAAIol8l8AAAAAAAAAAAAAAAIol8l8AAAAAAAAAAAAAAAIol8l8AAAABHx08gAAAAAAAAAAj108dT1BffAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAgankAAAAAAAEZGnjqeoL74AAAAAAAAAAAAAAARRL5L4AAAAAAAAAAAAAAARRL5L4AAAAAAAAAAAAAAARRL5L4AAAAAAAAAAAAAAARRL5L4AAAAAAAAAAEYinkAAAAEH08dT1BffAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAA==","endPos":1024,"fileName":"测试传输文件.rar","fileUrl":"C:\\Users\\fuzhengwei1\\Desktop\\测试传输文件.rar","status":1}
92019-08-04 19:46:46 bugstack虫洞栈服务端,接收客户端传输文件数据。{"beginPos":1025,"bytes":"AAI8VPIAEfZTx1PUF98AAAAAAAAAAAAAAAIol8l8AAAAAAAAAAAAAAAIol8l8AAAAAAAAAAAAAAAIol8l8AAAAAAAAAAAAAAAIol8l8AAAAAAAAAAAAAAAIol8l8R+dPIAAAAAAAAAAAAAAAjt08dT1BffAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAAAAAAAAAAAAACKJfJfAAAIWp5AAAAAAAAAAAAEYGnjqeoL74AAAAAAAAAAAAAAARRL5L4AAAAAAAAAAAAAAARRL5L4AAAAAAAAAAAAAAARRL5L4AAAAAAAAAAAAAAARRL5L4AAAAABGYp5AAAAAAAAAEc/Tx1PUF98AAAAAAAAIyFPIAAAAAACB6eOp6gvvgAAAAAAAAAARiKeQAAAAQfTx1PUF98AAAAAAAAAAAAAjAU8gACF6eOp6gvvgAAAAAAAAAAAAAABFH9IDEPXsAQAcA","endPos":1400,"fileName":"测试传输文件.rar","fileUrl":"C:\\Users\\fuzhengwei1\\Desktop\\测试传输文件.rar","status":2}
10客户端断开链接/127.0.0.1:7397
11
12Process finished with exit code -1客户端执行结果
1itstack-demo-netty client start done. {关注公众号:bugstack虫洞栈,获取源码}
2链接报告开始
3链接报告信息:本客户端链接到服务端。channelId:71399d8c
4链接报告IP:127.0.0.1
5链接报告Port:54974
6链接报告完毕
72019-08-04 19:46:46 bugstack虫洞栈客户端传输文件信息。 FILE:测试传输文件.rar SIZE(byte):1024
82019-08-04 19:46:46 bugstack虫洞栈客户端传输文件信息。 FILE:测试传输文件.rar SIZE(byte):375
9
10Process finished with exit code -1