八、Netty 教程 – 编写自己的文件服务器

作者:唐亚峰 | 出自:唐亚峰博客

前面已经讲了Netty的基本用法(请求/应答拆包/粘包序列化),本章以文件下载为例,编写一个相比传统TomcatJetty等容器更加轻量级的文件服务器案例……

HTTP协议介绍

HTTP是一个属于应用层面向对象的协议(HTTP1.OHTTP1.1HTTP2.0),简洁,快速响应,几乎适用各大行业应用,覆盖广泛,但相比HTTPS安全性较差(具体区别不做过多概述,有兴趣可以百度百科一下)……

主要特点

  • 支持Client/Server模式
  • 简单,简洁,客户端只需要根据指定URL,带上规定的参数或者消息体请求即可
  • 灵活,允许传输任意对象传输,内容类型由请求头的Content-Type标记
  • 无状态,不存在对事务处理记忆功能,若存在后续请求,则需重新传输之前相关信息(容易导致每次连接传输的数据量增大),但在另外一方面,无状态就可以带来快速响应与轻量级负载的优势…

请求方式

GET:获取Request-URI所标识的资源,常见的查询操作
POST:在Request-URI所标识的资源后附加新的提交数据,可以存在消息体中,不一定体现在URL上,用于新增修改等操作
HEAD:请求获取Request-URI所标识的响应消息头
PUT:请求服务器存储的资源,以Request-URI做为标识,一般用作修改操作
DELETE:请求服务器删除Request-URI所标识的记录
TRACE:请求服务器回送收到的消息请求,测试或诊断
CONNECT:保留将来使用
OPTIONS:查询服务器性能,或查询与资源相关的选项和数据

标准路径:http://ip:port/path

HTTP请求头提供了关于请求,响应或者其他的发送实体的信息。HTTP的头信息包括通用头、请求头、响应头和实体头四个部分。每个头域由一个域名,冒号(:)和域值三部分组成。

  • 通用头标:即可用于请求,也可用于响应,是作为一个整体而不是特定资源与事务相关联。
  • 请求头标:允许客户端传递关于自身的信息和希望的响应形式。
  • 响应头标:服务器和于传递自身信息的响应。
  • 实体头标:定义被传送资源的信息。即可用于请求,也可用于响应。

HTTP响应头和请求头信息对照表:http://tools.jb51.net/table/http_header 有兴趣的可以看下,里面有详细介绍与描述

HTTP响应状态码和描述信息:http://tools.jb51.net/table/http_status_code

编写文件下载服务

简单描述了下HTTP,现在开始用Netty给我们提供的HTTP编写一个入门的服务端程序,含以下功能

  • 路径映射
  • 递归文件夹操作
  • 文件下载

HttpFileServer

@Override
protected void initChannel(SocketChannel channel) throws Exception {
    channel.pipeline().addLast("http-decoder", new HttpRequestDecoder());
    channel.pipeline().addLast("http-aggregator", new HttpObjectAggregator(8 * 1024));
    channel.pipeline().addLast("http-encoder", new HttpResponseEncoder());
    channel.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
    channel.pipeline().addLast("fileServerHandler", new HttpFileServerHandler(path));
}
  • 初始化添加HTTP相关编码器与解码器,对HTTP响应消息进行编码操作
  • 如果把解析这块理解是一个黑盒的话,则输入是ByteBuf,输出是FullHttpRequest,通过该对象便可获取到所有与HTTP协议有关的信息。
  • HttpRequestDecoder先通过RequestLine和Header解析成HttpRequest对象,传入到HttpObjectAggregator,然后再通过body解析出HttpContent对象,传入到HttpObjectAggregator,当HttpObjectAggregator发现是LastHttpContent,则代表HTTP协议解析完成,封装FullHttpRequest
  • 对于body内容的读取涉及到Content-Length和trunked两种方式,两种方式只是在解析协议时处理的不一致,最终输出是一致的。
  • ChunkedWriteHandler是为了支持异步发送过大数据流情况,不占用过多内存,防止JAVA内存溢出的问题…

挑优方案http://blog.csdn.net/xiangzhihong8/article/details/52029446

HttpFileServerHandler

