服务器发送事件(SSE)和EventSource能否通过POST方法传递参数?

56

我正在使用Html5服务器发送事件。 服务器端是Java Servlet。 我有一个JSON数组数据想要传递给服务器。

var source = new EventSource("../GetPointVal?id=100&jsondata=" + JSON.stringify(data));

如果数组大小较小,服务器端可以获取查询字符串。 但如果数组大小较大(可能超过数千个字符),则服务器无法获取查询字符串。 是否可以在new EventSource(...)中使用POST方法将json数组传递到服务器,以避免查询字符串长度限制?

4个回答

50

不,SSE标准不允许使用POST方法。

(据我所知,并没有技术上的限制,我认为这只是因为设计者从未考虑过这种用例: 不仅仅是传输大量数据,如果您想使用自定义身份验证方案,将密码放在GET请求中存在安全风险。)

XMLHttpRequest(即AJAX)允许使用POST方法,因此一种选择是回到旧的长轮询/彗星方法。(我的书,Data Push Apps with HTML5 SSE 对如何做到这一点进行了详细介绍。)

另一种方法是预先POST所有数据,并将其存储在HttpSession中,然后调用SSE进程,该进程可以利用那些会话数据。(SSE支持cookies,因此JSESSIONID cookie应该可以正常工作。)

P.S. 标准并没有明确说明不能使用POST。但是,与XMLHttpRequest不同的是,没有参数来指定要使用的http方法,也没有方法来指定您想要发布的数据。


9
"我认为只是设计师们从未看到这些使用案例"。另一个是,如果您想上传文件并获取服务器端处理的完成进度。 - Yaroslav
39
我90%的SSE用例是用于POST请求。在服务器上创建某种资源,需要花费一定的时间进行处理。 - Bryan Larsen
3
@BryanLarsen 看起来设计有误,如果您想将一些数据发送到服务器,则应使用fetch或AJAX Api。使用HTTP2更加高效,因为如果您有一个打开的SSE,则连接会被重用。请注意,当SSE请求连接时,也会发送cookie,因此您可以识别您的客户端。 - John Balvin Arias
3
@JohnBalvinArias 我不想将数据发送到服务器,我想在服务器上处理一些数据,并让它向我发送进度通知和结果。 - Bryan Larsen
1
在安全方面,将密码放在GET和POST中没有区别。 - rosemash
显示剩余4条评论

11

虽然您不能使用EventSource API来实现,但是从技术上讲,服务器可以为POST请求实现。关键在于让客户端“发送”请求。例如,这个答案讨论了sse.js作为EventSource的替代品。


1

或者,您可以从使用另一个 PHP 自定义的文件中读取数据

http://..../command_receiver.php?command=blablabla

command_receiver.php

<?php
$cmd = $_GET['command'];
$fh = fopen("command.txt","w");
fwrite($fh, $cmd);
fclose($fh);
?>

demo2_sse.php

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');

//$a = $_GET["what"];
$time = microtime(true); //date('r');

$fa = fopen("command.txt", "r");
$content = fread($fa,filesize("command.txt"));
fclose($fa);

echo "data: [{$content}][{$time}]\n\n";
flush();
?>

而EventSource包含在任意命名的HTML中,如下所示

<!DOCTYPE html>
<html>
<body>
<h1>Getting server updates</h1>
var source = new EventSource("demo2_sse.php");
source.onmessage = function (event) {
        mycommand = event.data.substring(1, event.data.indexOf("]"));
       mytime = event.data.substring(event.data.lastIndexOf("[") + 1, event.data.lastIndexOf("]"));
}
</script>
</body>
</html>

0

对于那些想要将此限制为一次请求的人,我有一个新颖的方法来解决这个问题。事件流请求像任何AJAX请求一样向服务器传递cookies。我的方法是在发出请求之前设置包含数据的cookies,然后在第一个响应后立即丢弃这些cookies。

