HTTP Server Side Events - mriksman/esp-idf-homekit GitHub Wiki

Server Side Events (SSE) allows a server to push data to a HTTP client asynchronously; avoiding the need for polling. A client will initiate an SSE listener using Javascript as follows;

document.addEventListener("DOMContentLoaded", function() {
	if (window.EventSource == undefined) {
		// inform client that SSE isn’t supported in browser.
		return;
	}
	var source = new EventSource('event');
	source.onopen = function (event) {
		// do stuff. or not.
	};
	source.onerror = function (event) {
		if (event.eventPhase == EventSource.CLOSED) {  
			// do stuff
		}
	};
// onmessage is all messages without a specific event:
	source.onmessage = function (event) { 
		// do stuff
	};
// event: status
	source.addEventListener("status", function(event) { 
		//do stuff
	});
});

This will send the following GET request to the server

GET /event HTTP/1.1 
Connection: keep-alive 
Accept: text/event-stream 

The server must reply with

HTTP/1.1 200 OK 
Connection: Keep-Alive 
Content-Type: text/event-stream 

Once this is done, the server can start sending data to the session’s open socket.

Using esp_http_server, after registering a URI with /event, within the URI handler you’ll need to

  1. Keep a record of the socket fd of the session
    /* Create session's context if not already available */
    if (!req->sess_ctx) {
        for (i = 0; i < MAX_SSE_CLIENTS; i++) {
            if (sse_sockets[i] == 0) {
                req->sess_ctx = malloc(sizeof(int)); 
                req->free_ctx = free_sse_ctx_func;  
                int client_fd = httpd_req_to_sockfd(req);
                *(int *)req->sess_ctx = client_fd;
                sse_sockets[i] = client_fd;
                ESP_LOGD(TAG, "sse_socket: %d slot %d", sse_sockets[i], i);
                break;
            }
        }

The ‘session context’ is just a way to store the socket fd with the session, and to call a custom free function when the session closes. The free function will allow us to remove the socket fd from the global sse_sockets[i] list.

void free_sse_ctx_func(void *ctx) {
    int client_fd = *((int*) ctx);
    for (int i = 0; i < MAX_SSE_CLIENTS; i++) {
        if (sse_sockets[i] == client_fd) {
            sse_sockets[i] = 0;
        }
    } 
    free(ctx);
}
  1. Respond correctly to the client (still within the URI handler)
        if (i == MAX_SSE_CLIENTS) {
            len = sprintf(buffer, "HTTP/1.1 503 Server Busy\r\nContent-Length: 0\r\n\r\n");
        }
        else {
            len = sprintf(buffer, "HTTP/1.1 200 OK\r\n"
                                    "Connection: Keep-Alive\r\n"
                                    "Content-Type: text/event-stream\r\n"
                                    "Cache-Control: no-cache\r\n\r\n");
        }
    } else {
        // Should never get here?
        len = sprintf(buffer, "HTTP/1.1 400 Session Already Active\r\n\r\n");
    }

    // need to use raw send function, as httpd_resp_send will add Content-Length: 0 which
    // will make the client disconnect
    httpd_send(req, buffer, len);

Now the session is established, and we have the socket fd stored, we can send events to the client.

    const char *sse_begin = "data: ";
    const char *sse_end_message = "\n\n";
    char recv_buf[LOG_BUF_MAX_LINE_SIZE + strlen(sse_begin) + strlen(sse_end_message)];
    strcpy(recv_buf, sse_begin);
    int return_code;

    while(1) {
        if (xQueueReceive(q_sse_message_queue, recv_buf + strlen(sse_begin), portMAX_DELAY) 
              == pdTRUE) {
            strcat(recv_buf, sse_end_message);
            for (int i = 0; i < MAX_SSE_CLIENTS; i++) {
                if (sse_sockets[i] != 0) {
                    return_code = send(sse_sockets[i], recv_buf, strlen(recv_buf), 0);
                    if (return_code < 0) {
                         httpd_sess_trigger_close(server, sse_sockets[i]);
                    }
                }
            } //for
        } // if
    } //while