You've already forked cpp-httplib
235 lines
8.2 KiB
C++
235 lines
8.2 KiB
C++
//
|
|
// ssecli-stream.cc
|
|
//
|
|
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
|
|
// MIT License
|
|
//
|
|
// SSE (Server-Sent Events) client example using Streaming API
|
|
// with automatic reconnection support (similar to JavaScript's EventSource)
|
|
//
|
|
|
|
#include <httplib.h>
|
|
|
|
#include <chrono>
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
//------------------------------------------------------------------------------
|
|
// SSE Event Parser
|
|
//------------------------------------------------------------------------------
|
|
// Parses SSE events from the stream according to the SSE specification.
|
|
// SSE format:
|
|
// event: <event-type> (optional, defaults to "message")
|
|
// data: <payload> (can have multiple lines)
|
|
// id: <event-id> (optional, used for reconnection)
|
|
// retry: <milliseconds> (optional, reconnection interval)
|
|
// <blank line> (signals end of event)
|
|
//
|
|
struct SSEEvent {
|
|
std::string event = "message"; // Event type (default: "message")
|
|
std::string data; // Event payload
|
|
std::string id; // Event ID for Last-Event-ID header
|
|
|
|
void clear() {
|
|
event = "message";
|
|
data.clear();
|
|
id.clear();
|
|
}
|
|
};
|
|
|
|
// Parse a single SSE field line (e.g., "data: hello")
|
|
// Returns true if this line ends an event (blank line)
|
|
bool parse_sse_line(const std::string &line, SSEEvent &event, int &retry_ms) {
|
|
// Blank line signals end of event
|
|
if (line.empty() || line == "\r") { return true; }
|
|
|
|
// Find the colon separator
|
|
auto colon_pos = line.find(':');
|
|
if (colon_pos == std::string::npos) {
|
|
// Line with no colon is treated as field name with empty value
|
|
return false;
|
|
}
|
|
|
|
std::string field = line.substr(0, colon_pos);
|
|
std::string value;
|
|
|
|
// Value starts after colon, skip optional single space
|
|
if (colon_pos + 1 < line.size()) {
|
|
size_t value_start = colon_pos + 1;
|
|
if (line[value_start] == ' ') { value_start++; }
|
|
value = line.substr(value_start);
|
|
// Remove trailing \r if present
|
|
if (!value.empty() && value.back() == '\r') { value.pop_back(); }
|
|
}
|
|
|
|
// Handle known fields
|
|
if (field == "event") {
|
|
event.event = value;
|
|
} else if (field == "data") {
|
|
// Multiple data lines are concatenated with newlines
|
|
if (!event.data.empty()) { event.data += "\n"; }
|
|
event.data += value;
|
|
} else if (field == "id") {
|
|
// Empty id is valid (clears the last event ID)
|
|
event.id = value;
|
|
} else if (field == "retry") {
|
|
// Parse retry interval in milliseconds
|
|
try {
|
|
retry_ms = std::stoi(value);
|
|
} catch (...) {
|
|
// Invalid retry value, ignore
|
|
}
|
|
}
|
|
// Unknown fields are ignored per SSE spec
|
|
|
|
return false;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Main - SSE Client with Auto-Reconnection
|
|
//------------------------------------------------------------------------------
|
|
int main(void) {
|
|
// Configuration
|
|
const std::string host = "http://localhost:1234";
|
|
const std::string path = "/event1";
|
|
|
|
httplib::Client cli(host);
|
|
|
|
// State for reconnection (persists across connections)
|
|
std::string last_event_id; // Sent as Last-Event-ID header on reconnect
|
|
int retry_ms = 3000; // Reconnection delay (server can override via retry:)
|
|
int connection_count = 0;
|
|
|
|
std::cout << "SSE Client starting...\n";
|
|
std::cout << "Target: " << host << path << "\n";
|
|
std::cout << "Press Ctrl+C to exit\n\n";
|
|
|
|
//----------------------------------------------------------------------------
|
|
// Main reconnection loop
|
|
// This mimics JavaScript's EventSource behavior:
|
|
// - Automatically reconnects on connection failure
|
|
// - Sends Last-Event-ID header to resume from last received event
|
|
// - Respects server's retry interval
|
|
//----------------------------------------------------------------------------
|
|
while (true) {
|
|
connection_count++;
|
|
std::cout << "[Connection #" << connection_count << "] Connecting...\n";
|
|
|
|
// Build headers, including Last-Event-ID if we have one
|
|
httplib::Headers headers;
|
|
if (!last_event_id.empty()) {
|
|
headers.emplace("Last-Event-ID", last_event_id);
|
|
std::cout << "[Connection #" << connection_count
|
|
<< "] Resuming from event ID: " << last_event_id << "\n";
|
|
}
|
|
|
|
// Open streaming connection
|
|
auto result = httplib::stream::Get(cli, path, headers);
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Connection error handling
|
|
//--------------------------------------------------------------------------
|
|
if (!result) {
|
|
std::cerr << "[Connection #" << connection_count
|
|
<< "] Failed: " << httplib::to_string(result.error()) << "\n";
|
|
std::cerr << "Reconnecting in " << retry_ms << "ms...\n\n";
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(retry_ms));
|
|
continue;
|
|
}
|
|
|
|
if (result.status() != 200) {
|
|
std::cerr << "[Connection #" << connection_count
|
|
<< "] HTTP error: " << result.status() << "\n";
|
|
|
|
// For certain errors, don't reconnect
|
|
if (result.status() == 204 || // No Content - server wants us to stop
|
|
result.status() == 404 || // Not Found
|
|
result.status() == 401 || // Unauthorized
|
|
result.status() == 403) { // Forbidden
|
|
std::cerr << "Permanent error, not reconnecting.\n";
|
|
return 1;
|
|
}
|
|
|
|
std::cerr << "Reconnecting in " << retry_ms << "ms...\n\n";
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(retry_ms));
|
|
continue;
|
|
}
|
|
|
|
// Verify Content-Type (optional but recommended)
|
|
auto content_type = result.get_header_value("Content-Type");
|
|
if (content_type.find("text/event-stream") == std::string::npos) {
|
|
std::cerr << "[Warning] Content-Type is not text/event-stream: "
|
|
<< content_type << "\n";
|
|
}
|
|
|
|
std::cout << "[Connection #" << connection_count << "] Connected!\n\n";
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Event receiving loop
|
|
// Reads chunks from the stream and parses SSE events
|
|
//--------------------------------------------------------------------------
|
|
std::string buffer;
|
|
SSEEvent current_event;
|
|
int event_count = 0;
|
|
|
|
// Read data from stream using httplib::stream API
|
|
while (result.next()) {
|
|
buffer.append(result.data(), result.size());
|
|
|
|
// Process complete lines in the buffer
|
|
size_t line_start = 0;
|
|
size_t newline_pos;
|
|
|
|
while ((newline_pos = buffer.find('\n', line_start)) !=
|
|
std::string::npos) {
|
|
std::string line = buffer.substr(line_start, newline_pos - line_start);
|
|
line_start = newline_pos + 1;
|
|
|
|
// Parse the line and check if event is complete
|
|
bool event_complete = parse_sse_line(line, current_event, retry_ms);
|
|
|
|
if (event_complete && !current_event.data.empty()) {
|
|
// Event received - process it
|
|
event_count++;
|
|
|
|
std::cout << "--- Event #" << event_count << " ---\n";
|
|
std::cout << "Type: " << current_event.event << "\n";
|
|
std::cout << "Data: " << current_event.data << "\n";
|
|
if (!current_event.id.empty()) {
|
|
std::cout << "ID: " << current_event.id << "\n";
|
|
}
|
|
std::cout << "\n";
|
|
|
|
// Update last_event_id for reconnection
|
|
// Note: Empty id clears the last event ID per SSE spec
|
|
if (!current_event.id.empty()) { last_event_id = current_event.id; }
|
|
|
|
current_event.clear();
|
|
}
|
|
}
|
|
|
|
// Keep unprocessed data in buffer
|
|
buffer.erase(0, line_start);
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Connection ended - check why
|
|
//--------------------------------------------------------------------------
|
|
if (result.read_error() != httplib::Error::Success) {
|
|
std::cerr << "\n[Connection #" << connection_count
|
|
<< "] Error: " << httplib::to_string(result.read_error())
|
|
<< "\n";
|
|
} else {
|
|
std::cout << "\n[Connection #" << connection_count
|
|
<< "] Stream ended normally\n";
|
|
}
|
|
|
|
std::cout << "Received " << event_count << " events in this connection\n";
|
|
std::cout << "Reconnecting in " << retry_ms << "ms...\n\n";
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(retry_ms));
|
|
}
|
|
|
|
return 0;
|
|
}
|