init
This commit is contained in:
commit
70d467d052
97 files changed
+10386
No files matched your search
@@ -0,0 +1,69 @@
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Linker files
|
||||
*.ilk
|
||||
|
||||
# Debugger Files
|
||||
*.pdb
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
*.so.*
|
||||
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# Build directories
|
||||
build/
|
||||
Build/
|
||||
build-*/
|
||||
|
||||
# CMake generated files
|
||||
CMakeFiles/
|
||||
CMakeCache.txt
|
||||
cmake_install.cmake
|
||||
#Makefile
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.log
|
||||
*.bak
|
||||
*.swp
|
||||
|
||||
# vcpkg
|
||||
vcpkg_installed/
|
||||
|
||||
# debug information files
|
||||
*.dwo
|
||||
|
||||
# test output & cache
|
||||
Testing/
|
||||
.cache/
|
||||
@@ -0,0 +1,145 @@
|
||||
cmake_minimum_required(VERSION 3.12)
|
||||
project(us VERSION 0.0.1)
|
||||
|
||||
# Set C++ standard
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
# Set compiler flags based on compiler type
|
||||
if(MSVC)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /O2 /W4 /bigobj")
|
||||
else()
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -pipe -Wall")
|
||||
endif()
|
||||
|
||||
# Set library names
|
||||
set(SHARED_LIB_NAME "us_http")
|
||||
set(STATIC_LIB_NAME "us_http_static")
|
||||
set(FRAMEWORK_NAME "us")
|
||||
|
||||
# ===== Boost Library Configuration (Header-Only) =====
|
||||
if(DEFINED ENV{BOOST_ROOT})
|
||||
set(BOOST_ROOT $ENV{BOOST_ROOT})
|
||||
message(STATUS "Using BOOST_ROOT from environment: ${BOOST_ROOT}")
|
||||
elseif(NOT DEFINED BOOST_ROOT)
|
||||
set(BOOST_ROOT "C:/local/boost_1_91_0" CACHE PATH "Boost installation root")
|
||||
endif()
|
||||
|
||||
message(STATUS "BOOST_ROOT: ${BOOST_ROOT}")
|
||||
|
||||
set(Boost_INCLUDE_DIR "${BOOST_ROOT}" CACHE PATH "Boost include directory")
|
||||
message(STATUS "Boost include directory: ${Boost_INCLUDE_DIR}")
|
||||
|
||||
if(NOT EXISTS ${Boost_INCLUDE_DIR})
|
||||
message(FATAL_ERROR "Boost include directory not found: ${Boost_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
include_directories(${Boost_INCLUDE_DIR})
|
||||
|
||||
# Include directory (project headers)
|
||||
set(INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/inc")
|
||||
include_directories(${INCLUDE_DIR})
|
||||
|
||||
# Source files
|
||||
set(SOURCES
|
||||
src/HttpServer.cpp
|
||||
src/WebSocketServer.cpp
|
||||
src/WebSocketClient.cpp
|
||||
)
|
||||
|
||||
# Header files list
|
||||
set(HEADERS
|
||||
inc/${FRAMEWORK_NAME}/HttpServer.hpp
|
||||
inc/${FRAMEWORK_NAME}/HttpClient.hpp
|
||||
inc/${FRAMEWORK_NAME}/WebSocketServer.hpp
|
||||
inc/${FRAMEWORK_NAME}/WebSocketClient.hpp
|
||||
)
|
||||
|
||||
# Create shared library (DLL)
|
||||
add_library(${SHARED_LIB_NAME} SHARED ${SOURCES})
|
||||
|
||||
# Create static library (LIB)
|
||||
add_library(${STATIC_LIB_NAME} STATIC ${SOURCES})
|
||||
|
||||
# Set properties for shared library
|
||||
if(WIN32)
|
||||
target_link_libraries(${SHARED_LIB_NAME}
|
||||
ws2_32
|
||||
)
|
||||
else()
|
||||
target_link_libraries(${SHARED_LIB_NAME}
|
||||
pthread
|
||||
)
|
||||
endif()
|
||||
|
||||
# Set output directories
|
||||
set_target_properties(${SHARED_LIB_NAME} ${STATIC_LIB_NAME} PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/build"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/build"
|
||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/build"
|
||||
)
|
||||
|
||||
# Additional properties for Windows
|
||||
if(WIN32)
|
||||
set_target_properties(${SHARED_LIB_NAME} PROPERTIES
|
||||
PREFIX ""
|
||||
SUFFIX ".dll"
|
||||
)
|
||||
set_target_properties(${STATIC_LIB_NAME} PROPERTIES
|
||||
PREFIX ""
|
||||
SUFFIX ".lib"
|
||||
)
|
||||
else()
|
||||
set_target_properties(${SHARED_LIB_NAME} PROPERTIES
|
||||
PREFIX "lib"
|
||||
SUFFIX ".so"
|
||||
)
|
||||
set_target_properties(${STATIC_LIB_NAME} PROPERTIES
|
||||
PREFIX "lib"
|
||||
SUFFIX ".a"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Position independent code for static library on Unix
|
||||
if(UNIX AND NOT APPLE)
|
||||
set_target_properties(${STATIC_LIB_NAME} PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
set_target_properties(${SHARED_LIB_NAME} PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
endif()
|
||||
|
||||
# Install targets
|
||||
install(TARGETS ${SHARED_LIB_NAME} ${STATIC_LIB_NAME}
|
||||
LIBRARY DESTINATION lib
|
||||
RUNTIME DESTINATION bin
|
||||
ARCHIVE DESTINATION lib
|
||||
)
|
||||
|
||||
# Install headers
|
||||
install(FILES ${HEADERS}
|
||||
DESTINATION include/${FRAMEWORK_NAME}
|
||||
)
|
||||
|
||||
# Clean target
|
||||
add_custom_target(clear
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory "${CMAKE_CURRENT_BINARY_DIR}/build"
|
||||
COMMENT "Cleaning build directory..."
|
||||
)
|
||||
|
||||
# Print configuration info
|
||||
message(STATUS "========================================")
|
||||
message(STATUS "Project: ${PROJECT_NAME}")
|
||||
message(STATUS "Version: ${PROJECT_VERSION}")
|
||||
message(STATUS "Shared library: ${SHARED_LIB_NAME}")
|
||||
message(STATUS "Static library: ${STATIC_LIB_NAME}")
|
||||
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
|
||||
message(STATUS "Install prefix: ${CMAKE_INSTALL_PREFIX}")
|
||||
message(STATUS "Compiler: ${CMAKE_CXX_COMPILER_ID}")
|
||||
if(MSVC)
|
||||
message(STATUS "MSVC version: ${MSVC_VERSION}")
|
||||
endif()
|
||||
message(STATUS "BOOST_ROOT: ${BOOST_ROOT}")
|
||||
message(STATUS "========================================")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 dashun
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,49 @@
|
||||
VERSION := 0.0.1
|
||||
FRAMEWORK_NAME := us
|
||||
LIB_NAME := libus_http
|
||||
INSTL_TDIR := /lib
|
||||
INCLU_TDIR := /usr/include
|
||||
INCLU_SDIR := inc
|
||||
CPP := g++
|
||||
INCS := -I inc
|
||||
CPPFLAGS := --std=c++20 -O3 -pipe -Wall -Werror
|
||||
LIBS :=
|
||||
BUILD_TDIR := build
|
||||
$(shell mkdir -p $(BUILD_TDIR))
|
||||
|
||||
#--------
|
||||
all: $(BUILD_TDIR)/$(LIB_NAME).so
|
||||
|
||||
$(BUILD_TDIR)/$(LIB_NAME).so: $(BUILD_TDIR)/HttpServer.o #$(BUILD_TDIR)/WebSocketServer.o $(BUILD_TDIR)/WebSocketClient.o
|
||||
$(CPP) -shared $^ $(LIBS) -o $@
|
||||
|
||||
$(BUILD_TDIR)/HttpServer.o: src/HttpServer.cpp
|
||||
$(CPP) $(CPPFLAGS) -fPIC -c $^ $(INCS) -o $@
|
||||
|
||||
$(BUILD_TDIR)/WebSocketServer.o: src/WebSocketServer.cpp
|
||||
$(CPP) $(CPPFLAGS) -fPIC -c $^ $(INCS) -o $@
|
||||
|
||||
$(BUILD_TDIR)/WebSocketClient.o: src/WebSocketClient.cpp
|
||||
$(CPP) $(CPPFLAGS) -fPIC -c $^ $(INCS) -o $@
|
||||
|
||||
clear:
|
||||
@rm -rvf $(BUILD_TDIR)
|
||||
|
||||
install:
|
||||
@echo "install UniServe_libth..."
|
||||
@mkdir -vp $(INCLU_TDIR)/$(FRAMEWORK_NAME)
|
||||
@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/HttpServer.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/HttpClient.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
#@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/WebSocketServer.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
#@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/WebSocketClient.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/
|
||||
@cp -vf $(BUILD_TDIR)/$(LIB_NAME).so $(INSTL_TDIR)/
|
||||
@echo "done."
|
||||
|
||||
uninstall:
|
||||
@echo "uninstall UniServe_libth..."
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/HttpServer.hpp
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/HttpClient.hpp
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/WebSocketServer.hpp
|
||||
@rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/WebSocketClient.hpp
|
||||
@rm -vf $(INSTL_TDIR)/$(LIB_NAME).so
|
||||
@echo "done."
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
make clear
|
||||
make
|
||||
make install
|
||||
@@ -0,0 +1,200 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <curl/curl.h>
|
||||
|
||||
namespace pmc::net {
|
||||
|
||||
enum class HttpMethod {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
PATCH,
|
||||
HEAD,
|
||||
OPTIONS
|
||||
};
|
||||
|
||||
class HttpClient {
|
||||
public:
|
||||
HttpClient() {
|
||||
curl_ = curl_easy_init();
|
||||
if (!curl_) {
|
||||
throw std::runtime_error("Failed to initialize CURL");
|
||||
}
|
||||
}
|
||||
|
||||
~HttpClient() {
|
||||
if (curl_) {
|
||||
curl_easy_cleanup(curl_);
|
||||
}
|
||||
if (headers_) {
|
||||
curl_slist_free_all(headers_);
|
||||
}
|
||||
}
|
||||
|
||||
void set_header(const std::string& header) {
|
||||
headers_ = curl_slist_append(headers_, header.c_str());
|
||||
}
|
||||
|
||||
void clear_headers() {
|
||||
if (headers_) {
|
||||
curl_slist_free_all(headers_);
|
||||
headers_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
const std::map<std::string, std::string>& get_response_headers() const {
|
||||
return response_headers_;
|
||||
}
|
||||
|
||||
long get_response_code() const {
|
||||
return response_code_;
|
||||
}
|
||||
|
||||
// ===== 修复:直接转发,不按行处理 =====
|
||||
void request_stream_sync(
|
||||
HttpMethod method,
|
||||
const std::string& url,
|
||||
const std::string& data,
|
||||
std::function<void(const std::string&)> callback
|
||||
) {
|
||||
if (!curl_) {
|
||||
throw std::runtime_error("CURL not initialized");
|
||||
}
|
||||
|
||||
response_headers_.clear();
|
||||
response_code_ = 0;
|
||||
|
||||
curl_easy_reset(curl_);
|
||||
curl_easy_setopt(curl_, CURLOPT_URL, url.c_str());
|
||||
set_http_method(method, data);
|
||||
curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers_);
|
||||
curl_easy_setopt(curl_, CURLOPT_HEADERFUNCTION, header_callback);
|
||||
curl_easy_setopt(curl_, CURLOPT_HEADERDATA, this);
|
||||
|
||||
// ===== 关键修复:直接收集原始数据,不按行处理 =====
|
||||
std::string full_response;
|
||||
curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &full_response);
|
||||
|
||||
curl_easy_setopt(curl_, CURLOPT_TIMEOUT, 30L);
|
||||
curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT, 10L);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl_);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res));
|
||||
}
|
||||
|
||||
curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &response_code_);
|
||||
|
||||
// ===== 完整转发所有数据(包括换行符) =====
|
||||
if (!full_response.empty()) {
|
||||
callback(full_response);
|
||||
}
|
||||
}
|
||||
|
||||
void get_stream_sync(const std::string& url, std::function<void(const std::string&)> callback) {
|
||||
request_stream_sync(HttpMethod::GET, url, "", callback);
|
||||
}
|
||||
|
||||
void post_stream_sync(const std::string& url, const std::string& data, std::function<void(const std::string&)> callback) {
|
||||
request_stream_sync(HttpMethod::POST, url, data, callback);
|
||||
}
|
||||
|
||||
void put_stream_sync(const std::string& url, const std::string& data, std::function<void(const std::string&)> callback) {
|
||||
request_stream_sync(HttpMethod::PUT, url, data, callback);
|
||||
}
|
||||
|
||||
void delete_stream_sync(const std::string& url, std::function<void(const std::string&)> callback) {
|
||||
request_stream_sync(HttpMethod::DELETE, url, "", callback);
|
||||
}
|
||||
|
||||
private:
|
||||
CURL* curl_ = nullptr;
|
||||
struct curl_slist* headers_ = nullptr;
|
||||
std::map<std::string, std::string> response_headers_;
|
||||
long response_code_ = 0;
|
||||
|
||||
void set_http_method(HttpMethod method, const std::string& data) {
|
||||
switch (method) {
|
||||
case HttpMethod::GET:
|
||||
curl_easy_setopt(curl_, CURLOPT_HTTPGET, 1L);
|
||||
break;
|
||||
|
||||
case HttpMethod::POST:
|
||||
curl_easy_setopt(curl_, CURLOPT_POST, 1L);
|
||||
if (!data.empty()) {
|
||||
curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str());
|
||||
curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, data.size());
|
||||
}
|
||||
break;
|
||||
|
||||
case HttpMethod::PUT:
|
||||
curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "PUT");
|
||||
if (!data.empty()) {
|
||||
curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str());
|
||||
curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, data.size());
|
||||
}
|
||||
break;
|
||||
|
||||
case HttpMethod::DELETE:
|
||||
curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "DELETE");
|
||||
break;
|
||||
|
||||
case HttpMethod::PATCH:
|
||||
curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "PATCH");
|
||||
if (!data.empty()) {
|
||||
curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str());
|
||||
curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, data.size());
|
||||
}
|
||||
break;
|
||||
|
||||
case HttpMethod::HEAD:
|
||||
curl_easy_setopt(curl_, CURLOPT_NOBODY, 1L);
|
||||
break;
|
||||
|
||||
case HttpMethod::OPTIONS:
|
||||
curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "OPTIONS");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static size_t header_callback(char* buffer, size_t size, size_t nitems, void* userdata) {
|
||||
size_t total_size = size * nitems;
|
||||
HttpClient* client = static_cast<HttpClient*>(userdata);
|
||||
|
||||
std::string header_line(buffer, total_size);
|
||||
size_t colon_pos = header_line.find(':');
|
||||
if (colon_pos != std::string::npos) {
|
||||
std::string key = header_line.substr(0, colon_pos);
|
||||
std::string value = header_line.substr(colon_pos + 1);
|
||||
|
||||
key.erase(0, key.find_first_not_of(" \t"));
|
||||
key.erase(key.find_last_not_of(" \t") + 1);
|
||||
value.erase(0, value.find_first_not_of(" \t"));
|
||||
value.erase(value.find_last_not_of(" \t\r\n") + 1);
|
||||
|
||||
if (!key.empty() && !value.empty()) {
|
||||
client->response_headers_[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return total_size;
|
||||
}
|
||||
|
||||
// ===== 修复:直接写入原始数据,不按行处理 =====
|
||||
static size_t write_callback(char* ptr, size_t size, size_t nmemb, void* userdata) {
|
||||
size_t total_size = size * nmemb;
|
||||
std::string* response = static_cast<std::string*>(userdata);
|
||||
|
||||
// 直接追加原始数据,保留所有换行符和特殊字符
|
||||
response->append(ptr, total_size);
|
||||
|
||||
return total_size;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace pmc::net
|
||||
@@ -0,0 +1,126 @@
|
||||
#ifndef HTTPSERVER_HPP
|
||||
#define HTTPSERVER_HPP
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/beast.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/version.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/config.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <regex>
|
||||
|
||||
namespace pmc {
|
||||
namespace net {
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace http = beast::http;
|
||||
namespace asio = boost::asio;
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
using HttpRequestHandler = std::function<http::response<http::string_body>(
|
||||
const http::request<http::string_body>&,
|
||||
const std::unordered_map<std::string, std::string>&)>;
|
||||
|
||||
using MiddlewareHandler = std::function<bool(
|
||||
const http::request<http::string_body>&,
|
||||
http::response<http::string_body>&,
|
||||
const std::unordered_map<std::string, std::string>&)>;
|
||||
|
||||
struct RouteEntry {
|
||||
std::string path;
|
||||
std::regex pattern;
|
||||
HttpRequestHandler handler;
|
||||
bool is_wildcard;
|
||||
};
|
||||
|
||||
class HttpServer {
|
||||
public:
|
||||
explicit HttpServer(const std::string& address, unsigned short port,
|
||||
unsigned int threads = 1);
|
||||
explicit HttpServer(const std::string& address, unsigned short port,
|
||||
const std::string& cert_file, const std::string& key_file,
|
||||
unsigned int threads = 1);
|
||||
~HttpServer();
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
void get(const std::string& path, HttpRequestHandler handler);
|
||||
void post(const std::string& path, HttpRequestHandler handler);
|
||||
void put(const std::string& path, HttpRequestHandler handler);
|
||||
void del(const std::string& path, HttpRequestHandler handler);
|
||||
void use(MiddlewareHandler middleware);
|
||||
void setStaticDirectory(const std::string& path);
|
||||
|
||||
bool isRunning() const;
|
||||
unsigned short getPort() const;
|
||||
std::string getAddress() const;
|
||||
bool isSSLEnabled() const;
|
||||
|
||||
void run();
|
||||
void showStatus() const;
|
||||
|
||||
void setPrivateKeyPassword(const std::string& password);
|
||||
|
||||
class debug {
|
||||
public:
|
||||
static void print_all_header_fields(const http::request<http::string_body>& req);
|
||||
};
|
||||
|
||||
private:
|
||||
class Session;
|
||||
class Listener;
|
||||
|
||||
std::string address_;
|
||||
unsigned short port_;
|
||||
unsigned int threads_;
|
||||
std::atomic<bool> running_{false};
|
||||
|
||||
bool ssl_enabled_{false};
|
||||
std::string cert_file_;
|
||||
std::string key_file_;
|
||||
std::string private_key_password_;
|
||||
|
||||
std::unique_ptr<asio::ssl::context> ssl_ctx_;
|
||||
std::unique_ptr<asio::io_context> ioc_;
|
||||
|
||||
std::shared_ptr<Listener> listener_;
|
||||
std::vector<std::thread> worker_threads_;
|
||||
|
||||
std::vector<RouteEntry> get_handlers_;
|
||||
std::vector<RouteEntry> post_handlers_;
|
||||
std::vector<RouteEntry> put_handlers_;
|
||||
std::vector<RouteEntry> delete_handlers_;
|
||||
std::vector<MiddlewareHandler> middlewares_;
|
||||
std::string static_directory_;
|
||||
|
||||
std::vector<std::weak_ptr<Session>> sessions_;
|
||||
std::mutex sessions_mutex_;
|
||||
|
||||
std::unordered_map<std::string, std::string> parseQueryParams(const std::string& query);
|
||||
std::regex wildcardToRegex(const std::string& pattern);
|
||||
bool matchPath(const std::string& path, const std::regex& pattern);
|
||||
http::response<http::string_body> handleRequest(const http::request<http::string_body>& req);
|
||||
HttpRequestHandler findRoute(const std::string& method, const std::string& path);
|
||||
|
||||
void addSession(const std::shared_ptr<Session>& session);
|
||||
void removeSession(const Session* session);
|
||||
void closeAllSessions();
|
||||
void initSSLContext();
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace pmc
|
||||
|
||||
#endif // HTTPSERVER_HPP
|
||||
@@ -0,0 +1,63 @@
|
||||
#ifndef PMC_NET_WEBSOCKET_CLIENT_HPP
|
||||
#define PMC_NET_WEBSOCKET_CLIENT_HPP
|
||||
|
||||
#include "WebSocketClientBase.hpp"
|
||||
|
||||
namespace pmc {
|
||||
namespace net {
|
||||
|
||||
/**
|
||||
* @brief Asynchronous WebSocket client based on Boost.Beast (non-SSL version)
|
||||
*/
|
||||
class WebSocketClient : public WebSocketClientBase {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor
|
||||
* @param host Server hostname or IP address
|
||||
* @param port Server port number
|
||||
* @param path WebSocket path (default "/")
|
||||
*/
|
||||
WebSocketClient(const std::string& host, const std::string& port, const std::string& path = "/");
|
||||
|
||||
/**
|
||||
* @brief Destructor
|
||||
*/
|
||||
~WebSocketClient();
|
||||
|
||||
// Implement WebSocketClientBase interface
|
||||
void setMessageCallback(MessageCallback callback) override;
|
||||
void setErrorCallback(ErrorCallback callback) override;
|
||||
void setConnectCallback(ConnectCallback callback) override;
|
||||
void setCloseCallback(CloseCallback callback) override;
|
||||
|
||||
bool connect(int timeout_ms = 5000) override;
|
||||
void disconnect() override;
|
||||
bool isConnected() const override;
|
||||
|
||||
bool send(const std::string& message) override;
|
||||
bool sendBinary(const void* data, size_t size) override;
|
||||
|
||||
void setAutoReconnect(bool enable, int interval_ms = 3000) override;
|
||||
void setHeartbeatInterval(int interval_ms = 30000) override;
|
||||
|
||||
std::string getHost() const override;
|
||||
std::string getPort() const override;
|
||||
std::string getPath() const override;
|
||||
|
||||
// SSL/TLS related (non-SSL version returns default values)
|
||||
bool isSecure() const override { return false; }
|
||||
void setVerifyCertificate(bool verify) override { (void)verify;/* Ignored */ }
|
||||
void setCertificateFile(const std::string& cert_file) override { (void)cert_file; /* Ignored */ }
|
||||
void setPrivateKeyFile(const std::string& key_file) override { (void)key_file;/* Ignored */ }
|
||||
void setCertificateAuthorityFile(const std::string& ca_file) override { (void)ca_file;/* Ignored */ }
|
||||
|
||||
private:
|
||||
// Forward declaration of implementation class
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace pmc
|
||||
|
||||
#endif // PMC_NET_WEBSOCKET_CLIENT_HPP
|
||||
@@ -0,0 +1,61 @@
|
||||
#ifndef PMC_NET_WEBSOCKET_CLIENT_BASE_HPP
|
||||
#define PMC_NET_WEBSOCKET_CLIENT_BASE_HPP
|
||||
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace pmc {
|
||||
namespace net {
|
||||
|
||||
/**
|
||||
* @brief WebSocket client base class
|
||||
*
|
||||
* Defines the common interface for all WebSocket clients
|
||||
*/
|
||||
class WebSocketClientBase {
|
||||
public:
|
||||
// Callback function types
|
||||
using MessageCallback = std::function<void(const std::string&)>;
|
||||
using ErrorCallback = std::function<void(const std::string&)>;
|
||||
using ConnectCallback = std::function<void(bool)>;
|
||||
using CloseCallback = std::function<void()>;
|
||||
|
||||
virtual ~WebSocketClientBase() = default;
|
||||
|
||||
// Callback setters
|
||||
virtual void setMessageCallback(MessageCallback callback) = 0;
|
||||
virtual void setErrorCallback(ErrorCallback callback) = 0;
|
||||
virtual void setConnectCallback(ConnectCallback callback) = 0;
|
||||
virtual void setCloseCallback(CloseCallback callback) = 0;
|
||||
|
||||
// Connection management
|
||||
virtual bool connect(int timeout_ms = 5000) = 0;
|
||||
virtual void disconnect() = 0;
|
||||
virtual bool isConnected() const = 0;
|
||||
|
||||
// Message sending
|
||||
virtual bool send(const std::string& message) = 0;
|
||||
virtual bool sendBinary(const void* data, size_t size) = 0;
|
||||
|
||||
// Configuration options
|
||||
virtual void setAutoReconnect(bool enable, int interval_ms = 3000) = 0;
|
||||
virtual void setHeartbeatInterval(int interval_ms = 30000) = 0;
|
||||
|
||||
// Information retrieval
|
||||
virtual std::string getHost() const = 0;
|
||||
virtual std::string getPort() const = 0;
|
||||
virtual std::string getPath() const = 0;
|
||||
|
||||
// SSL/TLS related
|
||||
virtual bool isSecure() const = 0;
|
||||
virtual void setVerifyCertificate(bool verify) = 0;
|
||||
virtual void setCertificateFile(const std::string& cert_file) = 0;
|
||||
virtual void setPrivateKeyFile(const std::string& key_file) = 0;
|
||||
virtual void setCertificateAuthorityFile(const std::string& ca_file) = 0;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace pmc
|
||||
|
||||
#endif // PMC_NET_WEBSOCKET_CLIENT_BASE_HPP
|
||||
@@ -0,0 +1,176 @@
|
||||
// WebSocketServer.hpp - WebSocket server header file
|
||||
// Based on Boost.Beast and Boost.Asio
|
||||
|
||||
#ifndef PMC_NET_WEBSOCKETSERVER_HPP
|
||||
#define PMC_NET_WEBSOCKETSERVER_HPP
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
|
||||
namespace pmc::net {
|
||||
|
||||
/**
|
||||
* @brief WebSocket connection handler type definition
|
||||
*
|
||||
* Handles message sending and receiving after WebSocket connection is established
|
||||
*/
|
||||
using WebSocketMessageHandler = std::function<void(
|
||||
const std::string& clientId,
|
||||
const std::string& message,
|
||||
std::function<void(const std::string&)> sendCallback)>;
|
||||
|
||||
/**
|
||||
* @brief WebSocket connection callback type definition
|
||||
*/
|
||||
using WebSocketConnectHandler = std::function<void(
|
||||
const std::string& clientId,
|
||||
const std::string& remoteAddress)>;
|
||||
|
||||
/**
|
||||
* @brief WebSocket disconnection callback type definition
|
||||
*/
|
||||
using WebSocketDisconnectHandler = std::function<void(
|
||||
const std::string& clientId)>;
|
||||
|
||||
/**
|
||||
* @brief WebSocket server class
|
||||
*
|
||||
* WebSocket server implemented based on Boost.Beast and Boost.Asio
|
||||
*/
|
||||
class WebSocketServer {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor
|
||||
* @param port Listening port
|
||||
* @param threads Number of threads (default 2)
|
||||
*/
|
||||
WebSocketServer(unsigned short port, unsigned int threads = 2);
|
||||
|
||||
/**
|
||||
* @brief Destructor
|
||||
*/
|
||||
~WebSocketServer();
|
||||
|
||||
/**
|
||||
* @brief Start the server
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* @brief Stop the server
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* @brief Register message handler
|
||||
* @param path Path (for routing)
|
||||
* @param handler Message handler
|
||||
*/
|
||||
void onMessage(const std::string& path, WebSocketMessageHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Register connection handler
|
||||
* @param handler Connection handler
|
||||
*/
|
||||
void onConnect(WebSocketConnectHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Register disconnection handler
|
||||
* @param handler Disconnection handler
|
||||
*/
|
||||
void onDisconnect(WebSocketDisconnectHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Send message to a specific client
|
||||
* @param clientId Client ID
|
||||
* @param message Message content
|
||||
* @return Whether sending was successful
|
||||
*/
|
||||
bool sendToClient(const std::string& clientId, const std::string& message);
|
||||
|
||||
/**
|
||||
* @brief Broadcast message to all clients
|
||||
* @param message Message content
|
||||
* @return Number of clients that successfully received the message
|
||||
*/
|
||||
size_t broadcast(const std::string& message);
|
||||
|
||||
/**
|
||||
* @brief Disconnect a specific client
|
||||
* @param clientId Client ID
|
||||
*/
|
||||
void disconnectClient(const std::string& clientId);
|
||||
|
||||
/**
|
||||
* @brief Get the number of currently connected clients
|
||||
* @return Number of clients
|
||||
*/
|
||||
size_t getClientCount() const;
|
||||
|
||||
/**
|
||||
* @brief Check if the server is running
|
||||
* @return Running status
|
||||
*/
|
||||
bool isRunning() const { return running_; }
|
||||
|
||||
/**
|
||||
* @brief Get the server listening port
|
||||
* @return Port number
|
||||
*/
|
||||
unsigned short getPort() const { return port_; }
|
||||
|
||||
private:
|
||||
// Internal Session class
|
||||
class Session;
|
||||
|
||||
// Internal Listener class
|
||||
class Listener;
|
||||
|
||||
// Internal implementation structure
|
||||
struct Impl {
|
||||
boost::asio::io_context ioc_;
|
||||
std::shared_ptr<Listener> listener_;
|
||||
std::vector<std::thread> threads_;
|
||||
};
|
||||
|
||||
// Server configuration
|
||||
unsigned short port_;
|
||||
unsigned int threads_;
|
||||
std::atomic<bool> running_{false};
|
||||
|
||||
// Internal implementation
|
||||
std::unique_ptr<Impl> impl_;
|
||||
|
||||
// Generate client ID
|
||||
std::string generateClientId() const;
|
||||
|
||||
// Add client
|
||||
void addClient(const std::string& clientId, std::shared_ptr<Session> session);
|
||||
|
||||
// Remove client
|
||||
void removeClient(const std::string& clientId);
|
||||
|
||||
// Get client session
|
||||
std::shared_ptr<Session> getClient(const std::string& clientId);
|
||||
|
||||
// Callback functions
|
||||
std::unordered_map<std::string, WebSocketMessageHandler> messageHandlers_;
|
||||
WebSocketConnectHandler connectHandler_;
|
||||
WebSocketDisconnectHandler disconnectHandler_;
|
||||
|
||||
// Client management
|
||||
mutable std::mutex clientsMutex_;
|
||||
std::unordered_map<std::string, std::shared_ptr<Session>> clients_;
|
||||
};
|
||||
|
||||
} // namespace pmc::net
|
||||
|
||||
#endif // PMC_NET_WEBSOCKETSERVER_HPP
|
||||
@@ -0,0 +1,604 @@
|
||||
#include "us/HttpServer.hpp"
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/ssl.h> // SSL_CTX_set_cipher_list
|
||||
|
||||
namespace pmc {
|
||||
namespace net {
|
||||
|
||||
// ===================== 自定义异常 =====================
|
||||
class HttpServerException : public std::runtime_error {
|
||||
public:
|
||||
explicit HttpServerException(const std::string& msg) : runtime_error(msg) {}
|
||||
};
|
||||
class HttpListenerException : public HttpServerException {
|
||||
public:
|
||||
explicit HttpListenerException(const std::string& msg) : HttpServerException(msg) {}
|
||||
};
|
||||
class SSLInitException : public HttpServerException {
|
||||
public:
|
||||
explicit SSLInitException(const std::string& msg) : HttpServerException(msg) {}
|
||||
};
|
||||
class CreateObjectFailed : public HttpServerException {
|
||||
public:
|
||||
explicit CreateObjectFailed(const std::string& msg) : HttpServerException(msg) {}
|
||||
};
|
||||
|
||||
// ===================== Session =====================
|
||||
class HttpServer::Session : public std::enable_shared_from_this<Session> {
|
||||
public:
|
||||
// HTTP 构造
|
||||
Session(tcp::socket&& socket, HttpServer* server)
|
||||
: socket_(std::move(socket)), server_(server), ssl_enabled_(false),
|
||||
strand_(asio::make_strand(socket_.get_executor())) {}
|
||||
|
||||
// HTTPS 构造
|
||||
Session(tcp::socket&& socket, asio::ssl::context& ssl_ctx, HttpServer* server)
|
||||
: socket_(std::move(socket)), server_(server), ssl_enabled_(true),
|
||||
strand_(asio::make_strand(socket_.get_executor())) {
|
||||
ssl_stream_ = std::make_unique<asio::ssl::stream<tcp::socket>>(
|
||||
std::move(socket_), ssl_ctx);
|
||||
}
|
||||
|
||||
~Session() { close(); }
|
||||
|
||||
void run() {
|
||||
if (ssl_enabled_) doSSLHandshake();
|
||||
else doRead();
|
||||
}
|
||||
|
||||
void setRemoteInfo(const std::string& ip, unsigned short port) {
|
||||
remote_ip_ = ip;
|
||||
remote_port_ = port;
|
||||
}
|
||||
|
||||
private:
|
||||
void close() {
|
||||
beast::error_code ec;
|
||||
if (ssl_enabled_ && ssl_stream_) {
|
||||
ssl_stream_->shutdown(ec);
|
||||
ssl_stream_->lowest_layer().close(ec);
|
||||
} else if (socket_.is_open()) {
|
||||
socket_.shutdown(tcp::socket::shutdown_send, ec);
|
||||
socket_.close(ec);
|
||||
}
|
||||
}
|
||||
|
||||
void doSSLHandshake() {
|
||||
auto self = shared_from_this();
|
||||
std::cout << "[SSL] Starting handshake for "
|
||||
<< remote_ip_ << ":" << remote_port_ << std::endl;
|
||||
|
||||
ssl_stream_->async_handshake(
|
||||
asio::ssl::stream_base::server,
|
||||
asio::bind_executor(strand_,
|
||||
[this, self](beast::error_code ec) {
|
||||
if (!ec) {
|
||||
std::cout << "[SSL] Handshake succeeded: "
|
||||
<< remote_ip_ << ":" << remote_port_ << std::endl;
|
||||
doRead();
|
||||
} else {
|
||||
std::cerr << "[SSL] Handshake error: "
|
||||
<< remote_ip_ << ":" << remote_port_
|
||||
<< " - " << ec.message()
|
||||
<< " (code=" << ec.value() << ")" << std::endl;
|
||||
// 输出 OpenSSL 详细错误堆栈
|
||||
ERR_print_errors_fp(stderr);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void doRead() {
|
||||
auto self = shared_from_this();
|
||||
if (ssl_enabled_ && ssl_stream_) {
|
||||
http::async_read(*ssl_stream_, buffer_, request_,
|
||||
asio::bind_executor(strand_,
|
||||
[this, self](beast::error_code ec, std::size_t) { handleRead(ec); }));
|
||||
} else {
|
||||
http::async_read(socket_, buffer_, request_,
|
||||
asio::bind_executor(strand_,
|
||||
[this, self](beast::error_code ec, std::size_t) { handleRead(ec); }));
|
||||
}
|
||||
}
|
||||
|
||||
void handleRead(beast::error_code ec) {
|
||||
if (!ec) {
|
||||
auto req_copy = std::move(request_);
|
||||
request_ = {};
|
||||
processRequest(std::move(req_copy));
|
||||
} else if (ec != http::error::end_of_stream) {
|
||||
std::cerr << "[Read] Error from " << remote_ip_ << ":" << remote_port_
|
||||
<< ": " << ec.message() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void processRequest(http::request<http::string_body> req) {
|
||||
try {
|
||||
auto resp = std::make_shared<http::response<http::string_body>>(
|
||||
server_->handleRequest(req));
|
||||
doWrite(resp);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[Request] Handler exception: " << e.what() << std::endl;
|
||||
auto err_resp = std::make_shared<http::response<http::string_body>>();
|
||||
err_resp->version(req.version());
|
||||
err_resp->result(http::status::internal_server_error);
|
||||
err_resp->set(http::field::content_type, "text/plain");
|
||||
err_resp->body() = "500 Internal Server Error\n";
|
||||
err_resp->prepare_payload();
|
||||
doWrite(err_resp);
|
||||
}
|
||||
}
|
||||
|
||||
void doWrite(std::shared_ptr<http::response<http::string_body>> response) {
|
||||
auto self = shared_from_this();
|
||||
if (ssl_enabled_ && ssl_stream_) {
|
||||
http::async_write(*ssl_stream_, *response,
|
||||
asio::bind_executor(strand_,
|
||||
[this, self, response](beast::error_code ec, std::size_t) {
|
||||
if (ec) {
|
||||
std::cerr << "[Write] Error to " << remote_ip_ << ":" << remote_port_
|
||||
<< ": " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
if (!response->keep_alive()) close();
|
||||
else doRead();
|
||||
}));
|
||||
} else {
|
||||
http::async_write(socket_, *response,
|
||||
asio::bind_executor(strand_,
|
||||
[this, self, response](beast::error_code ec, std::size_t) {
|
||||
if (ec) {
|
||||
std::cerr << "[Write] Error to " << remote_ip_ << ":" << remote_port_
|
||||
<< ": " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
if (!response->keep_alive()) close();
|
||||
else doRead();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
tcp::socket socket_;
|
||||
std::unique_ptr<asio::ssl::stream<tcp::socket>> ssl_stream_;
|
||||
beast::flat_buffer buffer_;
|
||||
http::request<http::string_body> request_;
|
||||
HttpServer* server_;
|
||||
bool ssl_enabled_;
|
||||
|
||||
// 使用 any_io_executor 兼容多种执行器
|
||||
asio::strand<asio::any_io_executor> strand_;
|
||||
|
||||
std::string remote_ip_ = "unknown";
|
||||
unsigned short remote_port_ = 0;
|
||||
};
|
||||
|
||||
// ===================== Listener =====================
|
||||
class HttpServer::Listener : public std::enable_shared_from_this<Listener> {
|
||||
public:
|
||||
Listener(asio::io_context& ioc, tcp::endpoint endpoint, HttpServer* server)
|
||||
: ioc_(ioc), acceptor_(ioc), server_(server), ssl_enabled_(false) {
|
||||
initAcceptor(endpoint);
|
||||
}
|
||||
|
||||
Listener(asio::io_context& ioc, tcp::endpoint endpoint,
|
||||
asio::ssl::context& ssl_ctx, HttpServer* server)
|
||||
: ioc_(ioc), acceptor_(ioc), ssl_ctx_(&ssl_ctx), server_(server), ssl_enabled_(true) {
|
||||
initAcceptor(endpoint);
|
||||
}
|
||||
|
||||
void run() {
|
||||
if (acceptor_.is_open()) doAccept();
|
||||
}
|
||||
|
||||
private:
|
||||
void initAcceptor(tcp::endpoint ep) {
|
||||
beast::error_code ec;
|
||||
acceptor_.open(ep.protocol(), ec);
|
||||
if (!ec) {
|
||||
acceptor_.set_option(asio::socket_base::reuse_address(true), ec);
|
||||
acceptor_.bind(ep, ec);
|
||||
if (!ec) acceptor_.listen(asio::socket_base::max_listen_connections, ec);
|
||||
}
|
||||
if (ec) throw HttpListenerException("Listener error: " + ec.message());
|
||||
}
|
||||
|
||||
void doAccept() {
|
||||
acceptor_.async_accept(
|
||||
[this, self = shared_from_this()](beast::error_code ec, tcp::socket socket) {
|
||||
if (!ec) {
|
||||
auto ep = socket.remote_endpoint(ec);
|
||||
std::string ip = ec ? "unknown" : ep.address().to_string();
|
||||
unsigned short port = ec ? 0 : ep.port();
|
||||
std::cout << "[Listener] New connection from " << ip << ":" << port << std::endl;
|
||||
|
||||
std::shared_ptr<Session> session;
|
||||
if (ssl_enabled_ && ssl_ctx_) {
|
||||
session = std::make_shared<Session>(std::move(socket), *ssl_ctx_, server_);
|
||||
} else {
|
||||
session = std::make_shared<Session>(std::move(socket), server_);
|
||||
}
|
||||
session->setRemoteInfo(ip, port);
|
||||
session->run();
|
||||
} else {
|
||||
std::cerr << "[Listener] Accept error: " << ec.message() << std::endl;
|
||||
}
|
||||
doAccept();
|
||||
});
|
||||
}
|
||||
|
||||
asio::io_context& ioc_;
|
||||
tcp::acceptor acceptor_;
|
||||
asio::ssl::context* ssl_ctx_ = nullptr;
|
||||
HttpServer* server_;
|
||||
bool ssl_enabled_;
|
||||
};
|
||||
|
||||
// ===================== HttpServer 实现 =====================
|
||||
|
||||
// ---- 构造函数 ----
|
||||
HttpServer::HttpServer(const std::string& address, unsigned short port, unsigned int threads)
|
||||
: address_(address), port_(port), threads_(threads), ssl_enabled_(false),
|
||||
ioc_(std::make_unique<asio::io_context>(threads)) {
|
||||
if (!ioc_) throw CreateObjectFailed("Failed to create io_context");
|
||||
}
|
||||
|
||||
HttpServer::HttpServer(const std::string& address, unsigned short port,
|
||||
const std::string& cert_file, const std::string& key_file,
|
||||
unsigned int threads)
|
||||
: address_(address), port_(port), threads_(threads), ssl_enabled_(true),
|
||||
cert_file_(cert_file), key_file_(key_file),
|
||||
ioc_(std::make_unique<asio::io_context>(threads)) {
|
||||
if (!ioc_) throw CreateObjectFailed("Failed to create io_context");
|
||||
initSSLContext();
|
||||
}
|
||||
|
||||
HttpServer::~HttpServer() { stop(); }
|
||||
|
||||
// ---- SSL 初始化 ----
|
||||
void HttpServer::setPrivateKeyPassword(const std::string& password) {
|
||||
private_key_password_ = password;
|
||||
}
|
||||
|
||||
void HttpServer::initSSLContext() {
|
||||
if (!ssl_enabled_) return;
|
||||
try {
|
||||
ssl_ctx_ = std::make_unique<asio::ssl::context>(asio::ssl::context::tls);
|
||||
ssl_ctx_->set_options(
|
||||
asio::ssl::context::default_workarounds |
|
||||
asio::ssl::context::no_sslv2 |
|
||||
asio::ssl::context::no_sslv3 |
|
||||
asio::ssl::context::single_dh_use);
|
||||
|
||||
// 设置现代加密套件,确保兼容主流客户端
|
||||
SSL_CTX_set_cipher_list(ssl_ctx_->native_handle(),
|
||||
"ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS");
|
||||
|
||||
if (!private_key_password_.empty()) {
|
||||
ssl_ctx_->set_password_callback(
|
||||
[this](std::size_t, asio::ssl::context::password_purpose) {
|
||||
return private_key_password_;
|
||||
});
|
||||
}
|
||||
|
||||
std::cout << "[SSL] Loading certificate chain: " << cert_file_ << std::endl;
|
||||
ssl_ctx_->use_certificate_chain_file(cert_file_);
|
||||
|
||||
std::cout << "[SSL] Loading private key: " << key_file_ << std::endl;
|
||||
ssl_ctx_->use_private_key_file(key_file_, asio::ssl::context::pem);
|
||||
|
||||
std::cout << "[SSL] SSL context ready." << std::endl;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[SSL] Init failed: " << e.what() << std::endl;
|
||||
ERR_print_errors_fp(stderr);
|
||||
throw SSLInitException("Failed to initialize SSL context");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 启动/停止 ----
|
||||
void HttpServer::start() {
|
||||
if (running_) return;
|
||||
try {
|
||||
auto addr = asio::ip::make_address(address_);
|
||||
tcp::endpoint ep(addr, port_);
|
||||
if (ssl_enabled_ && ssl_ctx_) {
|
||||
listener_ = std::make_shared<Listener>(*ioc_, ep, *ssl_ctx_, this);
|
||||
} else {
|
||||
listener_ = std::make_shared<Listener>(*ioc_, ep, this);
|
||||
}
|
||||
listener_->run();
|
||||
running_ = true;
|
||||
|
||||
for (unsigned i = 0; i < threads_; ++i) {
|
||||
worker_threads_.emplace_back([this]() {
|
||||
try { ioc_->run(); }
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "[Thread] Exception: " << e.what() << std::endl;
|
||||
}
|
||||
});
|
||||
}
|
||||
std::cout << (ssl_enabled_ ? "HTTPS" : "HTTP")
|
||||
<< " Server started on " << address_ << ":" << port_
|
||||
<< " with " << threads_ << " threads\n";
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[Start] Failed: " << e.what() << std::endl;
|
||||
running_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServer::stop() {
|
||||
if (!running_) return;
|
||||
running_ = false;
|
||||
closeAllSessions();
|
||||
if (ioc_) ioc_->stop();
|
||||
for (auto& t : worker_threads_) if (t.joinable()) t.join();
|
||||
worker_threads_.clear();
|
||||
listener_.reset();
|
||||
std::cout << (ssl_enabled_ ? "HTTPS" : "HTTP") << " Server stopped\n";
|
||||
}
|
||||
|
||||
void HttpServer::run() {
|
||||
start();
|
||||
if (ioc_) ioc_->run();
|
||||
}
|
||||
|
||||
// ---- 状态查询 ----
|
||||
bool HttpServer::isRunning() const { return running_; }
|
||||
unsigned short HttpServer::getPort() const { return port_; }
|
||||
std::string HttpServer::getAddress() const { return address_; }
|
||||
bool HttpServer::isSSLEnabled() const { return ssl_enabled_; }
|
||||
|
||||
// ---- 路由注册 ----
|
||||
void HttpServer::get(const std::string& path, HttpRequestHandler handler) {
|
||||
RouteEntry entry;
|
||||
entry.path = path;
|
||||
entry.handler = handler;
|
||||
entry.is_wildcard = (path.find('*') != std::string::npos ||
|
||||
path.find('?') != std::string::npos);
|
||||
if (entry.is_wildcard) entry.pattern = wildcardToRegex(path);
|
||||
get_handlers_.push_back(entry);
|
||||
}
|
||||
|
||||
void HttpServer::post(const std::string& path, HttpRequestHandler handler) {
|
||||
RouteEntry entry;
|
||||
entry.path = path;
|
||||
entry.handler = handler;
|
||||
entry.is_wildcard = (path.find('*') != std::string::npos ||
|
||||
path.find('?') != std::string::npos);
|
||||
if (entry.is_wildcard) entry.pattern = wildcardToRegex(path);
|
||||
post_handlers_.push_back(entry);
|
||||
}
|
||||
|
||||
void HttpServer::put(const std::string& path, HttpRequestHandler handler) {
|
||||
RouteEntry entry;
|
||||
entry.path = path;
|
||||
entry.handler = handler;
|
||||
entry.is_wildcard = (path.find('*') != std::string::npos ||
|
||||
path.find('?') != std::string::npos);
|
||||
if (entry.is_wildcard) entry.pattern = wildcardToRegex(path);
|
||||
put_handlers_.push_back(entry);
|
||||
}
|
||||
|
||||
void HttpServer::del(const std::string& path, HttpRequestHandler handler) {
|
||||
RouteEntry entry;
|
||||
entry.path = path;
|
||||
entry.handler = handler;
|
||||
entry.is_wildcard = (path.find('*') != std::string::npos ||
|
||||
path.find('?') != std::string::npos);
|
||||
if (entry.is_wildcard) entry.pattern = wildcardToRegex(path);
|
||||
delete_handlers_.push_back(entry);
|
||||
}
|
||||
|
||||
void HttpServer::use(MiddlewareHandler m) {
|
||||
middlewares_.push_back(m);
|
||||
}
|
||||
|
||||
void HttpServer::setStaticDirectory(const std::string& p) {
|
||||
static_directory_ = p;
|
||||
}
|
||||
|
||||
// ---- 通配符转换 ----
|
||||
std::regex HttpServer::wildcardToRegex(const std::string& pattern) {
|
||||
std::string r;
|
||||
r.reserve(pattern.size() * 2);
|
||||
for (char c : pattern) {
|
||||
if (c == '*') r += ".*";
|
||||
else if (c == '?') r += ".";
|
||||
else if (c == '.' || c == '+' || c == '(' || c == ')' || c == '[' || c == ']' ||
|
||||
c == '{' || c == '}' || c == '^' || c == '$' || c == '|' || c == '\\') {
|
||||
r += '\\';
|
||||
r += c;
|
||||
} else r += c;
|
||||
}
|
||||
return std::regex(r, std::regex::icase);
|
||||
}
|
||||
|
||||
bool HttpServer::matchPath(const std::string& path, const std::regex& pattern) {
|
||||
return std::regex_match(path, pattern);
|
||||
}
|
||||
|
||||
// ---- 查询参数解析 ----
|
||||
std::unordered_map<std::string, std::string> HttpServer::parseQueryParams(const std::string& query) {
|
||||
std::unordered_map<std::string, std::string> params;
|
||||
if (query.empty()) return params;
|
||||
|
||||
std::vector<std::string> pairs;
|
||||
boost::split(pairs, query, boost::is_any_of("&"));
|
||||
for (const auto& pair : pairs) {
|
||||
std::vector<std::string> kv;
|
||||
boost::split(kv, pair, boost::is_any_of("="));
|
||||
if (kv.size() == 2) params[kv[0]] = kv[1];
|
||||
else if (kv.size() == 1) params[kv[0]] = "";
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
// ---- 路由查找 ----
|
||||
HttpRequestHandler HttpServer::findRoute(const std::string& method, const std::string& path) {
|
||||
const std::vector<RouteEntry>* handlers = nullptr;
|
||||
if (method == "GET") handlers = &get_handlers_;
|
||||
else if (method == "POST") handlers = &post_handlers_;
|
||||
else if (method == "PUT") handlers = &put_handlers_;
|
||||
else if (method == "DELETE") handlers = &delete_handlers_;
|
||||
else return nullptr;
|
||||
|
||||
// 精确匹配优先
|
||||
for (const auto& e : *handlers) {
|
||||
if (!e.is_wildcard && e.path == path) return e.handler;
|
||||
}
|
||||
// 通配符匹配
|
||||
for (const auto& e : *handlers) {
|
||||
if (e.is_wildcard && matchPath(path, e.pattern)) return e.handler;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---- 请求处理核心 ----
|
||||
http::response<http::string_body> HttpServer::handleRequest(const http::request<http::string_body>& req) {
|
||||
std::string target_path(req.target().data(), req.target().size());
|
||||
std::string query_string;
|
||||
auto pos = target_path.find('?');
|
||||
if (pos != std::string::npos) {
|
||||
query_string = target_path.substr(pos + 1);
|
||||
target_path = target_path.substr(0, pos);
|
||||
}
|
||||
auto params = parseQueryParams(query_string);
|
||||
|
||||
http::response<http::string_body> res;
|
||||
res.version(req.version());
|
||||
res.set(http::field::server, "Boost.Beast HttpServer");
|
||||
res.set(http::field::content_type, "text/plain");
|
||||
res.keep_alive(req.keep_alive());
|
||||
|
||||
// 执行中间件
|
||||
bool middleware_handled = false;
|
||||
for (auto& m : middlewares_) {
|
||||
if (!m(req, res, params)) {
|
||||
middleware_handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (middleware_handled) {
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string method = http::to_string(req.method());
|
||||
auto handler = findRoute(method, target_path);
|
||||
|
||||
// 静态文件
|
||||
if (!handler && !static_directory_.empty()) {
|
||||
std::string file_path = static_directory_ + target_path;
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
if (file.is_open()) {
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
res.result(http::status::ok);
|
||||
res.body() = buffer.str();
|
||||
|
||||
if (boost::algorithm::ends_with(file_path, ".html"))
|
||||
res.set(http::field::content_type, "text/html");
|
||||
else if (boost::algorithm::ends_with(file_path, ".css"))
|
||||
res.set(http::field::content_type, "text/css");
|
||||
else if (boost::algorithm::ends_with(file_path, ".js"))
|
||||
res.set(http::field::content_type, "application/javascript");
|
||||
else if (boost::algorithm::ends_with(file_path, ".json"))
|
||||
res.set(http::field::content_type, "application/json");
|
||||
else if (boost::algorithm::ends_with(file_path, ".png"))
|
||||
res.set(http::field::content_type, "image/png");
|
||||
else if (boost::algorithm::ends_with(file_path, ".jpg") ||
|
||||
boost::algorithm::ends_with(file_path, ".jpeg"))
|
||||
res.set(http::field::content_type, "image/jpeg");
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// 路由处理
|
||||
if (handler) {
|
||||
try {
|
||||
return handler(req, params);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[Handler] Exception: " << e.what() << std::endl;
|
||||
res.result(http::status::internal_server_error);
|
||||
res.body() = "500 Internal Server Error\n";
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// 404
|
||||
res.result(http::status::not_found);
|
||||
res.body() = "404 Not Found\n";
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
}
|
||||
|
||||
// ---- 状态显示 ----
|
||||
void HttpServer::showStatus() const {
|
||||
std::cout << "=== " << (ssl_enabled_ ? "HTTPS" : "HTTP") << " Server Status ===" << std::endl;
|
||||
std::cout << "Protocol: " << (ssl_enabled_ ? "HTTPS (SSL/TLS)" : "HTTP") << std::endl;
|
||||
std::cout << "Address: " << address_ << std::endl;
|
||||
std::cout << "Port: " << port_ << std::endl;
|
||||
std::cout << "Threads: " << threads_ << std::endl;
|
||||
std::cout << "Running: " << (running_ ? "Yes" : "No") << std::endl;
|
||||
if (ssl_enabled_) {
|
||||
std::cout << "Certificate: " << cert_file_ << std::endl;
|
||||
std::cout << "Private Key: " << key_file_ << std::endl;
|
||||
}
|
||||
std::cout << "Static Directory: " << (static_directory_.empty() ? "Not set" : static_directory_) << std::endl;
|
||||
std::cout << "Registered Routes:" << std::endl;
|
||||
std::cout << " GET: " << get_handlers_.size() << " handler(s)" << std::endl;
|
||||
for (const auto& entry : get_handlers_)
|
||||
std::cout << " GET " << entry.path << (entry.is_wildcard ? " (wildcard)" : "") << std::endl;
|
||||
std::cout << " POST: " << post_handlers_.size() << " handler(s)" << std::endl;
|
||||
for (const auto& entry : post_handlers_)
|
||||
std::cout << " POST " << entry.path << (entry.is_wildcard ? " (wildcard)" : "") << std::endl;
|
||||
std::cout << " PUT: " << put_handlers_.size() << " handler(s)" << std::endl;
|
||||
for (const auto& entry : put_handlers_)
|
||||
std::cout << " PUT " << entry.path << (entry.is_wildcard ? " (wildcard)" : "") << std::endl;
|
||||
std::cout << " DELETE: " << delete_handlers_.size() << " handler(s)" << std::endl;
|
||||
for (const auto& entry : delete_handlers_)
|
||||
std::cout << " DELETE " << entry.path << (entry.is_wildcard ? " (wildcard)" : "") << std::endl;
|
||||
std::cout << "Middlewares: " << middlewares_.size() << std::endl;
|
||||
std::cout << "==========================" << std::endl;
|
||||
}
|
||||
|
||||
// ---- 会话管理 ----
|
||||
void HttpServer::addSession(const std::shared_ptr<Session>& session) {
|
||||
std::lock_guard<std::mutex> lock(sessions_mutex_);
|
||||
sessions_.erase(std::remove_if(sessions_.begin(), sessions_.end(),
|
||||
[](const std::weak_ptr<Session>& wp) { return wp.expired(); }), sessions_.end());
|
||||
sessions_.push_back(session);
|
||||
}
|
||||
|
||||
void HttpServer::removeSession(const Session* session) {
|
||||
std::lock_guard<std::mutex> lock(sessions_mutex_);
|
||||
sessions_.erase(std::remove_if(sessions_.begin(), sessions_.end(),
|
||||
[session](const std::weak_ptr<Session>& wp) {
|
||||
auto sp = wp.lock();
|
||||
return !sp || sp.get() == session;
|
||||
}), sessions_.end());
|
||||
}
|
||||
|
||||
void HttpServer::closeAllSessions() {
|
||||
std::lock_guard<std::mutex> lock(sessions_mutex_);
|
||||
for (auto& weak : sessions_) {
|
||||
auto sp = weak.lock();
|
||||
if (sp) { /* 会话析构时自动关闭 socket */ }
|
||||
}
|
||||
sessions_.clear();
|
||||
}
|
||||
|
||||
// ---- 调试工具 ----
|
||||
void HttpServer::debug::print_all_header_fields(const http::request<http::string_body>& req) {
|
||||
for (auto const& field : req) {
|
||||
std::cout << "Name: " << field.name_string()
|
||||
<< ", Value: " << field.value() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace pmc
|
||||
@@ -0,0 +1,386 @@
|
||||
#include "us/WebSocketClient.hpp"
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/connect.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <condition_variable>
|
||||
#include <atomic>
|
||||
|
||||
namespace pmc {
|
||||
namespace net {
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace asio = boost::asio;
|
||||
using tcp = asio::ip::tcp;
|
||||
|
||||
// Internal implementation structure
|
||||
struct WebSocketClient::Impl {
|
||||
// Configuration parameters
|
||||
std::string host;
|
||||
std::string port;
|
||||
std::string path;
|
||||
|
||||
// Boost objects
|
||||
asio::io_context ioc;
|
||||
tcp::resolver resolver;
|
||||
beast::websocket::stream<beast::tcp_stream> ws;
|
||||
|
||||
// Thread management
|
||||
std::thread io_thread;
|
||||
std::atomic<bool> running{false};
|
||||
|
||||
// Callback functions
|
||||
WebSocketClientBase::MessageCallback message_callback;
|
||||
WebSocketClientBase::ErrorCallback error_callback;
|
||||
WebSocketClientBase::ConnectCallback connect_callback;
|
||||
WebSocketClientBase::CloseCallback close_callback;
|
||||
|
||||
// Message queue
|
||||
std::queue<std::string> send_queue;
|
||||
std::mutex queue_mutex;
|
||||
std::condition_variable queue_cv;
|
||||
std::atomic<bool> sending{false};
|
||||
|
||||
// Status flags
|
||||
std::atomic<bool> connected{false};
|
||||
std::atomic<bool> connecting{false};
|
||||
std::atomic<bool> stopping{false};
|
||||
|
||||
// Configuration options
|
||||
bool auto_reconnect{false};
|
||||
int reconnect_interval_ms{3000};
|
||||
int heartbeat_interval_ms{30000};
|
||||
|
||||
// Constructor
|
||||
Impl(const std::string& host, const std::string& port, const std::string& path)
|
||||
: host(host)
|
||||
, port(port)
|
||||
, path(path)
|
||||
, resolver(asio::make_strand(ioc))
|
||||
, ws(asio::make_strand(ioc))
|
||||
{
|
||||
}
|
||||
|
||||
// Destructor
|
||||
~Impl() {
|
||||
stop();
|
||||
}
|
||||
|
||||
// Start IO thread
|
||||
void start() {
|
||||
if (!running) {
|
||||
running = true;
|
||||
io_thread = std::thread([this]() {
|
||||
while (running) {
|
||||
try {
|
||||
ioc.run();
|
||||
break;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "IO context exception: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Stop IO thread
|
||||
void stop() {
|
||||
running = false;
|
||||
stopping = true;
|
||||
|
||||
if (io_thread.joinable()) {
|
||||
ioc.stop();
|
||||
io_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
// Perform connection
|
||||
void doConnect() {
|
||||
if (connecting || connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
connecting = true;
|
||||
connected = false;
|
||||
|
||||
// Resolve hostname
|
||||
resolver.async_resolve(host, port,
|
||||
[this](beast::error_code ec, tcp::resolver::results_type results) {
|
||||
onResolve(ec, results);
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve completion callback
|
||||
void onResolve(beast::error_code ec, tcp::resolver::results_type results) {
|
||||
if (ec) {
|
||||
onError("Resolve failed: " + ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to resolved endpoints
|
||||
beast::get_lowest_layer(ws).async_connect(results,
|
||||
[this](beast::error_code ec, tcp::resolver::results_type::endpoint_type) {
|
||||
onConnect(ec);
|
||||
});
|
||||
}
|
||||
|
||||
// Connection completion callback
|
||||
void onConnect(beast::error_code ec) {
|
||||
if (ec) {
|
||||
onError("Connection failed: " + ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
// Set WebSocket options
|
||||
ws.set_option(beast::websocket::stream_base::timeout::suggested(
|
||||
beast::role_type::client));
|
||||
|
||||
// Perform WebSocket handshake
|
||||
ws.async_handshake(host, path,
|
||||
[this](beast::error_code ec) {
|
||||
onHandshake(ec);
|
||||
});
|
||||
}
|
||||
|
||||
// Handshake completion callback
|
||||
void onHandshake(beast::error_code ec) {
|
||||
connecting = false;
|
||||
|
||||
if (ec) {
|
||||
onError("Handshake failed: " + ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
connected = true;
|
||||
|
||||
// Call connection success callback
|
||||
if (connect_callback) {
|
||||
connect_callback(true);
|
||||
}
|
||||
|
||||
// Start reading messages
|
||||
doRead();
|
||||
|
||||
// Start sending queued messages
|
||||
doSend();
|
||||
}
|
||||
|
||||
// Read messages
|
||||
void doRead() {
|
||||
if (!connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
ws.async_read(buffer,
|
||||
[this](beast::error_code ec, std::size_t bytes_transferred) {
|
||||
onRead(ec, bytes_transferred);
|
||||
});
|
||||
}
|
||||
|
||||
// Read completion callback
|
||||
void onRead(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
if (ec) {
|
||||
if (ec == beast::websocket::error::closed) {
|
||||
// Normal close
|
||||
onClose();
|
||||
} else {
|
||||
onError("Read failed: " + ec.message());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Process received message
|
||||
std::string message = beast::buffers_to_string(buffer.data());
|
||||
buffer.consume(bytes_transferred);
|
||||
|
||||
if (message_callback) {
|
||||
message_callback(message);
|
||||
}
|
||||
|
||||
// Continue reading
|
||||
doRead();
|
||||
}
|
||||
|
||||
// Send messages
|
||||
void doSend() {
|
||||
if (!connected || sending) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
if (send_queue.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
sending = true;
|
||||
std::string message = std::move(send_queue.front());
|
||||
send_queue.pop();
|
||||
lock.unlock();
|
||||
|
||||
ws.async_write(asio::buffer(message),
|
||||
[this](beast::error_code ec, std::size_t bytes_transferred) {
|
||||
onWrite(ec, bytes_transferred);
|
||||
});
|
||||
}
|
||||
|
||||
// Write completion callback
|
||||
void onWrite(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
sending = false;
|
||||
|
||||
if (ec) {
|
||||
onError("Write failed: " + ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
// Continue sending queued messages
|
||||
doSend();
|
||||
}
|
||||
|
||||
// Error handling
|
||||
void onError(const std::string& error) {
|
||||
connecting = false;
|
||||
connected = false;
|
||||
|
||||
std::cerr << "WebSocket error: " << error << std::endl;
|
||||
|
||||
if (error_callback) {
|
||||
error_callback(error);
|
||||
}
|
||||
|
||||
// Auto reconnect
|
||||
if (auto_reconnect && !stopping) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(reconnect_interval_ms));
|
||||
asio::post(ioc, [this]() { doConnect(); });
|
||||
}
|
||||
}
|
||||
|
||||
// Connection close
|
||||
void onClose() {
|
||||
connected = false;
|
||||
|
||||
if (close_callback) {
|
||||
close_callback();
|
||||
}
|
||||
|
||||
// Auto reconnect
|
||||
if (auto_reconnect && !stopping) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(reconnect_interval_ms));
|
||||
asio::post(ioc, [this]() { doConnect(); });
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
beast::flat_buffer buffer;
|
||||
};
|
||||
|
||||
// WebSocketClient implementation
|
||||
WebSocketClient::WebSocketClient(const std::string& host, const std::string& port, const std::string& path)
|
||||
: impl_(std::make_unique<Impl>(host, port, path))
|
||||
{
|
||||
impl_->start();
|
||||
}
|
||||
|
||||
WebSocketClient::~WebSocketClient() {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
void WebSocketClient::setMessageCallback(MessageCallback callback) {
|
||||
impl_->message_callback = callback;
|
||||
}
|
||||
|
||||
void WebSocketClient::setErrorCallback(ErrorCallback callback) {
|
||||
impl_->error_callback = callback;
|
||||
}
|
||||
|
||||
void WebSocketClient::setConnectCallback(ConnectCallback callback) {
|
||||
impl_->connect_callback = callback;
|
||||
}
|
||||
|
||||
void WebSocketClient::setCloseCallback(CloseCallback callback) {
|
||||
impl_->close_callback = callback;
|
||||
}
|
||||
|
||||
bool WebSocketClient::connect(int timeout_ms) {
|
||||
if (impl_->connecting || impl_->connected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
impl_->doConnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebSocketClient::disconnect() {
|
||||
if (!impl_->connected && !impl_->connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
impl_->stopping = true;
|
||||
|
||||
asio::post(impl_->ioc, [this]() {
|
||||
beast::error_code ec;
|
||||
impl_->ws.close(beast::websocket::close_code::normal, ec);
|
||||
impl_->connected = false;
|
||||
impl_->connecting = false;
|
||||
});
|
||||
}
|
||||
|
||||
bool WebSocketClient::isConnected() const {
|
||||
return impl_->connected;
|
||||
}
|
||||
|
||||
bool WebSocketClient::send(const std::string& message) {
|
||||
if (!impl_->connected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(impl_->queue_mutex);
|
||||
impl_->send_queue.push(message);
|
||||
}
|
||||
|
||||
impl_->queue_cv.notify_one();
|
||||
asio::post(impl_->ioc, [this]() { impl_->doSend(); });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebSocketClient::sendBinary(const void* data, size_t size) {
|
||||
if (!impl_->connected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string message(static_cast<const char*>(data), size);
|
||||
return send(message);
|
||||
}
|
||||
|
||||
void WebSocketClient::setAutoReconnect(bool enable, int interval_ms) {
|
||||
impl_->auto_reconnect = enable;
|
||||
impl_->reconnect_interval_ms = interval_ms;
|
||||
}
|
||||
|
||||
void WebSocketClient::setHeartbeatInterval(int interval_ms) {
|
||||
impl_->heartbeat_interval_ms = interval_ms;
|
||||
}
|
||||
|
||||
std::string WebSocketClient::getHost() const {
|
||||
return impl_->host;
|
||||
}
|
||||
|
||||
std::string WebSocketClient::getPort() const {
|
||||
return impl_->port;
|
||||
}
|
||||
|
||||
std::string WebSocketClient::getPath() const {
|
||||
return impl_->path;
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace pmc
|
||||
@@ -0,0 +1,383 @@
|
||||
// WebSocketServer.cpp - WebSocket server implementation
|
||||
// Based on Boost.Beast and Boost.Asio
|
||||
|
||||
#include "us/WebSocketServer.hpp"
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace websocket = beast::websocket;
|
||||
namespace asio = boost::asio;
|
||||
using tcp = asio::ip::tcp;
|
||||
|
||||
namespace pmc::net {
|
||||
|
||||
// Internal Session class implementation
|
||||
class WebSocketServer::Session : public std::enable_shared_from_this<Session> {
|
||||
public:
|
||||
Session(tcp::socket socket, WebSocketServer* server)
|
||||
: ws_(std::move(socket))
|
||||
, server_(server) {
|
||||
|
||||
// Get remote address
|
||||
auto endpoint = ws_.next_layer().socket().remote_endpoint();
|
||||
remoteAddress_ = endpoint.address().to_string() + ":" + std::to_string(endpoint.port());
|
||||
}
|
||||
|
||||
void start() {
|
||||
// Generate client ID
|
||||
if (server_) {
|
||||
clientId_ = server_->generateClientId();
|
||||
} else {
|
||||
// If server is null, use default ID
|
||||
static std::atomic<uint64_t> counter{0};
|
||||
clientId_ = "client_" + std::to_string(++counter);
|
||||
}
|
||||
|
||||
// Asynchronously accept WebSocket handshake
|
||||
ws_.async_accept(
|
||||
beast::bind_front_handler(
|
||||
&Session::onAccept,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void send(const std::string& message) {
|
||||
// Add message to send queue
|
||||
bool write_in_progress = !sendQueue_.empty();
|
||||
sendQueue_.push_back(message);
|
||||
|
||||
if (!write_in_progress) {
|
||||
// Start asynchronous write
|
||||
doWrite();
|
||||
}
|
||||
}
|
||||
|
||||
const std::string& getClientId() const { return clientId_; }
|
||||
const std::string& getRemoteAddress() const { return remoteAddress_; }
|
||||
|
||||
private:
|
||||
websocket::stream<beast::tcp_stream> ws_;
|
||||
beast::flat_buffer buffer_;
|
||||
std::deque<std::string> sendQueue_;
|
||||
WebSocketServer* server_;
|
||||
std::string clientId_;
|
||||
std::string remoteAddress_;
|
||||
|
||||
void onAccept(beast::error_code ec) {
|
||||
if (ec) {
|
||||
std::cerr << "WebSocket accept error: " << ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to server client list
|
||||
if (server_) {
|
||||
server_->addClient(clientId_, shared_from_this());
|
||||
|
||||
// Call connection handler
|
||||
if (server_->connectHandler_) {
|
||||
server_->connectHandler_(clientId_, remoteAddress_);
|
||||
}
|
||||
}
|
||||
|
||||
// Start reading messages
|
||||
doRead();
|
||||
}
|
||||
|
||||
void doRead() {
|
||||
// Asynchronously read message
|
||||
ws_.async_read(
|
||||
buffer_,
|
||||
beast::bind_front_handler(
|
||||
&Session::onRead,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void onRead(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
if (ec == websocket::error::closed) {
|
||||
// Normal close
|
||||
if (server_) {
|
||||
server_->removeClient(clientId_);
|
||||
if (server_->disconnectHandler_) {
|
||||
server_->disconnectHandler_(clientId_);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ec) {
|
||||
std::cerr << "WebSocket read error: " << ec.message() << std::endl;
|
||||
if (server_) {
|
||||
server_->removeClient(clientId_);
|
||||
if (server_->disconnectHandler_) {
|
||||
server_->disconnectHandler_(clientId_);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Process received message
|
||||
std::string message = beast::buffers_to_string(buffer_.data());
|
||||
buffer_.consume(bytes_transferred);
|
||||
|
||||
if (server_) {
|
||||
// Find message handler
|
||||
for (const auto& handler : server_->messageHandlers_) {
|
||||
// Path matching logic can be added here
|
||||
handler.second(clientId_, message, [this](const std::string& response) {
|
||||
send(response);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Continue reading
|
||||
doRead();
|
||||
}
|
||||
|
||||
void doWrite() {
|
||||
if (sendQueue_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get first message from queue
|
||||
auto message = sendQueue_.front();
|
||||
|
||||
// Asynchronously write message
|
||||
ws_.async_write(
|
||||
asio::buffer(message),
|
||||
beast::bind_front_handler(
|
||||
&Session::onWrite,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void onWrite(beast::error_code ec, std::size_t bytes_transferred) {
|
||||
(void)bytes_transferred;
|
||||
if (ec) {
|
||||
std::cerr << "WebSocket write error: " << ec.message() << std::endl;
|
||||
if (server_) {
|
||||
server_->removeClient(clientId_);
|
||||
if (server_->disconnectHandler_) {
|
||||
server_->disconnectHandler_(clientId_);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove sent message
|
||||
sendQueue_.pop_front();
|
||||
|
||||
// Continue sending next message in queue
|
||||
if (!sendQueue_.empty()) {
|
||||
doWrite();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Internal Listener class implementation
|
||||
class WebSocketServer::Listener : public std::enable_shared_from_this<Listener> {
|
||||
public:
|
||||
static std::shared_ptr<Listener> create(asio::io_context& ioc, tcp::endpoint endpoint, WebSocketServer* server) {
|
||||
return std::shared_ptr<Listener>(new Listener(ioc, endpoint, server));
|
||||
}
|
||||
|
||||
void run() {
|
||||
if (!acceptor_.is_open()) {
|
||||
throw std::runtime_error("Acceptor is not open");
|
||||
}
|
||||
|
||||
doAccept();
|
||||
}
|
||||
|
||||
private:
|
||||
Listener(asio::io_context& ioc, tcp::endpoint endpoint, WebSocketServer* server)
|
||||
: ioc_(ioc)
|
||||
, acceptor_(asio::make_strand(ioc))
|
||||
, server_(server) {
|
||||
|
||||
beast::error_code ec;
|
||||
|
||||
// Open acceptor
|
||||
acceptor_.open(endpoint.protocol(), ec);
|
||||
if (ec) {
|
||||
std::cerr << "Failed to open acceptor: " << ec.message() << std::endl;
|
||||
throw std::runtime_error("Failed to open acceptor: " + ec.message());
|
||||
}
|
||||
|
||||
// Set address reuse
|
||||
acceptor_.set_option(asio::socket_base::reuse_address(true), ec);
|
||||
if (ec) {
|
||||
std::cerr << "Failed to set reuse address: " << ec.message() << std::endl;
|
||||
throw std::runtime_error("Failed to set reuse address: " + ec.message());
|
||||
}
|
||||
|
||||
// Bind to endpoint
|
||||
acceptor_.bind(endpoint, ec);
|
||||
if (ec) {
|
||||
std::cerr << "Failed to bind to endpoint: " << ec.message() << std::endl;
|
||||
throw std::runtime_error("Failed to bind to endpoint: " + ec.message());
|
||||
}
|
||||
|
||||
// Start listening
|
||||
acceptor_.listen(asio::socket_base::max_listen_connections, ec);
|
||||
if (ec) {
|
||||
std::cerr << "Failed to listen: " << ec.message() << std::endl;
|
||||
throw std::runtime_error("Failed to listen: " + ec.message());
|
||||
}
|
||||
}
|
||||
|
||||
void doAccept() {
|
||||
acceptor_.async_accept(
|
||||
asio::make_strand(ioc_),
|
||||
beast::bind_front_handler(
|
||||
&Listener::onAccept,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void onAccept(beast::error_code ec, tcp::socket socket) {
|
||||
if (ec) {
|
||||
std::cerr << "Accept error: " << ec.message() << std::endl;
|
||||
} else {
|
||||
// Create new Session to handle connection
|
||||
std::make_shared<Session>(std::move(socket), server_)->start();
|
||||
}
|
||||
|
||||
// Continue accepting new connections
|
||||
doAccept();
|
||||
}
|
||||
|
||||
asio::io_context& ioc_;
|
||||
tcp::acceptor acceptor_;
|
||||
WebSocketServer* server_;
|
||||
};
|
||||
|
||||
WebSocketServer::WebSocketServer(unsigned short port, unsigned int threads)
|
||||
: port_(port)
|
||||
, threads_(threads)
|
||||
, impl_(std::make_unique<Impl>()) {}
|
||||
|
||||
WebSocketServer::~WebSocketServer() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void WebSocketServer::start() {
|
||||
if (running_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create listener
|
||||
tcp::endpoint endpoint(tcp::v4(), port_);
|
||||
impl_->listener_ = Listener::create(impl_->ioc_, endpoint, this);
|
||||
|
||||
// Start listener
|
||||
impl_->listener_->run();
|
||||
|
||||
// Start worker threads
|
||||
for (unsigned int i = 0; i < threads_; ++i) {
|
||||
impl_->threads_.emplace_back([this]() {
|
||||
impl_->ioc_.run();
|
||||
});
|
||||
}
|
||||
|
||||
running_ = true;
|
||||
std::cout << "WebSocketServer started on port " << port_ << " with " << threads_ << " threads" << std::endl;
|
||||
}
|
||||
|
||||
void WebSocketServer::stop() {
|
||||
if (!running_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop io_context
|
||||
impl_->ioc_.stop();
|
||||
|
||||
// Wait for all threads to finish
|
||||
for (auto& thread : impl_->threads_) {
|
||||
if (thread.joinable()) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
impl_->threads_.clear();
|
||||
running_ = false;
|
||||
std::cout << "WebSocketServer stopped" << std::endl;
|
||||
}
|
||||
|
||||
void WebSocketServer::onMessage(const std::string& path, WebSocketMessageHandler handler) {
|
||||
messageHandlers_[path] = std::move(handler);
|
||||
}
|
||||
|
||||
void WebSocketServer::onConnect(WebSocketConnectHandler handler) {
|
||||
connectHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void WebSocketServer::onDisconnect(WebSocketDisconnectHandler handler) {
|
||||
disconnectHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
bool WebSocketServer::sendToClient(const std::string& clientId, const std::string& message) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
auto it = clients_.find(clientId);
|
||||
if (it != clients_.end()) {
|
||||
it->second->send(message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t WebSocketServer::broadcast(const std::string& message) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
size_t count = 0;
|
||||
for (auto& pair : clients_) {
|
||||
pair.second->send(message);
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void WebSocketServer::disconnectClient(const std::string& clientId) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
auto it = clients_.find(clientId);
|
||||
if (it != clients_.end()) {
|
||||
// Should close WebSocket connection here
|
||||
// Currently only removing from list
|
||||
clients_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
size_t WebSocketServer::getClientCount() const {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
return clients_.size();
|
||||
}
|
||||
|
||||
std::string WebSocketServer::generateClientId() const {
|
||||
static std::atomic<uint64_t> counter{0};
|
||||
return "client_" + std::to_string(++counter);
|
||||
}
|
||||
|
||||
void WebSocketServer::addClient(const std::string& clientId, std::shared_ptr<Session> session) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
clients_[clientId] = session;
|
||||
}
|
||||
|
||||
void WebSocketServer::removeClient(const std::string& clientId) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
clients_.erase(clientId);
|
||||
}
|
||||
|
||||
std::shared_ptr<WebSocketServer::Session> WebSocketServer::getClient(const std::string& clientId) {
|
||||
std::lock_guard<std::mutex> lock(clientsMutex_);
|
||||
auto it = clients_.find(clientId);
|
||||
if (it != clients_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace pmc::net
|
||||
Reference in new issue
Block a user