只当记录自己的日记,大家权当小说看,上班故事会,里面也会写出我在开发过程中的想法心的,遇到的困难和解决方法 ,代码也不优雅,纯当练习,如果你遇到和我一样的问题能在我这里找到答案那就再好不过.有什么好的建议或者看到了什么bug也可以在留言了告诉我,毕竟我对在线文件储存还是缺少经验.
GitHub: GitHub - LATHX/netty4file
有几个想法驱使我想要去做这个事情
- 2019大学刚毕业,坐标广州,Java后端程序员,出来参加工作刚满一年了,想想在大学里刚学会连接数据库操作,写几个接口,在学校里感觉就这样简简单单就这样混程序员还挺好的,操作也不复杂,无非就是增删改查也很满足.等到真的从学校毕业出来工作才一年,就感觉到枯燥乏味的生活,每天上班就是在公司做数据库增删改查,想想以后十几年都做这样的事情就好难过啊,迫不得已,不想作为一个增删改查工具人的,只能自己去学习新技术了,一眼就看上了Netty的高性能高并发,买了几本Netty最热门的书之后,感觉还是有点云里雾里,无奈国内网上对Netty的资料也不是很多,要想真正了解Netty只好自己撸代码了.
- 仔细想一下人还是要往高处走,不能总当一名初级程序员.
- 或许有生之年还想去大厂见识一下.
今天遇到的问题
如何读取写入文件
想做一个高性能的文件上传服务,肯定要先了解怎么存文件和读文件快.看了网上找到了两个可以对文件I/O异步
- FileChannel
- RandomAccessFile
对比了一下RandomAccessFile已经老了,虽然也可以通过channel()方法获得通道去给FileChannel用,但是还是一步到位用了FileChannel这个来作为文件读取和写入.
如何应对大文件分片上传,网络中断端点续传
解决了存文件和读文件的问题 ,那就要来解决分片上传或断点续传的问题了,仔细研究了一下FileChannel可以通过指定的位置position来指定文件开始读取或写入的位置,这样整个系统就有了大概的想法样子
协议问题
需要自己定义文件传输协议,包含当前分片的信息和传输指令,使服务端和客户端能够进行协作 .先做一个简单的协议,后续肯定是需要升级的.使用JBoss的marshalling编解码器对Java Bean进行编码传输.
大概的想法如下:
public class Msg implements Serializable { // 文件名 private String fileName; // 文件分片字节数组 private byte[] fileByte; // 文件当前读取位置 private Long position; // 是否传输完成标记 private Boolean isFinish; public Boolean getFinish() { return isFinish; } public void setFinish(Boolean finish) { isFinish = finish; } public Long getPosition() { return position; } public void setPosition(Long position) { this.position = position; } public String getFileName() { return fileName; } public void setFileName(String fileName) { this.fileName = fileName; } public byte[] getFileByte() { return fileByte; } public void setFileByte(byte[] fileByte) { this.fileByte = fileByte; } }
开始实践
Maven依赖
<dependencies> <!-- https://mvnrepository.com/artifact/io.netty/netty-all --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.48.Final</version> </dependency> <dependency> <groupId>org.jboss.marshalling</groupId> <artifactId>jboss-marshalling-serial</artifactId> <version>1.4.11.Final</version> </dependency> </dependencies>
JBoss的marshalling解编码配置
import io.netty.handler.codec.marshalling.*; import org.jboss.marshalling.MarshallerFactory; import org.jboss.marshalling.Marshalling; import org.jboss.marshalling.MarshallingConfiguration; public class MarshallingCodeCFactory { public static MarshallingDecoder buildMarshallingDecoder() { final MarshallerFactory factory = Marshalling.getProvidedMarshallerFactory("serial"); final MarshallingConfiguration configuration = new MarshallingConfiguration(); configuration.setVersion(5); UnmarshallerProvider provider = new DefaultUnmarshallerProvider(factory, configuration); MarshallingDecoder decoder = new MarshallingDecoder(provider, 1024 * 1024); return decoder; } public static MarshallingEncoder buildMarshallingEncoder() { final MarshallerFactory factory = Marshalling.getProvidedMarshallerFactory("serial"); final MarshallingConfiguration configuration = new MarshallingConfiguration(); configuration.setVersion(5); MarshallerProvider provider = new DefaultMarshallerProvider(factory, configuration); MarshallingEncoder encoder = new MarshallingEncoder(provider); return encoder; } }
熟悉FileChannel怎么使用
整个文件系统最关键的部分,当然先要熟悉起来,大致看了一下网上API,圈定了我大概要使用的范围
open() 获取文件通道
read() 读取文件
write() 写入文件
map(FileChannel.MapMode mode,long position,long size) 把文件映射到内存(看到这个方法用来做分片最适合)
MappedByteBuffer.load() 将此缓冲区内容加载到物理内存中
初试FileChannel
服务器端:
当服务器读到有内容进入首先解码,然后存文件的过程.代码如下
import com.file.modal.Msg;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.SimpleChannelInboundHandler;
import java.io.File;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;
public class MsgInBound extends SimpleChannelInboundHandler<Msg> {
private static final String filePath = "/Users/ljl/Documents/netty/receive";
@Override
protected void channelRead0(ChannelHandlerContext ctx, Msg msg) throws Exception {
File file = new File(filePath, msg.getFileName());
if (msg.getFinish()) {
ctx.close();
}
if (!file.exists()) {
msg.setPosition(0L);
file.createNewFile();
ctx.writeAndFlush(msg);
} else {
try (FileChannel fileChannel = (FileChannel.open(file.toPath(),
StandardOpenOption.WRITE,StandardOpenOption.APPEND))) {
ByteBuffer wrap = ByteBuffer.wrap(msg.getFileByte());
// wrap.flip();
fileChannel.write(wrap, msg.getPosition());
msg.setPosition(msg.getPosition() + 10240L);
ctx.writeAndFlush(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
主函数
import com.file.code.MarshallingCodeCFactory;
import com.file.server.bound.MsgInBound;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class serverMain {
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap().group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG,100)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());
socketChannel.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());
socketChannel.pipeline().addLast(new MsgInBound());
}
});
ChannelFuture future = serverBootstrap.bind(8765).sync();//绑定端口
future.channel().closeFuture().sync();//等待关闭(程序阻塞在这里等待客户端请求)
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
客户端
分片文件,放入文件字节数组,发送到服务端,代码如下
当第一次连接成功的时候,发送传输文件初始化信号,让服务端创建文件
import com.file.modal.Msg;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class MsgInitInBound extends ChannelInboundHandlerAdapter {
private static final String filePath = "/Users/ljl/Documents/netty/send";
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Msg msg = new Msg();
msg.setFileName("123.pdf");
msg.setPosition(0L);
msg.setFinish(false);
ctx.writeAndFlush(msg);
}
}
服务器返回确认信息后开始传输文件
import com.file.modal.Msg;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.io.File;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.StandardOpenOption;
public class MsgOutBound extends SimpleChannelInboundHandler<Msg> {
private static final String filePath = "/Users/ljl/Documents/netty/send";
@Override
protected void channelRead0(ChannelHandlerContext ctx, Msg msg) throws Exception {
Long position = 0L;
if (msg.getPosition() != 0L) {
position = msg.getPosition();
}
File file = new File(filePath, msg.getFileName());
try (FileChannel fileChannel = (FileChannel.open(file.toPath(),
StandardOpenOption.READ))) {
Long size = 10240L;
if ((position + size) >= fileChannel.size()) {
size = fileChannel.size() - position;
msg.setFinish(true);
}
MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_ONLY, position, size).load();
map.asReadOnlyBuffer().flip();
byte[] arr = new byte[map.asReadOnlyBuffer().remaining()];
map.asReadOnlyBuffer().get(arr);
msg.setFileByte(arr);
ctx.pipeline().writeAndFlush(msg);
System.out.println("Client Info:" + msg.toString());
map.clear();
} catch (Exception e) {
e.printStackTrace();
}
}
}
主函数
import com.file.client.bound.MsgInitInBound;
import com.file.client.bound.MsgOutBound;
import com.file.code.MarshallingCodeCFactory;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class clientMain {
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap().group(bossGroup).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY,true)
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder());
socketChannel.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingEncoder());
socketChannel.pipeline().addLast(new MsgInitInBound());
socketChannel.pipeline().addLast(new MsgOutBound());
}
});
ChannelFuture future = bootstrap.connect("127.0.0.1", 8765).sync();//绑定端口
future.channel().closeFuture().sync();//等待关闭(程序阻塞在这里等待客户端请求)
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
}
}
}
遇到的问题
- 在客户端出现MappedByteBuffer用array()会报:Error Exception : java.lang.UnsupportedOperationException
- 在服务端的ByteBuffer flip()导致文件写入为空的问题
- 在客户端出现Channel not open for writing FileChannel越界问题
解决问题
1.MappedByteBuffer的direct模式下直接用array()会出现此错误,需要做如下操作,其中 map.asReadOnlyBuffer().get(arr)方法会把分片好的内容放入到arr的byte数组中
MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_ONLY, position, size).load(); map.asReadOnlyBuffer().flip(); byte[] arr = new byte[map.asReadOnlyBuffer().remaining()]; map.asReadOnlyBuffer().get(arr);
2.在服务端不需要再次flip(),当前的limit和position位置已经是正确的,再次flip()会导致limit变为0,导致写入空数据
3.越界问题是由于文件分片到文件结尾的时候没有控制好
// 文件当前读取位置 Long position = 0L; ...... // 文件每次分片大小 Long size = 10240L; // 文件当前读取位置加上分片大小之后超过文件总大小需要做调整,并且告诉服务器这次是最后一个分片 if ((position + size) >= fileChannel.size()) { size = fileChannel.size() - position; msg.setFinish(true); }
本文分享了使用Netty框架开发高性能文件上传服务的经验,详细介绍了利用FileChannel进行文件读写优化,解决大文件分片上传及断点续传问题,并自定义文件传输协议。

3313

被折叠的 条评论
为什么被折叠?



