Netty-6 Http protocol support - File Server

HTTP support on one pair Netty were basically used to achieve the two cases, this chapter is mainly using Netty to complete an HTTP server,

I. Features

Mainly to complete the following functions:

  1. When the browser enter the address, show all files and directories under the directory
  2. Click on the directory, go to the next level directory;
  3. Click the text file, the contents of the display;
  4. Click on other types of files to download;

Second, the service-side implementation

(A) the master boot class

In fact, before and nothing changes

package com.firewolf.java.io.http.ftp;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
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.HttpResponseEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

public class NettyFileServer {

  private String url;  //文件服务器根目录
  private Integer port; //服务监听端口号

  public NettyFileServer(String url, Integer port) {
    this.url = url;
    this.port = port;
  }

  public void start() throws Exception {
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workergGroup = new NioEventLoopGroup();
    try {
      ServerBootstrap b = new ServerBootstrap();
      b.group(bossGroup, workergGroup)
          .channel(NioServerSocketChannel.class)
          .handler(new LoggingHandler(LogLevel.INFO))
          .childHandler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel ch)
                throws Exception {
              //HTTP请求消息解码器
              ch.pipeline().addLast("http-decoder",
                  new HttpRequestDecoder());
              //将多个消息转换为单一的FullHttpRequest或者FullHttpResponse
              ch.pipeline().addLast("http-aggregator",
                  new HttpObjectAggregator(65536));
              //HTTP响应消息编码器
              ch.pipeline().addLast("http-encoder",
                  new HttpResponseEncoder());
              //支持异步发送大的码流,但不占用过多内存
              ch.pipeline().addLast("http-chunked",
                  new ChunkedWriteHandler());
              ch.pipeline().addLast(
                  new NettyFileServerHandler(url));
            }

          });
      ChannelFuture f = b.bind(port).sync();
      System.out.println("HTTP文件服务器启动,网址是:" + "http://127.0.0.1:" + port + url);
      f.channel().closeFuture().sync();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      bossGroup.shutdownGracefully();
      workergGroup.shutdownGracefully();
    }
  }

  public static void main(String[] args) throws Exception {
    new NettyFileServer("/", 8989).start();
  }
}

(B) Request Handler process

package com.firewolf.java.io.http.ftp;

import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaderNames.LOCATION;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpUtil.isKeepAlive;
import static io.netty.handler.codec.http.HttpUtil.setContentLength;

import com.firewolf.java.io.http.utils.NettyHttpUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelProgressiveFuture;
import io.netty.channel.ChannelProgressiveFutureListener;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URLDecoder;
import java.util.regex.Pattern;
import javax.activation.MimetypesFileTypeMap;

