You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

从零实现EventSource(SSE)服务器端:迷你Web服务器开发中的问题咨询

如何用C++实现主动推送的Server-Sent Events(SSE)

你遇到的问题核心是对SSE的HTTP协议模型理解有误——SSE依赖持久的HTTP连接,而不是每次推送都重新发送完整的HTTP响应头。让我一步步帮你修正实现:

错误原因分析

你的第一个版本有两个关键问题:

  1. 错误的HTTP头使用:每次推送更新时都发送了完整的HTTP/1.1 200 OK头和Content-Length,这不符合SSE规范。SSE只需要在初始连接时发送一次响应头,之后在同一个连接上持续发送SSE格式的消息即可。
  2. 请求处理逻辑混乱:你先返回HTML页面,然后试图读取客户端的"更新请求",但实际上EventSource会发起一个独立的GET请求(带有Accept: text/event-stream头),而不是在HTML连接后续发请求。这导致服务器的read操作阻塞或出错,最终引发崩溃。
  3. 忽略分块传输:SSE的内容是流式的,长度不确定,应该使用Transfer-Encoding: chunked而非固定Content-Length,否则浏览器会认为内容已完整,关闭连接。

正确实现方案

我们需要修改服务器逻辑,让它能区分两种请求:

  • 普通请求:返回包含EventSource的HTML页面
  • SSE请求:建立持久连接,主动推送每秒更新

修改后的完整代码

#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>
#include <fcntl.h>

#define PORT 80
using namespace std;

// 普通HTML响应的头
const string HTML_HEAD = "HTTP/1.1 200 OK\n"
                         "Content-Type: text/html\n"
                         "Content-Length: %zu\n\n";

// SSE响应的头(只发送一次)
const string SSE_HEAD = "HTTP/1.1 200 OK\n"
                        "Content-Type: text/event-stream\n"
                        "Cache-Control: no-cache\n"
                        "Connection: keep-alive\n"
                        "Transfer-Encoding: chunked\n\n";

// 前端HTML页面,注意EventSource指向"/sse"路径
const string HTML_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(\"/sse\");\n\
eSource.onmessage = function(event) {\n\
document.getElementById(\"serverData\").innerHTML = event.data;\n\
};\n\
eSource.onerror = function(error) {\n\
console.log(\"SSE error:\", error);\n\
eSource.close();\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>";

// 判断请求是否为SSE请求
bool isSSERequest(const char* request) {
    return strstr(request, "Accept: text/event-stream") != nullptr;
}

// 判断请求是否为根路径的HTML请求
bool isHTMLRequest(const char* request) {
    return strstr(request, "GET / HTTP/1.1") != nullptr;
}

int serverMain() {
    int listen_sock, new_sock;
    struct sockaddr_in addr;
    socklen_t addr_len = sizeof(addr);

    listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_sock == 0) {
        perror("Error creating socket");
        return 1;
    }

    // 地址复用,避免重启服务器时端口占用
    int opt = 1;
    setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    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 listener");
        return 3;
    }

    while(1) {
        char buff[2048] = {0};
        printf("Waiting for clients...\n\n");

        new_sock = accept(listen_sock, (struct sockaddr*)&addr, &addr_len);
        if(new_sock < 0) {
            perror("Error accepting client connection");
            continue; // 出错后继续等待下一个连接,不要退出
        }

        // 关闭TCP Nagle算法,确保消息立即发送
        int tcp_nodelay = 1;
        setsockopt(new_sock, IPPROTO_TCP, TCP_NODELAY, &tcp_nodelay, sizeof(tcp_nodelay));

        // 读取客户端请求
        long bytes_read = read(new_sock, buff, sizeof(buff)-1);
        if(bytes_read <= 0) {
            perror("Error reading request");
            close(new_sock);
            continue;
        }
        printf("------------------Client-Request------------------\n%s\
------------------Client-Request------------------\n", buff);

        if(isHTMLRequest(buff)) {
            // 处理HTML请求
            char head_buf[256];
            snprintf(head_buf, sizeof(head_buf), HTML_HEAD.c_str(), HTML_RESPONSE.size());
            string reply = string(head_buf) + HTML_RESPONSE;
            write(new_sock, reply.c_str(), reply.size());
            printf("HTML response sent.\n\n");
            close(new_sock);
        }
        else if(isSSERequest(buff)) {
            // 处理SSE请求:先发送响应头
            write(new_sock, SSE_HEAD.c_str(), SSE_HEAD.size());
            printf("SSE connection established. Starting push...\n\n");

            // 主动推送60次,每秒一次
            for(int i = 0; i < 60; ++i) {
                sleep(1);
                // 构造SSE格式的消息:data字段,结尾两个换行
                string msg = "data: SERVER SAYS: " + to_string(i) + "\ndata: some other stuff " + to_string(i) + "\n\n";
                
                // 分块传输需要先发送块的长度(十六进制),再发送内容
                char chunk_header[32];
                snprintf(chunk_header, sizeof(chunk_header), "%x\r\n", (unsigned int)msg.size());
                string chunk = string(chunk_header) + msg + "\r\n";
                
                ssize_t bytes_written = write(new_sock, chunk.c_str(), chunk.size());
                if(bytes_written <= 0) {
                    perror("Error sending SSE message (client disconnected?)");
                    break;
                }
                printf("Server UPDATE %d sent.\n", i);
            }

            // 发送结束块(0长度),关闭连接
            write(new_sock, "0\r\n\r\n", 5);
            close(new_sock);
            printf("SSE connection closed.\n\n");
        }
        else {
            // 处理其他请求,返回404
            const string NOT_FOUND = "HTTP/1.1 404 Not Found\nContent-Length: 0\n\n";
            write(new_sock, NOT_FOUND.c_str(), NOT_FOUND.size());
            close(new_sock);
            printf("404 response sent.\n\n");
        }
    }

    close(listen_sock);
    return 0;
}

int main() {
    return serverMain();
}

关键改进点说明

  1. 请求区分:通过检查请求头,分别处理HTML页面请求和SSE流请求,避免逻辑混乱。
  2. 正确的SSE响应头:只在初始连接时发送一次,包含Connection: keep-aliveTransfer-Encoding: chunked,告诉浏览器保持连接并接收流式内容。
  3. 分块传输:HTTP/1.1的分块传输允许我们发送长度不确定的内容,每个块以十六进制长度开头,结尾以\r\n分隔,最后发送0\r\n\r\n表示传输结束。
  4. 关闭Nagle算法:使用TCP_NODELAY确保每次推送的消息立即发送到客户端,避免延迟。
  5. 错误处理:增加了对readwrite错误的处理,比如客户端断开连接时能及时退出推送循环并关闭socket,避免崩溃。

为什么PHP/Node.js示例能持续推送?

它们的底层HTTP服务器默认支持持久连接和流式响应,比如Node.js的res.write()可以多次调用发送内容,而不需要每次重新构造HTTP头;PHP的flush()ob_flush()可以强制输出缓冲区内容,保持连接打开。本质上和我们上面的C++实现逻辑一致——都是在同一个HTTP连接上持续发送符合格式的消息。

内容的提问来源于stack exchange,提问作者ScienceDiscoverer

火山引擎 最新活动