在机器 运行 Sophos 上,为什么我的所有浏览器都无法实时接收来自我的 Python 应用程序的服务器发送事件 (sse)?

On a machine running Sophos, why do all my browsers fail to receive server sent events (sse) from my Python apps in realtime?

我的 ASGI 应用程序将事件发送到 curl,并发送到我的 phone。但是,即使服务器正在发送事件,并且 headers 看起来是正确的,但在连接关闭之前,我的 Windows 机器上的 Firefox 和 Chrome 都不会收到事件。

无论我是在 WSL、Powershell 终端还是在单独的 Linux 盒子上托管服务器,都会发生这种情况。

但是,如果我托管 server on repl.it,那些相同的浏览器工作正常(请分叉并试用)。

我试过 Windows 防火墙设置,但无济于事。

申请代码如下:

import asyncio
import datetime


async def app(scope, receive, send):
    headers = [(b"content-type", b"text/html")]
    if scope["path"] == "/":
        body = (
            "<html>"
            "<body>"
            "</body>"
            "<script>"
            "  let eventSource = new EventSource('/sse');"
            "  eventSource.addEventListener('message', (e) => {"
            "    document.body.innerHTML += e.data + '<br>';"
            "  });"
            "</script>"
            "</html>"
        ).encode()

        await send({"type": "http.response.start", "status": 200, "headers": headers})
        await send({"type": "http.response.body", "body": body})
    elif scope["path"] == "/sse":
        headers = [
            (b"content-type", b"text/event-stream"),
            (b"cache-control", b"no-cache"),
            (b"connection", b"keep-alive"),
        ]

        async def body():
            ongoing = True
            while ongoing:
                try:
                    payload = datetime.datetime.now()
                    yield f"data: {payload}\n\n".encode()
                    await asyncio.sleep(10)
                except asyncio.CancelledError:
                    ongoing = False

        await send({"type": "http.response.start", "status": 200, "headers": headers})
        async for chunk in body():
            await send({"type": "http.response.body", "body": chunk, "more_body": True})
        await send({"type": "http.response.body", "body": b""})
    else:
        await send({"type": "http.response.start", "status": 404, "headers": headers})
        await send({"type": "http.response.body", "body": b""})

这可以是 运行,方法是将上面的文件命名为 asgi_sse.py,然后是 pip install uvicorn,然后使用类似

的名称
uvicorn asgi_sse:app

(用 daphnehypercorn 代替上面的 uvicorn 以查看这些服务器如何处理应用程序。)

headers:

$ curl -I http://localhost:8000/sse
HTTP/1.1 200 OK
date: Mon, 01 Jun 2020 09:51:41 GMT
server: uvicorn
content-type: text/event-stream
cache-control: no-cache
connection: keep-alive

响应:

$ curl http://localhost:8000/sse
data: 2020-06-01 05:52:40.735403

data: 2020-06-01 05:52:50.736378

data: 2020-06-01 05:53:00.736812

非常欢迎任何见解!

解释

我公司启用了 Sophos Endpoint Security 的网络保护。根据 this entry in Sophos's community,Web 保护会缓冲并扫描 text/event-stream 内容以查找恶意软件。因此出现了意外的缓冲。

解决方案

我发现了两种解决方法:

  1. 在第一个事件之前发送一个 2 兆字节(越大越好;越小越好)的数据块。您不需要在每个事件中都发送它;只是第一个。
  2. 或使用 https (SSL/TLS)。对于本地开发,请考虑使用 mkcert 作为一种方便的设置方式。

Python 之谜

上述问题不仅仅是我的代码的问题,也不是 Uvicorn、Hypercorn 或 ASGI 的问题。事实上,我什至尝试了一个 aiohttp 实现,结果同样令人遗憾。但是,当我尝试 a Go example implementation of SSE, and another in Node.js 时它运行良好,不需要解决方法。我能看到的唯一区别是 Go 实现在每个事件之后使用了一个 flush 方法。我不确定为什么 ASGI 和 aiohttp 不公开某种刷新方法,或者,如果他们公开,为什么我找不到它。如果他们这样做了,是否会使这些变通办法变得不必要?我不确定。

这是更新后的代码,它可以与 Sophos 一起工作,并检查是否通过 https 提供服务:

async def app(scope, receive, send):
    headers = [(b"content-type", b"text/html")]
    if scope["path"] == "/":
        body = (
            "<!DOCTYPE html>"
            "<html>"
            "<body>"
            "</body>"
            "<script>"
            "  let eventSource = new EventSource('/sse');"
            "  eventSource.addEventListener('message', (e) => {"
            "    document.body.innerHTML += e.data + '<br>';"
            "  });"
            "</script>"
            "</html>"
        ).encode()

        await send({"type": "http.response.start", "status": 200, "headers": headers})
        await send({"type": "http.response.body", "body": body})
    elif scope["path"] == "/sse":
        headers = [
            (b"Content-Type", b"text/event-stream"),
            (b"Cache-Control", b"no-cache"),
            (b"Connection", b"keep-alive"),
        ]

        async def body():
            ongoing = True
            while ongoing:
                try:
                    payload = datetime.datetime.now()
                    yield f"data: {payload}\n\n".encode()
                    await asyncio.sleep(10)
                except asyncio.CancelledError:
                    ongoing = False

        await send({"type": "http.response.start", "status": 200, "headers": headers})
        if scope["scheme"] != "https": # Sophos will buffer, so send 2MB first
            two_meg_chunk = "." * 2048 ** 2
            await send(
                {
                    "type": "http.response.body",
                    "body": f": {two_meg_chunk}\n\n".encode(),
                    "more_body": True,
                }
            )
        async for chunk in body():
            await send({"type": "http.response.body", "body": chunk, "more_body": True})
        await send({"type": "http.response.body", "body": b""})
    else:
        await send({"type": "http.response.start", "status": 404, "headers": headers})
        await send({"type": "http.response.body", "body": b""})

我 运行 最近遇到了同样的问题,但使用的是 NodeJS + Express 应用程序。如果连接不安全

,我最终发送了一个 2 兆字节的块
if (!req.secure) {
  res.write(new Array(1024 * 1024).fill(0).toString());
}

这是为了让服务器发送的事件在没有安全连接的开发环境中工作。

完整的实现:

app.use('/stream', (req, res) => {
  res.set({
    'Content-Type': 'text/event-stream;charset=utf-8',
    'Cache-Control': 'no-cache, no-transform',
    'Content-Encoding': 'none',
    'Connection': 'keep-alive'
  });
  res.flushHeaders();

  if (!req.secure) {
    res.write(new Array(1024 * 1024).fill(0).toString());
  }

  const sendEvent = (event, data) => {
    res.write(`event: ${String(event)}\n`);
    res.write(`data: ${data}`);
    res.write('\n\n');
    res.flushHeaders();
  };

  const intervalId = setInterval(() => {
    sendEvent('ping', new Date().toLocaleTimeString());
  }, 5000);

  req.on('close', () => {
    clearInterval(intervalId);
  });
});