Серия Netty: создайте собственный файловый сервер загрузки

Java Netty реактивное программирование
Серия Netty: создайте собственный файловый сервер загрузки

Это 30-й день моего участия в августовском испытании обновлений. Узнайте подробности события:Испытание августовского обновления

Введение

В прошлой статье мы узнали, как построить HTTP-сервер в netty, обсудили, как обрабатывать запросы, отправленные клиентами, и отвечать на них, а сегодня мы обсудим вопросы, на которые стоит обратить внимание при построении файлового сервера в netty для передача файлов.

тип содержимого файла

Когда клиент запрашивает файл с сервера, сервер включает тип содержимого в возвращаемый заголовок HTTP, а тип содержимого указывает тип возвращаемого файла. Как подтвердить этот тип?

Обычно тип файла идентифицируется по расширению файла.Согласно спецификации RFC 4288, все типы сетевых носителей должны быть зарегистрированы. Apache также предоставляет таблицу сопоставления типов и расширений файлов MIME.

Поскольку существует много типов файлов, мы рассмотрим несколько наиболее часто используемых типов:

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, который может вывести свой тип MIME на основе запрошенной информации о пути к файлу:

    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 будет кэшироваться. При следующем вызове вам нужно только добавить в заголовок 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 и установить местоположение в заголовке ответа в качестве URL-адреса для перенаправления:

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

Keep-Alive — это оптимизация HTTP, позволяющая избежать установления соединения для каждого запроса. В HTTP/1.0 по умолчанию для проверки активности установлено значение false, в HTTP/1.1 для проверки активности по умолчанию установлено значение true. Если в заголовке вручную установлено соединение: false, для возврата запроса на стороне сервера также необходимо установить соединение: ложь.

Кроме того, поскольку по умолчанию для проверки активности в HTTP/1.1 задано значение true, при принятии решения HttpUtil.isKeepAlive необходимо определить, является ли это HTTP/1.0, и показать, что для проверки активности задано значение 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, то есть длина файла ответа, которую можно получить с помощью метода length файла:

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

Затем нам нужно установить соответствующий CONTENT_TYPE в соответствии с расширением файла, которое было введено в первом разделе.

Затем установите дату и свойства кэша. Таким образом, мы получаем DefaultHttpResponse, который содержит только заголовок ответа.Сначала мы записываем ответ, который содержит только заголовок ответа, в ctx.

После написания HTTP-заголовка следующим шагом является запись HTTP-контента.

Для файлов, доставленных по HTTP, существует два метода обработки: при первом способе, если вы знаете размер содержимого всего ответа, вы можете напрямую скопировать и передать весь файл в фоновом режиме. Если сам сервер поддерживает нулевое копирование, вы можете использовать метод transferTo DefaultFileRegion для передачи файлов File или Channel.

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

Если вы не знаете размер контекста всего ответа, вы можете разбить большой файл на куски и установить для параметра Transfer-coding значение chunked в заголовке ответа.Netty предоставляет HttpChunkedInput и ChunkedFile для разделения больших файлов. для передачи.

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

Если вы записываете ChunkedFile в канал, вам нужно добавить соответствующий ChunkedWriteHandler для обработки фрагментированного файла.

pipeline.addLast(new ChunkedWriteHandler());

Обратите внимание, что если это полная передача файла, вам необходимо вручную добавить последний раздел содержимого:

lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);

Если это ChunkedFile, последняя часть содержимого уже включена в chunkedFile, поэтому нет необходимости добавлять ее вручную.

прогресс передачи файлов

ChannelFuture может добавить соответствующий листнер для отслеживания хода передачи файла.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

Эта статья была включена вWoohoo.Floyd Press.com/20-Netty-FI…

Самая популярная интерпретация, самая глубокая галантерея, самые краткие уроки и множество трюков, о которых вы не знаете, ждут вас!

Добро пожаловать, чтобы обратить внимание на мой официальный аккаунт: «Программируйте эти вещи», разбирайтесь в технологиях, лучше поймите себя!