From 55632a9a5380515401db395ca0025905dd4aa5ff Mon Sep 17 00:00:00 2001 From: Quentin Chateau Date: Sat, 12 Sep 2020 19:58:34 +0200 Subject: [PATCH] initial code --- .clang-format | 86 ++++++ .gitignore | 6 + README.md | 20 +- client/index.html | 13 + client/index.js | 62 ++++ docker-compose.yml | 20 ++ nginx/default.conf.template | 25 ++ server/CMakeLists.txt | 51 ++++ server/cmake/conan.cmake | 562 ++++++++++++++++++++++++++++++++++++ server/config.h | 22 ++ server/listener.cpp | 42 +++ server/listener.h | 27 ++ server/main.cpp | 47 +++ server/session.cpp | 60 ++++ server/session.h | 24 ++ workspace.code-workspace | 21 ++ 16 files changed, 1087 insertions(+), 1 deletion(-) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 client/index.html create mode 100644 client/index.js create mode 100644 docker-compose.yml create mode 100644 nginx/default.conf.template create mode 100644 server/CMakeLists.txt create mode 100644 server/cmake/conan.cmake create mode 100644 server/config.h create mode 100644 server/listener.cpp create mode 100644 server/listener.h create mode 100644 server/main.cpp create mode 100644 server/session.cpp create mode 100644 server/session.h create mode 100644 workspace.code-workspace diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..420c48d --- /dev/null +++ b/.clang-format @@ -0,0 +1,86 @@ +Language: Cpp +BasedOnStyle: Google +AccessModifierOffset: -4 +AlignAfterOpenBracket: AlwaysBreak +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: true +BinPackArguments: false +BinPackParameters: false +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Stroustrup +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] +FixNamespaceComments: false +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^<[a-z_]+>$' + Priority: 10 + - Regex: '^<.*>$' + Priority: 50 + - Regex: '.*' + Priority: 1 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: false +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: 'vlc_module_begin' +MacroBlockEnd: 'vlc_module_end' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +PenaltyBreakAssignment: 50 +PenaltyBreakBeforeFirstCallParameter: 100 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 0 +PenaltyBreakString: 100 +PenaltyExcessCharacter: 10 +PenaltyReturnTypeOnItsOwnLine: 10000 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 4 +UseTab: Never +--- +Language: Proto +TabWidth: 4 +IndentWidth: 4 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67619f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build/ +.history/ +.vs-code/ +.clangd/ + +compile_commands.json \ No newline at end of file diff --git a/README.md b/README.md index f876290..5f934d0 100644 --- a/README.md +++ b/README.md @@ -1 +1,19 @@ -# experimental-ws-game \ No newline at end of file +# experimental-ws-game + +Trying out boost.beast with C++20 coroutines and websockets. + +Maybe this will become a mini-game one day. + +# How to run + +```bash +# build the server +mkdir -p server/build +cd server/build +CXX=clang++-10 CC=clang-10 cmake .. -DCMAKE_BUILD_TYPE=Release +make -j +cd ../../ + +# serve the application +docker-compose up -d +``` diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..daafe8a --- /dev/null +++ b/client/index.html @@ -0,0 +1,13 @@ + + + + + Test client + + + + +
Hello !
+ + + diff --git a/client/index.js b/client/index.js new file mode 100644 index 0000000..d88046f --- /dev/null +++ b/client/index.js @@ -0,0 +1,62 @@ +class CanvasManager { + constructor(id) { + this.canvas = document.getElementById(id); + this.canvas.width = window.innerWidth; + this.canvas.height = window.innerHeight; + this.ctx = this.canvas.getContext("2d"); + } + + drawRect(x, y) { + const w = 100; + const h = 100; + + this.clear(); + this.ctx.fillStyle = "green"; + this.ctx.fillRect(x - w / 2, y - h / 2, w, h); + } + + clear() { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + } +} + +class Engine { + constructor(url, canvasId) { + this.canvas = new CanvasManager(canvasId); + this.sock = new WebSocket(url); + this.sock.onopen = function () { + this.onOpen(); + }.bind(this); + + // Log errors + this.sock.onerror = function (error) { + this.onError(error); + }.bind(this); + + // Log messages from the server + this.sock.onmessage = function (e) { + const msg = JSON.parse(e.data); + this.onMessage(msg); + }.bind(this); + } + + onOpen() { + console.log("Starting communication"); + } + + onError(error) { + console.error("WebSocket error", error); + } + + onMessage(msg) { + console.log(msg); + this.canvas.drawRect(msg.x, msg.y); + } +} + +function main() { + console.log("Running ..."); + engine = new Engine("ws://localhost:8080/ws", "canvas"); +} + +window.onload = main; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6739a81 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: "3.2" + +services: + nginx: + restart: always + image: nginx:1.19 + volumes: + - ./nginx:/etc/nginx/templates + - ./client:/www/data + ports: + - "8080:80" + environment: + - WS_SERVER=ws_backend:5678 + + ws_backend: + restart: always + image: alpine:3.12 + volumes: + - ./server/build/bin:/server/ + command: /server/server 0.0.0.0 5678 1 diff --git a/nginx/default.conf.template b/nginx/default.conf.template new file mode 100644 index 0000000..e768fe0 --- /dev/null +++ b/nginx/default.conf.template @@ -0,0 +1,25 @@ +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +upstream websocket { + server ${WS_SERVER}; +} + +server { + listen 80; + + location / { + root /www/data; + } + + location /ws { + proxy_pass http://websocket; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} \ No newline at end of file diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt new file mode 100644 index 0000000..22406d1 --- /dev/null +++ b/server/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.10.2) +project(spaceinvader_server) + +include(cmake/conan.cmake) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_compile_options(-fcoroutines-ts -stdlib=libc++) +# set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -stdlib=libc++ -lc++abi") + +# add this to have a working compile_commands.json ... +include_directories(SYSTEM /usr/lib/llvm-10/include/c++/v1/) + +conan_cmake_run( + BASIC_SETUP + CMAKE_TARGETS + REQUIRES + fmt/7.0.1 + spdlog/1.8.0 + boost/1.74.0 + nlohmann_json/3.9.1 + INSTALL_ARGS + --build missing +) + +add_executable( + server + + main.cpp + listener.cpp + session.cpp +) +target_link_libraries( + server + + CONAN_PKG::fmt + CONAN_PKG::spdlog + CONAN_PKG::boost + CONAN_PKG::nlohmann_json +) +target_link_options( + server + PUBLIC + + -static + -stdlib=libc++ + -lc++abi + -fuse-ld=lld +) diff --git a/server/cmake/conan.cmake b/server/cmake/conan.cmake new file mode 100644 index 0000000..488b2a2 --- /dev/null +++ b/server/cmake/conan.cmake @@ -0,0 +1,562 @@ +# The MIT License (MIT) + +# Copyright (c) 2018 JFrog + +# 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. + + + +# This file comes from: https://github.com/conan-io/cmake-conan. Please refer +# to this repository for issues and documentation. + +# Its purpose is to wrap and launch Conan C/C++ Package Manager when cmake is called. +# It will take CMake current settings (os, compiler, compiler version, architecture) +# and translate them to conan settings for installing and retrieving dependencies. + +# It is intended to facilitate developers building projects that have conan dependencies, +# but it is only necessary on the end-user side. It is not necessary to create conan +# packages, in fact it shouldn't be use for that. Check the project documentation. + + +include(CMakeParseArguments) + +function(_get_msvc_ide_version result) + set(${result} "" PARENT_SCOPE) + if(NOT MSVC_VERSION VERSION_LESS 1400 AND MSVC_VERSION VERSION_LESS 1500) + set(${result} 8 PARENT_SCOPE) + elseif(NOT MSVC_VERSION VERSION_LESS 1500 AND MSVC_VERSION VERSION_LESS 1600) + set(${result} 9 PARENT_SCOPE) + elseif(NOT MSVC_VERSION VERSION_LESS 1600 AND MSVC_VERSION VERSION_LESS 1700) + set(${result} 10 PARENT_SCOPE) + elseif(NOT MSVC_VERSION VERSION_LESS 1700 AND MSVC_VERSION VERSION_LESS 1800) + set(${result} 11 PARENT_SCOPE) + elseif(NOT MSVC_VERSION VERSION_LESS 1800 AND MSVC_VERSION VERSION_LESS 1900) + set(${result} 12 PARENT_SCOPE) + elseif(NOT MSVC_VERSION VERSION_LESS 1900 AND MSVC_VERSION VERSION_LESS 1910) + set(${result} 14 PARENT_SCOPE) + elseif(NOT MSVC_VERSION VERSION_LESS 1910 AND MSVC_VERSION VERSION_LESS 1920) + set(${result} 15 PARENT_SCOPE) + elseif(NOT MSVC_VERSION VERSION_LESS 1920 AND MSVC_VERSION VERSION_LESS 1930) + set(${result} 16 PARENT_SCOPE) + else() + message(FATAL_ERROR "Conan: Unknown MSVC compiler version [${MSVC_VERSION}]") + endif() +endfunction() + +function(conan_cmake_settings result) + #message(STATUS "COMPILER " ${CMAKE_CXX_COMPILER}) + #message(STATUS "COMPILER " ${CMAKE_CXX_COMPILER_ID}) + #message(STATUS "VERSION " ${CMAKE_CXX_COMPILER_VERSION}) + #message(STATUS "FLAGS " ${CMAKE_LANG_FLAGS}) + #message(STATUS "LIB ARCH " ${CMAKE_CXX_LIBRARY_ARCHITECTURE}) + #message(STATUS "BUILD TYPE " ${CMAKE_BUILD_TYPE}) + #message(STATUS "GENERATOR " ${CMAKE_GENERATOR}) + #message(STATUS "GENERATOR WIN64 " ${CMAKE_CL_64}) + + message(STATUS "Conan: Automatic detection of conan settings from cmake") + + parse_arguments(${ARGV}) + + if(ARGUMENTS_BUILD_TYPE) + set(_CONAN_SETTING_BUILD_TYPE ${ARGUMENTS_BUILD_TYPE}) + elseif(CMAKE_BUILD_TYPE) + set(_CONAN_SETTING_BUILD_TYPE ${CMAKE_BUILD_TYPE}) + else() + message(FATAL_ERROR "Please specify in command line CMAKE_BUILD_TYPE (-DCMAKE_BUILD_TYPE=Release)") + endif() + + string(TOUPPER ${_CONAN_SETTING_BUILD_TYPE} _CONAN_SETTING_BUILD_TYPE_UPPER) + if (_CONAN_SETTING_BUILD_TYPE_UPPER STREQUAL "DEBUG") + set(_CONAN_SETTING_BUILD_TYPE "Debug") + elseif(_CONAN_SETTING_BUILD_TYPE_UPPER STREQUAL "RELEASE") + set(_CONAN_SETTING_BUILD_TYPE "Release") + elseif(_CONAN_SETTING_BUILD_TYPE_UPPER STREQUAL "RELWITHDEBINFO") + set(_CONAN_SETTING_BUILD_TYPE "RelWithDebInfo") + elseif(_CONAN_SETTING_BUILD_TYPE_UPPER STREQUAL "MINSIZEREL") + set(_CONAN_SETTING_BUILD_TYPE "MinSizeRel") + endif() + + if(ARGUMENTS_ARCH) + set(_CONAN_SETTING_ARCH ${ARGUMENTS_ARCH}) + endif() + #handle -s os setting + if(CMAKE_SYSTEM_NAME) + #use default conan os setting if CMAKE_SYSTEM_NAME is not defined + set(CONAN_SYSTEM_NAME ${CMAKE_SYSTEM_NAME}) + if(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + set(CONAN_SYSTEM_NAME Macos) + endif() + set(CONAN_SUPPORTED_PLATFORMS Windows Linux Macos Android iOS FreeBSD WindowsStore) + list (FIND CONAN_SUPPORTED_PLATFORMS "${CONAN_SYSTEM_NAME}" _index) + if (${_index} GREATER -1) + #check if the cmake system is a conan supported one + set(_CONAN_SETTING_OS ${CONAN_SYSTEM_NAME}) + else() + message(FATAL_ERROR "cmake system ${CONAN_SYSTEM_NAME} is not supported by conan. Use one of ${CONAN_SUPPORTED_PLATFORMS}") + endif() + endif() + + get_property(_languages GLOBAL PROPERTY ENABLED_LANGUAGES) + if (";${_languages};" MATCHES ";CXX;") + set(LANGUAGE CXX) + set(USING_CXX 1) + elseif (";${_languages};" MATCHES ";C;") + set(LANGUAGE C) + set(USING_CXX 0) + else () + message(FATAL_ERROR "Conan: Neither C or C++ was detected as a language for the project. Unabled to detect compiler version.") + endif() + + if (${CMAKE_${LANGUAGE}_COMPILER_ID} STREQUAL GNU) + # using GCC + # TODO: Handle other params + string(REPLACE "." ";" VERSION_LIST ${CMAKE_${LANGUAGE}_COMPILER_VERSION}) + list(GET VERSION_LIST 0 MAJOR) + list(GET VERSION_LIST 1 MINOR) + set(COMPILER_VERSION ${MAJOR}.${MINOR}) + if(${MAJOR} GREATER 4) + set(COMPILER_VERSION ${MAJOR}) + endif() + set(_CONAN_SETTING_COMPILER gcc) + set(_CONAN_SETTING_COMPILER_VERSION ${COMPILER_VERSION}) + if (USING_CXX) + conan_cmake_detect_unix_libcxx(_LIBCXX) + set(_CONAN_SETTING_COMPILER_LIBCXX ${_LIBCXX}) + endif () + elseif (${CMAKE_${LANGUAGE}_COMPILER_ID} STREQUAL AppleClang) + # using AppleClang + string(REPLACE "." ";" VERSION_LIST ${CMAKE_${LANGUAGE}_COMPILER_VERSION}) + list(GET VERSION_LIST 0 MAJOR) + list(GET VERSION_LIST 1 MINOR) + set(_CONAN_SETTING_COMPILER apple-clang) + set(_CONAN_SETTING_COMPILER_VERSION ${MAJOR}.${MINOR}) + if (USING_CXX) + conan_cmake_detect_unix_libcxx(_LIBCXX) + set(_CONAN_SETTING_COMPILER_LIBCXX ${_LIBCXX}) + endif () + elseif (${CMAKE_${LANGUAGE}_COMPILER_ID} STREQUAL Clang) + string(REPLACE "." ";" VERSION_LIST ${CMAKE_${LANGUAGE}_COMPILER_VERSION}) + list(GET VERSION_LIST 0 MAJOR) + list(GET VERSION_LIST 1 MINOR) + set(_CONAN_SETTING_COMPILER clang) + set(_CONAN_SETTING_COMPILER_VERSION ${MAJOR}.${MINOR}) + if(APPLE) + cmake_policy(GET CMP0025 APPLE_CLANG_POLICY) + if(NOT APPLE_CLANG_POLICY STREQUAL NEW) + message(STATUS "Conan: APPLE and Clang detected. Assuming apple-clang compiler. Set CMP0025 to avoid it") + set(_CONAN_SETTING_COMPILER apple-clang) + endif() + endif() + if(${_CONAN_SETTING_COMPILER} STREQUAL clang AND ${MAJOR} GREATER 7) + set(_CONAN_SETTING_COMPILER_VERSION ${MAJOR}) + endif() + if (USING_CXX) + conan_cmake_detect_unix_libcxx(_LIBCXX) + set(_CONAN_SETTING_COMPILER_LIBCXX ${_LIBCXX}) + endif () + elseif(${CMAKE_${LANGUAGE}_COMPILER_ID} STREQUAL MSVC) + set(_VISUAL "Visual Studio") + _get_msvc_ide_version(_VISUAL_VERSION) + if("${_VISUAL_VERSION}" STREQUAL "") + message(FATAL_ERROR "Conan: Visual Studio not recognized") + else() + set(_CONAN_SETTING_COMPILER ${_VISUAL}) + set(_CONAN_SETTING_COMPILER_VERSION ${_VISUAL_VERSION}) + endif() + + if(NOT _CONAN_SETTING_ARCH) + if (MSVC_${LANGUAGE}_ARCHITECTURE_ID MATCHES "64") + set(_CONAN_SETTING_ARCH x86_64) + elseif (MSVC_${LANGUAGE}_ARCHITECTURE_ID MATCHES "^ARM") + message(STATUS "Conan: Using default ARM architecture from MSVC") + set(_CONAN_SETTING_ARCH armv6) + elseif (MSVC_${LANGUAGE}_ARCHITECTURE_ID MATCHES "86") + set(_CONAN_SETTING_ARCH x86) + else () + message(FATAL_ERROR "Conan: Unknown MSVC architecture [${MSVC_${LANGUAGE}_ARCHITECTURE_ID}]") + endif() + endif() + + conan_cmake_detect_vs_runtime(_vs_runtime) + message(STATUS "Conan: Detected VS runtime: ${_vs_runtime}") + set(_CONAN_SETTING_COMPILER_RUNTIME ${_vs_runtime}) + + if (CMAKE_GENERATOR_TOOLSET) + set(_CONAN_SETTING_COMPILER_TOOLSET ${CMAKE_VS_PLATFORM_TOOLSET}) + elseif(CMAKE_VS_PLATFORM_TOOLSET AND (CMAKE_GENERATOR STREQUAL "Ninja")) + set(_CONAN_SETTING_COMPILER_TOOLSET ${CMAKE_VS_PLATFORM_TOOLSET}) + endif() + else() + message(FATAL_ERROR "Conan: compiler setup not recognized") + endif() + + # If profile is defined it is used + if(CMAKE_BUILD_TYPE STREQUAL "Debug" AND ARGUMENTS_DEBUG_PROFILE) + set(_APPLIED_PROFILES ${ARGUMENTS_DEBUG_PROFILE}) + elseif(CMAKE_BUILD_TYPE STREQUAL "Release" AND ARGUMENTS_RELEASE_PROFILE) + set(_APPLIED_PROFILES ${ARGUMENTS_RELEASE_PROFILE}) + elseif(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo" AND ARGUMENTS_RELWITHDEBINFO_PROFILE) + set(_APPLIED_PROFILES ${ARGUMENTS_RELWITHDEBINFO_PROFILE}) + elseif(CMAKE_BUILD_TYPE STREQUAL "MinSizeRel" AND ARGUMENTS_MINSIZEREL_PROFILE) + set(_APPLIED_PROFILES ${ARGUMENTS_MINSIZEREL_PROFILE}) + elseif(ARGUMENTS_PROFILE) + set(_APPLIED_PROFILES ${ARGUMENTS_PROFILE}) + endif() + + foreach(ARG ${_APPLIED_PROFILES}) + set(_SETTINGS ${_SETTINGS} -pr=${ARG}) + endforeach() + + if(NOT _SETTINGS OR ARGUMENTS_PROFILE_AUTO STREQUAL "ALL") + set(ARGUMENTS_PROFILE_AUTO arch build_type compiler compiler.version + compiler.runtime compiler.libcxx compiler.toolset) + endif() + + # Automatic from CMake + foreach(ARG ${ARGUMENTS_PROFILE_AUTO}) + string(TOUPPER ${ARG} _arg_name) + string(REPLACE "." "_" _arg_name ${_arg_name}) + if(_CONAN_SETTING_${_arg_name}) + set(_SETTINGS ${_SETTINGS} -s ${ARG}=${_CONAN_SETTING_${_arg_name}}) + endif() + endforeach() + + foreach(ARG ${ARGUMENTS_SETTINGS}) + set(_SETTINGS ${_SETTINGS} -s ${ARG}) + endforeach() + + message(STATUS "Conan: Settings= ${_SETTINGS}") + + set(${result} ${_SETTINGS} PARENT_SCOPE) +endfunction() + + +function(conan_cmake_detect_unix_libcxx result) + # Take into account any -stdlib in compile options + get_directory_property(compile_options DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMPILE_OPTIONS) + + # Take into account any _GLIBCXX_USE_CXX11_ABI in compile definitions + get_directory_property(defines DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMPILE_DEFINITIONS) + foreach(define ${defines}) + if(define MATCHES "_GLIBCXX_USE_CXX11_ABI") + if(define MATCHES "^-D") + set(compile_options ${compile_options} "${define}") + else() + set(compile_options ${compile_options} "-D${define}") + endif() + endif() + endforeach() + + execute_process( + COMMAND ${CMAKE_COMMAND} -E echo "#include " + COMMAND ${CMAKE_CXX_COMPILER} -x c++ ${compile_options} -E -dM - + OUTPUT_VARIABLE string_defines + ) + + if(string_defines MATCHES "#define __GLIBCXX__") + # Allow -D_GLIBCXX_USE_CXX11_ABI=ON/OFF as argument to cmake + if(DEFINED _GLIBCXX_USE_CXX11_ABI) + if(_GLIBCXX_USE_CXX11_ABI) + set(${result} libstdc++11 PARENT_SCOPE) + return() + else() + set(${result} libstdc++ PARENT_SCOPE) + return() + endif() + endif() + + if(string_defines MATCHES "#define _GLIBCXX_USE_CXX11_ABI 1\n") + set(${result} libstdc++11 PARENT_SCOPE) + else() + # Either the compiler is missing the define because it is old, and so + # it can't use the new abi, or the compiler was configured to use the + # old abi by the user or distro (e.g. devtoolset on RHEL/CentOS) + set(${result} libstdc++ PARENT_SCOPE) + endif() + else() + set(${result} libc++ PARENT_SCOPE) + endif() +endfunction() + +function(conan_cmake_detect_vs_runtime result) + string(TOUPPER ${CMAKE_BUILD_TYPE} build_type) + set(variables CMAKE_CXX_FLAGS_${build_type} CMAKE_C_FLAGS_${build_type} CMAKE_CXX_FLAGS CMAKE_C_FLAGS) + foreach(variable ${variables}) + if(NOT "${${variable}}" STREQUAL "") + string(REPLACE " " ";" flags ${${variable}}) + foreach (flag ${flags}) + if(${flag} STREQUAL "/MD" OR ${flag} STREQUAL "/MDd" OR ${flag} STREQUAL "/MT" OR ${flag} STREQUAL "/MTd") + string(SUBSTRING ${flag} 1 -1 runtime) + set(${result} ${runtime} PARENT_SCOPE) + return() + endif() + endforeach() + endif() + endforeach() + if(${build_type} STREQUAL "DEBUG") + set(${result} "MDd" PARENT_SCOPE) + else() + set(${result} "MD" PARENT_SCOPE) + endif() +endfunction() + + +macro(parse_arguments) + set(options BASIC_SETUP CMAKE_TARGETS UPDATE KEEP_RPATHS NO_LOAD NO_OUTPUT_DIRS OUTPUT_QUIET NO_IMPORTS) + set(oneValueArgs CONANFILE ARCH BUILD_TYPE INSTALL_FOLDER CONAN_COMMAND) + set(multiValueArgs DEBUG_PROFILE RELEASE_PROFILE RELWITHDEBINFO_PROFILE MINSIZEREL_PROFILE + PROFILE REQUIRES OPTIONS IMPORTS SETTINGS BUILD ENV GENERATORS PROFILE_AUTO + INSTALL_ARGS) + cmake_parse_arguments(ARGUMENTS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) +endmacro() + +function(conan_cmake_install) + # Calls "conan install" + # Argument BUILD is equivalant to --build={missing, PkgName,...} or + # --build when argument is 'BUILD all' (which builds all packages from source) + # Argument CONAN_COMMAND, to specify the conan path, e.g. in case of running from source + # cmake does not identify conan as command, even if it is +x and it is in the path + parse_arguments(${ARGV}) + + if(CONAN_CMAKE_MULTI) + set(ARGUMENTS_GENERATORS ${ARGUMENTS_GENERATORS} cmake_multi) + else() + set(ARGUMENTS_GENERATORS ${ARGUMENTS_GENERATORS} cmake) + endif() + + set(CONAN_BUILD_POLICY "") + foreach(ARG ${ARGUMENTS_BUILD}) + if(${ARG} STREQUAL "all") + set(CONAN_BUILD_POLICY ${CONAN_BUILD_POLICY} --build) + break() + else() + set(CONAN_BUILD_POLICY ${CONAN_BUILD_POLICY} --build=${ARG}) + endif() + endforeach() + if(ARGUMENTS_CONAN_COMMAND) + set(CONAN_CMD ${ARGUMENTS_CONAN_COMMAND}) + else() + conan_check(REQUIRED) + endif() + set(CONAN_OPTIONS "") + if(ARGUMENTS_CONANFILE) + set(CONANFILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARGUMENTS_CONANFILE}) + # A conan file has been specified - apply specified options as well if provided + foreach(ARG ${ARGUMENTS_OPTIONS}) + set(CONAN_OPTIONS ${CONAN_OPTIONS} -o=${ARG}) + endforeach() + else() + set(CONANFILE ".") + endif() + if(ARGUMENTS_UPDATE) + set(CONAN_INSTALL_UPDATE --update) + endif() + if(ARGUMENTS_NO_IMPORTS) + set(CONAN_INSTALL_NO_IMPORTS --no-imports) + endif() + set(CONAN_INSTALL_FOLDER "") + if(ARGUMENTS_INSTALL_FOLDER) + set(CONAN_INSTALL_FOLDER -if=${ARGUMENTS_INSTALL_FOLDER}) + endif() + foreach(ARG ${ARGUMENTS_GENERATORS}) + set(CONAN_GENERATORS ${CONAN_GENERATORS} -g=${ARG}) + endforeach() + foreach(ARG ${ARGUMENTS_ENV}) + set(CONAN_ENV_VARS ${CONAN_ENV_VARS} -e=${ARG}) + endforeach() + set(conan_args install ${CONANFILE} ${settings} ${CONAN_ENV_VARS} ${CONAN_GENERATORS} ${CONAN_BUILD_POLICY} ${CONAN_INSTALL_UPDATE} ${CONAN_INSTALL_NO_IMPORTS} ${CONAN_OPTIONS} ${CONAN_INSTALL_FOLDER} ${ARGUMENTS_INSTALL_ARGS}) + + string (REPLACE ";" " " _conan_args "${conan_args}") + message(STATUS "Conan executing: ${CONAN_CMD} ${_conan_args}") + + if(ARGUMENTS_OUTPUT_QUIET) + execute_process(COMMAND ${CONAN_CMD} ${conan_args} + RESULT_VARIABLE return_code + OUTPUT_VARIABLE conan_output + ERROR_VARIABLE conan_output + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + else() + execute_process(COMMAND ${CONAN_CMD} ${conan_args} + RESULT_VARIABLE return_code + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + endif() + + if(NOT "${return_code}" STREQUAL "0") + message(FATAL_ERROR "Conan install failed='${return_code}'") + endif() + +endfunction() + + +function(conan_cmake_setup_conanfile) + parse_arguments(${ARGV}) + if(ARGUMENTS_CONANFILE) + get_filename_component(_CONANFILE_NAME ${ARGUMENTS_CONANFILE} NAME) + # configure_file will make sure cmake re-runs when conanfile is updated + configure_file(${ARGUMENTS_CONANFILE} ${CMAKE_CURRENT_BINARY_DIR}/${_CONANFILE_NAME}.junk COPYONLY) + file(REMOVE ${CMAKE_CURRENT_BINARY_DIR}/${_CONANFILE_NAME}.junk) + else() + conan_cmake_generate_conanfile(${ARGV}) + endif() +endfunction() + +function(conan_cmake_generate_conanfile) + # Generate, writing in disk a conanfile.txt with the requires, options, and imports + # specified as arguments + # This will be considered as temporary file, generated in CMAKE_CURRENT_BINARY_DIR) + parse_arguments(${ARGV}) + set(_FN "${CMAKE_CURRENT_BINARY_DIR}/conanfile.txt") + + file(WRITE ${_FN} "[generators]\ncmake\n\n[requires]\n") + foreach(ARG ${ARGUMENTS_REQUIRES}) + file(APPEND ${_FN} ${ARG} "\n") + endforeach() + + file(APPEND ${_FN} ${ARG} "\n[options]\n") + foreach(ARG ${ARGUMENTS_OPTIONS}) + file(APPEND ${_FN} ${ARG} "\n") + endforeach() + + file(APPEND ${_FN} ${ARG} "\n[imports]\n") + foreach(ARG ${ARGUMENTS_IMPORTS}) + file(APPEND ${_FN} ${ARG} "\n") + endforeach() +endfunction() + + +macro(conan_load_buildinfo) + if(CONAN_CMAKE_MULTI) + set(_CONANBUILDINFO conanbuildinfo_multi.cmake) + else() + set(_CONANBUILDINFO conanbuildinfo.cmake) + endif() + if(ARGUMENTS_INSTALL_FOLDER) + set(_CONANBUILDINFOFOLDER ${ARGUMENTS_INSTALL_FOLDER}) + else() + set(_CONANBUILDINFOFOLDER ${CMAKE_CURRENT_BINARY_DIR}) + endif() + # Checks for the existence of conanbuildinfo.cmake, and loads it + # important that it is macro, so variables defined at parent scope + if(EXISTS "${_CONANBUILDINFOFOLDER}/${_CONANBUILDINFO}") + message(STATUS "Conan: Loading ${_CONANBUILDINFO}") + include(${_CONANBUILDINFOFOLDER}/${_CONANBUILDINFO}) + else() + message(FATAL_ERROR "${_CONANBUILDINFO} doesn't exist in ${CMAKE_CURRENT_BINARY_DIR}") + endif() +endmacro() + + +macro(conan_cmake_run) + parse_arguments(${ARGV}) + + if(CMAKE_CONFIGURATION_TYPES AND NOT CMAKE_BUILD_TYPE AND NOT CONAN_EXPORTED + AND NOT ARGUMENTS_BUILD_TYPE) + set(CONAN_CMAKE_MULTI ON) + message(STATUS "Conan: Using cmake-multi generator") + else() + set(CONAN_CMAKE_MULTI OFF) + endif() + + if(NOT CONAN_EXPORTED) + conan_cmake_setup_conanfile(${ARGV}) + if(CONAN_CMAKE_MULTI) + foreach(CMAKE_BUILD_TYPE "Release" "Debug") + set(ENV{CONAN_IMPORT_PATH} ${CMAKE_BUILD_TYPE}) + conan_cmake_settings(settings ${ARGV}) + conan_cmake_install(SETTINGS ${settings} ${ARGV}) + endforeach() + set(CMAKE_BUILD_TYPE) + else() + conan_cmake_settings(settings ${ARGV}) + conan_cmake_install(SETTINGS ${settings} ${ARGV}) + endif() + endif() + + if (NOT ARGUMENTS_NO_LOAD) + conan_load_buildinfo() + endif() + + if(ARGUMENTS_BASIC_SETUP) + foreach(_option CMAKE_TARGETS KEEP_RPATHS NO_OUTPUT_DIRS) + if(ARGUMENTS_${_option}) + if(${_option} STREQUAL "CMAKE_TARGETS") + list(APPEND _setup_options "TARGETS") + else() + list(APPEND _setup_options ${_option}) + endif() + endif() + endforeach() + conan_basic_setup(${_setup_options}) + endif() +endmacro() + +macro(conan_check) + # Checks conan availability in PATH + # Arguments REQUIRED and VERSION are optional + # Example usage: + # conan_check(VERSION 1.0.0 REQUIRED) + message(STATUS "Conan: checking conan executable") + set(options REQUIRED) + set(oneValueArgs VERSION) + cmake_parse_arguments(CONAN "${options}" "${oneValueArgs}" "" ${ARGN}) + + find_program(CONAN_CMD conan) + if(NOT CONAN_CMD AND CONAN_REQUIRED) + message(FATAL_ERROR "Conan executable not found!") + endif() + message(STATUS "Conan: Found program ${CONAN_CMD}") + execute_process(COMMAND ${CONAN_CMD} --version + OUTPUT_VARIABLE CONAN_VERSION_OUTPUT + ERROR_VARIABLE CONAN_VERSION_OUTPUT + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_STRIP_TRAILING_WHITESPACE) + message(STATUS "Conan: Version found ${CONAN_VERSION_OUTPUT}") + + if(DEFINED CONAN_VERSION) + string(REGEX MATCH ".*Conan version ([0-9]+\.[0-9]+\.[0-9]+)" FOO + "${CONAN_VERSION_OUTPUT}") + if(${CMAKE_MATCH_1} VERSION_LESS ${CONAN_VERSION}) + message(FATAL_ERROR "Conan outdated. Installed: ${CMAKE_MATCH_1}, \ + required: ${CONAN_VERSION}. Consider updating via 'pip \ + install conan==${CONAN_VERSION}'.") + endif() + endif() +endmacro() + +function(conan_add_remote) + # Adds a remote + # Arguments URL and NAME are required, INDEX and COMMAND are optional + # Example usage: + # conan_add_remote(NAME bincrafters INDEX 1 + # URL https://api.bintray.com/conan/bincrafters/public-conan) + set(oneValueArgs URL NAME INDEX COMMAND) + cmake_parse_arguments(CONAN "" "${oneValueArgs}" "" ${ARGN}) + + if(DEFINED CONAN_INDEX) + set(CONAN_INDEX_ARG "-i ${CONAN_INDEX}") + endif() + if(CONAN_COMMAND) + set(CONAN_CMD ${CONAN_COMMAND}) + else() + conan_check(REQUIRED) + endif() + message(STATUS "Conan: Adding ${CONAN_NAME} remote repository (${CONAN_URL})") + execute_process(COMMAND ${CONAN_CMD} remote add ${CONAN_NAME} ${CONAN_URL} + ${CONAN_INDEX_ARG} -f) +endfunction() diff --git a/server/config.h b/server/config.h new file mode 100644 index 0000000..0819303 --- /dev/null +++ b/server/config.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace si { + +namespace beast = boost::beast; +namespace net = boost::asio; +namespace http = beast::http; +namespace websocket = beast::websocket; +using tcp = net::ip::tcp; + +template +struct use_awaitable_executor { + using type = typename net::use_awaitable_t::template as_default_on_t; +}; + +template +using use_awaitable_executor_t = typename use_awaitable_executor::type; + +} // si diff --git a/server/listener.cpp b/server/listener.cpp new file mode 100644 index 0000000..4eac9e9 --- /dev/null +++ b/server/listener.cpp @@ -0,0 +1,42 @@ +#include "listener.h" +#include "session.h" + +namespace si { + +listener::listener(net::io_context& ioc, tcp::endpoint endpoint) + : ioc_(ioc), acceptor_(ioc) +{ + acceptor_.open(endpoint.protocol()); + acceptor_.set_option(net::socket_base::reuse_address(true)); + acceptor_.bind(endpoint); + acceptor_.listen(net::socket_base::max_listen_connections); +} + +void listener::run() +{ + net::co_spawn( + ioc_, + [self = shared_from_this()]() -> net::awaitable { + co_await self->on_run(); + }, + net::detached); +} + +net::awaitable listener::on_run() +{ + spdlog::info( + "listening on {}:{}", + acceptor_.local_endpoint().address().to_string(), + acceptor_.local_endpoint().port()); + + while (true) { + // The new connection gets its own strand + auto socket = co_await acceptor_.async_accept( + net::make_strand(ioc_), net::use_awaitable); + + // Create the http session and run it + std::make_shared(std::move(socket))->run(); + } +} + +} // si \ No newline at end of file diff --git a/server/listener.h b/server/listener.h new file mode 100644 index 0000000..7b2f0bf --- /dev/null +++ b/server/listener.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#include "config.h" + +namespace si { +// Accepts incoming connections and launches the sessions +class listener : public std::enable_shared_from_this { +public: + listener(net::io_context& ioc, tcp::endpoint endpoint); + + void run(); + +private: + net::awaitable on_run(); + + net::io_context& ioc_; + tcp::acceptor acceptor_; +}; + +} // si \ No newline at end of file diff --git a/server/main.cpp b/server/main.cpp new file mode 100644 index 0000000..b526560 --- /dev/null +++ b/server/main.cpp @@ -0,0 +1,47 @@ +#include +#include + +#include "listener.h" + +using namespace si; + +int main(int argc, char* argv[]) +{ + // Check command line arguments. + if (argc != 4) { + std::cerr << "Usage: server
\n" + << "Example:\n" + << " advanced-server 0.0.0.0 8080 1\n"; + return EXIT_FAILURE; + } + + auto const address = net::ip::make_address(argv[1]); + auto const port = static_cast(std::atoi(argv[2])); + auto const threads = std::max(1, std::atoi(argv[3])); + + // The io_context is required for all I/O + net::io_context ioc{threads}; + + // Create and launch a listening port + std::make_shared(ioc, tcp::endpoint{address, port})->run(); + + // Capture SIGINT and SIGTERM to perform a clean shutdown + net::signal_set signals(ioc, SIGINT, SIGTERM); + signals.async_wait([&](beast::error_code const&, int) { ioc.stop(); }); + + // Run the I/O service on the requested number of threads + std::vector v; + v.reserve(threads - 1); + for (auto i = threads - 1; i > 0; --i) { + v.emplace_back([&ioc] { ioc.run(); }); + } + + ioc.run(); + + // Block until all the threads exit + for (auto& t : v) { + t.join(); + } + + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/server/session.cpp b/server/session.cpp new file mode 100644 index 0000000..b2417c8 --- /dev/null +++ b/server/session.cpp @@ -0,0 +1,60 @@ +#include "session.h" + +#include +#include + +namespace si { + +session::session(tcp::socket&& socket) : ws_(std::move(socket)) +{ + spdlog::info("new session 0x{:p}", static_cast(this)); +} + +session::~session() +{ + spdlog::info("closing session 0x{:p}", static_cast(this)); +} + +void session::run() +{ + net::co_spawn( + ws_.get_executor(), + [self = shared_from_this()]() -> net::awaitable { + co_await self->on_run(); + }, + net::detached); +} + +net::awaitable session::on_run() +{ + // Set suggested timeout settings for the websocket + ws_.set_option( + websocket::stream_base::timeout::suggested(beast::role_type::server)); + + // Accept the websocket handshake + co_await ws_.async_accept(net::use_awaitable); + + const int w{800}; + const int h{800}; + int x{w / 2}; + int y{h / 2}; + int xinc = 2; + int yinc = 3; + + auto executor = co_await net::this_coro::executor; + net::steady_timer timer(executor); + + while (true) { + x = (x + xinc) % w; + y = (y + yinc) % h; + + auto msg = nlohmann::json::object({{"x", x}, {"y", y}}); + auto serialized = msg.dump(); + co_await ws_.async_write(net::buffer(serialized), net::use_awaitable); + + timer.expires_after(std::chrono::milliseconds{10}); + co_await timer.async_wait(net::use_awaitable); + } +} + +} // si \ No newline at end of file diff --git a/server/session.h b/server/session.h new file mode 100644 index 0000000..28544bf --- /dev/null +++ b/server/session.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#include + +#include "config.h" + +namespace si { + +// Echoes back all received WebSocket messages +class session : public std::enable_shared_from_this { +public: + explicit session(tcp::socket&& socket); + ~session(); + void run(); + net::awaitable on_run(); + +private: + websocket::stream ws_; +}; + +} // si \ No newline at end of file diff --git a/workspace.code-workspace b/workspace.code-workspace new file mode 100644 index 0000000..bf4ea56 --- /dev/null +++ b/workspace.code-workspace @@ -0,0 +1,21 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[cpp]": { + "editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd" + } + } +} \ No newline at end of file