通过 HTTP 流式传输纯文本

Posted

技术标签:

【中文标题】通过 HTTP 流式传输纯文本【英文标题】:streaming plain text over HTTP 【发布时间】:2017-05-07 16:11:22 【问题描述】:

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

我目前的方法是在响应中将Content-Type 设置为text/plain; charset=UTF-8,然后根据需要从服务器开始流式传输线路,它们之间存在延迟。我确保在每次写入后刷新所有相关的输出流。

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

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

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

【问题讨论】:

我仍然会检查分块/传输编码如何为您和 chrome 工作。否则,您是否了解过网络套接字和流式传输? 是的,我对 websockets 很熟悉,但希望能找到一个更简单的纯文本流解决方案。至于分块传输编码,你知道我是否可以将它用于纯文本流,你知道浏览器是否会实时呈现流吗? 我想这应该可行,但您必须进行测试,因为里程可能会有所不同。例如。见how much data must be sent before browsers start rendering (text/html, image/jpeg examples) 你考虑过服务器发送的事件吗? developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/…。涉及一些客户端代码,但除此之外,规范与您的用例非常匹配。此外,客户端代码可能非常简单: (new EventSource(URL)).onmessage = (message)=> document.write($message<br />) 【参考方案1】:

由于Ivan 链接的question and answer 中提到的问题,我认为您无法在此处完成“最正确”的解决方案。至少我的 Chrome 和 Firefox 可以毫不费力地逐行渲染他们收到的最新内容,但是,正如上面所说,它需要修改或更改要求以使其更加透明。

这里要做的第一件事是获取但抑制前导 n 个字节以触发浏览器呈现。

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

检测用户代理以确定标头块长度(您需要知道这些预定义值)。 编写 UTF-8 BOM(0xEF0xBB0xBF)以确保 Chrome won't start the download the remote output to a file。 写入\u200B字符n次,其中n在前一个项目中确定并刷新输出。 生成一些带有暂停的虚拟内容,以便在每 n 秒后立即刷新新的内容行。

但是,如果您不希望出现像 Firefox 那样的 \u200B 字符的输出渲染问题,您可能需要切换到 text/html。 HTML 支持标记 cmets,因此我们可以将某些内容排除在渲染之外。这允许完全依赖 HTML,而不是特定的浏览器细节。知道了这一点,算法就变得有些不同了:

检测用户代理以确定标头块长度。 使用<!-- 渲染块的开头,然后是一些n 个空格(但据我所知至少有一个;或者任何HTML 注释),然后是-->n 应该是上面块的长度减去注释开始/结束标记的长度。 生成一些虚拟输出,其中每一行都经过 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();
            
        

    


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

【讨论】:

以上是关于通过 HTTP 流式传输纯文本的主要内容,如果未能解决你的问题,请参考以下文章

使用分块传输通过 HTTP POST 流式传输麦克风输出

通过 CloudFront 进行私有 HTTP 实时流式传输

通过压缩将HTTP发布多部分/表单数据流式传输并上传到存储中?

通过 HTTP (MPEG-DASH) 进行流式传输有啥意义?

使用 Gorilla Mux 端点通过 HTTP 流式传输数据

将通过 HTTP 流式传输的 wav 实时转换为 mp3