java.nio.ScoketChannel игнорирует Content-Length и использует Transfer-Encoding: chunked на основе User-Agent - PullRequest
0 голосов
/ 14 декабря 2018

Я хочу сжать тело ответа в javax.servlet.Filter.Вот мой код

byte[] bytes =  // compressing response body
response.addHeader("Content-Encoding", "gzip");
response.addHeader("Content-Length", String.valueOf(bytes.length));
response.setContentLength(bytes.length);
response.setBufferSize(bytes.length * 2);
ServletOutputStream output = response.getOutputStream();
output.write(bytes);
output.flush();
output.close();

Но фактический ответ, который я вижу в Chrome Dev Tool:

Accept-Ranges: bytes
Cache-Control: max-age=2592000
Content-Type: application/javascript;charset=UTF-8
Date: Fri, 14 Dec 2018 15:34:25 GMT
Last-Modified: Tue, 09 Oct 2018 13:42:54 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked

Я не ожидаю Transfer-Encoding: chunked, потому что я объявляю "Content-Length",Я написал простой тест на java

URLConnection connection = new URL("http://127.0.0.1:8081/js/ads.js").openConnection();
connection.addRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
connection.addRequestProperty("Accept-Encoding", "gzip, deflate");
connection.addRequestProperty("Accept-Language", "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7");
connection.addRequestProperty("Cache-Control", "no-cache");
connection.addRequestProperty("Connection", "keep-alive");
connection.addRequestProperty("Host", "127.0.0.1:8081");
connection.addRequestProperty("Pragma", "no-cache");
connection.addRequestProperty("Upgrade-Insecure-Requests", "1");
connection.addRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"); 
connection.connect();
connection.getHeaderFields().forEach((s, strings) ->
        System.out.println(s + ":" + String.join(",", strings)));

, и вот что я нашел:

  • , если я прокомментирую настройку заголовка «Пользователь-агент» или изменит «Пользователь-агент» налюбое другое значение, тогда я получаю ответ с «Content-Length»
  • , если «User-Agent» указывает на Chrome, тогда я получаю Transfer-Encoding: chunked.

Я отлажен доsun.nio.ch.SocketChannel # write метод, и он получает правильные ByteBuffers со значениями заголовка Content-Length.

Не могу понять, где происходит эта магическая трансформация на куски?

Обновление

Странно то, что я записываю сжатые байты в Socket (я уверен, что я отлажен до вызова записи собственного метода в реализации SocketChannel).Но URLConnection возвращает мой распакованный байтовый массив с пользовательским агентом Chrome и правильный gziped байтовый массив, если я не укажу заголовок User-Agent или не добавлю какую-нибудь случайную строку.Похоже, что волшебство происходит где-то в реализации сокета Windows.

1 Ответ

0 голосов
/ 26 декабря 2018

Показанный код

Я бы предположил, что показанный вами код работает и проблема в другом месте.

Настройка

  • Windows 10
  • Tomcat 7.0.92
  • Chrome 71.0.3578.98

Testcase

Iпопытался создать небольшой пример фильтра, чтобы можно было опробовать ваш тестовый код.

Кстати, фильтр сжатия, более подходящий для продуктивного использования, можно найти в примерах, поставляемых с Tomcat (webapps \ examples \ WEB).-INF \ classes \ressionFilters).

import java.io.*;
import java.util.zip.GZIPOutputStream;
import javax.servlet.*;
import javax.servlet.http.*;

public class CompressionFilter  implements Filter {

    public void init(FilterConfig filterConfig) { }
    public void destroy() { }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;

        ResponseWrapper wrapper = new ResponseWrapper(response);
        filterChain.doFilter(request, wrapper);
        byte[] uncompressed = wrapper.getBytes();

        byte[] bytes = compress(uncompressed);
        response.addHeader("Content-Encoding", "gzip");
        response.addHeader("Content-Length", String.valueOf(bytes.length));
        response.setContentLength(bytes.length);
        //response.setBufferSize(bytes.length * 2);
        ServletOutputStream output = response.getOutputStream();
        output.write(bytes);
        output.flush();
        output.close();

        System.out.println("request to:" +  request.getServletPath()
                + " size changed from: " + uncompressed.length
                + " to " + bytes.length);
    }

    private byte[] compress(byte[] bytes) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        GZIPOutputStream gzipOutputStream = new GZIPOutputStream(baos);
        gzipOutputStream.write(bytes);
        gzipOutputStream.close();
        return baos.toByteArray();
    }


    public class ResponseWrapper extends HttpServletResponseWrapper {
        private ByteArrayOutputStream output = new ByteArrayOutputStream();
        private PrintWriter printWriter = null;

        ResponseWrapper(HttpServletResponse response) {
            super(response);
        }

        byte[] getBytes() {
            if (printWriter != null)
                printWriter.flush();
            return output.toByteArray();
        }

        public PrintWriter getWriter() {
            if (printWriter == null)
                printWriter = new PrintWriter(output);
            return printWriter;
        }

        public ServletOutputStream getOutputStream() {
            return new ServletOutputStream() {
                private WriteListener writeListener;
                public boolean isReady() { return true; }
                public void setWriteListener(WriteListener writeListener) { this.writeListener  = writeListener; }
                public void write(int b) {
                    output.write(b);
                    if(writeListener != null)
                        writeListener.notify();
                }
            };
        }
    }

}

Результат

Три теста со статическим HTML, страница, сгенерированная JSP, и страница, сгенерированная сервлетом, с некоторым фиктивным содержимым:как показано в инструментах разработчика Chrome:

a) с использованием статического html

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Accept-Ranges: bytes
ETag: W/"108-1545775482914"
Last-Modified: Tue, 25 Dec 2018 22:04:42 GMT
Content-Encoding: gzip
Content-Type: text/html
Content-Length: 97
Date: Tue, 25 Dec 2018 22:34:41 GMT

b) сгенерированный JSP

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Encoding: gzip
Content-Type: text/html
Content-Length: 38
Date: Tue, 25 Dec 2018 22:49:17 GMT

c) сгенерированный сервлет

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Encoding: gzip
Content-Type: text/html
Content-Length: 65
Date: Tue, 25 Dec 2018 22:49:43 GMT

При этой настройке Transfer-Encoding: chunked отсутствует.Так что, может быть, причину этого фрагментированного заголовка можно найти где-то еще?

...