有阵子没把折腾的东西总结下来了,前阵子想利用Haskell来开发除了系统本身以外不依赖其他运行环境的工具,后来发现Haskell这思路确实和普通的命令式、过程式编程不太一样,需要补课、练习的概念颇多,切忌功利心。所以还是老老实实写C,然而C的基础功夫也不够。下次总结一下在写monctl时遇到的一些问题。

这次来看一下在做web相关工作时经常会遇到的一个问题:输出流式内容。

我们知道浏览器在把请求发送给服务器后,服务器的后端会将内容输出给客户端(根据w3的定义更严格来讲,叫用户代理User-Agent,大多数情况下即浏览器),除去浏览器对输出的HTML页面内容进行解析,JavaScript与CSS样式的解析,绘制页面外,还会遇到一种需求,例如,将后台运行的命令行输出,“源源不断”地显示到页面上。熟悉消息机制的实现,可以拿相对现代的WebSocket或者SSE来做,但这次某介绍的是更容易理解与实现的,将输出内容刷出来,即相当于流式内容。

写过PHP的各位巨巨对输出缓存(output buffer)应该相当熟悉,而由于PHP的运行模式主要是(Fast-)CGI,对于缓存的操作使用全局函数即可,例如flush,还有 ob_*家族函数。在单位工作时,我们主要使用Flask作为web框架使用,这里就来介绍一下Flask如何实现这样的需求。

通过搜索,很容易可以找到Flask的例子

我们来看代码例子

# -*- coding: utf-8 -*-
import os
import subprocess
import time

from flask import Response



@app.route('/stream')
def stream():

    # 调用一个子进程,将标准输出转到管道,
    # 注意把标准错误也重定向,以便在一个文件里读取
    p = subprocess.Popen(os.path.expanduser('ls -a1 2>&1'),
                         shell=True,
                         stdout=subprocess.PIPE)

    def generate():
        # 将标准输出的行读取转换为一个迭代器
        for line in iter(p.stdout.readline, b''):
            time.sleep(0.01) # 模拟缓慢运行,真实环境运行时不需要
            # 每次得出一行内容
            yield line.rstrip() + '\n'

    # 构造一个响应
    return Response(generate(), mimetype='text/plain')

这样我们在浏览器里运行起来,访问http://127.0.0.1:5000/stream时就可以看到效果了。同样在命令行使用curl也可以。

仅仅是这样的话,只能在纯文本页面看到内容,以下为如何在另一个页面将其展示出来。我们会需要一个HTML页面,上面有简单的按钮与一个文本框,点击按钮触发读取事件,文本框用于展示内容。(为了节省代码,这里使用jQuery作为主要框架)

$(function () {
  $('#btn').on('click', function (ev) {
    ev.preventDefault();
    
    // 发送AJAX异步请求
    var xhr = new XMLHttpRequest();
    
    xhr.open('GET', '/stream', true);
    xhr.send();
    
    // 每100毫秒,将AJAX请求的输出填充至文本框
    var timer = setInterval(function () {
      $('#cmd-output').text(xhr.responseText());
      
      if (xhr.readyState == XMLHttpRequest.DONE) {
          // 请求结束,清理
          $('#cmd-output').append('done\n');
          clearInterval(timer);
      }
    
    }, 100);
    
    return false;
  })
});

请注意这里发送AJAX请求时并没有使用jQuery自带的$.ajax方法,原因是其只提供了success的API,即请求结束后的回调函数,这并不是我们在此情景下的需求,而是“源源不断”地将输出内容填充到页面里来。对于现代浏览器而言,直接使用XMLHttpRequest已不再是很严重的兼容性问题。

针对Flask而言,输出流式内容时,缓存的大小默认是不受控制的,而基于Jinja2模板的流式内容则有可选参数供选择。

会有以上需求是因为我们在提供项目的上线发布工具,原本是Fabric脚本,现在工程师多了需要让其他人也可以(受限地)进行上线操作,直接在原有的Fabric脚本前,再套一个web人机交互界面,即可。

__END__