public class HttpFileServerHandler extends
        SimpleChannelInboundHandler<FullHttpRequest> {
    private final String path;
    HttpFileServerHandler(String path) {
        this.path = path;
    }
    @Override
    public void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        if (!request.decoderResult().isSuccess()) {//判断解码结果,如果失败,回写400错误
            sendError(ctx, BAD_REQUEST);
            return;
        }
        if (request.method() != GET) {//判断请求方法,错误回写405
            sendError(ctx, METHOD_NOT_ALLOWED);
            return;
        }
        final String uri = request.uri();
        final String path = sanitizeUri(uri);//构建映射后的路径
        if (path == null) {//构建失败,回写403
            sendError(ctx, FORBIDDEN);
            return;
        }
        File file = new File(path);
        if (file.isHidden() || !file.exists()) {//如果文件不存在,或者文件为隐藏,回写404
            sendError(ctx, NOT_FOUND);
            return;
        }
        if (file.isDirectory()) {//如果为目录,列出新目录下的文件
            if (uri.endsWith("/")) {
                sendListing(ctx, file);
            } else {
                sendRedirect(ctx, uri + '/');//否则打开或下载文件
            }
            return;
        }
        if (!file.isFile()) {//如果不是一个文件或者文件夹回写403
            sendError(ctx, FORBIDDEN);
            return;
        }
        RandomAccessFile randomAccessFile;
        try {
            randomAccessFile = new RandomAccessFile(file, "r");// 以只读的方式打开文件
        } catch (FileNotFoundException e) {
            sendError(ctx, NOT_FOUND);//异常情况,回写404
            return;
        }
        long fileLength = randomAccessFile.length();
        HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
        setContentLength(response, fileLength);
        setContentTypeHeader(response, file);
        if (isKeepAlive(request)) {
            response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }
        ctx.write(response);
        ChannelFuture sendFileFuture;
        sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0,
                fileLength, 8192), ctx.newProgressivePromise());
        sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
            @Override
            public void operationProgressed(ChannelProgressiveFuture future,
                                            long progress, long total) {
                if (total < 0) { // 为知长度
                    System.err.println("进度: " + progress);
                } else {
                    System.err.println("进度: " + progress + " / " + total);
                }
            }
            @Override
            public void operationComplete(ChannelProgressiveFuture future)
                    throws Exception {
                System.out.println("Transfer complete.");
            }
        });
        ChannelFuture lastContentFuture = ctx
                .writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        if (!isKeepAlive(request)) {
            lastContentFuture.addListener(ChannelFutureListener.CLOSE);
        }
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        cause.printStackTrace();
        if (ctx.channel().isActive()) {
            sendError(ctx, INTERNAL_SERVER_ERROR);
        }
    }
    private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
    private String sanitizeUri(String 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();
            }
        }
        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;
        }
        System.out.println(path + File.separator + uri);
        return path + File.separator + uri;
    }
    private static final Pattern ALLOWED_FILE_NAME = Pattern
            .compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
    private static void sendListing(ChannelHandlerContext ctx, File dir) {
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
        response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
        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);
        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);
    }
    private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
        //采用HTTP1.1协议传输
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
        response.headers().set(LOCATION, newUri);
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }
    private static void sendError(ChannelHandlerContext ctx,
                                  HttpResponseStatus status) {
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1,
                status, Unpooled.copiedBuffer("Failure: " + status.toString()
                + "\r\n", CharsetUtil.UTF_8));
        response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }
    private static void setContentTypeHeader(HttpResponse response, File file) {
        MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
        response.headers().set(CONTENT_TYPE,
                mimeTypesMap.getContentType(file.getPath()));
    }
}
  • ChannelProgressiveFutureListener 可以监听当前Channel所关联的任务

实验一把

运行HttpFileServer,将会看到如下日志输出

HTTP文件目录服务器启动,网址是 : http://127.0.0.1:4040

打开浏览器访问http://127.0.0.1:4040,如图显示说明服务运行成功,然后就可以下载文件了…

 

– 说点什么

全文代码:https://git.oschina.net/battcn/battcn-netty/tree/master/Chapter8-1/battcn-netty-8-1-1

附录:Netty 教程系列文章