现在需要注意一些关于cookies的限制。首先,一个cookie的最大长度是4096字节,包括名称以及可能的3字节开销。其次,每个浏览器可以安全存储的cookie数量为50个。不同浏览器的最大数量不同,谷歌浏览器是180个,安卓浏览器是50个。

我将让您确定如何在这些限制周围实现逻辑,但我会提供我的实现示例。请注意,此实现使用js-cookies来操作cookies。我们还在服务器端使用了cookie-parser中间件。


浏览器JS
const productIDs = ["b0708d2c-fe46-4251-96e0-1cfb3cd05eb0", "244d1e73-b5b4-4c59-8d4e-006fc1b190fe"];
const size = new TextEncoder().encode(JSON.stringify(productIDs)).length;
const chunkSize = Math.floor(1024 * 3.5);
const segments = Math.ceil(size / chunkSize);
const currentCookies = Object.keys(Cookies.get()).length;

if (segments + currentCookies <= 50){
    let payloads = [];
    for (let segment = 0; segment < segments; segment++){
        const payloadLength = Math.ceil(productIDs.length / segments);
        const startPosition = segment * payloadLength;
        payloads[segment] = productIDs.slice(startPosition, startPosition + payloadLength);
    }

    for (const [index, payload] of Object.entries(payloads)){
        Cookies.set(`qp[${index}]`, JSON.stringify(payload));
    }

    const source = new EventSource("start-processing");
    source.addEventListener("connected", () => {
        for (const [index, payload] of Object.entries(payloads)){
            Cookies.remove(`qp[${index}]`);
        }
    });
}else{
    console.log(`ERROR: Cookie limit was reached.`);
}

如你所见,我们将chunkSize设置为3584字节。这似乎是一个相当安全的填充量,但你可以根据自己的需求进行调整。请记住,这种方法会均匀分配数组值到所有有效负载数组中。这意味着你不太可能完全达到chunkSize的值。
你可以将最大cookie数从50调整为任何你想要的值。如果你不关心移动设备或旧版浏览器,只想支持现代桌面浏览器,那么150是支持Firefox的最低要求。请查看this page以获取更多有关浏览器限制的信息。
在我们将产品ID拆分为各自的数组之后,我们为每个块生成cookie。在这种情况下,我将其标记为qp,并在后面加上一个索引到块的[0]标识符。
最后,我们启动事件流。你需要有一个立即响应的事件,以便浏览器知道要清除新创建的cookie。此外,最好在某个地方有一个独立的函数来清除这些cookie,以防响应没有返回,或者在响应到达之前刷新了浏览器。

我建议在上述代码块的开头添加以下代码,以清除可能残留的任何先前数据。

for (const cookie of Object.keys(Cookies.get()).filter(x => x.startsWith("qp"))){
    Cookies.remove(cookie);
}

服务器端JS(Express)

async (request, response) => {
    response.writeHead(200, {
        'Content-Type': 'text/event-stream', 
        'Connection': 'keep-alive',
        'Cache-Control': 'no-cache'
    });

    const productIDs = Object.entries(request.cookies).filter(([name, value]) => name.startsWith("qp")).map(([index, value]) => JSON.parse(value)).flat();

    const sendEvent = (event, data) => response.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
            
    sendEvent("connected", {});
}

相当简单的东西。那个相当长的一行代码是一种非常简单的方法,可以获取所有的数组并将它们展平为一个单一的数组。只需确保startsWith("qp")部分设置为您的cookie名称,并且显然要确保您网站上没有其他以该字符串开头的cookie。 sendEvent()是一个非常简单的函数,用于响应客户端。只需立即使用connected事件进行响应,以确保清理cookies。
这就是全部了!这确实是一个非常简单的方法,虽然在cookie限制方面有一些缺陷,但我觉得一个有预警意识的开发者将能够判断是否使用这种技术。一旦转化为一个易于重复使用的函数,你可以轻松地在任何需要的地方实现它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接