netty搭建简单的文件服务器

在工作中,文件服务器是很常用的,我们经常需要把一些公共资源放到服务器上的某个目录下,通过IP加端口就可以实现文件的查看,下载等功能,

常见的方法像tomcat,将文件放到webapps下,启动tomcat后,设置对外访问的端口就可以达到目的,但是这种方式有点局限,就是需要你提前知道文件的全路径,不够直观,另一种做法,也是比较简单、性能高效的方式,就是在服务器安装nginx,然后通过nginx建立资源目录,达到对公共资源进行操作的目的,也是生产中常用的做法,但是nginx的做法和tomcat有一个共同的缺点,就是资源不能直观的对用户展示出来;

这里引出本篇要讲解的另一种方式,就是通过写netty程序作为服务端,然后暴露相关端口和资源目录即可,用户既能看到文件,也能进去进行读取、下载,对于运维人员来说,可以通过开发目录达到对资源目录权限的控制;

代码编写

在编写netty代码之前,个人觉得需要对netty的代码编写结构和思路进行简单的梳理,其实掌握了netty的逻辑,不难发现,netty的服务器程序主要包括三部分,用一张简单的图展示如下,
在这里插入图片描述
简单解释一下,需要一个服务端的server,这个server的代码比较固定,两个包装线程的NioEventLoopGroup ,然后加入相关的配置,配置包括 pipline的代码,可以理解为处理流通在服务端和客户端之间的数据流的一个组件,通过这个组件,我们可以对流通在channel中的数据进行具体的业务处理,比如控制编码格式,控制数据流的大小,对数据的规则进行校验、过滤等操作,这是比较粗糙的层面,如果需要在这个过程加入自身业务逻辑更加细致的处理,就需要一个个的handler了,即自定义的handler,这个就是我们熟悉的责任链模式在netty框架中的具体应用,不同的handler关注自身的那部分业务处理即可

有了上述的基本概念,再写代码时就可以按照这个思路去编写代码,更加具体的概念大家可以参考相关的资料进行脑补;

1、添加pom依赖

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.7.0</version>
        </dependency>

        <!--<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>-->

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

        <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.25.Final</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.0</version>
        </dependency>

    </dependencies>

2、编写文件处理的server

/**
 * 文件服务器启动配置类
 */

@Configuration
@EnableConfigurationProperties({NettyFileProperties.class})
@ConditionalOnProperty(
        value = {"netty.file.enabled"},
        matchIfMissing = false
)
public class FileServer {

    private Logger logger = LoggerFactory.getLogger(FileServer.class);

    @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 {
                logger.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) {
                logger.error("启动NettyServer错误", e);
            } finally {
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
            }
        });
        thread.setName("File_Server");
        thread.start();
        return "file start";
    }

    private ServerBootstrap options(ServerBootstrap boot) {
        return boot;
    }

}

从这段代码即可以看到,pipline就加入了server代码中,
在这里插入图片描述

3、编写filePipeline

@Component
@ConditionalOnProperty( 
        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());
        //引入文件处理的handler
        p.addLast("fileServerHandler",fleServerHandler);
    }

}

4、filePipeline 添加自定义文件处理handler

/**
 * @Sharable 注解用来说明ChannelHandler是否可以在多个channel直接共享使用
 * 文件处理handler
 */

@Component
@ChannelHandler.Sharable
public class FileServerHandler extends ChannelInboundHandlerAdapter {

    //https://blog.csdn.net/cowbin2012/article/details/85290876
    private Logger logger = LoggerFactory.getLogger(FileServerHandler.class);

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

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        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) {

        }

    }

    /**
     * 传输文件
     *
     * @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) {
            logger.error("Exception:{}", e);
        }
    }

    /**
     * 监听传输状态
     *
     * @param sendFileFuture
     */
    private void addListener(ChannelFuture sendFileFuture) {
        sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
            @Override
            public void operationComplete(ChannelProgressiveFuture future)
                    throws Exception {
                logger.debug("Transfer complete.");
            }

            @Override
            public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) throws Exception {
                if (total < 0) {
                    logger.debug("Transfer progress: " + progress);
                } else {
                    logger.debug("Transfer progress: " + progress + "/" + total);
                }
            }
        });
    }


    /**
     * 跳转链接
     *
     * @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 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>\r\n");
        buf.append("<html><head><title>");
        buf.append(dirPath);
        buf.append("目录:");
        buf.append("</title></head><body>\r\n");

        buf.append("<h3>");
        buf.append(dirPath).append(" 目录:");
        buf.append("</h3>\r\n");
        buf.append("<ul>");
        buf.append("<li>链接:<a href=\" ../\")..</a></li>\r\n");
        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>\r\n");
        }
        buf.append("</ul></body></html>\r\n");
        ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
        response.content().writeBytes(buffer);
        buffer.release();
        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() + "\r\n", CharsetUtil.UTF_8));
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }


}

5、辅助配置类

@ConfigurationProperties(prefix="netty.file")
@Data
@Validated
public class NettyFileProperties {

    @NotNull(message = "端口不能为空")
    @Range(min=1000, max=60000)
    private Integer port;

    @NotNull(message = "文件路径不能为空")
    private String path;

    @Pattern(regexp="((25[0-5]|2[0-4]\\d|((1\\d{2})|([1-9]?\\d)))\\.){3}(25[0-5]|2[0-4]\\d|((1\\d{2})|([1-9]?\\d)))",message="ip地址格式不正确")
    private String bindIp;

    //必须大于1 ,老板线程,即认为是分配工作的线程
    @DecimalMin("1")
    private Integer bossThreads = Math.max(1, SystemPropertyUtil.getInt(
            "io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));

    //必须大于1,实际工作线程数量,这个数量最好根据JVM的系统信息进行配置,这里直接动态获取
    @DecimalMin("1")
    private Integer workThreads = Math.max(1, SystemPropertyUtil.getInt(
            "io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));

}

7、启动类

@SpringBootApplication
public class NettyApplication {

    public static void main(String[] args) {
       SpringApplication.run(NettyApplication.class,args);
    }

}

8、yml文件

netty:
   file:
     enabled: true
     path: c:\
     port: 3456

启动程序,浏览器访问:http://127.0.0.1:3456/,看到如下效果,
在这里插入图片描述

是不是这个效果很像我们访问本地磁盘上的文件,我们可以进入某个目录下,尝试下载一个本地文件,可以看到就像下载浏览器上的某个文件一样,
在这里插入图片描述

看图片也很方便,
在这里插入图片描述

本篇的讲解到此结束,最后,感谢观看!

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小码农叔叔

谢谢鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值