mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-24 12:04:40 +00:00
Move stream and sse implementations from the decl area to the impl area. (#2352)
This commit is contained in:
1 parent
f0b7d4161d
commit
a875292153
1 file changed
+378
-328
@@ -2975,98 +2975,30 @@ namespace stream {
|
|||||||
|
|
||||||
class Result {
|
class Result {
|
||||||
public:
|
public:
|
||||||
Result() : chunk_size_(8192) {}
|
Result();
|
||||||
|
explicit Result(ClientImpl::StreamHandle &&handle, size_t chunk_size = 8192);
|
||||||
explicit Result(ClientImpl::StreamHandle &&handle, size_t chunk_size = 8192)
|
Result(Result &&other) noexcept;
|
||||||
: handle_(std::move(handle)), chunk_size_(chunk_size) {}
|
Result &operator=(Result &&other) noexcept;
|
||||||
|
|
||||||
Result(Result &&other) noexcept
|
|
||||||
: handle_(std::move(other.handle_)), buffer_(std::move(other.buffer_)),
|
|
||||||
current_size_(other.current_size_), chunk_size_(other.chunk_size_),
|
|
||||||
finished_(other.finished_) {
|
|
||||||
other.current_size_ = 0;
|
|
||||||
other.finished_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result &operator=(Result &&other) noexcept {
|
|
||||||
if (this != &other) {
|
|
||||||
handle_ = std::move(other.handle_);
|
|
||||||
buffer_ = std::move(other.buffer_);
|
|
||||||
current_size_ = other.current_size_;
|
|
||||||
chunk_size_ = other.chunk_size_;
|
|
||||||
finished_ = other.finished_;
|
|
||||||
other.current_size_ = 0;
|
|
||||||
other.finished_ = true;
|
|
||||||
}
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result(const Result &) = delete;
|
Result(const Result &) = delete;
|
||||||
Result &operator=(const Result &) = delete;
|
Result &operator=(const Result &) = delete;
|
||||||
|
|
||||||
// Check if the result is valid (connection succeeded and response received)
|
// Response info
|
||||||
bool is_valid() const { return handle_.is_valid(); }
|
bool is_valid() const;
|
||||||
explicit operator bool() const { return is_valid(); }
|
explicit operator bool() const;
|
||||||
|
int status() const;
|
||||||
// Response status code
|
const Headers &headers() const;
|
||||||
int status() const {
|
|
||||||
return handle_.response ? handle_.response->status : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response headers
|
|
||||||
const Headers &headers() const {
|
|
||||||
static const Headers empty_headers;
|
|
||||||
return handle_.response ? handle_.response->headers : empty_headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string get_header_value(const std::string &key,
|
std::string get_header_value(const std::string &key,
|
||||||
const char *def = "") const {
|
const char *def = "") const;
|
||||||
return handle_.response ? handle_.response->get_header_value(key, def)
|
bool has_header(const std::string &key) const;
|
||||||
: def;
|
Error error() const;
|
||||||
}
|
Error read_error() const;
|
||||||
|
bool has_read_error() const;
|
||||||
|
|
||||||
bool has_header(const std::string &key) const {
|
// Stream reading
|
||||||
return handle_.response ? handle_.response->has_header(key) : false;
|
bool next();
|
||||||
}
|
const char *data() const;
|
||||||
|
size_t size() const;
|
||||||
// Error information
|
std::string read_all();
|
||||||
Error error() const { return handle_.error; }
|
|
||||||
Error read_error() const { return handle_.get_read_error(); }
|
|
||||||
bool has_read_error() const { return handle_.has_read_error(); }
|
|
||||||
|
|
||||||
// Streaming iteration API
|
|
||||||
// Call next() to read the next chunk, then access data via data()/size()
|
|
||||||
// Returns true if data was read, false when stream is exhausted
|
|
||||||
bool next() {
|
|
||||||
if (!handle_.is_valid() || finished_) { return false; }
|
|
||||||
|
|
||||||
if (buffer_.size() < chunk_size_) { buffer_.resize(chunk_size_); }
|
|
||||||
|
|
||||||
ssize_t n = handle_.read(&buffer_[0], chunk_size_);
|
|
||||||
if (n > 0) {
|
|
||||||
current_size_ = static_cast<size_t>(n);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
current_size_ = 0;
|
|
||||||
finished_ = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pointer to current chunk data (valid after next() returns true)
|
|
||||||
const char *data() const { return buffer_.data(); }
|
|
||||||
|
|
||||||
// Size of current chunk (valid after next() returns true)
|
|
||||||
size_t size() const { return current_size_; }
|
|
||||||
|
|
||||||
// Convenience method: read all remaining data into a string
|
|
||||||
std::string read_all() {
|
|
||||||
std::string result;
|
|
||||||
while (next()) {
|
|
||||||
result.append(data(), size());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ClientImpl::StreamHandle handle_;
|
ClientImpl::StreamHandle handle_;
|
||||||
@@ -3331,13 +3263,8 @@ struct SSEMessage {
|
|||||||
std::string data; // Event payload
|
std::string data; // Event payload
|
||||||
std::string id; // Event ID for Last-Event-ID header
|
std::string id; // Event ID for Last-Event-ID header
|
||||||
|
|
||||||
SSEMessage() : event("message") {}
|
SSEMessage();
|
||||||
|
void clear();
|
||||||
void clear() {
|
|
||||||
event = "message";
|
|
||||||
data.clear();
|
|
||||||
id.clear();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class SSEClient {
|
class SSEClient {
|
||||||
@@ -3346,255 +3273,40 @@ public:
|
|||||||
using ErrorHandler = std::function<void(Error)>;
|
using ErrorHandler = std::function<void(Error)>;
|
||||||
using OpenHandler = std::function<void()>;
|
using OpenHandler = std::function<void()>;
|
||||||
|
|
||||||
SSEClient(Client &client, const std::string &path)
|
SSEClient(Client &client, const std::string &path);
|
||||||
: client_(client), path_(path) {}
|
SSEClient(Client &client, const std::string &path, const Headers &headers);
|
||||||
|
~SSEClient();
|
||||||
SSEClient(Client &client, const std::string &path, const Headers &headers)
|
|
||||||
: client_(client), path_(path), headers_(headers) {}
|
|
||||||
|
|
||||||
~SSEClient() { stop(); }
|
|
||||||
|
|
||||||
SSEClient(const SSEClient &) = delete;
|
SSEClient(const SSEClient &) = delete;
|
||||||
SSEClient &operator=(const SSEClient &) = delete;
|
SSEClient &operator=(const SSEClient &) = delete;
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
SSEClient &on_message(MessageHandler handler) {
|
SSEClient &on_message(MessageHandler handler);
|
||||||
on_message_ = std::move(handler);
|
SSEClient &on_event(const std::string &type, MessageHandler handler);
|
||||||
return *this;
|
SSEClient &on_open(OpenHandler handler);
|
||||||
}
|
SSEClient &on_error(ErrorHandler handler);
|
||||||
|
SSEClient &set_reconnect_interval(int ms);
|
||||||
SSEClient &on_event(const std::string &type, MessageHandler handler) {
|
SSEClient &set_max_reconnect_attempts(int n);
|
||||||
event_handlers_[type] = std::move(handler);
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
SSEClient &on_open(OpenHandler handler) {
|
|
||||||
on_open_ = std::move(handler);
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
SSEClient &on_error(ErrorHandler handler) {
|
|
||||||
on_error_ = std::move(handler);
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
SSEClient &set_reconnect_interval(int ms) {
|
|
||||||
reconnect_interval_ms_ = ms;
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
SSEClient &set_max_reconnect_attempts(int n) {
|
|
||||||
max_reconnect_attempts_ = n;
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// State accessors
|
// State accessors
|
||||||
bool is_connected() const { return connected_.load(); }
|
bool is_connected() const;
|
||||||
const std::string &last_event_id() const { return last_event_id_; }
|
const std::string &last_event_id() const;
|
||||||
|
|
||||||
// Blocking start - runs event loop with auto-reconnect
|
// Blocking start - runs event loop with auto-reconnect
|
||||||
void start() {
|
void start();
|
||||||
running_.store(true);
|
|
||||||
run_event_loop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-blocking start - runs in background thread
|
// Non-blocking start - runs in background thread
|
||||||
void start_async() {
|
void start_async();
|
||||||
running_.store(true);
|
|
||||||
async_thread_ = std::thread([this]() { run_event_loop(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the client (thread-safe)
|
// Stop the client (thread-safe)
|
||||||
void stop() {
|
void stop();
|
||||||
running_.store(false);
|
|
||||||
client_.stop(); // Cancel any pending operations
|
|
||||||
if (async_thread_.joinable()) { async_thread_.join(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Parse a single SSE field line
|
bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms);
|
||||||
// Returns true if this line ends an event (blank line)
|
void run_event_loop();
|
||||||
bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms) {
|
void dispatch_event(const SSEMessage &msg);
|
||||||
// Blank line signals end of event
|
bool should_reconnect(int count) const;
|
||||||
if (line.empty() || line == "\r") { return true; }
|
void wait_for_reconnect();
|
||||||
|
|
||||||
// Lines starting with ':' are comments (ignored)
|
|
||||||
if (!line.empty() && line[0] == ':') { return false; }
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto field = line.substr(0, colon_pos);
|
|
||||||
std::string value;
|
|
||||||
|
|
||||||
// Value starts after colon, skip optional single space
|
|
||||||
if (colon_pos + 1 < line.size()) {
|
|
||||||
auto 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") {
|
|
||||||
msg.event = value;
|
|
||||||
} else if (field == "data") {
|
|
||||||
// Multiple data lines are concatenated with newlines
|
|
||||||
if (!msg.data.empty()) { msg.data += "\n"; }
|
|
||||||
msg.data += value;
|
|
||||||
} else if (field == "id") {
|
|
||||||
// Empty id is valid (clears the last event ID)
|
|
||||||
msg.id = value;
|
|
||||||
} else if (field == "retry") {
|
|
||||||
// Parse retry interval in milliseconds
|
|
||||||
{
|
|
||||||
int v = 0;
|
|
||||||
auto res =
|
|
||||||
detail::from_chars(value.data(), value.data() + value.size(), v);
|
|
||||||
if (res.ec == std::errc{}) { retry_ms = v; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Unknown fields are ignored per SSE spec
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main event loop with auto-reconnect
|
|
||||||
void run_event_loop() {
|
|
||||||
auto reconnect_count = 0;
|
|
||||||
|
|
||||||
while (running_.load()) {
|
|
||||||
// Build headers, including Last-Event-ID if we have one
|
|
||||||
auto request_headers = headers_;
|
|
||||||
if (!last_event_id_.empty()) {
|
|
||||||
request_headers.emplace("Last-Event-ID", last_event_id_);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open streaming connection
|
|
||||||
auto result = stream::Get(client_, path_, request_headers);
|
|
||||||
|
|
||||||
// Connection error handling
|
|
||||||
if (!result) {
|
|
||||||
connected_.store(false);
|
|
||||||
if (on_error_) { on_error_(result.error()); }
|
|
||||||
|
|
||||||
if (!should_reconnect(reconnect_count)) { break; }
|
|
||||||
wait_for_reconnect();
|
|
||||||
reconnect_count++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.status() != 200) {
|
|
||||||
connected_.store(false);
|
|
||||||
// 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
|
|
||||||
if (on_error_) { on_error_(Error::Connection); }
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (on_error_) { on_error_(Error::Connection); }
|
|
||||||
|
|
||||||
if (!should_reconnect(reconnect_count)) { break; }
|
|
||||||
wait_for_reconnect();
|
|
||||||
reconnect_count++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connection successful
|
|
||||||
connected_.store(true);
|
|
||||||
reconnect_count = 0;
|
|
||||||
if (on_open_) { on_open_(); }
|
|
||||||
|
|
||||||
// Event receiving loop
|
|
||||||
std::string buffer;
|
|
||||||
SSEMessage current_msg;
|
|
||||||
|
|
||||||
while (running_.load() && 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) {
|
|
||||||
auto line = buffer.substr(line_start, newline_pos - line_start);
|
|
||||||
line_start = newline_pos + 1;
|
|
||||||
|
|
||||||
// Parse the line and check if event is complete
|
|
||||||
auto event_complete =
|
|
||||||
parse_sse_line(line, current_msg, reconnect_interval_ms_);
|
|
||||||
|
|
||||||
if (event_complete && !current_msg.data.empty()) {
|
|
||||||
// Update last_event_id for reconnection
|
|
||||||
if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; }
|
|
||||||
|
|
||||||
// Dispatch event to appropriate handler
|
|
||||||
dispatch_event(current_msg);
|
|
||||||
|
|
||||||
current_msg.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep unprocessed data in buffer
|
|
||||||
buffer.erase(0, line_start);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connection ended
|
|
||||||
connected_.store(false);
|
|
||||||
|
|
||||||
if (!running_.load()) { break; }
|
|
||||||
|
|
||||||
// Check for read errors
|
|
||||||
if (result.has_read_error()) {
|
|
||||||
if (on_error_) { on_error_(result.read_error()); }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!should_reconnect(reconnect_count)) { break; }
|
|
||||||
wait_for_reconnect();
|
|
||||||
reconnect_count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
connected_.store(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch event to appropriate handler
|
|
||||||
void dispatch_event(const SSEMessage &msg) {
|
|
||||||
// Check for specific event type handler first
|
|
||||||
auto it = event_handlers_.find(msg.event);
|
|
||||||
if (it != event_handlers_.end()) {
|
|
||||||
it->second(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to generic message handler
|
|
||||||
if (on_message_) { on_message_(msg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we should attempt to reconnect
|
|
||||||
bool should_reconnect(int count) const {
|
|
||||||
if (!running_.load()) { return false; }
|
|
||||||
if (max_reconnect_attempts_ == 0) { return true; } // unlimited
|
|
||||||
return count < max_reconnect_attempts_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for reconnect interval
|
|
||||||
void wait_for_reconnect() {
|
|
||||||
// Use small increments to check running_ flag frequently
|
|
||||||
auto waited = 0;
|
|
||||||
while (running_.load() && waited < reconnect_interval_ms_) {
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
||||||
waited += 100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client and path
|
// Client and path
|
||||||
Client &client_;
|
Client &client_;
|
||||||
@@ -3628,6 +3340,344 @@ private:
|
|||||||
* Implementation that will be part of the .cc file if split into .h + .cc.
|
* Implementation that will be part of the .cc file if split into .h + .cc.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
namespace stream {
|
||||||
|
|
||||||
|
// stream::Result implementations
|
||||||
|
inline Result::Result() : chunk_size_(8192) {}
|
||||||
|
|
||||||
|
inline Result::Result(ClientImpl::StreamHandle &&handle, size_t chunk_size)
|
||||||
|
: handle_(std::move(handle)), chunk_size_(chunk_size) {}
|
||||||
|
|
||||||
|
inline Result::Result(Result &&other) noexcept
|
||||||
|
: handle_(std::move(other.handle_)), buffer_(std::move(other.buffer_)),
|
||||||
|
current_size_(other.current_size_), chunk_size_(other.chunk_size_),
|
||||||
|
finished_(other.finished_) {
|
||||||
|
other.current_size_ = 0;
|
||||||
|
other.finished_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Result &Result::operator=(Result &&other) noexcept {
|
||||||
|
if (this != &other) {
|
||||||
|
handle_ = std::move(other.handle_);
|
||||||
|
buffer_ = std::move(other.buffer_);
|
||||||
|
current_size_ = other.current_size_;
|
||||||
|
chunk_size_ = other.chunk_size_;
|
||||||
|
finished_ = other.finished_;
|
||||||
|
other.current_size_ = 0;
|
||||||
|
other.finished_ = true;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool Result::is_valid() const { return handle_.is_valid(); }
|
||||||
|
inline Result::operator bool() const { return is_valid(); }
|
||||||
|
|
||||||
|
inline int Result::status() const {
|
||||||
|
return handle_.response ? handle_.response->status : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const Headers &Result::headers() const {
|
||||||
|
static const Headers empty_headers;
|
||||||
|
return handle_.response ? handle_.response->headers : empty_headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string Result::get_header_value(const std::string &key,
|
||||||
|
const char *def) const {
|
||||||
|
return handle_.response ? handle_.response->get_header_value(key, def) : def;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool Result::has_header(const std::string &key) const {
|
||||||
|
return handle_.response ? handle_.response->has_header(key) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Error Result::error() const { return handle_.error; }
|
||||||
|
inline Error Result::read_error() const { return handle_.get_read_error(); }
|
||||||
|
inline bool Result::has_read_error() const { return handle_.has_read_error(); }
|
||||||
|
|
||||||
|
inline bool Result::next() {
|
||||||
|
if (!handle_.is_valid() || finished_) { return false; }
|
||||||
|
|
||||||
|
if (buffer_.size() < chunk_size_) { buffer_.resize(chunk_size_); }
|
||||||
|
|
||||||
|
ssize_t n = handle_.read(&buffer_[0], chunk_size_);
|
||||||
|
if (n > 0) {
|
||||||
|
current_size_ = static_cast<size_t>(n);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_size_ = 0;
|
||||||
|
finished_ = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const char *Result::data() const { return buffer_.data(); }
|
||||||
|
inline size_t Result::size() const { return current_size_; }
|
||||||
|
|
||||||
|
inline std::string Result::read_all() {
|
||||||
|
std::string result;
|
||||||
|
while (next()) {
|
||||||
|
result.append(data(), size());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace stream
|
||||||
|
|
||||||
|
namespace sse {
|
||||||
|
|
||||||
|
// SSEMessage implementations
|
||||||
|
inline SSEMessage::SSEMessage() : event("message") {}
|
||||||
|
|
||||||
|
inline void SSEMessage::clear() {
|
||||||
|
event = "message";
|
||||||
|
data.clear();
|
||||||
|
id.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSEClient implementations
|
||||||
|
inline SSEClient::SSEClient(Client &client, const std::string &path)
|
||||||
|
: client_(client), path_(path) {}
|
||||||
|
|
||||||
|
inline SSEClient::SSEClient(Client &client, const std::string &path,
|
||||||
|
const Headers &headers)
|
||||||
|
: client_(client), path_(path), headers_(headers) {}
|
||||||
|
|
||||||
|
inline SSEClient::~SSEClient() { stop(); }
|
||||||
|
|
||||||
|
inline SSEClient &SSEClient::on_message(MessageHandler handler) {
|
||||||
|
on_message_ = std::move(handler);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline SSEClient &SSEClient::on_event(const std::string &type,
|
||||||
|
MessageHandler handler) {
|
||||||
|
event_handlers_[type] = std::move(handler);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline SSEClient &SSEClient::on_open(OpenHandler handler) {
|
||||||
|
on_open_ = std::move(handler);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline SSEClient &SSEClient::on_error(ErrorHandler handler) {
|
||||||
|
on_error_ = std::move(handler);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline SSEClient &SSEClient::set_reconnect_interval(int ms) {
|
||||||
|
reconnect_interval_ms_ = ms;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline SSEClient &SSEClient::set_max_reconnect_attempts(int n) {
|
||||||
|
max_reconnect_attempts_ = n;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool SSEClient::is_connected() const { return connected_.load(); }
|
||||||
|
|
||||||
|
inline const std::string &SSEClient::last_event_id() const {
|
||||||
|
return last_event_id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void SSEClient::start() {
|
||||||
|
running_.store(true);
|
||||||
|
run_event_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void SSEClient::start_async() {
|
||||||
|
running_.store(true);
|
||||||
|
async_thread_ = std::thread([this]() { run_event_loop(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void SSEClient::stop() {
|
||||||
|
running_.store(false);
|
||||||
|
client_.stop(); // Cancel any pending operations
|
||||||
|
if (async_thread_.joinable()) { async_thread_.join(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
||||||
|
int &retry_ms) {
|
||||||
|
// Blank line signals end of event
|
||||||
|
if (line.empty() || line == "\r") { return true; }
|
||||||
|
|
||||||
|
// Lines starting with ':' are comments (ignored)
|
||||||
|
if (!line.empty() && line[0] == ':') { return false; }
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto field = line.substr(0, colon_pos);
|
||||||
|
std::string value;
|
||||||
|
|
||||||
|
// Value starts after colon, skip optional single space
|
||||||
|
if (colon_pos + 1 < line.size()) {
|
||||||
|
auto 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") {
|
||||||
|
msg.event = value;
|
||||||
|
} else if (field == "data") {
|
||||||
|
// Multiple data lines are concatenated with newlines
|
||||||
|
if (!msg.data.empty()) { msg.data += "\n"; }
|
||||||
|
msg.data += value;
|
||||||
|
} else if (field == "id") {
|
||||||
|
// Empty id is valid (clears the last event ID)
|
||||||
|
msg.id = value;
|
||||||
|
} else if (field == "retry") {
|
||||||
|
// Parse retry interval in milliseconds
|
||||||
|
{
|
||||||
|
int v = 0;
|
||||||
|
auto res =
|
||||||
|
detail::from_chars(value.data(), value.data() + value.size(), v);
|
||||||
|
if (res.ec == std::errc{}) { retry_ms = v; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Unknown fields are ignored per SSE spec
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void SSEClient::run_event_loop() {
|
||||||
|
auto reconnect_count = 0;
|
||||||
|
|
||||||
|
while (running_.load()) {
|
||||||
|
// Build headers, including Last-Event-ID if we have one
|
||||||
|
auto request_headers = headers_;
|
||||||
|
if (!last_event_id_.empty()) {
|
||||||
|
request_headers.emplace("Last-Event-ID", last_event_id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open streaming connection
|
||||||
|
auto result = stream::Get(client_, path_, request_headers);
|
||||||
|
|
||||||
|
// Connection error handling
|
||||||
|
if (!result) {
|
||||||
|
connected_.store(false);
|
||||||
|
if (on_error_) { on_error_(result.error()); }
|
||||||
|
|
||||||
|
if (!should_reconnect(reconnect_count)) { break; }
|
||||||
|
wait_for_reconnect();
|
||||||
|
reconnect_count++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status() != 200) {
|
||||||
|
connected_.store(false);
|
||||||
|
// 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
|
||||||
|
if (on_error_) { on_error_(Error::Connection); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (on_error_) { on_error_(Error::Connection); }
|
||||||
|
|
||||||
|
if (!should_reconnect(reconnect_count)) { break; }
|
||||||
|
wait_for_reconnect();
|
||||||
|
reconnect_count++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection successful
|
||||||
|
connected_.store(true);
|
||||||
|
reconnect_count = 0;
|
||||||
|
if (on_open_) { on_open_(); }
|
||||||
|
|
||||||
|
// Event receiving loop
|
||||||
|
std::string buffer;
|
||||||
|
SSEMessage current_msg;
|
||||||
|
|
||||||
|
while (running_.load() && 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) {
|
||||||
|
auto line = buffer.substr(line_start, newline_pos - line_start);
|
||||||
|
line_start = newline_pos + 1;
|
||||||
|
|
||||||
|
// Parse the line and check if event is complete
|
||||||
|
auto event_complete =
|
||||||
|
parse_sse_line(line, current_msg, reconnect_interval_ms_);
|
||||||
|
|
||||||
|
if (event_complete && !current_msg.data.empty()) {
|
||||||
|
// Update last_event_id for reconnection
|
||||||
|
if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; }
|
||||||
|
|
||||||
|
// Dispatch event to appropriate handler
|
||||||
|
dispatch_event(current_msg);
|
||||||
|
|
||||||
|
current_msg.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep unprocessed data in buffer
|
||||||
|
buffer.erase(0, line_start);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection ended
|
||||||
|
connected_.store(false);
|
||||||
|
|
||||||
|
if (!running_.load()) { break; }
|
||||||
|
|
||||||
|
// Check for read errors
|
||||||
|
if (result.has_read_error()) {
|
||||||
|
if (on_error_) { on_error_(result.read_error()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!should_reconnect(reconnect_count)) { break; }
|
||||||
|
wait_for_reconnect();
|
||||||
|
reconnect_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
connected_.store(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void SSEClient::dispatch_event(const SSEMessage &msg) {
|
||||||
|
// Check for specific event type handler first
|
||||||
|
auto it = event_handlers_.find(msg.event);
|
||||||
|
if (it != event_handlers_.end()) {
|
||||||
|
it->second(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to generic message handler
|
||||||
|
if (on_message_) { on_message_(msg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool SSEClient::should_reconnect(int count) const {
|
||||||
|
if (!running_.load()) { return false; }
|
||||||
|
if (max_reconnect_attempts_ == 0) { return true; } // unlimited
|
||||||
|
return count < max_reconnect_attempts_;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void SSEClient::wait_for_reconnect() {
|
||||||
|
// Use small increments to check running_ flag frequently
|
||||||
|
auto waited = 0;
|
||||||
|
while (running_.load() && waited < reconnect_interval_ms_) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
waited += 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sse
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
/*
|
/*
|
||||||
* TLS abstraction layer - internal function declarations
|
* TLS abstraction layer - internal function declarations
|
||||||
|
|||||||
Reference in new issue
Block a user