Netty的HTTP文件服务器无法返回响应消息

 按照《Netty权威指南》书中的内容 HTTP文件服务器,服务端已经接收到了消息,也将响应消息写入到缓存并冲刷,但是页面却显示没有什么响应。不知道为什么?

把上面的代码发出来看看,上面应该要绑定一个网络输出流。

package com.lsh.netty.http;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.regex.Pattern;

/**
 * @author :LiuShihao
 * @date :Created in 2021/6/2 11:55 上午
 * @desc :HttpFile文件服务器处理类
 */
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    private final String url ;
    public HttpFileServerHandler(String url) {
        this.url  = url;
    }
    //当服务器接收到消息时,会自动触发 messageReceived方法
    @Override
    protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        try {

            // 5.0.0  request.getDecoderRequest() 已经被弃用
            //对HTTP请求消息的解码结果进行判断,如果解码失败则返回400错误
            if (!request.decoderResult().isSuccess()) {
                System.out.println("解码失败返回400");
                sendError(ctx, HttpResponseStatus.BAD_REQUEST);
                return;
            }
            //对请求方法进行判断,如果不是从浏览器或者表单设置为GET发起的请求(例如POST),则返回405错误
            if (request.method() != HttpMethod.GET) {
                sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
                System.out.println("请求方式不是GET,返回405");
                return;
            }
            //如果URI不合法 返回403错误
            final String uri = request.uri();
            System.out.println("request.uri : " + uri);
            final String path = sanitizeUri(uri);
            System.out.println("path : " + path);
            if (path == null) {
                sendError(ctx, HttpResponseStatus.FORBIDDEN);
                System.out.println("URI不合法 返回403错误");
                return;
            }
            //如果文件不存在或者是系统隐藏文件 则返回404错误
            File file = new File(path);

            if (file.isHidden() || !file.exists()) {
                sendError(ctx, HttpResponseStatus.NOT_FOUND);
                System.out.println("文件不存在或者是系统隐藏文件 则返回404错误");
                return;
            }
            System.out.println("fileName : " + file.getName());

            if (file.isDirectory()) {
                if (uri.endsWith("/")) {
                    sendListing(ctx, file);
                } else {
                    sendRedirect(ctx, uri + "/");
                }
                return;
            }
            //判断文件合法性
            if (!file.isFile()) {
                sendError(ctx, HttpResponseStatus.FORBIDDEN);
                return;
            }
            RandomAccessFile randomAccessFile = null;

            try {
                //以只读方式打开文件
                randomAccessFile = new RandomAccessFile(file, "r");
            } catch (FileNotFoundException e) {
                sendError(ctx, HttpResponseStatus.NOT_FOUND);
                return;
            }
            //获取文件的长度构造成功的HTTP应答消息
            long fileLength = randomAccessFile.length();
//        DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            HttpHeaderUtil.setContentLength(response, fileLength);
            setContentTypeHeader(response, file);
            //判断是否是keepAlive,如果是就在响应头中设置CONNECTION为keepAlive
            if (HttpHeaderUtil.isKeepAlive(request)) {
                response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
            }
            ctx.write(response);
            ChannelFuture sendFileFuture;
            //通过Netty的ChunkedFile对象直接将文件写入到发送缓冲区中
            sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
            //为sendFileFuture添加监听器,如果发送完成打印发送完成的日志
            sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
                @Override
                public void operationComplete(ChannelProgressiveFuture channelProgressiveFuture) throws Exception {
                    System.out.println("Transfer complete.");

                }

                @Override
                public void operationProgressed(ChannelProgressiveFuture channelProgressiveFuture, long progress, long total) throws Exception {
                    if (total < 0) {
                        System.err.println("Transfer progress: " + progress);
                    } else {
                        System.err.println("Transfer progress: " + progress + "/" + total);
                    }
                }

            });
            //如果使用chunked编码,最后需要发送一个编码结束的空消息体,将LastHttpContent.EMPTY_LAST_CONTENT发送到缓冲区中,
            //来标示所有的消息体已经发送完成,同时调用flush方法将发送缓冲区中的消息刷新到SocketChannel中发送
            ChannelFuture lastContentFuture = ctx.
                    writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
            //如果是非keepAlive的,最后一包消息发送完成后,服务端要主动断开连接
            if (!HttpHeaderUtil.isKeepAlive(request)) {
                lastContentFuture.addListener(ChannelFutureListener.CLOSE);
            }
        }catch (Throwable e){
            System.out.println(e.getMessage());
            e.printStackTrace();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
        cause.printStackTrace();
        if(ctx.channel().isActive()){
            sendError(ctx,HttpResponseStatus.INTERNAL_SERVER_ERROR);
        }
    }
    private static final Pattern INSECURE_URI=Pattern.compile(".*[<>&\"].*");

    /**
     *
     *
     * @param uri
     * @return
     */
    private String sanitizeUri(String uri){
        //对URL进行解码 解码成功后对URI进行合法性判断 如果URI与允许访问的URI一直或者是其子目录(文件),则检验通过否则返回空
        try {
            uri = URLDecoder.decode(uri,"UTF-8");
        }catch (UnsupportedEncodingException e){
            try {
                uri = URLDecoder.decode(uri,"ISO-8859-1");
            }catch (UnsupportedEncodingException e1){
                throw  new Error();
            }
        }
        //解码成功后对uri进行合法性判断,避免访问无权限的目录
        if(!uri.startsWith(url)){
            return null;
        }
        if(!uri.startsWith("/")){
            return null;
        }

        //将硬编码的文件路径分隔符替换为本地操作系统的文件路径分割符
        uri = uri.replace('/',File.separatorChar);
        //对新的URI做二次合法性校验,如果校验失败则直接返回空
        if (uri.contains(File.separator+'.')
            || uri.contains('.'+File.separator) || uri.startsWith(".")
            || uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()){
            return null;
        }
        //最后对文件进行拼接,使用当前运行程序所在的工作目录 + URI 构造绝对路径返回
        return System.getProperty("user.dir") + File.separator + uri;

    }

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

    /**
     * 发送目录的链接到客户端浏览器
     * @param ctx
     * @param dir
     */
    private static void sendListing(ChannelHandlerContext ctx,File dir){
            //创建成功的http响应消息
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            //设置消息头的类型是html文件,不要设置为text/plain,客户端会当做文本解析
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
            //构造返回的html页面内容
            StringBuilder buf = new StringBuilder();
            String dirPath = dir.getPath();
            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");
            System.out.println("buf :" + buf);
            //分配消息缓冲对象
            ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
            //将缓冲区的内容写入响应对象,并释放缓冲区
            response.content().writeBytes(buffer);
            buffer.release();
            //将响应消息发送到缓冲区并刷新到SocketChannel中
            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        System.out.println("=========返回响应消息=========");

    }

    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);
    }

    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);
    }



    private static void setContentTypeHeader(HttpResponse response,File file){
        MimetypesFileTypeMap mimetypesTypeMap=new MimetypesFileTypeMap();
        response.headers().set(HttpHeaderNames.CONTENT_TYPE,mimetypesTypeMap.getContentType(file.getPath()));
    }

}
package com.lsh.netty.http;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

