- 新增 sse.h/sse.c 实现 Server-Sent Events - 可复用 API: sse_send_headers, sse_send_event, sse_send_comment - 内置 /_sse 演示端点,每秒推送时间戳事件 - 支持 Last-Event-ID 断线重连 - 每 15 秒心跳注释保活 - 非 GET 方法返回 405 - 集成到 server.c 请求处理流程 - 新增 7 项 SSE 单元测试 + 5 项集成测试 - 编译零警告,全部 458 单元测试 + 114 集成测试通过 ✓
187 lines
4.6 KiB
C
187 lines
4.6 KiB
C
/**
|
|
* sse.c - Server-Sent Events (SSE) 实现
|
|
*
|
|
* 轻量级 SSE 事件流支持,基于现有 HTTP/1.1 连接。
|
|
*
|
|
* @author xfy
|
|
*/
|
|
|
|
#include "sse.h"
|
|
#include "log.h"
|
|
#include "platform.h"
|
|
#include "../coco/include/coco.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
/**
|
|
* sse_send_headers - 发送 SSE 响应头,保持连接打开
|
|
*/
|
|
int sse_send_headers(int fd) {
|
|
const char *headers =
|
|
"HTTP/1.1 200 OK\r\n"
|
|
"Content-Type: text/event-stream\r\n"
|
|
"Cache-Control: no-cache\r\n"
|
|
"Connection: keep-alive\r\n"
|
|
"Server: Cocoon/1.0\r\n"
|
|
"\r\n";
|
|
|
|
ssize_t n = send(fd, headers, strlen(headers), 0);
|
|
if (n < 0) {
|
|
log_warn("SSE 发送响应头失败");
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* sse_send_event - 发送单个 SSE 事件
|
|
*/
|
|
int sse_send_event(int fd, const char *event, const char *data, uint32_t id) {
|
|
if (!data) return -1;
|
|
|
|
char buf[4096];
|
|
int n = 0;
|
|
|
|
if (id > 0) {
|
|
n += snprintf(buf + n, sizeof(buf) - n, "id: %u\n", id);
|
|
}
|
|
if (event && event[0]) {
|
|
n += snprintf(buf + n, sizeof(buf) - n, "event: %s\n", event);
|
|
}
|
|
|
|
/* data 可能包含多行,逐行处理 */
|
|
const char *p = data;
|
|
const char *end = data + strlen(data);
|
|
while (p < end) {
|
|
const char *line_end = strchr(p, '\n');
|
|
if (!line_end) line_end = end;
|
|
|
|
size_t line_len = (size_t)(line_end - p);
|
|
n += snprintf(buf + n, sizeof(buf) - n, "data: ");
|
|
if (line_len > 0) {
|
|
if (line_len > (size_t)(sizeof(buf) - n - 4)) {
|
|
line_len = (size_t)(sizeof(buf) - n - 4);
|
|
}
|
|
memcpy(buf + n, p, line_len);
|
|
n += (int)line_len;
|
|
}
|
|
n += snprintf(buf + n, sizeof(buf) - n, "\n");
|
|
|
|
if (line_end < end) {
|
|
p = line_end + 1; /* 跳过 \n */
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
n += snprintf(buf + n, sizeof(buf) - n, "\n");
|
|
|
|
ssize_t sent = send(fd, buf, (size_t)n, 0);
|
|
if (sent < 0) {
|
|
log_warn("SSE 发送事件失败");
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* sse_send_comment - 发送 SSE 注释(心跳)
|
|
*/
|
|
int sse_send_comment(int fd, const char *comment) {
|
|
if (!comment) comment = "";
|
|
|
|
char buf[512];
|
|
int n = snprintf(buf, sizeof(buf), ": %s\n\n", comment);
|
|
|
|
ssize_t sent = send(fd, buf, (size_t)n, 0);
|
|
if (sent < 0) {
|
|
log_warn("SSE 发送注释失败");
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* sse_send_time_event - 发送当前时间事件
|
|
*/
|
|
static int sse_send_time_event(int fd, uint32_t id) {
|
|
time_t now = time(NULL);
|
|
struct tm *tm = localtime(&now);
|
|
char buf[256];
|
|
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm);
|
|
|
|
return sse_send_event(fd, "time", buf, id);
|
|
}
|
|
|
|
/**
|
|
* sse_handle_request - 内置 SSE 端点 /_sse
|
|
*
|
|
* 提供演示时间流:每秒发送一个时间戳事件。
|
|
* 支持 Last-Event-ID 断线重连恢复。
|
|
*/
|
|
bool sse_handle_request(int fd, const http_request_t *req) {
|
|
if (!req || strcmp(req->path, "/_sse") != 0) {
|
|
return false;
|
|
}
|
|
|
|
if (req->method != HTTP_GET) {
|
|
/* 非 GET 方法明确返回 405 */
|
|
const char *resp =
|
|
"HTTP/1.1 405 Method Not Allowed\r\n"
|
|
"Allow: GET\r\n"
|
|
"Content-Length: 0\r\n"
|
|
"Connection: close\r\n"
|
|
"Server: Cocoon/1.0\r\n"
|
|
"\r\n";
|
|
send(fd, resp, strlen(resp), 0);
|
|
return true;
|
|
}
|
|
|
|
/* 解析 Last-Event-ID 头,用于断线重连 */
|
|
uint32_t last_id = 0;
|
|
for (int i = 0; i < req->num_headers; i++) {
|
|
if (strcasecmp(req->headers[i].name, "last-event-id") == 0) {
|
|
last_id = (uint32_t)strtoul(req->headers[i].value, NULL, 10);
|
|
break;
|
|
}
|
|
}
|
|
|
|
log_info("SSE 连接建立 fd=%d, last_event_id=%u", fd, last_id);
|
|
|
|
if (sse_send_headers(fd) != 0) {
|
|
return true; /* 已处理,但连接失败 */
|
|
}
|
|
|
|
/* 发送欢迎事件 */
|
|
sse_send_event(fd, "connected", "{\"status\":\"ok\",\"type\":\"sse\"}", 0);
|
|
|
|
uint32_t id = last_id + 1;
|
|
uint32_t tick = 0;
|
|
|
|
while (1) {
|
|
/* 发送时间事件 */
|
|
if (sse_send_time_event(fd, id) != 0) {
|
|
log_info("SSE 客户端断开 fd=%d", fd);
|
|
break;
|
|
}
|
|
id++;
|
|
|
|
/* 每 15 秒发送一次心跳注释 */
|
|
tick++;
|
|
if (tick % 15 == 0) {
|
|
if (sse_send_comment(fd, "heartbeat") != 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* 休眠 1 秒(协程安全) */
|
|
coco_sleep(1000);
|
|
}
|
|
|
|
log_info("SSE 连接结束 fd=%d", fd);
|
|
return true;
|
|
}
|