Netty-6 Http协议支持-文件服务器 - Go语言中文社区

Netty-6 Http协议支持-文件服务器


上一节对Netty的HTTP支持进行了基本使用,实现了两个案例,这个章节,主要是使用Netty完成一个HTTP服务器,

一、功能介绍

主要完成了如下功能:

  1. 在浏览器输入地址的时候,显示目录下的所有文件及目录
  2. 点击目录,进入下一层目录;
  3. 点击文本文件,显示内容;
  4. 点击其他类型的文件,进行下载;

二、服务端实现

(一)主启动类

其实和之前的没什么变化

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

(二)请求处理Handler

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>rn");
    buf.append("<html><head><title>");
    buf.append(dirPath);
    buf.append("目录:");
    buf.append("</title></head><body>rn");
    buf.append("<h3>");
    buf.append(dirPath).append("目录:");
    buf.append("</h3>rn");
    buf.append("<ul>");
    buf.append("<li>链接:<a href="../">..</a></li>rn");
    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>rn");
    }
    buf.append("</ul></body></html>rn");
    //分配消息缓冲对象
    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() + "rn", 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()));
  }
}

(三)工具类

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

(四)效果

在这里插入图片描述
点击pom.xml:
在这里插入图片描述
点击java-io.iml:
在这里插入图片描述
点击文件夹,则会进入下一层。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/mail_liuxing/article/details/90711321
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