/**
 * @author :LiuShihao
 * @date :Created in 2021/6/2 9:10 上午
 * @desc :HTTP 服务端开发
 */
public class HttpFileServer {
    private String ipAddress = "127.0.0.1";
    private static final String DEFAULT_URL = "/src/com/exp/netty/";

    public void run(final int port,final String url) throws  Exception{
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

           try {
               ServerBootstrap b = new ServerBootstrap();
               b.group(bossGroup,workerGroup)
                       .channel(NioServerSocketChannel.class)
                       .handler(new LoggingHandler(LogLevel.INFO))
                       .childHandler(new ChannelInitializer<SocketChannel>() {
                           @Override
                           protected void initChannel(SocketChannel ch) throws Exception {
                               //想ChannelPipeline中添加HTTP请求消息解码器
                               ch.pipeline().addLast("http-decoder",new HttpRequestDecoder());
                               //添加HttpObjectAggregator解码器,作用是将多个消息转换为单一的FullHttpRequest或者FullHttpResponse,
                               // 原因是HTTP解码器在每个HTTP消息中会生成多个消息对象。
                               ch.pipeline().addLast("http-aggregator",new HttpObjectAggregator(65536));
                               //增加HTTP响应编码器,对HTTP响应消息进行编码
                               ch.pipeline().addLast("http-encoder", new HttpResponseDecoder());
                               //添加Chunked handler 作用是支持异步发送大的码流(例如文件传输),但不占用过多的内存,防止发生Java内存溢出错误
                               ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());
                               //最后添加HTTPFileServerHandler 用于文件服务器的业务逻辑处理
                               ch.pipeline().addLast("fileServerHandler",new HttpFileServerHandler(url));
                           }
                       });
               ChannelFuture future = b.bind(ipAddress, port).sync();
               System.out.println("HTTP 文 件 目 录 服 务 器  启 动 ,网 址 是 :http://"+ipAddress+":"+port+url);
               future.channel().closeFuture().sync();

           }catch (Exception e){
               System.out.println(e.getMessage());
           }finally {
               bossGroup.shutdownGracefully();
               workerGroup.shutdownGracefully();
           }

    }

    public static void main(String[] args) throws Exception {
        int port = 8080;
        String url = DEFAULT_URL;
        new HttpFileServer().run(port,url);
    }
}