Using Server Sent Events with express

这一生的挚爱 提交于 2019-12-08 03:00:23

问题


I'll try to make this as simple as possible so i'm not having to post a ton of code. Heres what my app does right now:

  • User uploads an audio file from the browser
  • That file is processed on my server, this process takes some time and has about 8 or so steps to complete.
  • Once everything is finished, the user gets feedback in the browser that the process is complete.

What I want to add to this, is after every step in the process that is completed, send some data back to the server. For example: "Your file is uploaded", "Meta data processed", "image extracted" etc etc so the user gets incremental feedback about what is happening and I believe Server Sent Events can help me do this.

Currently, the file is POSTed to the server with app.post('/api/track', upload.single('track'), audio.process). audio.process is where all the magic happens and sends the data back to the browser with res.send(). Pretty typical.

While trying to get the events working, I have implemented this function

app.get('/stream', function(req, res) {
  res.sseSetup()

  for (var i = 0; i < 5; i++) {
    res.sseSend({count: i})
  }
})

and when the user uploads a file from the server I just make a call to this route and register all the necessary events with this function on the client side:

progress : () => {
if (!!window.EventSource) {
  const source = new EventSource('/stream')

  source.addEventListener('message', function(e) {
    let data = JSON.parse(e.data)
    console.log(e);
  }, false)

  source.addEventListener('open', function(e) {
    console.log("Connected to /stream");
  }, false)

  source.addEventListener('error', function(e) {
    if (e.target.readyState == EventSource.CLOSED) {
      console.log("Disconnected from /stream");
    } else if (e.target.readyState == EventSource.CONNECTING) {
      console.log('Connecting to /stream');
    }
  }, false)
} else {
  console.log("Your browser doesn't support SSE")
}
}

this works as expected, when I upload a track, i get a stream of events counting from 0-4. So thats great!

My Problem/Question: How do i send relevant messages from the audio.process route, to the /stream route so that the messages can be related to whats happening. audio.process has to be a POST, and /stream has to be a GET with the header 'Content-Type': 'text/event-stream'. It seems kind of weird to make GET requests from within audio.process but is this the best way?

Any and all advice/tips are appreciated! Let me know if you need any more info.


回答1:


New Answer:

Just use socket.io, it's so much easier and better! https://www.npmjs.com/package/socket.io#in-conjunction-with-express

basic setup:

const express = require('express');
const PORT = process.env.PORT || 5000;
const app = express();
const server = require('http').createServer(app);
const io = require('socket.io')(server);
// listen to socket connections
io.on('connection', function(socket){
  // get that socket and listen to events
  socket.on('chat message', function(msg){
    // emit data from the server
    io.emit('chat message', msg);
  });
});
// Tip: add the `io` reference to the request object through a middleware like so:
app.use(function(request, response, next){
  request.io = io;
  next();
});
server.listen(PORT);
console.log(`Listening on port ${PORT}...`);

and in any route handler, you can use socket.io:

app.post('/post/:post_id/like/:user_id', function likePost(request, response) {
  //...
  request.io.emit('action', 'user liked your post');
})

client side:

<script src="/socket.io/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-1.11.1.js"></script>
<script>
  $(function () {
    var socket = io();
    $('form').submit(function(e){
      e.preventDefault(); // prevents page reloading
      socket.emit('chat message', $('#m').val());
      $('#m').val('');
      return false;
    });
    socket.on('chat message', function(msg){
      $('#messages').append($('<li>').text(msg));
    });
  });
</script>

full example: https://socket.io/get-started/chat/

Original Answer

Someone (user: https://stackoverflow.com/users/451634/benny-neugebauer | from this article: addEventListener on custom object) literally gave me a hint on how to implement this without any other package except express! I have it working!

First, import Node's EventEmitter:

const EventEmitter = require('events');

Then create an instance:

const Stream = new EventEmitter();

Then create a GET route for event streaming:

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

  Stream.on("push", function(event, data) {
    response.write("event: " + String(event) + "\n" + "data: " + JSON.stringify(data) + "\n\n");
  });
});

In this GET route, you are writing back that the request is 200 OK, content-type is text/event-stream, no cache, and to keep-alive.

You are also going to call the .on method of your EventEmitter instance, which takes 2 parameters: a string of the event to listen for and a function to handle that event(that function can take as much params as it is given)

Now.... all you have to do to send a server event is to call the .emit method of your EventEmitter instance:

Stream.emit("push", "test", { msg: "admit one" });

The first parameter is a string of the event you want to trigger (make sure that it is the same as the one in the GET route). Every subsequent parameter to the .emit method will be passed to the listener's callback!

That is it!

Since your instance was defined in a scope above your route definitions, you can call the .emit method from any other route:

app.get('/', function(request, response){
  Stream.emit("push", "test", { msg: "admit one" });
  response.render("welcome.html", {});
});

Thanks to how JavaScript scoping works, you can even pass that EventEmitter instance around to other function, even from other modules:

const someModule = require('./someModule');

app.get('/', function(request, response){
  someModule.someMethod(request, Stream)
  .then(obj => { return response.json({}) });
});

In someModule:

function someMethod(request, Stream) { 
  return new Promise((resolve, reject) => { 
    Stream.emit("push", "test", { data: 'some data' });
    return resolve();
  }) 
}

That easy! No other package needed!

Here is a link to Node's EventEmitter Class: https://nodejs.org/api/events.html#events_class_eventemitter

My example:

const EventEmitter = require('events');
const express = require('express');
const app = express();

const Stream = new EventEmitter(); // my event emitter instance

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

  Stream.on("push", function(event, data) {
    response.write("event: " + String(event) + "\n" + "data: " + JSON.stringify(data) + "\n\n");
  });
});

setInterval(function(){
  Stream.emit("push", "test", { msg: "admit one" });
}, 10000)

UPDATE:

i created a module/file that is easier to use and doesn't cause memory leaks!

const Stream = function() {
  var self = this;

  // object literal of connections; IP addresses as the key
  self.connections = {};

  self.enable = function() {
    return function(req, res, next) {
      res.sseSetup = function() {
        res.writeHead(200, {
          'Content-Type': 'text/event-stream',
          'Cache-Control': 'no-cache',
          'Connection': 'keep-alive'
        })
      }

      res.sseSend = function(id, event, data) {
        var stream = "id: " + String(id) + "\n" +
        "event: " + String(event) + "\n" +
        "data: " + JSON.stringify(data) +
        "\n\n";

        // console.log(id, event, data, stream);

        res.write(stream);
      }

      next()
    }
  }

  self.add = function(request, response) {
    response.sseSetup();
    var ip = String(request.ip);
    self.connections[ip] = response;
  }.bind(self);

  self.push_sse = function(id, type, obj) {
    Object.keys(self.connections).forEach(function(key){
      self.connections[key].sseSend(id, type, obj);
    });
  }.bind(self);

}

/*
  Usage:
  ---
  const express = require('express');
  const Stream = require('./express-eventstream');
  const app = express();
  const stream = new Stream();
  app.use(stream.enable());
  app.get('/stream', function(request, response) {
    stream.add(request, response);
    stream.push_sse(1, "opened", { msg: 'connection opened!' });
  });
  app.get('/test_route', function(request, response){
    stream.push_sse(2, "new_event", { event: true });
    return response.json({ msg: 'admit one' });
  });
*/

module.exports = Stream;

Script located here - https://github.com/ryanwaite28/script-store/blob/master/js/express-eventstream.js



来源:https://stackoverflow.com/questions/48131448/using-server-sent-events-with-express

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!