简体   繁体   中英

How to implement EventSource (SSE) server side from scratch?

I'm trying to implement simple, bare-bones mini web server, it's main and only task would be to send simple html/js page to client and then do real-time updates to it. Implementing transport layer is pretty simple, but I met surprising difficulties in server-side implementation of EventSource... Initially I tried this straightforward approach:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>

#define PORT       80

using namespace std;

string head =
"HTTP/1.1 200 OK\n\
Content-Type: text/html\n\
Content-Length: ";
string update_head =
"HTTP/1.1 200 OK\n\
Content-Type: text/event-stream\n\
Cache-Control: no-cache\n\
Content-Length: ";

string update = "retry: 10000\ndata: SERVER SAYS: ";

string response =
"<!DOCTYPE html>\
<html>\n\
<head>\n\
</head>\n\
<body>\n\
<div id=\"serverData\">Here is where the server sent data will appear</div>\n\
<script>\n\
if(typeof(EventSource)!==\"undefined\") {\n\
    var eSource = new EventSource(\"/\");\n\
    eSource.onmessage = function(event) {\n\
        document.getElementById(\"serverData\").innerHTML = event.data;\n\
    };\n\
}\n\
else {\n\
    document.getElementById(\"serverData\").innerHTML=\"Whoops! Your browser doesn't receive server-sent events.\";\n\
}\n\
</script>\n\
</body>\n\
</html>";

int serverMain()
{
    int listen_sock, new_sock;
    struct sockaddr_in addr;
    int addr_len = sizeof(addr);
    
    listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_sock == 0)
    {
        perror("Error creating socket");
        return 1;
    }
    
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(PORT);
    
    memset(addr.sin_zero, 0, sizeof(addr.sin_zero));
    
    int ret = bind(listen_sock, (struct sockaddr*)&addr, addr_len);
    if(ret < 0)
    {
        perror("Error binding socket");
        return 2;
    }
    
    ret = listen(listen_sock, 10);
    if(ret < 0)
    {
        perror("Error setting up server as listеner");
        return 3;
    }
    
    while(1)
    {
        char buff[2048] = {0};
        
        printf("Waiting for clients...\n\n");
        new_sock = accept(listen_sock, (struct sockaddr*)&addr, (socklen_t*)&addr_len);
        if(new_sock < 0)
        {
            perror("Error accepting client connection into new socket");
            return 4;
        }
        
        long bytes_read = read(new_sock, buff, 2048);
        printf("------------------Client-Request------------------\n%s\
        \n------------------Client-Request------------------\n", buff);
        
        string reply = head + to_string(response.size()) + "\n\n" + response;
        write(new_sock, reply.c_str(), reply.size());
        printf("Server response sent.\n\n");        
        
        bytes_read = read(new_sock, buff, 2048);
        printf("------------------Client-Request------------------\n%s\
        \n------------------Client-Request------------------\n", buff);         
        
        for(int i = 0; i < 60; ++i)
        {               
            sleep(1);
            string msg = update + to_string(i) + "\n\ndata: some other stufff morestuff "
            + to_string(i) + "\n\n";
            string upd = update_head + to_string(msg.size()) + "\n\n" + msg;
            write(new_sock, upd.c_str(), upd.size());
            printf("Server UPDATE %d sent.\n", i);
        }
        
        close(new_sock);
    }
    
    return 0;
}

TLDR: basically, I was just pushing "updates" wrapped in a header every second. Result was not good at all:

错误尝试编号 1

Only first update was actually received by browser, all subsequent updates where ignored. What's even worse — after browser sent another request for EventStream data 10 s later (look at retry: 10000\n I sent with each massage), server crashed with no error messages (I still have no idea what was the reason).

After this I tried another approach:

for(int i = 0; i < 60; ++i)
{
    bytes_read = read(new_sock, buff, 2048);
    printf("------------------Client-Request------------------\n%s\
    \n------------------Client-Request------------------\n", buff);
            
    string msg = update + to_string(i) + "\n\ndata: some other stufff morestuff "
    + to_string(i) + "\n\n";
    string upd = update_head + to_string(msg.size()) + "\n\n" + msg;
    write(new_sock, upd.c_str(), upd.size());
    printf("Server UPDATE %d sent.\n", i);
}

I removed sleep(1) from the server update loop and allowed client to send me requests, and only after that server could send update (header + data). This, kind of, sort of worked:

错误尝试编号 2

In a way that, yes, browser really received all updates and correctly displayed it in the html page. But something is still off... I need 1 second intervals. Of course, I can set retry: 1000\n and browser will send requests every second and everything will work "perfectly". But actually not so. Because its not server who decides when to push update, its client who does it. It's not much different to clicking "refresh page" button every second...

In php and node.js examples I saw on the Internet, it seems to me that they somehow send data continuously without waiting for client. Maybe they use some sort of buffer or memory mapping or something?

So, apparently, I was doing everything in the right direction apart from tiny little undocumented (at least I didn't found anything at all about it) detail on how exactly to send updates correctly.

First change header to this:

string update_head =
"HTTP/1.1 200 OK\n\
Content-Type: text/event-stream\n\
Cache-Control: no-cache\n\n";

There is no need for content length, Now, after sending actual HTML page, client will send request for text/event-stream . You need to read it and reply with bare header ( important, no data or anything else! ).

write(new_sock, update_head.c_str(), update_head.size());
printf("Server HEAD UPDATE sent.\n");

And only after this you can start sending actual updates without any header or Content-Length :

for(int i = 0; i < 60; ++i)
{           
    sleep(1);
    string msg = update + to_string(i) + "\n\ndata: some other stufff morestuff "
    + to_string(i) + "\n\n";
    write(new_sock, msg.c_str(), msg.size());
    printf("Server UPDATE %d sent.\n", i);
}

This results in browser correctly interpreting event stream:

在此处输入图像描述

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM