乐趣区

关于netty:netty系列之搭建自己的下载文件服务器

简介

上一篇文章咱们学习了如何在 netty 中搭建一个 HTTP 服务器,探讨了如何对客户端发送的申请进行解决和响应,明天咱们来讨论一下在 netty 中搭建文件服务器进行文件传输中应该留神的问题。

文件的 content-type

客户端向服务器端申请一个文件,服务器端在返回的 HTTP 头中会蕴含一个 content-type 的内容,这个 content-type 示意的是返回的文件类型。这个类型应该怎么确认呢?

一般来说,文件类型是依据文件的的扩展名来确认的,依据 RFC 4288 的标准,所有的网络媒体类型都必须注册。apache 也提供了一个文件 MIME type 和扩展名的映射关系表。

因为文件类型比拟多,咱们看几个比拟罕用到的类型如下:

MIME type 扩展名
image/jpeg jpg
image/jpeg jpeg
image/png png
text/plain txt text conf def list log in
image/webp webp
application/vnd.ms-excel xls
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx
application/msword doc
application/vnd.openxmlformats-officedocument.wordprocessingml.document docx
application/vnd.openxmlformats-officedocument.presentationml.presentation pptx
application/vnd.ms-powerpoint ppt
application/pdf pdf

JDK 提供了一个 MimetypesFileTypeMap 的类,这个类提供了一个 getContentType 办法,能够依据申请的文件 path 信息,来推断其 MIME type 类型:

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

客户端缓存文件

对于 HTTP 的文件申请来说,为了保障申请的速度,会应用客户端缓存的机制。比方客户端向服务器端申请一个文件 A.txt。服务器在接管到该申请之后会将 A.txt 文件发送给客户端。

其申请流程如下:

   步骤 1:客户端申请服务器端的文件
   ===================
   GET /file1.txt HTTP/1.1
   步骤 2:服务器端返回文件,并且附带额定的文件工夫信息:===================
   HTTP/1.1 200 OK
   Date:               Mon, 23 Aug 2021 17:52:30 GMT+08:00
   Last-Modified:      Tue, 10 Aug 2021 18:05:35 GMT+08:00
   Expires:            Mon, 23 Aug 2021 17:53:30 GMT+08:00
   Cache-Control:      private, max-age=60

一般来说如果客户端是古代浏览器的话,就会把 A.txt 缓存起来。在下次调用的时候只须要在 head 中增加 If-Modified-Since,询问服务器该文件是否被批改了即可,如果文件没有被批改,则服务器会返回一个 304 Not Modified,客户端失去该状态之后就会应用本地的缓存文件。

   步骤 3:客户端再次申请该文件
   ===================
   GET /file1.txt HTTP/1.1
   If-Modified-Since:  Mon, 23 Aug 2021 17:55:30 GMT+08:00
  
   步骤 4:服务器端响应该申请
   ===================
   HTTP/1.1 304 Not Modified
   Date:               Mon, 23 Aug 2021 17:55:32 GMT+08:00

在服务器的代码层面,咱们首先须要返回一个响应中通常须要的日期字段,如 Date、Last-Modified、Expires、Cache-Control 等:

 SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
        dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));

        // 日期 header
        Calendar time = new GregorianCalendar();
        log.info(dateFormatter.format(time.getTime()));

        response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime()));

        // 缓存 headers
        time.add(Calendar.SECOND, HTTP_CACHE_SECONDS);
        response.headers().set(HttpHeaderNames.EXPIRES, dateFormatter.format(time.getTime()));
        response.headers().set(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS);
        response.headers().set(HttpHeaderNames.LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified())));

而后在收到客户端的二次申请之后,须要比拟文件的最初批改工夫和 If-Modified-Since 中自带的工夫,如果没有发送变动,则发送 304 状态:

FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED, Unpooled.EMPTY_BUFFER);
        setDateHeader(response);

其余 HTTP 中罕用的解决

咱们探讨了文件类型和缓存,对于一个通用的 HTTP 服务器来说,还须要思考很多其余罕用的解决,比方异样、重定向和 Keep-Alive 设置。

对于异样,咱们须要依据异样的代码来结构一个 DefaultFullHttpResponse,并且设置相应的 CONTENT_TYPE 头即可,如下所示:

FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, Unpooled.copiedBuffer("异样:" + status + "\r\n", CharsetUtil.UTF_8));
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");

重定向同样须要构建一个 DefaultFullHttpResponse,其状态是 302 Found,并且在响应头中设置 location 为要跳转的 URL 地址即可:

FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND, Unpooled.EMPTY_BUFFER);
        response.headers().set(HttpHeaderNames.LOCATION, newUri);

Keep-Alive 是 HTTP 中为了防止每次申请都建设连贯而做的一个优化形式。在 HTTP/1.0 中默认是的 keep-alive 是 false,在 HTTP/1.1 中默认的 keep-alive 是 true。如果在 header 中手动设置了 connection:false,则 server 端申请返回也须要同样设置 connection:false。

另外,因为 HTTP/1.1 中默认的 keep-alive 是 true,如果通过 HttpUtil.isKeepAlive 判断通过之后,还须要判断是否是 HTTP/1.0,并显示设置 keep-alive 为 true。

final boolean keepAlive = HttpUtil.isKeepAlive(request);
        HttpUtil.setContentLength(response, response.content().readableBytes());
        if (!keepAlive) {response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
        } else if (request.protocolVersion().equals(HTTP_1_0)) {response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }

文件内容展现解决

文件内容展现解决是 http 服务器的外围,也是比拟难以了解的中央。

首先要设置的是 ContentLength,也就是响应的文件长度,这个能够应用 file 的 length 办法来获取:

RandomAccessFile raf;raf = new RandomAccessFile(file, "r");
long fileLength = raf.length();
HttpUtil.setContentLength(response, fileLength);

而后咱们须要依据文件的扩展名设置对应的 CONTENT_TYPE,这个在第一大节曾经介绍过了。

而后再设置 date 和缓存属性。这样咱们就失去了一个只蕴含响应头的 DefaultHttpResponse,咱们先把这个只蕴含响应头的 respose 写到 ctx 中。

写完 HTTP 头,接下来就是写 HTTP 的 Content 了。

对于 HTTP 传递的文件来说,有两种解决形式,第一种形式状况下如果晓得整个响应的 content 大小,则能够在后盾间接进行整个文件的拷贝传输。如果服务器自身反对零拷贝的话,则能够应用 DefaultFileRegion 的 transferTo 办法将 File 或者 Channel 的文件进行转移。

sendFileFuture =
                    ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength), ctx.newProgressivePromise());
            // 完结局部
            lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);

如果并不知道整个响应的 context 大小,则能够将大文件拆分成为一个个的 chunk,并且在响应的头中设置 transfer-coding 为 chunked,netty 提供了 HttpChunkedInput 和 ChunkedFile,用来将大文件拆分成为一个个的 Chunk 进行传输。

sendFileFuture =
                    ctx.writeAndFlush(new HttpChunkedInput(new ChunkedFile(raf, 0, fileLength, 8192)),
                            ctx.newProgressivePromise());

如果向 channel 中写入 ChunkedFile,则须要增加相应的 ChunkedWriteHandler 对 chunked 文件进行解决。

pipeline.addLast(new ChunkedWriteHandler());

留神,如果是残缺文件传输,则须要手动增加 last content 局部:

lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);

如果是 ChunkedFile,last content 局部曾经蕴含在了 chunkedFile 中,不须要再手动增加了。

文件传输进度

ChannelFuture 能够增加对应的 listner,用来监控文件传输的进度,netty 提供了一个 ChannelProgressiveFutureListener,用于监控文件的过程,能够重写 operationProgressed 和 operationComplete 办法对进度监控进行定制:

        sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
            @Override
            public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {if (total < 0) {log.info(future.channel() + "传输进度:" + progress);
                } else {log.info(future.channel() + "传输进度:" + progress + "/" + total);
                }
            }

            @Override
            public void operationComplete(ChannelProgressiveFuture future) {log.info(future.channel() + "传输结束.");
            }
        });

总结

咱们思考了一个 HTTP 文件服务器最根本的一些思考因素,当初能够应用这个文件服务器来提供服务啦!

本文的例子能够参考:learn-netty4

本文已收录于 http://www.flydean.com/20-netty-fileserver/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

欢送关注我的公众号:「程序那些事」, 懂技术,更懂你!

退出移动版