从零开始做Netty高性能文件上传

本文分享了使用Netty框架开发高性能文件上传服务的经验,详细介绍了利用FileChannel进行文件读写优化,解决大文件分片上传及断点续传问题,并自定义文件传输协议。

只当记录自己的日记,大家权当小说看,上班故事会,里面也会写出我在开发过程中的想法心的,遇到的困难和解决方法 ,代码也不优雅,纯当练习,如果你遇到和我一样的问题能在我这里找到答案那就再好不过.有什么好的建议或者看到了什么bug也可以在留言了告诉我,毕竟我对在线文件储存还是缺少经验.

GitHub: GitHub - LATHX/netty4file

有几个想法驱使我想要去做这个事情

  1. 2019大学刚毕业,坐标广州,Java后端程序员,出来参加工作刚满一年了,想想在大学里刚学会连接数据库操作,写几个接口,在学校里感觉就这样简简单单就这样混程序员还挺好的,操作也不复杂,无非就是增删改查也很满足.等到真的从学校毕业出来工作才一年,就感觉到枯燥乏味的生活,每天上班就是在公司做数据库增删改查,想想以后十几年都做这样的事情就好难过啊,迫不得已,不想作为一个增删改查工具人的,只能自己去学习新技术了,一眼就看上了Netty的高性能高并发,买了几本Netty最热门的书之后,感觉还是有点云里雾里,无奈国内网上对Netty的资料也不是很多,要想真正了解Netty只好自己撸代码了.
  2. 仔细想一下人还是要往高处走,不能总当一名初级程序员.
  3. 或许有生之年还想去大厂见识一下.

今天遇到的问题

如何读取写入文件

想做一个高性能的文件上传服务,肯定要先了解怎么存文件和读文件快.看了网上找到了两个可以对文件I/O异步

  1. FileChannel
  2. 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();
        }
    }
}

遇到的问题

  1. 在客户端出现MappedByteBuffer用array()会报:Error Exception : java.lang.UnsupportedOperationException
  2. 在服务端的ByteBuffer flip()导致文件写入为空的问题
  3. 在客户端出现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);
   }

至此,一个简单的文件上传服务完成 .

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值