This commit is contained in:
qing committed 2026-07-20 11:12:29 +08:00
commit 70d467d052
97 files changed
+10386

No files matched your search

+69
View File
@@ -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/
+145
View File
@@ -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 "========================================")
+21
View File
@@ -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.
+49
View File
@@ -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."
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
make clear
make
make install
+200
View File
@@ -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
+126
View File
@@ -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
+63
View File
@@ -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
+61
View File
@@ -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
+176
View File
@@ -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
+604
View File
@@ -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
+386
View File
@@ -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
+383
View File
@@ -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