通过HTTP传输纯文本

Ale*_*int 8 http stream http-headers

我希望通过HTTP流式传输一组日志消息.我想一次发送一行消息,可能在行之间有延迟,我希望在服务器发送后尽快在浏览器中显示每一行.

我目前的方法是设置Content-Typetext/plain; charset=UTF-8响应,并从服务器开始必要时启动它们之间的延迟流线.我确保在每次写入后刷新所有相关的输出流.

我在Chrome中观察到的行为是它等待响应完全完成后再显示任何内容.但我想要的行为是看到发送的每一行.这可能吗?

我已经提出了很多关于这个主题的stackoverflow问题,但没有一个问题已经回答了我的问题.我认为Transfer-Encoding这与我无关,因为这似乎是为了下载大文件(如果我错了,请纠正我).

不是关于下载文件的问题,因为我希望直接在浏览器中呈现这些行.

Lyu*_*riv 8

我不认为你可以在这里完成"最正确"的解决方案,因为问题和答案中提到的问题Ivan联系起来.至少我的Chrome和Firefox 可以无需任何努力即可逐行呈现他们收到的最新内容,但是,如上所述,它需要黑客攻击或更改要求以使其更加透明.

这里要做的第一件事就是获取但是为了触发浏览器渲染而抑制第一个前导n个字节.

如果你使用text/plain,你只能依赖于特定浏览器如何呈现输出文本.为了抑制第一个虚拟块输出,你可以只渲染空格,因为它们不是由人或浏览器解析(至少我是这么认为的,因为你想要在浏览器中输出,因此可能不会使它成为机器 - 可解析).这里的一个技巧是编写Unicode \u200B(零宽度空间),希望目标浏览器将使用它在输出窗口中不呈现任何内容.不幸的是,我的Firefox实例无法识别该字符,并且确实呈现了默认的未知字符占位符.但是,Chrome完全忽略了这些角色,在视觉上它们看起来什么也没有!它似乎是你需要的.所以,这里的一般算法是:

  • 检测用户代理以确定标头块长度(您需要知道这些预定义值).
  • 写UTF-8 BOM( ,0xEF,0xBB),0xBF以确保Chrome浏览器不会开始下载远程输出到文件.
  • 写入\u200B字符n次,其中n在前一项中确定并刷新输出.
  • 产生一些假的内容与暂停,以获得新的内容行,每行ň秒每行后立即冲洗.

但是,如果您希望没有类似Firefox的输出渲染问题\u200B,则可能需要切换到text/html.HTML支持标记注释,因此我们可以排除某些内容被呈现.这允许完全依赖HTML,而不是特定的浏览器细节.知道了,算法变得有些不同:

  • 检测用户代理以确定标头块长度.
  • 渲染块的开头<!--,然后是一些n个空格(但至少有一个我记得的;或者任何HTML注释),然后-->.所述Ñ应该是块的长度减去上述注释的长度开始/结束标记.
  • 生成一些虚拟输出,其中每一行都是HTML转义的,以<br/>或终止<br>,然后立即刷新.

这种方法在Chrome和Firefox中都可以正常使用.如果您对某些Java没问题,可以使用以下代码来实现上述内容:

@RestController
@RequestMapping("/messages")
public final class MessagesController {

    private static final List<String> lines = asList(
            "Lorem ipsum dolor sit amet,",
            "consectetur adipiscing elit,",
            "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
    );

    @RequestMapping(value = "html", method = GET, produces = "text/html")
    public void getHtml(final HttpServletRequest request, final ServletResponse response)
            throws IOException, InterruptedException {
        render(Renderers.HTML, request, response);
    }

    @RequestMapping(value = "text", method = GET, produces = "text/plain")
    public void getText(final HttpServletRequest request, final ServletResponse response)
            throws IOException, InterruptedException {
        render(Renderers.PLAIN, request, response);
    }

    private static void render(final IRenderer renderer, final HttpServletRequest request, final ServletResponse response)
            throws IOException, InterruptedException {
        final int stubLength = getStubLength(request);
        final ServletOutputStream outputStream = response.getOutputStream();
        renderer.renderStub(stubLength, outputStream);
        renderInfiniteContent(renderer, outputStream);
    }

    private static int getStubLength(final HttpServletRequest request) {
        final String userAgent = request.getHeader("User-Agent");
        if ( userAgent == null ) {
            return 0;
        }
        if ( userAgent.contains("Chrome") ) {
            return 1024;
        }
        if ( userAgent.contains("Firefox") ) {
            return 1024;
        }
        return 0;
    }

    private static void renderInfiniteContent(final IRenderer renderer, final ServletOutputStream outputStream)
            throws IOException, InterruptedException {
        for ( ; ; ) {
            for ( final String line : lines ) {
                renderer.renderLine(line, outputStream);
                sleep(5000);
            }
        }
    }

    private interface IRenderer {

        void renderStub(int length, ServletOutputStream outputStream)
                throws IOException;

        void renderLine(String line, ServletOutputStream outputStream)
                throws IOException;

    }

    private enum Renderers
            implements IRenderer {

        HTML {
            private static final String HTML_PREFIX = "<!-- ";
            private static final String HTML_SUFFIX = " -->";
            private final int HTML_PREFIX_SUFFIX_LENGTH = HTML_PREFIX.length() + HTML_SUFFIX.length();

            @Override
            public void renderStub(final int length, final ServletOutputStream outputStream)
                    throws IOException {
                outputStream.print(HTML_PREFIX);
                for ( int i = 0; i < length - HTML_PREFIX_SUFFIX_LENGTH; i++ ) {
                    outputStream.write('\u0020');
                }
                outputStream.print(HTML_SUFFIX);
                outputStream.flush();
            }

            @Override
            public void renderLine(final String line, final ServletOutputStream outputStream)
                    throws IOException {
                outputStream.print(htmlEscape(line, "UTF-8"));
                outputStream.print("<br/>");
            }
        },

        PLAIN {
            private static final char ZERO_WIDTH_CHAR = '\u200B';
            private final byte[] bom = { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF };

            @Override
            public void renderStub(final int length, final ServletOutputStream outputStream)
                    throws IOException {
                outputStream.write(bom);
                for ( int i = 0; i < length; i++ ) {
                    outputStream.write(ZERO_WIDTH_CHAR);
                }
                outputStream.flush();
            }

            @Override
            public void renderLine(final String line, final ServletOutputStream outputStream)
                    throws IOException {
                outputStream.println(line);
                outputStream.flush();
            }
        }

    }

}
Run Code Online (Sandbox Code Playgroud)

此外,您要完成的方法不会向下滚动浏览器窗口.您可能希望在Chrome中使用用户脚本自动向下滚动特定的URL页面,但据我所知,它不适用于text/plain输出.