Netty 入门案例之静态文件服务器实现

668 阅读4分钟

Netty 入门案例之静态文件服务器实现

最近用Netty实现了一个类似于Nginx的静态文件服务器功能。遂在个人博客中记录下过程

设计阶段

功能设计

  1. 支持自定义配置文件(提供默认配置文件和外部配置文件两种方式)
  2. 支持静态文件路由配置
  3. 静态文件下载

实现过程

配置读取

设计一个ResourcesService类,使用单例模式,在程序启动时读取默认路径下的config.properties文件。同时也读取Jar文件所在目录的的config.properties文件。 静态文件的路由配置需要按照规则,使用web.static.path做为前缀。例如web.static.path.hello.a=/data/static/,那么请求的url为 /hello/a/test.js时,就会去/data/static 目录下找test.js文件 ResourcesService 中有2个map对象,一个用于存储路由配置,另一个用于存储其他配置,暂时没有用到。

Http请求解析与响应

可以使用ByteBuf对Http请求做解析,但是这样很浪费时间,Netty内置了Http请求编解码器和Http响应编解码器,我们可以使用它对Http请求进行解析与响应。由于这次写的是服务端,所以只需要用到Http请求解码器和Http响应编码器,他们分别是HttpRequestDecoderHttpResponseEncoder,仅仅使用这两个编/解码器还不够,HttpRequestDecoder会将Http请求解析为 HttpResquest、HttpContent、LastHttpContent 这几个对象。所以我们需要在引入一个HttpObjectAggregator 解码器,它可以将多个消息体转换为单一的HttpFullRequest对象。最后我们还要使用一个ChunkedWriteHandler 帮助我们可以在程序中异步发送文件的码流,而且不会占用过多的内存导致Java内存溢出。 参考代码

public class HttpServerInitializer  extends ChannelInitializer<SocketChannel> {
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        //HttpObjectAggregator HTTP 消息解码器, 作用时将多个消息转换为1个FullHttpRequest 或者 FullHttpResponse 对象
        /**
         * HttpRequestDecoder 会将每个 HTTP 消息转换为 多个消息对象
         * HttpResquest / HttpResponse
         * HttpContent
         * LastHttpContent
         */
        //将请求和应答消息编码或解码为HTTP消息
        socketChannel.pipeline().addLast(new HttpRequestDecoder());
        socketChannel.pipeline().addLast(new HttpObjectAggregator(65536));// 目的是将多个消息转换为单一的request或者response对象
        socketChannel.pipeline().addLast(new HttpResponseEncoder());
        socketChannel.pipeline().addLast(new ChunkedWriteHandler());//目的是支持异步大文件传输()
        socketChannel.pipeline().addLast("file-handler", new FileServerHandler());
        socketChannel.pipeline().addLast("handler", new ServerHandler(false));
    }
}

FileServerHandler就是我们的静态文件服务器主要的处理逻辑,如果请求不是对静态文件的请求,那么就会进入下一个ServerHandler,ServerHandler主要是用于转发请求(目前还未实现)

静态文件处理的思路

首先从请求中拿到请求的URI。先判断是否为静态文件请求,支持GET请求,如果不是,则当前Handler处理完成,直接进入下一个Handler中进行处理。 如果是静态文件请求,则解析其URL,去ResourcesService类的单例对象中找到是否有对应得配置路径。如果没有,则返回404。如果有对应的物理路径那么需要将拼接好物理路径,使用RandomAccessFile对象对文件进行读取,然后使用ChunkedNioFile对其进行封装返回Http响应。 参考代码

public class FileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {



    protected void channelRead0(ChannelHandlerContext channelHandlerContext, FullHttpRequest request) throws Exception {
        //request.retain();
        HttpResponse response = null;
        RandomAccessFile randomAccessFile = null;
        try{
            // 状态为1xx的话,继续请求
            if (HttpHeaders.is100ContinueExpected(request)) {
                send100Continue(channelHandlerContext);
            }
            String uri = request.uri();
            if(!uri.endsWith(".js") && !uri.endsWith(".css") && !uri.endsWith(".html")){
                channelHandlerContext.fireChannelRead(request);
                return;
            }
            // hello/a.js
            int index = uri.lastIndexOf("/") + 1;
            if(index == -1){
               DonkeyHttpUtil.writeResponse(request, OK, channelHandlerContext);
               return;
            }
            String filename = uri.substring(index);
            uri = uri.substring(0, index-1);
            String path = ResourcesService.getInstance().getPath(uri);
            if(StringUtil.isNullOrEmpty(path)){
                DonkeyHttpUtil.writeResponse(request, NOT_FOUND, channelHandlerContext);
                return;
            }
            String fullPath = path+ "/"+filename;
            File file = new File(fullPath);
            try {
                randomAccessFile = new RandomAccessFile(file, "r");
            } catch (FileNotFoundException e) {
                DonkeyHttpUtil.writeResponse(request, NOT_FOUND, channelHandlerContext);
                e.printStackTrace();
                return;
            }

            if(!file.exists() || file.isHidden()){
                DonkeyHttpUtil.writeResponse(request, NOT_FOUND, channelHandlerContext);
                return;
            }
            long fileLength = randomAccessFile.length();
            response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);

            setContentType(response, file);
            boolean keepAlive =  HttpUtil.isKeepAlive(request);
            if (keepAlive) {
                response.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength);
                response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
            }
            channelHandlerContext.write(response);


            ChannelFuture sendFileFuture = channelHandlerContext.write(new ChunkedNioFile(randomAccessFile.getChannel()), channelHandlerContext.newProgressivePromise());
            // 写入文件尾部
            sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
                @Override
                public void operationProgressed(ChannelProgressiveFuture future,
                                                long progress, long total) {
                    if (total < 0) { // total unknown
                        System.out.println("Transfer progress: " + progress);
                    } else {
                        System.out.println("Transfer progress: " + progress + " / "
                                + total);
                    }
                }

                @Override
                public void operationComplete(ChannelProgressiveFuture future)
                        throws Exception {
                    System.out.println("Transfer complete.");
                }


            });
            ChannelFuture lastContentFuture =  channelHandlerContext.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
            if (!keepAlive) {
                lastContentFuture.addListener(ChannelFutureListener.CLOSE);
            }
        }finally {
            if(randomAccessFile != null){
                try {
                    randomAccessFile.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void setContentType(HttpResponse response, File file){
        //MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
        if(file.getName().endsWith(".js")){
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/x-javascript");
        }else if(file.getName().endsWith(".css")){
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/css; charset=UTF-8");
        }else if (file.getName().endsWith(".html")){
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
        }
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }

    private static void send100Continue(ChannelHandlerContext ctx) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
        ctx.writeAndFlush(response);
    }
}

实现效果

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

总结

  1. 本次学习了如何使用Netty对Http请求做响应,对Netty中的FullHttpRequest,DefaultHttpResponse等类有了深入的了解,对Http协议也了解了更多。
  2. 虽然实现了静态文件服务器的基本功能,但是缺点也有很多,1.配置文件的设计做得不够灵活;2.没有实现请求转发功能

完整代码地址: github.com/catch-fish-…