天天看點

netty系列之:搭建自己的下載下傳檔案伺服器簡介檔案的content-type用戶端緩存檔案其他HTTP中常用的處理檔案内容展示處理檔案傳輸進度總結

簡介

上一篇文章我們學習了如何在netty中搭建一個HTTP伺服器,讨論了如何對用戶端發送的請求進行處理和響應,今天我們來讨論一下在netty中搭建檔案伺服器進行檔案傳輸中應該注意的問題。

檔案的content-type

用戶端向伺服器端請求一個檔案,伺服器端在傳回的HTTP頭中會包含一個content-type的内容,這個content-type表示的是傳回的檔案類型。這個類型應該怎麼确認呢?

一般來說,檔案類型是根據檔案的的擴充名來确認的,根據 RFC 4288的規範,所有的網絡媒體類型都必須注冊。apache也提供了一個檔案MIME type和擴充名的映射關系表。

因為檔案類型比較多,我們看幾個比較常用到的類型如下:

MIME type 擴充名
image/jpeg jpg
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/

最通俗的解讀,最深刻的幹貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!

歡迎關注我的公衆号:「程式那些事」,懂技術,更懂你!