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
- 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);
}
- 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