Search code examples
c++server-sideserver-sent-eventscpprest-sdk

Server-side-event C++ implementation?


I'm trying to implement a C++ server to generate events for a javascript EventSource, I'm building it with Cpprest. From the examples I've seen in PHP or Node.js, it looked pretty straight-forward but I must be missing something since I'm getting this in the Firefox console:

Firefox can’t establish a connection to the server at http://localhost:32123/data.

With Postman, I'm correctly receiving "data: test" so I think I'm missing some continuation, probably have to do something more than just reply to the request, but I didn't find a good explanation on how this is supposed to work yet. If you have some documentation you could point me to, that would be greatly appreciated!

The HTML page script looks like this:

var source = new EventSource("http://localhost:32123/data");

source.onmessage = function (event) {
    document.getElementById("result1").innerHTML += event.data + "<br>";
};

The C++ server reponse :

wResponse.set_status_code(status_codes::OK);
wResponse.headers().add(U("Access-Control-Allow-Origin"), U("*"));
wResponse.set_body(U("data: test"));
iRequest.reply(wResponse);

And the request my server is receiving:

GET /data HTTP/1.1
Accept: text/event-stream
Accept-Encoding: gzip, deflate
Accept-Language: en-us, en;q=0.5
Cache-Control: no-cache
Connection: keep-alive
Host: localhost:32123
Origin: null
Pragma: no-cache
User-Agent: Mozilla/5.0 (Windows NT6.1; Win64, x64; rv:61.0) Gecko/20100101 Firefox/61.0

Solution

  • Found a solution here

    Here's a small proof. It's not perfect at all but it's working. Next step is to figure how to store the connections, check that they're alive, etc...

    EDIT : updating answer after Darren's comment

    The proper solution seems to revolve around feeding a producer_consumer_buffer<char> bound to a basic_istream<uint8_t> that is set as the http_response body.

    Then once the http_request::reply is done, the connection will stay opened until the buffer is closed, which could be done with wBuffer.close(std::ios_base::out).wait();.

    I'm not 100% sure, but it seems that wBuffer.sync().wait(); acts like the PHP flush command would be used in a similar event-providing-server scenario.

    A working example has been added below.

    This is not a complete solution, obviously. There's still more fun ahead with managing the connections and all. Instanciating some Connection with make_unique and storing them to an container visited on events would probably be my way to go...

    main.cpp

    #include "cpprest/uri.h"
    #include "cpprest/producerconsumerstream.h"
    #include "cpprest/http_listener.h"
    
    using namespace std;
    using namespace web;
    using namespace http;
    using namespace utility;
    using namespace concurrency;
    using namespace http::experimental::listener;
    
    struct MyServer
    {
      MyServer(string_t url);
      pplx::task<void> open()  { return mListener.open(); };
      pplx::task<void> close() { return mListener.close(); };
    
    private:
    
      void handleGet(http_request iRequest);
      http_listener mListener;
    };
    
    MyServer::MyServer(utility::string_t url) : mListener(url)
    {
      mListener.support(methods::GET, bind(&MyServer::handleGet, this, placeholders::_1));
    }
    
    void MyServer::handleGet(http_request iRequest)
    {
      ucout << iRequest.to_string() << endl;
    
      http_response wResponse;
    
      // Setting headers
      wResponse.set_status_code(status_codes::OK);
      wResponse.headers().add(header_names::access_control_allow_origin, U("*"));
      wResponse.headers().add(header_names::content_type, U("text/event-stream"));
    
      // Preparing buffer
      streams::producer_consumer_buffer<char> wBuffer;
      streams::basic_istream<uint8_t> wStream(wBuffer);
      wResponse.set_body(wStream);
    
      auto wReplyTask = iRequest.reply(wResponse);
    
      wBuffer.putn_nocopy("data: a\n",10).wait();
      wBuffer.putn_nocopy("data: b\n\n",12).wait();
      wBuffer.sync().wait();  // seems equivalent to 'flush'
    
      this_thread::sleep_for(chrono::milliseconds(2000));
    
      wBuffer.putn_nocopy("data: c\n", 10).wait();
      wBuffer.putn_nocopy("data: d\n\n", 12).wait();
      wBuffer.sync().wait();
      // wBuffer.close(std::ios_base::out).wait();    // closes the connection
      wReplyTask.wait();      // blocking!
    }
    
    unique_ptr<MyServer> gHttp;
    
    void onInit(const string_t iAddress)
    {
      uri_builder wUri(iAddress);
      auto wAddress = wUri.to_uri().to_string();
      gHttp = unique_ptr<MyServer>(new MyServer(wAddress));
    
      gHttp->open().wait();
      ucout << string_t(U("Listening for requests at: ")) << wAddress << endl;
    
    }
    
    void onShutdown()
    {
      gHttp->close().wait();
    }
    
    
    void main(int argc, wchar_t* argv[])
    {
    
      onInit(U("http://*:32123"));
    
      cout << "Wait until connection occurs..." << endl;
      getchar();
    
      onShutdown();
    }
    

    sse.htm

    <!DOCTYPE html>
    <html lang="en-US">
    <head>
    <meta charset="utf-8">
    </head>
        <body>
            <div id="result"></div>
        </body>
    </html>
    
    <script>
    
        if (typeof (EventSource) !== undefined)
        {
            document.getElementById("result").innerHTML += "SSE supported" + "<br>";
        } 
        else
        {
            document.getElementById("result").innerHTML += "SSE NOT supported" + "<br>";
        }
    
        var source = new EventSource("http://localhost:32123/");
    
        source.onopen = function ()
        {
            document.getElementById("result").innerHTML += "open" + "<br>";
        };
    
        source.onerror = function ()
        {        
            document.getElementById("result").innerHTML += "error" + "<br>";
        };
    
        source.onmessage = function (event) {
            document.getElementById("result").innerHTML += event.data + "<br>";
        };
    
    </script>