How to close a "Server-Sent Events"-connection on the server?
Regarding this specification: http://www.w3.org/TR/eventsource/
How does one close the opened connection on the server? Client-side wise it is easy, just 开发者_StackOverflow社区call close()
, but what should I do on the server? Just kill it?
So, I've searched around for a solution built into the protocol, and it there does not appear to be one. If your server calls response.emit('close')
or response.end()
, the client will treat this like an error and attempt to reconnect to the server. (At least in the case of Chrome, it will attempt to reconnect indefinitely, unless it considers the network error fatal).
So it appears that one way or another, your client has to close the connection. That leaves two options. The first, is to simply assume any error from the server should close the EventSource.
const sse = new EventSource('/events')
sse.onmessage = m => console.log(m.data)
sse.onerror = () => sse.close()
The above leaves a few things to be desired though. We are assuming that a network error is a graceful shutdown, this may not be the case. There may also be some scenarios where we do want the reconnect behavior.
So instead, why don't we just ask the client to gracefully shutdown itself! We have a way to send messages to the client from the server, so all we need to do is send a message from the server that says "shut me down".
// client.js
const maxReconnectTries = 3
let reconnectAttempts = 0
const sse = new EventSource('/events')
sse.onmessage = m => {
const { type, data } = JSON.parse(m.data)
if (type === 'close') sse.close()
else console.log(data)
}
sse.onerror = () => {
if (reconnectAttempts > maxReconnectTries) {
sse.close()
alert("We have a baaad network error!")
} else {
reconnectAttempts++
}
}
// server.js
const express = require('express')
function sendEvent(res, type, data) {
res.write(`data: ${JSON.stringify({ type, data })}\n\n`)
}
function sseHandler(req, res) {
response.writeHead(200, {
'Connection': 'keep-alive',
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
}
let manualShutdown
request.on('close', () => {
console.log('disconnected.')
clearTimeout(manualShutdown) // prevent shutting down the connection twice
})
sendEvent(res, 'message', `Ping sent at ${new Date()}`)
// when it comes time to shutdown the event stream (for one reason or another)
setTimeout(() => {
sendEvent(res, 'close', null)
// give it a safe buffer of time before we shut it down manually
manualShutdown = setTimeout(() => res.end(), clientShutdownTimeout)
}, 10000)
}
const clientShutdownTimeout = 2000
const app = express()
app.get('/events', sseHandler)
app.listen(4000, () => console.log('server started on 4000'))
This covers all the areas that we need for a safe client/server implementation. If there's an issue on the server, we attempt to reconnect but can still notify the client if there's a failure. When the server wants the connection closed, it asks the client to close the connection. After two seconds, if the client has not shut down the connection we can assume something went wrong and close the connection server side.
What we are doing here is building a protocol on top of server sent events. It has a very simple api: { "type": "close" }
tells the client to close the server, and { "type": "message", "data": {"some": "data" }
tells the client this is a regular message.
node.js:
http.createServer(function (req, res) {
//...
// client closes connection
res.socket.on('close', function () {
res.end();
//...
});
});
see example for implementation of SSE server in node.js:
https://github.com/Yaffle/EventSource/blob/master/nodechat/server.js
I guess you just close the connection (kill it). I have not seen any talk about graceful disconnect.
you can change the content-type the server sents other than "text/event-stream". This will close the clients eventsource.
Caution: If you use a package to manage server-sent events in node.js, making a direct call to response.end()
may cause the package to send additional data after response.end()
, and this will crash the server with a "WRITE after close" error.
Instead of directly calling response.end()
, my workaround was to call response.emit('close')
, which allowed the package to handle the close.
Node.js documentation on http.ServerResponse > Event.close:
https://nodejs.org/api/http.html#http_event_close_1
res.end() is not the way to go if the connection is being closed from the client side and might result in a ERR_STREAM_WRITE_AFTER_END error later. If this the case, you are better off just undoing the logic that you added in the route.
An example can be found Node.JS Server Sent Events: Route continues to run after res.end() resulting in ERR_STREAM_WRITE_AFTER_END error.
精彩评论