public class NettyFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

  //匹配不合格文件名
  private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");

  //匹配正确的文件名
  private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");

  @Override
  protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request)
      throws Exception {
    //对HTTP请求消息的解码结果进行判断
    if (!request.decoderResult().isSuccess()) {
      //如果解码失败直接构造400错误返回
      sendError(ctx, HttpResponseStatus.BAD_REQUEST);
      return;
    }
    //如果不是GET请求就返回405错误
    if (request.method() != HttpMethod.GET) {
      sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
      return;
    }
    final String uri = request.uri();
    final String path = absoluteFileUrl(uri);
    //如果构造的路径不合法就返回403错误
    if (path == null) {
      sendError(ctx, HttpResponseStatus.FORBIDDEN);
      return;
    }
    //使用URI路径构造file对象,如果是文件不存在或是隐藏文件就返回404
    File file = new File(path);
    if (file.isHidden() || !file.exists()) {
      sendError(ctx, HttpResponseStatus.NOT_FOUND);
      return;
    }
    //如果是目录就发送目录的链接给客户端
    if (file.isDirectory()) {
      if (uri.endsWith("/")) {
        showDirectory(ctx, file);
      } else {
        sendRedirect(ctx, uri + "/");
      }
      return;
    }
    //判断文件合法性
    if (!file.isFile()) {
      sendError(ctx, HttpResponseStatus.FORBIDDEN);
      return;
    }

    if (FileUtils.isASCIIFileBySuffix(file)) {
      //如果是文件文件,展示文本内容
      BufferedReader reader = new BufferedReader(new FileReader(file));
      String s = null;
      StringBuffer result = new StringBuffer();
      while ((s = reader.readLine()) != null) {//使用readLine方法,一次读一行
        result.append(System.lineSeparator() + s);
      }

      boolean keepAlive = HttpUtil.isKeepAlive(request);
      //构造响应数据
      FullHttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(), OK,
          Unpooled.wrappedBuffer(result.toString().getBytes()));
      response.headers()
          .set(CONTENT_TYPE, "text/plain;charset=utf-8"); //设置响应类型
      //给客户端响应信息
      NettyHttpUtils.sendResponse(ctx, request, keepAlive, response);
      return;
    } else {
      //下载文件
      downloadFile(ctx, request, file);
    }

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

  }

  /**
   * 下载文件
   */
  private void downloadFile(ChannelHandlerContext ctx, FullHttpRequest request, File file) throws IOException {
    RandomAccessFile randomAccessFile = null;
    try {
      //以只读的方式打开文件,如果打开失败返回404错误
      randomAccessFile = new RandomAccessFile(file, "r");
    } catch (FileNotFoundException e) {
      sendError(ctx, HttpResponseStatus.NOT_FOUND);
      return;
    }
    //获取文件的长度构造成功的HTTP应答消息
    long fileLength = randomAccessFile.length();
    HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
    setContentLength(response, fileLength);
    setContentTypeHeader(response, file);

    //判断是否是keepAlive,如果是就在响应头中设置CONNECTION为keepAlive
    if (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 future)
          throws Exception {
        System.out.println("Transfer complete.");
      }

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

  private final String url;

  public NettyFileServerHandler(String url) {
    this.url = url;
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    cause.printStackTrace();
    if (ctx.channel().isActive()) {
      sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
    }
  }

  //获取文件的绝对路径
  private String absoluteFileUrl(String uri) {
    try {
      //使用UTF-8对URL进行解码
      uri = URLDecoder.decode(uri, "UTF-8");
    } catch (Exception e) {
      try {
        //解码失败就使用ISO-8859-1进行解码
        uri = URLDecoder.decode(uri, "ISO-8859-1");
      } catch (Exception e2) {
        //仍然失败就返回错误
        throw new Error();
      }
    }
    //解码成功后对uri进行合法性判断,避免访问无权限的目录
    if (!uri.startsWith(url)) {
      return null;
    }
    if (!uri.startsWith("/")) {
      return null;
    }
    //将硬编码的文件路径分隔符替换为本地操作系统的文件路径分隔符
    uri = uri.replace('/', File.separatorChar);
    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 void showDirectory(ChannelHandlerContext ctx, File dir) {
    //创建成功的http响应消息
    FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
    //设置消息头的类型是html文件,不要设置为text/plain,客户端会当做文本解析
    response.headers().set(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;
      }
      String type = f.isDirectory() ? "目录" : "文件";
      buf.append("<li>").append(type).append(":<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();
    //将响应消息发送到缓冲区并刷新到SocketChannel中
    ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
  }

  private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
    FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
    response.headers().set(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(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(CONTENT_TYPE, mimetypesTypeMap.getContentType(file.getPath()));
  }
}

(C) Tools

public class FileUtils {

  /**************************** 先使用文件名后缀来判断文件类型 ****************************/
  public static String getFileSuffix(File file) {
    String fileName = file.getName();
    return fileName.substring(fileName.lastIndexOf(".") + 1);
  }


  /**
   * 判断是否是文本文件
   */
  public static boolean isASCIIFileBySuffix(File file) {
    List<String> asciis = Arrays.asList("html", "xml", "txt", "java", "md");
    return asciis.contains(getFileSuffix(file).toLowerCase());
  }
 }

(D) the effect of

Here Insert Picture Description
Click pom.xml:
Here Insert Picture Description
Click java-io.iml:
Here Insert Picture Description
click on the folder, it will go to the next level.

Guess you like

Origin blog.csdn.net/mail_liuxing/article/details/90711321