|
|
@@ -0,0 +1,616 @@
|
|
|
+#include "smartbotic/webserver/http_server.hpp"
|
|
|
+
|
|
|
+#include <spdlog/spdlog.h>
|
|
|
+
|
|
|
+#include <chrono>
|
|
|
+#include <filesystem>
|
|
|
+#include <fstream>
|
|
|
+#include <tuple>
|
|
|
+#include <utility>
|
|
|
+
|
|
|
+namespace smartbotic::webserver {
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// WebSocketSession Implementation
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+WebSocketSession::WebSocketSession(tcp::socket socket, const HttpServerConfig& config)
|
|
|
+ : ws_(std::move(socket)), config_(config) {}
|
|
|
+
|
|
|
+WebSocketSession::~WebSocketSession() {
|
|
|
+ spdlog::debug("WebSocket session destroyed");
|
|
|
+}
|
|
|
+
|
|
|
+void WebSocketSession::Run(http::request<http::string_body> req) {
|
|
|
+ DoAccept(std::move(req));
|
|
|
+}
|
|
|
+
|
|
|
+void WebSocketSession::Send(const std::string& message) {
|
|
|
+ auto msg = std::make_shared<std::string const>(message);
|
|
|
+
|
|
|
+ std::lock_guard<std::mutex> lock(mutex_);
|
|
|
+ queue_.push_back(msg);
|
|
|
+
|
|
|
+ if (queue_.size() > 1) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ ws_.async_write(asio::buffer(*queue_.front()),
|
|
|
+ beast::bind_front_handler(&WebSocketSession::OnWrite, shared_from_this()));
|
|
|
+}
|
|
|
+
|
|
|
+void WebSocketSession::Close() {
|
|
|
+ beast::error_code ec;
|
|
|
+ ws_.close(websocket::close_code::normal, ec);
|
|
|
+}
|
|
|
+
|
|
|
+auto WebSocketSession::IsOpen() const -> bool {
|
|
|
+ return ws_.is_open();
|
|
|
+}
|
|
|
+
|
|
|
+void WebSocketSession::SetMessageHandler(WebSocketMessageHandler handler) {
|
|
|
+ message_handler_ = std::move(handler);
|
|
|
+}
|
|
|
+
|
|
|
+void WebSocketSession::DoAccept(http::request<http::string_body> req) { // NOLINT(performance-unnecessary-value-param)
|
|
|
+ ws_.set_option(websocket::stream_base::timeout::suggested(beast::role_type::server));
|
|
|
+
|
|
|
+ ws_.set_option(websocket::stream_base::decorator(
|
|
|
+ [](websocket::response_type& res) { res.set(http::field::server, "SmartBotic-Server/0.1.0"); }));
|
|
|
+
|
|
|
+ ws_.async_accept(req, beast::bind_front_handler(&WebSocketSession::OnAccept, shared_from_this()));
|
|
|
+}
|
|
|
+
|
|
|
+void WebSocketSession::OnAccept(beast::error_code ec) {
|
|
|
+ if (ec) {
|
|
|
+ spdlog::error("WebSocket accept error: {}", ec.message());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ spdlog::info("WebSocket connection established");
|
|
|
+ DoRead();
|
|
|
+}
|
|
|
+
|
|
|
+void WebSocketSession::DoRead() {
|
|
|
+ ws_.async_read(buffer_, beast::bind_front_handler(&WebSocketSession::OnRead, shared_from_this()));
|
|
|
+}
|
|
|
+
|
|
|
+void WebSocketSession::OnRead(beast::error_code ec, std::size_t bytes_transferred) {
|
|
|
+ if (ec == websocket::error::closed) {
|
|
|
+ spdlog::info("WebSocket connection closed by client");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ec) {
|
|
|
+ spdlog::error("WebSocket read error: {}", ec.message());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string message = beast::buffers_to_string(buffer_.data());
|
|
|
+ buffer_.consume(buffer_.size());
|
|
|
+
|
|
|
+ spdlog::debug("WebSocket received: {} bytes", bytes_transferred);
|
|
|
+
|
|
|
+ if (message_handler_) {
|
|
|
+ message_handler_(*this, message);
|
|
|
+ }
|
|
|
+
|
|
|
+ DoRead();
|
|
|
+}
|
|
|
+
|
|
|
+void WebSocketSession::OnWrite(beast::error_code ec, std::size_t bytes_transferred) {
|
|
|
+ if (ec) {
|
|
|
+ spdlog::error("WebSocket write error: {}", ec.message());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ spdlog::debug("WebSocket sent: {} bytes", bytes_transferred);
|
|
|
+
|
|
|
+ std::lock_guard<std::mutex> lock(mutex_);
|
|
|
+ queue_.erase(queue_.begin());
|
|
|
+
|
|
|
+ if (!queue_.empty()) {
|
|
|
+ ws_.async_write(asio::buffer(*queue_.front()),
|
|
|
+ beast::bind_front_handler(&WebSocketSession::OnWrite, shared_from_this()));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// HttpSession Implementation
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+HttpSession::HttpSession(tcp::socket socket, const HttpServerConfig& config,
|
|
|
+ std::function<void(std::shared_ptr<WebSocketSession>)> ws_connect_handler,
|
|
|
+ std::function<void(std::shared_ptr<WebSocketSession>)> ws_disconnect_handler)
|
|
|
+ : stream_(std::move(socket)),
|
|
|
+ config_(config),
|
|
|
+ ws_connect_handler_(std::move(ws_connect_handler)),
|
|
|
+ ws_disconnect_handler_(std::move(ws_disconnect_handler)) {}
|
|
|
+
|
|
|
+HttpSession::~HttpSession() {
|
|
|
+ spdlog::debug("HTTP session destroyed");
|
|
|
+}
|
|
|
+
|
|
|
+void HttpSession::Run() {
|
|
|
+ asio::dispatch(stream_.get_executor(), beast::bind_front_handler(&HttpSession::DoRead, shared_from_this()));
|
|
|
+}
|
|
|
+
|
|
|
+void HttpSession::DoRead() {
|
|
|
+ req_ = {};
|
|
|
+ stream_.expires_after(std::chrono::seconds(30));
|
|
|
+ http::async_read(stream_, buffer_, req_, beast::bind_front_handler(&HttpSession::OnRead, shared_from_this()));
|
|
|
+}
|
|
|
+
|
|
|
+void HttpSession::OnRead(beast::error_code ec, std::size_t bytes_transferred) {
|
|
|
+ std::ignore = bytes_transferred;
|
|
|
+
|
|
|
+ if (ec == http::error::end_of_stream) {
|
|
|
+ std::ignore = stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ec) {
|
|
|
+ spdlog::error("HTTP read error: {}", ec.message());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Capture request info for logging
|
|
|
+ request_start_ = std::chrono::steady_clock::now();
|
|
|
+ request_method_ = std::string(req_.method_string());
|
|
|
+ request_target_ = std::string(req_.target());
|
|
|
+
|
|
|
+ HandleRequest(std::move(req_));
|
|
|
+}
|
|
|
+
|
|
|
+void HttpSession::HandleRequest(http::request<http::string_body>&& req) {
|
|
|
+ std::string target(req.target());
|
|
|
+
|
|
|
+ // Handle CORS preflight
|
|
|
+ if (req.method() == http::verb::options && config_.cors.enabled) {
|
|
|
+ auto res = http::response<http::string_body>{http::status::no_content, req.version()};
|
|
|
+ res.set(http::field::server, "SmartBotic-Server/0.1.0");
|
|
|
+ ApplyCorsHeaders(res);
|
|
|
+ res.prepare_payload();
|
|
|
+ SendResponse(std::move(res));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check for WebSocket upgrade
|
|
|
+ if (websocket::is_upgrade(req) && target == config_.ws_path) {
|
|
|
+ spdlog::info("WebSocket upgrade request for {}", target);
|
|
|
+
|
|
|
+ auto ws_session = std::make_shared<WebSocketSession>(stream_.release_socket(), config_);
|
|
|
+
|
|
|
+ if (ws_connect_handler_) {
|
|
|
+ ws_connect_handler_(ws_session);
|
|
|
+ }
|
|
|
+
|
|
|
+ ws_session->Run(std::move(req));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ http::response<http::string_body> res;
|
|
|
+
|
|
|
+ // Route API requests
|
|
|
+ if (target.starts_with("/api/")) {
|
|
|
+ res = HandleApiRequest(req);
|
|
|
+ }
|
|
|
+ // Serve static files for WebUI
|
|
|
+ else {
|
|
|
+ res = HandleStaticFile(target);
|
|
|
+ }
|
|
|
+
|
|
|
+ res.version(req.version());
|
|
|
+ res.set(http::field::server, "SmartBotic-Server/0.1.0");
|
|
|
+
|
|
|
+ if (config_.cors.enabled) {
|
|
|
+ ApplyCorsHeaders(res);
|
|
|
+ }
|
|
|
+
|
|
|
+ res.prepare_payload();
|
|
|
+ SendResponse(std::move(res));
|
|
|
+}
|
|
|
+
|
|
|
+auto HttpSession::HandleStaticFile(const std::string& target) -> http::response<http::string_body> {
|
|
|
+ std::string path = target;
|
|
|
+
|
|
|
+ // Default to index.html for root
|
|
|
+ if (path == "/" || path.empty()) {
|
|
|
+ path = "/index.html";
|
|
|
+ }
|
|
|
+
|
|
|
+ // Security: prevent directory traversal
|
|
|
+ if (path.find("..") != std::string::npos) {
|
|
|
+ return MakeErrorResponse(http::status::forbidden, "Invalid path");
|
|
|
+ }
|
|
|
+
|
|
|
+ std::filesystem::path file_path = config_.webui_path + path;
|
|
|
+
|
|
|
+ // If path is a directory, try index.html
|
|
|
+ if (std::filesystem::is_directory(file_path)) {
|
|
|
+ file_path /= "index.html";
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!std::filesystem::exists(file_path)) {
|
|
|
+ // For SPA routing, return index.html for non-file paths
|
|
|
+ std::filesystem::path index_path = config_.webui_path + "/index.html";
|
|
|
+ if (std::filesystem::exists(index_path)) {
|
|
|
+ file_path = index_path;
|
|
|
+ } else {
|
|
|
+ return MakeErrorResponse(http::status::not_found, "File not found");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Read file content
|
|
|
+ std::ifstream file(file_path, std::ios::binary);
|
|
|
+ if (!file) {
|
|
|
+ return MakeErrorResponse(http::status::internal_server_error, "Failed to read file");
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
|
|
+
|
|
|
+ http::response<http::string_body> res{http::status::ok, 11};
|
|
|
+ res.set(http::field::content_type, GetMimeType(file_path.string()));
|
|
|
+ res.body() = std::move(content);
|
|
|
+
|
|
|
+ return res;
|
|
|
+}
|
|
|
+
|
|
|
+auto HttpSession::HandleApiRequest(http::request<http::string_body>& req) -> http::response<http::string_body> {
|
|
|
+ std::string target(req.target());
|
|
|
+
|
|
|
+ // Health check endpoint
|
|
|
+ if (target == "/api/health" && req.method() == http::verb::get) {
|
|
|
+ http::response<http::string_body> res{http::status::ok, req.version()};
|
|
|
+ res.set(http::field::content_type, "application/json");
|
|
|
+ res.body() = R"({"status":"healthy","service":"smartbotic-server"})";
|
|
|
+ return res;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Version endpoint
|
|
|
+ if (target == "/api/version" && req.method() == http::verb::get) {
|
|
|
+ http::response<http::string_body> res{http::status::ok, req.version()};
|
|
|
+ res.set(http::field::content_type, "application/json");
|
|
|
+ res.body() = R"({"version":"0.1.0","name":"SmartBotic Web Server"})";
|
|
|
+ return res;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Default: not found
|
|
|
+ return MakeErrorResponse(http::status::not_found, "API endpoint not found");
|
|
|
+}
|
|
|
+
|
|
|
+auto HttpSession::MakeErrorResponse(
|
|
|
+ http::status status, const std::string& message) // NOLINT(readability-convert-member-functions-to-static)
|
|
|
+ -> http::response<http::string_body> {
|
|
|
+ http::response<http::string_body> res{status, 11};
|
|
|
+ res.set(http::field::content_type, "application/json");
|
|
|
+ res.body() = R"({"error":")" + message + R"("})";
|
|
|
+ return res;
|
|
|
+}
|
|
|
+
|
|
|
+void HttpSession::ApplyCorsHeaders(http::response<http::string_body>& res) {
|
|
|
+ if (!config_.cors.enabled) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Combine allowed origins
|
|
|
+ std::string origins;
|
|
|
+ for (size_t i = 0; i < config_.cors.allowed_origins.size(); ++i) {
|
|
|
+ if (i > 0) {
|
|
|
+ origins += ", ";
|
|
|
+ }
|
|
|
+ origins += config_.cors.allowed_origins[i];
|
|
|
+ }
|
|
|
+ res.set(http::field::access_control_allow_origin, origins);
|
|
|
+
|
|
|
+ // Combine allowed methods
|
|
|
+ std::string methods;
|
|
|
+ for (size_t i = 0; i < config_.cors.allowed_methods.size(); ++i) {
|
|
|
+ if (i > 0) {
|
|
|
+ methods += ", ";
|
|
|
+ }
|
|
|
+ methods += config_.cors.allowed_methods[i];
|
|
|
+ }
|
|
|
+ res.set(http::field::access_control_allow_methods, methods);
|
|
|
+
|
|
|
+ // Combine allowed headers
|
|
|
+ std::string headers;
|
|
|
+ for (size_t i = 0; i < config_.cors.allowed_headers.size(); ++i) {
|
|
|
+ if (i > 0) {
|
|
|
+ headers += ", ";
|
|
|
+ }
|
|
|
+ headers += config_.cors.allowed_headers[i];
|
|
|
+ }
|
|
|
+ res.set(http::field::access_control_allow_headers, headers);
|
|
|
+
|
|
|
+ if (config_.cors.allow_credentials) {
|
|
|
+ res.set(http::field::access_control_allow_credentials, "true");
|
|
|
+ }
|
|
|
+
|
|
|
+ res.set(http::field::access_control_max_age, std::to_string(config_.cors.max_age));
|
|
|
+}
|
|
|
+
|
|
|
+auto HttpSession::GetMimeType(const std::string& path)
|
|
|
+ -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
|
|
+ auto ext_pos = path.rfind('.');
|
|
|
+ if (ext_pos == std::string::npos) {
|
|
|
+ return "application/octet-stream";
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string ext = path.substr(ext_pos);
|
|
|
+
|
|
|
+ if (ext == ".html" || ext == ".htm") {
|
|
|
+ return "text/html";
|
|
|
+ }
|
|
|
+ if (ext == ".css") {
|
|
|
+ return "text/css";
|
|
|
+ }
|
|
|
+ if (ext == ".js") {
|
|
|
+ return "application/javascript";
|
|
|
+ }
|
|
|
+ if (ext == ".json") {
|
|
|
+ return "application/json";
|
|
|
+ }
|
|
|
+ if (ext == ".png") {
|
|
|
+ return "image/png";
|
|
|
+ }
|
|
|
+ if (ext == ".jpg" || ext == ".jpeg") {
|
|
|
+ return "image/jpeg";
|
|
|
+ }
|
|
|
+ if (ext == ".gif") {
|
|
|
+ return "image/gif";
|
|
|
+ }
|
|
|
+ if (ext == ".svg") {
|
|
|
+ return "image/svg+xml";
|
|
|
+ }
|
|
|
+ if (ext == ".ico") {
|
|
|
+ return "image/x-icon";
|
|
|
+ }
|
|
|
+ if (ext == ".woff") {
|
|
|
+ return "font/woff";
|
|
|
+ }
|
|
|
+ if (ext == ".woff2") {
|
|
|
+ return "font/woff2";
|
|
|
+ }
|
|
|
+ if (ext == ".ttf") {
|
|
|
+ return "font/ttf";
|
|
|
+ }
|
|
|
+ if (ext == ".eot") {
|
|
|
+ return "application/vnd.ms-fontobject";
|
|
|
+ }
|
|
|
+ if (ext == ".txt") {
|
|
|
+ return "text/plain";
|
|
|
+ }
|
|
|
+ if (ext == ".xml") {
|
|
|
+ return "application/xml";
|
|
|
+ }
|
|
|
+ if (ext == ".pdf") {
|
|
|
+ return "application/pdf";
|
|
|
+ }
|
|
|
+
|
|
|
+ return "application/octet-stream";
|
|
|
+}
|
|
|
+
|
|
|
+void HttpSession::SendResponse(http::response<http::string_body>&& res) {
|
|
|
+ auto sp = std::make_shared<http::response<http::string_body>>(std::move(res));
|
|
|
+ auto status = sp->result();
|
|
|
+ auto self = shared_from_this();
|
|
|
+
|
|
|
+ http::async_write(stream_, *sp, [self, sp, status](beast::error_code ec, std::size_t bytes_transferred) {
|
|
|
+ self->LogRequest(status);
|
|
|
+ self->OnWrite(ec, bytes_transferred, sp->need_eof());
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+void HttpSession::LogRequest(http::status status) {
|
|
|
+ auto now = std::chrono::steady_clock::now();
|
|
|
+ auto duration_us = std::chrono::duration_cast<std::chrono::microseconds>(now - request_start_).count();
|
|
|
+
|
|
|
+ // Format duration as ms with microsecond precision
|
|
|
+ double duration_ms = static_cast<double>(duration_us) / 1000.0;
|
|
|
+
|
|
|
+ auto status_int = static_cast<unsigned>(status);
|
|
|
+
|
|
|
+ // Log at appropriate level based on status code
|
|
|
+ if (status_int >= 500) {
|
|
|
+ spdlog::error("{} {} {} {:.2f}ms", request_method_, request_target_, status_int, duration_ms);
|
|
|
+ } else if (status_int >= 400) {
|
|
|
+ spdlog::warn("{} {} {} {:.2f}ms", request_method_, request_target_, status_int, duration_ms);
|
|
|
+ } else {
|
|
|
+ spdlog::info("{} {} {} {:.2f}ms", request_method_, request_target_, status_int, duration_ms);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void HttpSession::OnWrite(beast::error_code ec, std::size_t bytes_transferred, bool close) {
|
|
|
+ std::ignore = bytes_transferred;
|
|
|
+
|
|
|
+ if (ec) {
|
|
|
+ spdlog::error("HTTP write error: {}", ec.message());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (close) {
|
|
|
+ beast::error_code ec_close;
|
|
|
+ std::ignore = stream_.socket().shutdown(tcp::socket::shutdown_send, ec_close);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ DoRead();
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// HttpServer Implementation
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+HttpServer::HttpServer(HttpServerConfig config) : config_(std::move(config)), ioc_(1), acceptor_(ioc_) {}
|
|
|
+
|
|
|
+HttpServer::~HttpServer() {
|
|
|
+ if (running_.load()) {
|
|
|
+ Stop();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+auto HttpServer::Start() -> bool {
|
|
|
+ if (running_.load()) {
|
|
|
+ spdlog::warn("Server already running");
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ spdlog::info("Starting HTTP server on {}", config_.GetListenAddress());
|
|
|
+
|
|
|
+ try {
|
|
|
+ auto const kAddress = asio::ip::make_address(config_.address);
|
|
|
+ tcp::endpoint endpoint{kAddress, config_.port};
|
|
|
+
|
|
|
+ beast::error_code ec;
|
|
|
+
|
|
|
+ acceptor_.open(endpoint.protocol(), ec);
|
|
|
+ if (ec) {
|
|
|
+ spdlog::error("Failed to open acceptor: {}", ec.message());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ acceptor_.set_option(asio::socket_base::reuse_address(true), ec);
|
|
|
+ if (ec) {
|
|
|
+ spdlog::error("Failed to set reuse_address: {}", ec.message());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ acceptor_.bind(endpoint, ec);
|
|
|
+ if (ec) {
|
|
|
+ spdlog::error("Failed to bind to {}: {}", config_.GetListenAddress(), ec.message());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ acceptor_.listen(asio::socket_base::max_listen_connections, ec);
|
|
|
+ if (ec) {
|
|
|
+ spdlog::error("Failed to listen: {}", ec.message());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ running_.store(true);
|
|
|
+ DoAccept();
|
|
|
+
|
|
|
+ // Start the I/O context in a thread
|
|
|
+ threads_.emplace_back([this]() { ioc_.run(); });
|
|
|
+
|
|
|
+ spdlog::info("HTTP server started successfully on {}", config_.GetListenAddress());
|
|
|
+ spdlog::info("WebSocket endpoint: ws://{}{}", config_.GetListenAddress(), config_.ws_path);
|
|
|
+ spdlog::info("Static files: {}", config_.webui_path);
|
|
|
+
|
|
|
+ return true;
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ spdlog::error("Failed to start HTTP server: {}", e.what());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void HttpServer::Stop() {
|
|
|
+ if (!running_.load()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ spdlog::info("Stopping HTTP server...");
|
|
|
+
|
|
|
+ running_.store(false);
|
|
|
+
|
|
|
+ // Close all WebSocket sessions
|
|
|
+ {
|
|
|
+ std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
|
|
|
+ for (const auto& session : ws_sessions_) {
|
|
|
+ if (session->IsOpen()) {
|
|
|
+ session->Close();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ws_sessions_.clear();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Stop accepting new connections
|
|
|
+ beast::error_code ec;
|
|
|
+ std::ignore = acceptor_.close(ec);
|
|
|
+
|
|
|
+ // Stop the I/O context
|
|
|
+ ioc_.stop();
|
|
|
+
|
|
|
+ // Wait for threads to finish
|
|
|
+ for (auto& thread : threads_) {
|
|
|
+ if (thread.joinable()) {
|
|
|
+ thread.join();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ threads_.clear();
|
|
|
+
|
|
|
+ spdlog::info("HTTP server stopped");
|
|
|
+}
|
|
|
+
|
|
|
+void HttpServer::Wait() {
|
|
|
+ for (auto& thread : threads_) {
|
|
|
+ if (thread.joinable()) {
|
|
|
+ thread.join();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void HttpServer::BroadcastWebSocket(const std::string& message) {
|
|
|
+ std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
|
|
|
+ for (const auto& session : ws_sessions_) {
|
|
|
+ if (session->IsOpen()) {
|
|
|
+ session->Send(message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void HttpServer::SetWebSocketMessageHandler(WebSocketMessageHandler handler) {
|
|
|
+ ws_message_handler_ = std::move(handler);
|
|
|
+}
|
|
|
+
|
|
|
+auto HttpServer::GetWebSocketClientCount() const -> size_t {
|
|
|
+ std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
|
|
|
+ return ws_sessions_.size();
|
|
|
+}
|
|
|
+
|
|
|
+void HttpServer::DoAccept() {
|
|
|
+ acceptor_.async_accept(ioc_, beast::bind_front_handler(&HttpServer::OnAccept, this));
|
|
|
+}
|
|
|
+
|
|
|
+void HttpServer::OnAccept(beast::error_code ec, tcp::socket socket) {
|
|
|
+ if (ec) {
|
|
|
+ if (running_.load()) {
|
|
|
+ spdlog::error("Accept error: {}", ec.message());
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ auto endpoint = socket.remote_endpoint(ec);
|
|
|
+ if (!ec) {
|
|
|
+ spdlog::debug("New connection from {}:{}", endpoint.address().to_string(), endpoint.port());
|
|
|
+ }
|
|
|
+
|
|
|
+ auto session = std::make_shared<HttpSession>(
|
|
|
+ std::move(socket), config_, [this](std::shared_ptr<WebSocketSession> ws) { OnWebSocketConnect(std::move(ws)); },
|
|
|
+ [this](std::shared_ptr<WebSocketSession> ws) { OnWebSocketDisconnect(std::move(ws)); });
|
|
|
+
|
|
|
+ session->Run();
|
|
|
+
|
|
|
+ if (running_.load()) {
|
|
|
+ DoAccept();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void HttpServer::OnWebSocketConnect(const std::shared_ptr<WebSocketSession>& session) {
|
|
|
+ if (ws_message_handler_) {
|
|
|
+ session->SetMessageHandler(ws_message_handler_);
|
|
|
+ }
|
|
|
+
|
|
|
+ std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
|
|
|
+ ws_sessions_.insert(session);
|
|
|
+ spdlog::info("WebSocket client connected (total: {})", ws_sessions_.size());
|
|
|
+}
|
|
|
+
|
|
|
+void HttpServer::OnWebSocketDisconnect(const std::shared_ptr<WebSocketSession>& session) {
|
|
|
+ std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
|
|
|
+ ws_sessions_.erase(session);
|
|
|
+ spdlog::info("WebSocket client disconnected (total: {})", ws_sessions_.size());
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace smartbotic::webserver
|