SpringBoot集成Netty实现文件传输

实现浏览本地文件目录,实现文件夹目录的跳转和文件的下载

添加依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.1.Final</version>
</dependency>
<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>${commons.lang.version}</version>
</dependency>

排除tomcat的依赖

Netty Http服务端编写:

handler 处理类

@Component
@Slf4j
@ChannelHandler.Sharable //@Sharable 注解用来说明ChannelHandler是否可以在多个channel直接共享使用
public class FileServerHandler extends ChannelInboundHandlerAdapter {

   // private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\.]*");

    //文件存放路径
    @Value("${netty.file.path:}")
    String path;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        try{
            if (msg instanceof FullHttpRequest) {
                FullHttpRequest req = (FullHttpRequest) msg;
                if(req.method() != HttpMethod.GET) {
                    sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
                    return;
                }
                String url = req.uri();
                File file = new File(path + url);
                if(file.exists()){
                    if(file.isDirectory()){
                        if(url.endsWith("/")) {
                            sendListing(ctx, file);
                        }else{
                            sendRedirect(ctx, url + "/");
                        }
                        return;
                    }else {
                        transferFile( file,  ctx);
                    }
                }else{
                    sendError(ctx, HttpResponseStatus.NOT_FOUND);
                }
            }
        }catch(Exception e){
            log.error("Exception:{}",e);
            sendError(ctx, HttpResponseStatus.BAD_REQUEST);
        }
    }

    /**
     * 传输文件
     * @param file
     * @param ctx
     */
    private void transferFile(File file, ChannelHandlerContext ctx){
        try{
            RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
            long fileLength = randomAccessFile.length();
            HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength);
            ctx.write(response);
            ChannelFuture sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
            addListener( sendFileFuture);
            ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
            lastContentFuture.addListener(ChannelFutureListener.CLOSE);
        }catch (Exception e){
            log.error("Exception:{}",e);
        }
    }

    /**
     * 监听传输状态
     * @param sendFileFuture
     */
    private void addListener( ChannelFuture sendFileFuture){
        sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
                @Override
                public void operationComplete(ChannelProgressiveFuture future)
                        throws Exception {
                    log.debug("Transfer complete.");
                }
                @Override
                public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) throws Exception {
                    if(total < 0){
                        log.debug("Transfer progress: " + progress);
                    }else{
                        log.debug("Transfer progress: " + progress + "/" + total);
                    }
                }
        });
    }


    /**
     * 请求为目录时,显示文件列表
     * @param ctx
     * @param dir
     */
    private static void sendListing(ChannelHandlerContext ctx, File dir){
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");

        String dirPath = dir.getPath();
        StringBuilder buf = new StringBuilder();

        buf.append("<!DOCTYPE html>
");
        buf.append("<html><head><title>");
        buf.append(dirPath);
        buf.append("目录:");
        buf.append("</title></head><body>
");

        buf.append("<h3>");
        buf.append(dirPath).append(" 目录:");
        buf.append("</h3>
");
        buf.append("<ul>");
        buf.append("<li>链接:<a href=" ../")..</a></li>
");
        for (File f : dir.listFiles()) {
            if(f.isHidden() || !f.canRead()) {
                continue;
            }
            String name = f.getName();
            /*if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
                continue;
            }*/
            buf.append("<li>链接:<a href="");
            buf.append(name);
            buf.append("">");
            buf.append(name);
            buf.append("</a></li>
");
        }
        buf.append("</ul></body></html>
");
        ByteBuf buffer = Unpooled.copiedBuffer(buf,CharsetUtil.UTF_8);
        response.content().writeBytes(buffer);
        buffer.release();
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * 跳转链接
     * @param ctx
     * @param newUri
     */
    private static void sendRedirect(ChannelHandlerContext ctx, String newUri){
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
        response.headers().set(HttpHeaderNames.LOCATION, newUri);
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * 失败响应
     * @param ctx
     * @param status
     */
    private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status){
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status,
                Unpooled.copiedBuffer("Failure: " + status.toString() + "
", CharsetUtil.UTF_8));
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }


}

ChannelPipeline 实现:

@Component
@ConditionalOnProperty(  //配置文件属性是否为true
        value = {"netty.file.enabled"},
        matchIfMissing = false
)
public class FilePipeline extends ChannelInitializer<SocketChannel> {

    @Autowired
    FileServerHandler fleServerHandler;

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline p = socketChannel.pipeline();
        p.addLast("http-decoder", new HttpRequestDecoder());
        p.addLast("http-aggregator", new HttpObjectAggregator(65536));
        p.addLast("http-encoder", new HttpResponseEncoder());
        p.addLast("http-chunked", new ChunkedWriteHandler());
        p.addLast("fileServerHandler",fleServerHandler);
    }
}

服务实现:

@Configuration
@EnableConfigurationProperties({NettyFileProperties.class})
@ConditionalOnProperty(  //配置文件属性是否为true
        value = {"netty.file.enabled"},
        matchIfMissing = false
)
@Slf4j
public class FileServer {
    @Autowired
    FilePipeline filePipeline;

    @Autowired
    NettyFileProperties nettyFileProperties;

    @Bean("starFileServer")
    public String start() {
        Thread thread =  new Thread(() -> {
            NioEventLoopGroup bossGroup = new NioEventLoopGroup(nettyFileProperties.getBossThreads());
            NioEventLoopGroup workerGroup = new NioEventLoopGroup(nettyFileProperties.getWorkThreads());
            try {
                log.info("start netty [FileServer] server ,port: " + nettyFileProperties.getPort());
                ServerBootstrap boot = new ServerBootstrap();
                options(boot).group(bossGroup, workerGroup)
                        .channel(NioServerSocketChannel.class)
                        .handler(new LoggingHandler(LogLevel.INFO))
                        .childHandler(filePipeline);
                Channel ch = null;
              //是否绑定IP
                if(StringUtils.isNotEmpty(nettyFileProperties.getBindIp())){
                    ch = boot.bind(nettyFileProperties.getBindIp(),nettyFileProperties.getPort()).sync().channel();
                }else{
                    ch = boot.bind(nettyFileProperties.getPort()).sync().channel();
                }
                ch.closeFuture().sync();
            } catch (InterruptedException e) {
                log.error("启动NettyServer错误", e);
            } finally {
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
            }
        });
        thread.setName("File_Server");
        thread.start();
        return "file start";
    }


    private ServerBootstrap options(ServerBootstrap boot) {
 /*       boot.option(ChannelOption.SO_BACKLOG, 1024)
                .option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);*/
        return boot;
    }
}

启动配置:

---application.yml
spring.profiles.active: file

---application-file.yml
netty:
   file:
     enabled: true
     path: d:
     port: 3456

测试

在浏览器打开http://127.0.0.1:3456/

原文地址:https://www.cnblogs.com/47Gamer/p/13853126.html