Netty文件上传断点续传的演示
一、理论和协议规范和工具类等
1、实现原理:
netty文件上传采用自定义的协议方式实现,断点续传主要是依据RandomAccessFile类的随机读写能力,主要流程是客户端发起请求,将需要上传文件名称、路径、读取文件的数据、以及读取文件的起始位置等等信息,并且缓存在服务端中(以文件路径为key,自定义协议对象为value),服务端拿到客户端发送的上述数据,就会写文件,并且写完文件,也会记录写过数据位置等信息,再次发送信息给客户端下一次需要读取的数据。
假如这个过程中,客户端断开了链接,此时由于服务端缓存了已经写过文件的位置,那么只会从写过文件的位置进行读文件,再传文件给服务端写等循环操作,从而达到了断点续茶传的效果。
2、RandomAccessFile的基本使用
* RandomAccessFile的基本api
* .getFilePointer : 获取当前操作的位置
* .seek(index): 将操作位置设置到index
* .read(byte):读文件到byte数组中,返回读取文件的长度
*
* 构造函数
* rf = new RandomAccessFile(new File(filePath),"r"); // 参数2是模式
*
* 模式详解:
* “r” 以只读方式来打开指定文件夹。如果试图对该RandomAccessFile执行写入方法,都将抛出IOException异常。
* “rw” 以读,写方式打开指定文件。如果该文件尚不存在,则试图创建该文件。
* “rws” 以读,写方式打开指定文件。相对于”rw” 模式,还要求对文件内容或元数据的每个更新都同步写入到底层设备。
* “rwd” 以读,写方式打开指定文件。相对于”rw” 模式,还要求对文件内容每个更新都同步写入到底层设备。
* 测试1:下面演示基本的读文件操作。
*/
@Test
public void test1() throws IOException {
String filePath = "src/d00_file/a.txt";
RandomAccessFile rf = null;
try {
rf = new RandomAccessFile(new File(filePath), "r");
System.out.println("输入内容:" + rf.getFilePointer());
// 从10位置操作
rf.seek(10);
byte[] b = new byte[1024];
int len = 0;
// 循环读写 (len是读取的长度)
while ((len = rf.read(b)) > 0) {
System.out.print(new String(b, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
rf.close();
}
}
/**
* 测试2: 向文件追加内容
*/
@Test
public void test2() throws IOException{
String filePath="src/d00_file/a.txt";
RandomAccessFile rf=null;
try {
rf = new RandomAccessFile(new File(filePath), "rw");
// 将操作指针移动到文件的最后
rf.seek(rf.length());
// 向文件写出数据
rf.write("这是追加的内容。。".getBytes());
}catch (IOException e){
e.printStackTrace();
}finally {
rf.close();
}
}
/**
* 测试3:修改文件内容
*/
@Test
public void test3() {
RandomAccessFile rf = null;
String oldStr = "www.www.www";
String newStr = "hahahaha";
try {
rf = new RandomAccessFile("src/d00_file/a.txt", "rw");
String line = null;
// 记录上次操作点
long lastpoint = 0;
while ((line = rf.readLine()) != null) {
long ponit = rf.getFilePointer();
// 如果包含替换字符串
if (line.contains(oldStr)) {
// 替换字符串
String str = line.replace(oldStr, newStr);
// 恢复到上次读取位置 (readLine前的位置)
rf.seek(lastpoint);
// 重写行数据
if (oldStr.length() != newStr.length()) {
byte[] newb = Arrays.copyOf(str.getBytes(), line.getBytes().length);
rf.write(newb);
// lastpoint = lastpoint + newb.length;
lastpoint = rf.getFilePointer();
continue;
} else {
rf.write(str.getBytes());
}
}
lastpoint = ponit;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
rf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3、传输对象和协议对象设计
传输对象分为三种,一种是客户端发送给服务端的起始传输信息类、一种是分片文件数据类、一种是服务端返回给客户端分片文件指令类。
协议对象就是包含二个字段,一个是类型(上面三个对象的类型),一个是obejct字段,表示传输对象。
常量:
/**
* 文件传输常量
*/
public class Constants {
/**
* 文件传输状态的标示
*/
public static class FileStatus{
public static int BEGIN = 0; //开始
public static int CENTER = 1; //中间
public static int END = 2; //结尾
public static int COMPLETE = 3; //完成
}
/**
* 协议对象的传输对象类型
*/
public static class TransferType{
public static int REQUEST = 0; //文件信息类型
public static int INSTRUCT = 1; //文件指令类型
public static int DATA = 2; //文件分片类型
}
}
协议对象:
public class FileTransferProtocol {
private Integer transferType; // 类型
private Object transferObj; // 数据对象;(0)FileDescInfo、(1)FileBurstInstruct、(2)FileBurstData
public Integer getTransferType() {
return transferType;
}
public void setTransferType(Integer transferType) {
this.transferType = transferType;
}
public Object getTransferObj() {
return transferObj;
}
public void setTransferObj(Object transferObj) {
this.transferObj = transferObj;
}
}
文件传输相关对象:
/**
* 文件描述信息
*/
public class FileDescInfo {
private String fileUrl; // 文件url
private String fileName; // 文件名称
private Long fileSize; // 文件size
public String getFileUrl() {
return fileUrl;
}
public void setFileUrl(String fileUrl) {
this.fileUrl = fileUrl;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public Long getFileSize() {
return fileSize;
}
public void setFileSize(Long fileSize) {
this.fileSize = fileSize;
}
}
/**
* 文件分片指令
*/
public class FileBurstInstruct {
private Integer status; // Constants.FileStatus {0开始、1中间、2结尾、3完成}
private String clientFileUrl; // 客户端文件URL
private Integer readPosition; // 读取位置
public FileBurstInstruct() {
}
public FileBurstInstruct(Integer status) {
this.status = status;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getClientFileUrl() {
return clientFileUrl;
}
public void setClientFileUrl(String clientFileUrl) {
this.clientFileUrl = clientFileUrl;
}
public Integer getReadPosition() {
return readPosition;
}
public void setReadPosition(Integer readPosition) {
this.readPosition = readPosition;
}
}
/**
* 文件分片数据
*/
public class FileBurstData {
private String fileUrl; //客户端文件地址
private String fileName; //文件名称
private Integer beginPos; //开始位置
private Integer endPos; //结束位置
private byte[] bytes; //文件字节;再实际应用中可以使用非对称加密,以保证传输信息安全
private Integer status; //Constants.FileStatus {0开始、1中间、2结尾、3完成}
public FileBurstData(){
}
public String getFileUrl() {
return fileUrl;
}
public void setFileUrl(String fileUrl) {
this.fileUrl = fileUrl;
}
public FileBurstData(Integer status){
this.status = status;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public Integer getBeginPos() {
return beginPos;
}
public void setBeginPos(Integer beginPos) {
this.beginPos = beginPos;
}
public Integer getEndPos() {
return endPos;
}
public void setEndPos(Integer endPos) {
this.endPos = endPos;
}
public byte[] getBytes() {
return bytes;
}
public void setBytes(byte[] bytes) {
this.bytes = bytes;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}
4、编码解码器
从协议对象的设计我们知道,本次文件上传将采用对象传输的方式,那么就要自定义编码解码器,自定义编码解码器采用的是以protostuff为基础实现,编码时,通过加一个int类型的长度字段,以及序列化的协议对象,解码时,通过解码长度字段后,读取数据,然后反序列化协议对象。
1) 序列化工具
/**
* protostuff序列化工具类
*/
public class SerializingUtil {
/**
* 将目标类序列化为byte数组
*/
public static <T> byte[] serialize(T source) {
Schema<T> schema = RuntimeSchema.getSchema((Class<T>) source.getClass());
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
final byte[] result;
try {
result = ProtobufIOUtil.toByteArray(source, schema, buffer);
} finally {
buffer.clear();
}
return result;
}
/**
* 将byte数组序列化为目标类
*/
public static <T> T deserialize(byte[] source, Class<T> clazz) {
Schema<T> schema = RuntimeSchema.getSchema(clazz);
T t = schema.newMessage();
ProtobufIOUtil.mergeFrom(source, t, schema);
return t;
}
}
2) 对象解码器 (输入向处理--放置在处理器链第一个--后续处理器可直接获得Clazz类型对象)
/**
* 对象解码器
*/
public class ObjectDecode extends ByteToMessageDecoder{
private Class<?> clazz ;
public ObjectDecode(Class<?> clazz) {
super();
this.clazz = clazz;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() < 4) {
return;
}
// 标记包头位置
in.markReaderIndex();
// 读取长度
int len = in.readInt();
// 可读数少于长度
if(in.readableBytes() < len) {
// 重置到包头位置
in.resetReaderIndex();
return;
}
// 读取数据并序列化
byte[] bytes = new byte[len];
in.readBytes(bytes);
Object object = SerializingUtil.deserialize(bytes, clazz);
out.add(object);
}
}
3) 对象编码器 (输出编码器---编码协议对象---放置在处理器链倒数第二个)
public class ObjectEncode extends MessageToByteEncoder<Object> {
private Class<?> clazz;
public ObjectEncode(Class<?> clazz) {
super();
this.clazz = clazz;
}
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
if (clazz.isInstance(msg)) {
byte[] data = SerializingUtil.serialize(msg);
out.writeInt(data.length);
out.writeBytes(data);
}
}
}
5、底层文件读写工具
public class FileUtil {
// 默认一次只能读取10k数据
private static final int DEF_BUFF_SIZE = 1024*10;
private static int buff_size = DEF_BUFF_SIZE;
/**
* 客户端根据文件路径和position读取文件,返回文件分片数据FileBurstData对象。
*/
public static FileBurstData readFile(String fileUrl, Integer readPosition) throws IOException {
File file = new File(fileUrl);
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
randomAccessFile.seek(readPosition);
byte[] bytes = new byte[buff_size];
int readSize = randomAccessFile.read(bytes);
if (readSize <= 0) {
randomAccessFile.close();
return new FileBurstData(Constants.FileStatus.COMPLETE);
}
FileBurstData fileBurstData = new FileBurstData();
fileBurstData.setFileUrl(fileUrl);
fileBurstData.setFileName(file.getName());
fileBurstData.setBeginPos(readPosition);
fileBurstData.setEndPos(readPosition + readSize);
// 不足buff尺寸需要拷贝去掉空字节
if (readSize < buff_size) {
byte[] copy = new byte[readSize];
System.arraycopy(bytes, 0, copy, 0, readSize);
fileBurstData.setBytes(copy);
fileBurstData.setStatus(Constants.FileStatus.END);
} else {
fileBurstData.setBytes(bytes);
fileBurstData.setStatus(Constants.FileStatus.CENTER);
}
randomAccessFile.close();
return fileBurstData;
}
/**
* 服务端根据url和客户端的文件分片数据对象,进行写文件,并且返回文件分片指令(由客户端使用)
*/
public static FileBurstInstruct writeFile(String baseUrl, FileBurstData fileBurstData) throws IOException {
if (Constants.FileStatus.COMPLETE == fileBurstData.getStatus()) {
return new FileBurstInstruct(Constants.FileStatus.COMPLETE); // Constants.FileStatus {0开始、1中间、2结尾、3完成}
}
// 服务端写文件
File file = new File(baseUrl + "/" + fileBurstData.getFileName());
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
randomAccessFile.seek(fileBurstData.getBeginPos());
randomAccessFile.write(fileBurstData.getBytes());
randomAccessFile.close();
if (Constants.FileStatus.END == fileBurstData.getStatus()) {
return new FileBurstInstruct(Constants.FileStatus.COMPLETE);
}
// 构建文件分片指令
FileBurstInstruct fileBurstInstruct = new FileBurstInstruct();
fileBurstInstruct.setStatus(Constants.FileStatus.CENTER); // 字段:读取状态
fileBurstInstruct.setClientFileUrl(fileBurstData.getFileUrl());
fileBurstInstruct.setReadPosition(fileBurstData.getEndPos() + 1); // 字段(核心):下次读取位置
return fileBurstInstruct;
}
另外补充文件协议对象的一个构建工具类
/**
* 消息构建对象
*/
public class MsgUtil {
/**
* 构建对象;文件传输info(客户端)
*/
public static FileTransferProtocol buildRequestTransferFile(String fileUrl, String fileName, Long fileSize) {
FileDescInfo fileDescInfo = new FileDescInfo();
fileDescInfo.setFileUrl(fileUrl);
fileDescInfo.setFileName(fileName);
fileDescInfo.setFileSize(fileSize);
FileTransferProtocol fileTransferProtocol = new FileTransferProtocol();
fileTransferProtocol.setTransferType(0);//0请求传输文件、1文件传输指令、2文件传输数据
fileTransferProtocol.setTransferObj(fileDescInfo);
return fileTransferProtocol;
}
/**
* 构建对象;文件传输指令(服务端)
* @param status 文件读取状态
* @param clientFileUrl 客户端文件地址
* @param readPosition 读取位置
*/
public static FileTransferProtocol buildTransferInstruct(Integer status, String clientFileUrl, Integer readPosition) {
FileBurstInstruct fileBurstInstruct = new FileBurstInstruct();
fileBurstInstruct.setStatus(status);
fileBurstInstruct.setClientFileUrl(clientFileUrl);
fileBurstInstruct.setReadPosition(readPosition);
FileTransferProtocol fileTransferProtocol = new FileTransferProtocol();
fileTransferProtocol.setTransferType(Constants.TransferType.INSTRUCT); // 0 (info) 1 (INSTRUCT) 2 (data)
fileTransferProtocol.setTransferObj(fileBurstInstruct);
return fileTransferProtocol;
}
/**
* 构建对象;文件传输指令(服务端)
*/
public static FileTransferProtocol buildTransferInstruct(FileBurstInstruct fileBurstInstruct) {
FileTransferProtocol fileTransferProtocol = new FileTransferProtocol();
fileTransferProtocol.setTransferType(Constants.TransferType.INSTRUCT); // 0 (info) 1 (INSTRUCT) 2 (data)
fileTransferProtocol.setTransferObj(fileBurstInstruct);
return fileTransferProtocol;
}
/**
* 构建对象;文件传输数据(客户端)
*/
public static FileTransferProtocol buildTransferData(FileBurstData fileBurstData) {
FileTransferProtocol fileTransferProtocol = new FileTransferProtocol();
fileTransferProtocol.setTransferType(Constants.TransferType.DATA); // 0 (info) 1 (INSTRUCT) 2 (data)
fileTransferProtocol.setTransferObj(fileBurstData);
return fileTransferProtocol;
}
}
二、客户端、服务端实现代码
上面已经完成的代码包和类结构如下
1、客户端编写
public class FileTransferClient {
private EventLoopGroup workerGroup = new NioEventLoopGroup();
private Channel channel;
public ChannelFuture connect(String inetHost, int inetPort) {
ChannelFuture channelFuture = null;
try {
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.AUTO_READ, true);
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ObjectDecode(FileTransferProtocol.class));
ch.pipeline().addLast(new ObjectEncode(FileTransferProtocol.class));
ch.pipeline().addLast(new FileTransferHandler());
}
});
channelFuture = b.connect(inetHost, inetPort).syncUninterruptibly();
this.channel = channelFuture.channel();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != channelFuture && channelFuture.isSuccess()) {
System.out.println("client start done");
} else {
System.out.println("client start error");
}
}
return channelFuture;
}
public void destroy() {
if (null == channel)
return;
channel.close();
workerGroup.shutdownGracefully();
}
}
我们将客户端的channel返回,通过channel进行初始发起请求给服务端,自定义处理器只负责接收数据,经过解码器的处理,我们知道接收的数据都是协议对象FileTransferProtocol,而这个对象有三个类型,我们客户端接收的都是服务端的指令对象,服务端缓存的也是指令对象,即客户端读取数据是经过初始请求后,由接收到服务端返回的指令对象后才会进行读数据,发送读数据等等循环操作。
客户端发送数据的演示:
public class ClientMain {
public static void main(String[] args) {
FileTransferClient client = new FileTransferClient();
ChannelFuture connect = client.connect("127.0.0.1", 7000);
File file = new File("C:\\test\\src\\测试传输文件.rar");
// 构建传输协议对象 (是包装info对象)
FileTransferProtocol fileTransferProtocol = MsgUtil.buildRequestTransferFile(file.getAbsolutePath(),
file.getName(), file.length());
connect.channel().writeAndFlush(fileTransferProtocol);
}
}
客户端自定义处理器:
/**
* 客户端自定义处理器
*/
public class FileTransferHandler extends ChannelInboundHandlerAdapter{
/**
* 主要逻辑:
* 判断指令是否完成,完成退出,没完成,读数据,构建传输对象,并且写传输对象到服务端。
**/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 客户端接收FileTransferProtocol对象
if (!(msg instanceof FileTransferProtocol)) return;
FileTransferProtocol fileTransferProtocol = (FileTransferProtocol) msg;
switch (fileTransferProtocol.getTransferType()) {
// 客户端只会处理类型为1,即传输对象是FileBurstInstruct(指令)
case 1:
FileBurstInstruct fileBurstInstruct = (FileBurstInstruct) fileTransferProtocol.getTransferObj();
// 服务端返回的instruct的状态已经完成,客户端退出操作
if (Constants.FileStatus.COMPLETE == fileBurstInstruct.getStatus()) {
ctx.flush();
ctx.close();
System.exit(-1);
return;
}
// 客户端读文件数据返回fileBurstData
FileBurstData fileBurstData = FileUtil.readFile(fileBurstInstruct.getClientFileUrl(), fileBurstInstruct.getReadPosition());
System.out.println("客户端读取一次文件,结尾是:" + fileBurstData.getEndPos());
// 构建协议对象传输
ctx.writeAndFlush(MsgUtil.buildTransferData(fileBurstData));
break;
default:
break;
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
System.out.println("异常信息:\r\n" + cause.getMessage());
}
}
2、服务端编写
/**
* 服务端
*/
public class FileTransferServer {
/**
* Main函数
*/
public static void main(String[] args) {
String path = "";
FileTransferServer server = new FileTransferServer(path);
server.bind(7000);
}
private EventLoopGroup parentGroup = new NioEventLoopGroup(1);
private EventLoopGroup childGroup = new NioEventLoopGroup();
private Channel channel;
// 上传文件的服务端存储目标路径
private String dest_path;
public FileTransferServer(String dest_path) {
super();
this.dest_path = dest_path;
}
public ChannelFuture bind(int port) {
ChannelFuture channelFuture = null;
try {
ServerBootstrap b = new ServerBootstrap();
b.group(parentGroup, childGroup).channel(NioServerSocketChannel.class) // 非阻塞模式
.option(ChannelOption.SO_BACKLOG, 128).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ObjectDecode(FileTransferProtocol.class));
ch.pipeline().addLast(new ObjectEncode(FileTransferProtocol.class));
ch.pipeline().addLast(new FileTransferServerHandler(dest_path));
}
});
channelFuture = b.bind(port).sync();
this.channel = channelFuture.channel();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != channelFuture && channelFuture.isSuccess()) {
System.out.println("server start done");
} else {
System.out.println("server start error");
}
}
return channelFuture;
}
public void close() {
if (channel != null) {
channel.close();
}
parentGroup.shutdownGracefully();
childGroup.shutdownGracefully();
}
public Channel getChannel() {
return channel;
}
public void setChannel(Channel channel) {
this.channel = channel;
}
}
我们从客户端的发送请求的方法以及read方法可以知晓,服务端的处理器需要接收二种类型的传输对象,一个是info对象,一个是data对象,info对象是请求打开时处理用途,data对象是在已经传输过程中接收data对象,并且将data数据写入服务端。
在上面这个过程中,我们不管是接收info对象还是data对象,发送给客户端的都是instruct指令对象。
如果接收的是info对象,就要判断是否有instruct缓存?如果有,就是断点续传的,那么直接拿到此缓存对象发送给客户端,并且叫客户端读文件传数据。如果没有,那么就要初始构建instruct指令对象,即指定position位置为0起始。
如果接收的是data对象,就要将data对象的数据写入服务端存储地址,并且构建insturct指令存入缓存(指定position为上次读文件的结尾+1),然后发送此insturct对象给客户端,客户端根据是否完成来决定是否继续读取文件。
服务端自定义处理器实现:
public class FileTransferServerHandler extends ChannelInboundHandlerAdapter{
private String dest_path;
public FileTransferServerHandler(String dest_path) {
this.dest_path = dest_path;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端链接" + ctx.channel().localAddress().toString());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端断开链接" + ctx.channel().localAddress().toString());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (!(msg instanceof FileTransferProtocol))return;
FileTransferProtocol fileTransferProtocol = (FileTransferProtocol) msg;
switch (fileTransferProtocol.getTransferType()) {
case 0:
FileDescInfo info = (FileDescInfo) fileTransferProtocol.getTransferObj();
// 有缓存说明是断点续传,instruct记录的是下次需要读的postion
FileBurstInstruct old = CacheUtil.get(info.getFileName());
if(old!=null) {
if (old.getStatus() == Constants.FileStatus.COMPLETE) {
CacheUtil.remove(info.getFileName());
}
ctx.writeAndFlush(MsgUtil.buildTransferInstruct(old));
return ;
}
// 没缓存就是初始传输,主要是指定instruct的position为0
FileTransferProtocol sendFileTransferProtocol = MsgUtil.buildTransferInstruct(Constants.FileStatus.BEGIN, info.getFileUrl(), 0);
ctx.writeAndFlush(sendFileTransferProtocol);
break;
case 2:
FileBurstData fileBurstData = (FileBurstData) fileTransferProtocol.getTransferObj();
FileBurstInstruct fileBurstInstruct = FileUtil.writeFile(dest_path, fileBurstData);
// 保存断点续传信息
CacheUtil.put(fileBurstData.getFileName(), fileBurstInstruct);
ctx.writeAndFlush(MsgUtil.buildTransferInstruct(fileBurstInstruct));
// 传输完成删除断点信息
if (fileBurstInstruct.getStatus() == Constants.FileStatus.COMPLETE) {
CacheUtil.remove(fileBurstData.getFileName());
}
break;
default:
break;
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("异常信息:\r\n" + cause.getMessage());
}
}
end !!!