从零实现EventSource(SSE)服务器端:迷你Web服务器开发中的问题咨询
如何用C++实现主动推送的Server-Sent Events(SSE)
你遇到的问题核心是对SSE的HTTP协议模型理解有误——SSE依赖持久的HTTP连接,而不是每次推送都重新发送完整的HTTP响应头。让我一步步帮你修正实现:
错误原因分析
你的第一个版本有两个关键问题:
- 错误的HTTP头使用:每次推送更新时都发送了完整的
HTTP/1.1 200 OK头和Content-Length,这不符合SSE规范。SSE只需要在初始连接时发送一次响应头,之后在同一个连接上持续发送SSE格式的消息即可。 - 请求处理逻辑混乱:你先返回HTML页面,然后试图读取客户端的"更新请求",但实际上
EventSource会发起一个独立的GET请求(带有Accept: text/event-stream头),而不是在HTML连接后续发请求。这导致服务器的read操作阻塞或出错,最终引发崩溃。 - 忽略分块传输: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(); }
关键改进点说明
- 请求区分:通过检查请求头,分别处理HTML页面请求和SSE流请求,避免逻辑混乱。
- 正确的SSE响应头:只在初始连接时发送一次,包含
Connection: keep-alive和Transfer-Encoding: chunked,告诉浏览器保持连接并接收流式内容。 - 分块传输:HTTP/1.1的分块传输允许我们发送长度不确定的内容,每个块以十六进制长度开头,结尾以
\r\n分隔,最后发送0\r\n\r\n表示传输结束。 - 关闭Nagle算法:使用
TCP_NODELAY确保每次推送的消息立即发送到客户端,避免延迟。 - 错误处理:增加了对
read和write错误的处理,比如客户端断开连接时能及时退出推送循环并关闭socket,避免崩溃。
为什么PHP/Node.js示例能持续推送?
它们的底层HTTP服务器默认支持持久连接和流式响应,比如Node.js的res.write()可以多次调用发送内容,而不需要每次重新构造HTTP头;PHP的flush()和ob_flush()可以强制输出缓冲区内容,保持连接打开。本质上和我们上面的C++实现逻辑一致——都是在同一个HTTP连接上持续发送符合格式的消息。
内容的提问来源于stack exchange,提问作者ScienceDiscoverer




