├── .clang-tidy ├── .github └── workflows │ ├── build-macos.yml │ ├── build-ubuntu.yml │ └── build-windows.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── CMakeWindows.bat ├── LICENSE ├── README.md ├── cpack_config.cmake ├── samples ├── ecal_to_tcp │ ├── CMakeLists.txt │ └── src │ │ └── main.cpp ├── hello_world_publisher │ ├── CMakeLists.txt │ └── src │ │ └── main.cpp ├── hello_world_subscriber │ ├── CMakeLists.txt │ └── src │ │ └── main.cpp ├── integration_test │ ├── CMakeLists.txt │ └── src │ │ └── main.cpp ├── performance_publisher │ ├── CMakeLists.txt │ └── src │ │ └── main.cpp ├── performance_subscriber │ ├── CMakeLists.txt │ └── src │ │ └── main.cpp └── tcp_to_ecal │ ├── CMakeLists.txt │ └── src │ └── main.cpp ├── tcp_pubsub ├── CMakeLists.txt ├── cmake │ └── tcp_pubsubConfig.cmake.in ├── include │ └── tcp_pubsub │ │ ├── callback_data.h │ │ ├── executor.h │ │ ├── publisher.h │ │ ├── subscriber.h │ │ ├── subscriber_session.h │ │ └── tcp_pubsub_logger.h ├── src │ ├── executor.cpp │ ├── executor_impl.cpp │ ├── executor_impl.h │ ├── portable_endian.h │ ├── protocol_handshake_message.h │ ├── publisher.cpp │ ├── publisher_impl.cpp │ ├── publisher_impl.h │ ├── publisher_session.cpp │ ├── publisher_session.h │ ├── subscriber.cpp │ ├── subscriber_impl.cpp │ ├── subscriber_impl.h │ ├── subscriber_session.cpp │ ├── subscriber_session_impl.cpp │ ├── subscriber_session_impl.h │ ├── tcp_header.h │ └── tcp_pubsub_logger_abstraction.h ├── tcp_pubsub_version.h.in └── version.cmake ├── tests └── tcp_pubsub_test │ ├── CMakeLists.txt │ └── src │ ├── atomic_signalable.h │ └── tcp_pubsub_test.cpp └── thirdparty ├── asio ├── Module │ └── Findasio.cmake └── build-asio.cmake ├── gtest ├── Module │ └── FindGTest.cmake └── build-gtest.cmake ├── recycle ├── Module │ └── Findrecycle.cmake └── build-recycle.cmake └── tcp_pubsub └── Module └── Findtcp_pubsub.cmake /.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | # Resons why specific warnings have been turned off: 3 | # 4 | # -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling 5 | # This warns about memcpy and wants us to use memcpy_s, which is not available in our gcc setup. 6 | # 7 | # -cppcoreguidelines-pro-type-vararg 8 | # This forbids using functions like printf, snprintf etc. We would like to use those either way. 9 | # 10 | # -misc-no-recursion 11 | # Recursion with functions can be an elegant way of solving recursive problems 12 | # 13 | # These checks have been disabled to keep compatibility with C++14: 14 | # -modernize-concat-nested-namespaces 15 | # -modernize-use-nodiscard 16 | # 17 | 18 | Checks: "-*, 19 | clang-analyzer-*, 20 | -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, 21 | 22 | bugprone-*, 23 | -bugprone-easily-swappable-parameters, 24 | -bugprone-implicit-widening-of-multiplication-result, 25 | -bugprone-narrowing-conversions, 26 | 27 | cppcoreguidelines-*, 28 | -cppcoreguidelines-avoid-magic-numbers, 29 | -cppcoreguidelines-avoid-non-const-global-variables, 30 | -cppcoreguidelines-macro-usage, 31 | -cppcoreguidelines-narrowing-conversions, 32 | -cppcoreguidelines-non-private-member-variables-in-classes, 33 | -cppcoreguidelines-pro-bounds-array-to-pointer-decay, 34 | -cppcoreguidelines-pro-bounds-pointer-arithmetic, 35 | -cppcoreguidelines-pro-type-vararg, 36 | -cppcoreguidelines-pro-type-reinterpret-cast, 37 | 38 | misc-*, 39 | -misc-include-cleaner, 40 | -misc-non-private-member-variables-in-classes, 41 | -misc-no-recursion, 42 | 43 | modernize-*, 44 | -modernize-pass-by-value, 45 | -modernize-use-trailing-return-type, 46 | -modernize-use-auto, 47 | -modernize-concat-nested-namespaces, 48 | -modernize-return-braced-init-list, 49 | -modernize-use-nodiscard, 50 | -modernize-avoid-bind, 51 | 52 | performance-*, 53 | 54 | readability-*, 55 | -readability-braces-around-statements, 56 | -readability-identifier-length, 57 | -readability-magic-numbers, 58 | -readability-redundant-access-specifiers, 59 | -readability-function-cognitive-complexity, 60 | -readability-else-after-return, 61 | " 62 | WarningsAsErrors: '' 63 | HeaderFilterRegex: '^((?!/thirdparty/|/_deps/).)*$' 64 | FormatStyle: none 65 | -------------------------------------------------------------------------------- /.github/workflows/build-macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ master ] 7 | 8 | env: 9 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 10 | BUILD_TYPE: Release 11 | 12 | jobs: 13 | build-macos: 14 | # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. 15 | # You can convert this to a matrix build if you need cross-platform coverage. 16 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 17 | runs-on: macos-latest 18 | 19 | steps: 20 | 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | submodules: 'true' 25 | fetch-depth: 0 26 | 27 | - name: Configure CMake 28 | # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. 29 | # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type 30 | run: | 31 | cmake -B ${{github.workspace}}/_build \ 32 | -DTCP_PUBSUB_BUILD_SAMPLES=ON \ 33 | -DTCP_PUBSUB_BUILD_TESTS=ON \ 34 | -DTCP_PUBSUB_USE_BUILTIN_ASIO=ON \ 35 | -DTCP_PUBSUB_USE_BUILTIN_RECYCLE=ON \ 36 | -DTCP_PUBSUB_USE_BUILTIN_GTEST=ON \ 37 | -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ 38 | -DCMAKE_CXX_FLAGS=-DASIO_NO_DEPRECATED 39 | shell: bash 40 | 41 | - name: Build 42 | # Build your program with the given configuration 43 | run: cmake --build ${{github.workspace}}/_build --config ${{env.BUILD_TYPE}} 44 | 45 | - name: Run Tests 46 | run: ctest -C Release -V 47 | working-directory: ${{ github.workspace }}/_build 48 | 49 | -------------------------------------------------------------------------------- /.github/workflows/build-ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ master ] 7 | 8 | env: 9 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 10 | BUILD_TYPE: Release 11 | PROJECT_NAME: tcp_pubsub 12 | 13 | jobs: 14 | build-ubuntu: 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | library_type: [static, shared, object] 20 | os: [ubuntu-24.04, ubuntu-22.04] 21 | 22 | # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. 23 | # You can convert this to a matrix build if you need cross-platform coverage. 24 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 25 | runs-on: ${{ matrix.os }} 26 | 27 | steps: 28 | 29 | - name: Set Variables 30 | run: | 31 | if [[ '${{ matrix.library_type }}' == 'static' ]]; then 32 | echo "build_shared_libs=OFF" >> "$GITHUB_ENV" 33 | echo "tcp_pubsub_library_type=STATIC" >> "$GITHUB_ENV" 34 | echo "package_postfix=static" >> "$GITHUB_ENV" 35 | elif [[ '${{ matrix.library_type }}' == 'shared' ]]; then 36 | echo "build_shared_libs=ON" >> "$GITHUB_ENV" 37 | echo "tcp_pubsub_library_type=SHARED" >> "$GITHUB_ENV" 38 | echo "package_postfix=shared" >> "$GITHUB_ENV" 39 | elif [[ '${{ matrix.library_type }}' == 'object' ]]; then 40 | echo "build_shared_libs=OFF" >> "$GITHUB_ENV" 41 | echo "tcp_pubsub_library_type=OBJECT" >> "$GITHUB_ENV" 42 | echo "package_postfix=object" >> "$GITHUB_ENV" 43 | fi 44 | 45 | - name: Checkout 46 | uses: actions/checkout@v4 47 | with: 48 | submodules: 'true' 49 | fetch-depth: 0 50 | 51 | ############################################ 52 | # Test-compile the project 53 | ############################################ 54 | 55 | - name: Configure CMake 56 | # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. 57 | # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type 58 | run: | 59 | cmake -B ${{github.workspace}}/_build \ 60 | -DTCP_PUBSUB_BUILD_SAMPLES=ON \ 61 | -DTCP_PUBSUB_BUILD_TESTS=ON \ 62 | -DTCP_PUBSUB_USE_BUILTIN_ASIO=ON \ 63 | -DTCP_PUBSUB_USE_BUILTIN_RECYCLE=ON \ 64 | -DTCP_PUBSUB_USE_BUILTIN_GTEST=ON \ 65 | -DTCP_PUBSUB_LIBRARY_TYPE=${{env.tcp_pubsub_library_type}} \ 66 | -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ 67 | -DCMAKE_CXX_FLAGS=-DASIO_NO_DEPRECATED \ 68 | -DBUILD_SHARED_LIBS=${{ env.build_shared_libs }} 69 | 70 | - name: Build 71 | # Build your program with the given configuration 72 | run: cmake --build ${{github.workspace}}/_build --config ${{env.BUILD_TYPE}} 73 | 74 | - name: Run Tests 75 | run: ctest -C Release -V 76 | working-directory: ${{ github.workspace }}/_build 77 | 78 | - name: Read Project Version from CMakeCache 79 | run: | 80 | cmake_project_version_string=$(cat "${{github.workspace}}/_build/CMakeCache.txt" | grep "^CMAKE_PROJECT_VERSION:") 81 | arr=(${cmake_project_version_string//=/ }) 82 | cmake_project_version=${arr[1]} 83 | echo "CMAKE_PROJECT_VERSION=$cmake_project_version" >> "$GITHUB_ENV" 84 | shell: bash 85 | 86 | - name: CPack 87 | run: cpack -G DEB 88 | working-directory: ${{ github.workspace }}/_build 89 | if: ${{ matrix.library_type != 'object' }} 90 | 91 | - name: Rename .deb installer 92 | run: | 93 | mv *.deb '${{ env.PROJECT_NAME }}-${{ env.CMAKE_PROJECT_VERSION }}-${{ matrix.os }}-${{ env.package_postfix }}.deb' 94 | shell: bash 95 | working-directory: ${{github.workspace}}/_build/_package/ 96 | if: ${{ matrix.library_type != 'object' }} 97 | 98 | - name: Upload binaries 99 | uses: actions/upload-artifact@v4 100 | with: 101 | name: ${{ env.PROJECT_NAME }}-${{ env.CMAKE_PROJECT_VERSION }}-${{ matrix.os }}-${{ env.package_postfix }} 102 | path: ${{github.workspace}}/_build/_package/*.deb 103 | if: ${{ matrix.library_type != 'object' }} 104 | 105 | ############################################ 106 | # Test if our binary can be linked against 107 | ############################################ 108 | 109 | - name: Install binaries 110 | shell: bash 111 | run: sudo dpkg -i ${{ github.workspace }}/_build/_package/*.deb 112 | if: ${{ matrix.library_type != 'object' }} 113 | 114 | - name: Compile integration test (Release) 115 | run: | 116 | cmake -B ${{github.workspace}}/samples/integration_test/_build/release \ 117 | -DCMAKE_BUILD_TYPE=Release 118 | cmake --build ${{github.workspace}}/samples/integration_test/_build/release 119 | 120 | working-directory: ${{ github.workspace }}/samples/integration_test 121 | if: ${{ matrix.library_type != 'object' }} 122 | 123 | - name: Run integration test (Release) 124 | run: ./integration_test 125 | working-directory: ${{ github.workspace }}/samples/integration_test/_build/release 126 | if: ${{ matrix.library_type != 'object' }} 127 | 128 | - name: Compile integration test (Debug) 129 | run: | 130 | cmake -B ${{github.workspace}}/samples/integration_test/_build/debug \ 131 | -DCMAKE_BUILD_TYPE=Debug 132 | cmake --build ${{github.workspace}}/samples/integration_test/_build/debug 133 | 134 | working-directory: ${{ github.workspace }}/samples/integration_test 135 | if: ${{ matrix.library_type != 'object' }} 136 | 137 | - name: Run integration test (Debug) 138 | run: ./integration_test 139 | working-directory: ${{ github.workspace }}/samples/integration_test/_build/debug 140 | if: ${{ matrix.library_type != 'object' }} 141 | -------------------------------------------------------------------------------- /.github/workflows/build-windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ master ] 7 | 8 | env: 9 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 10 | INSTALL_PREFIX: _install 11 | PROJECT_NAME: tcp_pubsub 12 | VS_TOOLSET: v141 13 | VS_NAME: vs2017 14 | 15 | jobs: 16 | build-windows: 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | library_type: [static, shared, object] 22 | build_arch: [x64, win32] 23 | 24 | # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. 25 | # You can convert this to a matrix build if you need cross-platform coverage. 26 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 27 | runs-on: windows-2019 28 | 29 | steps: 30 | 31 | - name: Set Variables 32 | run: | 33 | if ( '${{ matrix.library_type }}' -eq 'static' ) 34 | { 35 | echo "tcp_pubsub_library_type=STATIC" >> "$Env:GITHUB_ENV" 36 | echo "package_postfix=static" >> "$Env:GITHUB_ENV" 37 | } 38 | elseif( '${{ matrix.library_type }}' -eq 'shared' ) 39 | { 40 | echo "tcp_pubsub_library_type=SHARED" >> "$Env:GITHUB_ENV" 41 | echo "package_postfix=shared" >> "$Env:GITHUB_ENV" 42 | } 43 | elseif( '${{ matrix.library_type }}' -eq 'object' ) 44 | { 45 | echo "tcp_pubsub_library_type=OBJECT" >> "$Env:GITHUB_ENV" 46 | echo "package_postfix=object" >> "$Env:GITHUB_ENV" 47 | } 48 | 49 | - name: Checkout 50 | uses: actions/checkout@v4 51 | with: 52 | submodules: 'true' 53 | fetch-depth: 0 54 | 55 | ############################################ 56 | # Test-compile the project 57 | ############################################ 58 | 59 | - name: Configure CMake 60 | # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. 61 | # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type 62 | shell: cmd 63 | run: | 64 | cmake -B ${{github.workspace}}/_build ^ 65 | -G "Visual Studio 16 2019" ^ 66 | -A ${{ matrix.build_arch }} ^ 67 | -T ${{ env.VS_TOOLSET }} ^ 68 | -DTCP_PUBSUB_BUILD_SAMPLES=ON ^ 69 | -DTCP_PUBSUB_BUILD_TESTS=ON ^ 70 | -DTCP_PUBSUB_USE_BUILTIN_ASIO=ON ^ 71 | -DTCP_PUBSUB_USE_BUILTIN_RECYCLE=ON ^ 72 | -DTCP_PUBSUB_USE_BUILTIN_GTEST=ON ^ 73 | -DTCP_PUBSUB_LIBRARY_TYPE=${{env.tcp_pubsub_library_type}} ^ 74 | -DCMAKE_CXX_FLAGS=/DASIO_NO_DEPRECATED ^ 75 | -DCMAKE_INSTALL_PREFIX=${{env.INSTALL_PREFIX}} 76 | 77 | - name: Build (Release) 78 | shell: cmd 79 | run: | 80 | cmake --build ${{github.workspace}}/_build --config Release --parallel 81 | 82 | - name: Install (Release) 83 | shell: cmd 84 | run: | 85 | cmake --build ${{github.workspace}}/_build --config Release --target INSTALL 86 | if: ${{ matrix.library_type != 'object' }} 87 | 88 | - name: Build (Debug) 89 | shell: cmd 90 | run: | 91 | cmake --build ${{github.workspace}}/_build --config Debug --parallel 92 | 93 | - name: Install (Debug) 94 | shell: cmd 95 | run: | 96 | cmake --build ${{github.workspace}}/_build --config Debug --target INSTALL 97 | if: ${{ matrix.library_type != 'object' }} 98 | 99 | - name: Run Tests 100 | run: ctest -C Release -V 101 | working-directory: ${{ github.workspace }}/_build 102 | 103 | - name: Read Project Version from CMakeCache 104 | run: | 105 | $cmake_project_version_line = cat ${{github.workspace}}/_build/CMakeCache.txt | Select-String -Pattern ^CMAKE_PROJECT_VERSION: 106 | $cmake_project_version = $cmake_project_version_line.Line.split("=")[1] 107 | echo "CMAKE_PROJECT_VERSION=$cmake_project_version" >> "$Env:GITHUB_ENV" 108 | 109 | - name: Upload binaries 110 | uses: actions/upload-artifact@v4 111 | with: 112 | name: ${{ env.PROJECT_NAME }}-${{ env.CMAKE_PROJECT_VERSION }}-windows-${{ matrix.build_arch }}-${{ env.VS_NAME }}-${{ matrix.library_type }} 113 | path: ${{github.workspace}}/${{env.INSTALL_PREFIX}} 114 | if: ${{ matrix.library_type != 'object' }} 115 | 116 | ############################################ 117 | # Test if our binary can be linked against 118 | ############################################ 119 | 120 | - name: CMake integration test 121 | shell: powershell 122 | run: | 123 | cmake -B "${{github.workspace}}/samples/integration_test/_build" ` 124 | -A ${{ matrix.build_arch }} ` 125 | -DCMAKE_PREFIX_PATH="${{github.workspace}}/${{env.INSTALL_PREFIX}}" 126 | 127 | working-directory: ${{ github.workspace }}/samples/integration_test 128 | if: ${{ matrix.library_type != 'object' }} 129 | 130 | - name: Compile integration test (Release) 131 | shell: cmd 132 | run: cmake --build ${{github.workspace}}/samples/integration_test/_build --config Release 133 | working-directory: ${{ github.workspace }}/samples/integration_test 134 | if: ${{ matrix.library_type != 'object' }} 135 | 136 | - name: Run integration test (Release) 137 | run: | 138 | if ( '${{ matrix.library_type }}' -eq 'shared' ) 139 | { 140 | $Env:Path = '${{github.workspace}}/${{env.INSTALL_PREFIX}}/bin;' + $Env:Path 141 | } 142 | .\integration_test.exe 143 | working-directory: ${{ github.workspace }}/samples/integration_test/_build/Release 144 | if: ${{ matrix.library_type != 'object' }} 145 | 146 | - name: Compile integration test (Debug) 147 | shell: cmd 148 | run: cmake --build ${{github.workspace}}/samples/integration_test/_build --config Debug 149 | working-directory: ${{ github.workspace }}/samples/integration_test 150 | if: ${{ matrix.library_type != 'object' }} 151 | 152 | - name: Run integration test (Debug) 153 | run: | 154 | if ( '${{ matrix.library_type }}' -eq 'shared' ) 155 | { 156 | $Env:Path = '${{github.workspace}}/${{env.INSTALL_PREFIX}}/bin;' + $Env:Path 157 | } 158 | .\integration_test.exe 159 | working-directory: ${{ github.workspace }}/samples/integration_test/_build/Debug 160 | if: ${{ matrix.library_type != 'object' }} 161 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Clion 35 | .idea 36 | cmake-build-* 37 | 38 | # QtCreator 39 | CMakeLists.txt.user 40 | 41 | # VS Code 42 | .vscode 43 | 44 | #Build dir 45 | /_build 46 | /samples/integration_test/_build 47 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "thirdparty/asio/asio"] 2 | path = thirdparty/asio/asio 3 | url = https://github.com/chriskohlhoff/asio.git 4 | [submodule "thirdparty/recycle/recycle"] 5 | path = thirdparty/recycle/recycle 6 | url = https://github.com/steinwurf/recycle.git 7 | [submodule "thirdparty/gtest/googletest"] 8 | path = thirdparty/gtest/googletest 9 | url = https://github.com/google/googletest.git 10 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13...4.0) 2 | 3 | include(CMakeDependentOption) 4 | 5 | # Project call 6 | include("${CMAKE_CURRENT_LIST_DIR}/tcp_pubsub/version.cmake") 7 | project(tcp_pubsub VERSION ${TCP_PUBSUB_VERSION_MAJOR}.${TCP_PUBSUB_VERSION_MINOR}.${TCP_PUBSUB_VERSION_PATCH}) 8 | 9 | # Normalize backslashes from Windows paths 10 | file(TO_CMAKE_PATH "${CMAKE_MODULE_PATH}" CMAKE_MODULE_PATH) 11 | file(TO_CMAKE_PATH "${CMAKE_PREFIX_PATH}" CMAKE_PREFIX_PATH) 12 | message(STATUS "Module Path: ${CMAKE_MODULE_PATH}") 13 | message(STATUS "Prefix Path: ${CMAKE_PREFIX_PATH}") 14 | 15 | # CMake Options 16 | option(TCP_PUBSUB_BUILD_SAMPLES 17 | "Build project samples" 18 | ON) 19 | 20 | option(TCP_PUBSUB_BUILD_ECAL_SAMPLES 21 | "Build eCAL-based project samples. Requires eCAL to be findable by CMake." 22 | OFF) 23 | 24 | option(TCP_PUBSUB_USE_BUILTIN_ASIO 25 | "Use the builtin asio submodule. If set to OFF, asio must be available from somewhere else (e.g. system libs)." 26 | ON) 27 | 28 | option(TCP_PUBSUB_USE_BUILTIN_RECYCLE 29 | "Use the builtin steinwurf::recycle submodule. If set to OFF, recycle must be available from somewhere else (e.g. system libs)." 30 | ON) 31 | 32 | option(TCP_PUBSUB_BUILD_TESTS 33 | "Build the tcp_pubsub tests. Requires Gtest::GTest to be findable by CMake." 34 | OFF) 35 | 36 | cmake_dependent_option(TCP_PUBSUB_USE_BUILTIN_GTEST 37 | "Use the builtin GoogleTest submodule. Only needed if TCP_PUBSUB_BUILD_TESTS is ON. If set to OFF, GoogleTest must be available from somewhere else (e.g. system libs)." 38 | ON # Default value if dependency is met 39 | "TCP_PUBSUB_BUILD_TESTS" # Dependency 40 | OFF) # Default value if dependency is not met 41 | 42 | 43 | # Module path for finding asio 44 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/modules) 45 | 46 | # Set Debug postfix 47 | set(CMAKE_DEBUG_POSTFIX d) 48 | set(CMAKE_MINSIZEREL_POSTFIX minsize) 49 | set(CMAKE_RELWITHDEBINFO_POSTFIX reldbg) 50 | 51 | 52 | # Use builtin asio 53 | if (TCP_PUBSUB_USE_BUILTIN_ASIO) 54 | include("${CMAKE_CURRENT_LIST_DIR}/thirdparty/asio/build-asio.cmake") 55 | endif() 56 | 57 | # Use builtin recycle 58 | if (TCP_PUBSUB_USE_BUILTIN_RECYCLE) 59 | include("${CMAKE_CURRENT_LIST_DIR}/thirdparty/recycle/build-recycle.cmake") 60 | endif() 61 | 62 | # Use builtin gtest 63 | if (TCP_PUBSUB_USE_BUILTIN_GTEST) 64 | include("${CMAKE_CURRENT_LIST_DIR}/thirdparty/gtest/build-gtest.cmake") 65 | endif() 66 | 67 | # For tests we need to make sure that all shared libraries and executables are 68 | # put into the same directory. Otherwise the tests will fail on windows. 69 | if(TCP_PUBSUB_BUILD_TESTS AND (BUILD_SHARED_LIBS OR (TCP_PUBSUB_LIBRARY_TYPE STREQUAL "SHARED"))) 70 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 71 | endif() 72 | 73 | # Module path for finding tcp_pubsub 74 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/tcp_pubsub/Module) 75 | 76 | # Add main tcp_pubsub library 77 | add_subdirectory(tcp_pubsub) 78 | 79 | # Generic samples 80 | if (TCP_PUBSUB_BUILD_SAMPLES) 81 | add_subdirectory(samples/performance_publisher) 82 | add_subdirectory(samples/performance_subscriber) 83 | add_subdirectory(samples/hello_world_publisher) 84 | add_subdirectory(samples/hello_world_subscriber) 85 | endif() 86 | 87 | # Specific eCAL Samples that tunnel an eCAL Topic through TCP 88 | if(TCP_PUBSUB_BUILD_ECAL_SAMPLES) 89 | add_subdirectory(samples/ecal_to_tcp) 90 | add_subdirectory(samples/tcp_to_ecal) 91 | endif() 92 | 93 | 94 | # Add Tests if enabled 95 | if (TCP_PUBSUB_BUILD_TESTS) 96 | enable_testing() 97 | add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/tests/tcp_pubsub_test") 98 | endif() 99 | 100 | # Make this package available for packing with CPack 101 | include("${CMAKE_CURRENT_LIST_DIR}/cpack_config.cmake") 102 | -------------------------------------------------------------------------------- /CMakeWindows.bat: -------------------------------------------------------------------------------- 1 | mkdir _build 2 | cd _build 3 | 4 | cmake .. -DCMAKE_INSTALL_PREFIX=_install -DBUILD_SHARED_LIBS=OFF 5 | cd .. 6 | pause 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Continental Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | --------- 24 | 25 | This repository contains a copy of "portable_endian.h", written by Mathias 26 | Panzenböck, which is licensed under Public Domain. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Windows](https://github.com/eclipse-ecal/tcp_pubsub/actions/workflows/build-windows.yml/badge.svg)](https://github.com/eclipse-ecal/tcp_pubsub/actions/workflows/build-windows.yml) [![Ubuntu](https://github.com/eclipse-ecal/tcp_pubsub/actions/workflows/build-ubuntu.yml/badge.svg)](https://github.com/eclipse-ecal/tcp_pubsub/actions/workflows/build-ubuntu.yml) [![macOS](https://github.com/eclipse-ecal/tcp_pubsub/actions/workflows/build-macos.yml/badge.svg)](https://github.com/eclipse-ecal/tcp_pubsub/actions/workflows/build-macos.yml) 2 | 3 | # tcp_pubsub - TCP Publish/Subscribe library 4 | 5 | tcp_pubsub is a minimal publish-subscribe library that transports data via TCP. The project is CMake based. The dependencies are integrated as git submodules. In your own Project you can either use those submodules as well, or provide the dependencies in your own manner. 6 | 7 | tcp_pubsub does not define a message format but only transports binary blobs. It does however define a protocol around that, which is kept as lightweight as possible. 8 | 9 | Dependencies: 10 | 11 | - [asio](https://github.com/chriskohlhoff/asio.git) 12 | - [recycle](https://github.com/steinwurf/recycle.git) 13 | 14 | ## Hello World Example 15 | 16 | A very similar Example is also provided in the repository. 17 | 18 | ### Publisher 19 | 20 | ```cpp 21 | #include 22 | 23 | #include 24 | #include 25 | 26 | int main() 27 | { 28 | // Create a "Hello World" buffer 29 | std::string data_to_send = "Hello World"; 30 | 31 | // Create an Executor with a thread-pool size of 6. If you create multiple 32 | // publishers and subscribers, they all should share the same Executor. 33 | std::shared_ptr executor = std::make_shared(6); 34 | 35 | // Create a publisher that will offer the data on port 1588 36 | tcp_pubsub::Publisher hello_world_publisher(executor, 1588); 37 | 38 | for (;;) 39 | { 40 | // Send the "Hello World" string by passing the pointer to the first 41 | // character and the length. 42 | hello_world_publisher.send(&data_to_send[0], data_to_send.size()); 43 | 44 | std::this_thread::sleep_for(std::chrono::milliseconds(500)); 45 | } 46 | 47 | return 0; 48 | } 49 | ``` 50 | 51 | ### Subscriber 52 | 53 | ```cpp 54 | #include 55 | #include 56 | 57 | #include 58 | #include 59 | 60 | int main() 61 | { 62 | // Create an Executor with a thread-pool size of 6. If you create multiple 63 | // publishers and subscribers, they all should share the same Executor. 64 | std::shared_ptr executor = std::make_shared(6); 65 | 66 | // Create a subscriber 67 | tcp_pubsub::Subscriber hello_world_subscriber(executor); 68 | 69 | // Add a session to the subscriber that connects to port 1588 on localhost. A 70 | // subscriber will aggregate traffic from multiple source, if you add multiple 71 | // sessions. 72 | hello_world_subscriber.addSession("127.0.0.1", 1588); 73 | 74 | // Create a Callback that will be called each time a data packet is received. 75 | // This function will create an std::string from the packet and print it to 76 | // the console. 77 | std::function callback_function 78 | = [](const tcp_pubsub::CallbackData& callback_data) -> void 79 | { 80 | std::cout << "Received playload: " 81 | << std::string(callback_data.buffer_->data(), callback_data.buffer_->size()) 82 | << std::endl; 83 | }; 84 | 85 | // Set the callback to the subsriber 86 | hello_world_subscriber.setCallback(callback_function); 87 | 88 | // Prevent the application from exiting immediatelly 89 | for (;;) std::this_thread::sleep_for(std::chrono::milliseconds(500)); 90 | return 0; 91 | } 92 | ``` 93 | 94 | ## CMake Options 95 | 96 | You can set the following CMake Options to control how tcp_pubsub is built: 97 | 98 | | Option | Type | Default | Explanation | 99 | |------------------------------------|-------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------| 100 | | `TCP_PUBSUB_BUILD_SAMPLES` | `BOOL`| `ON` | Build project samples. | 101 | | `TCP_PUBSUB_BUILD_ECAL_SAMPLES` | `BOOL`| `OFF` | Build eCAL-based project samples. Requires eCAL to be findable by CMake. | 102 | | `TCP_PUBSUB_USE_BUILTIN_ASIO` | `BOOL`| `ON` | Use the builtin asio submodule. If set to `OFF`, asio must be available from somewhere else (e.g. system libs). | 103 | | `TCP_PUBSUB_USE_BUILTIN_RECYCLE` | `BOOL`| `ON` | Use the builtin `steinwurf::recycle` submodule. If set to `OFF`, recycle must be available from somewhere else (e.g. system libs). | 104 | | `TCP_PUBSUB_BUILD_TESTS` | `BOOL`| `OFF` | Build the tcp_pubsub tests. Requires Gtest::GTest to be findable by CMake. | 105 | | `TCP_PUBSUB_USE_BUILTIN_GTEST` | `BOOL`| `ON` (if building tests) | Use the builtin GoogleTest submodule. Only needed if `TCP_PUBSUB_BUILD_TESTS` is `ON`. If set to `OFF`, GoogleTest must be available from elsewhere. | 106 | | `TCP_PUBSUB_LIBRARY_TYPE` | `STRING` | | Controls the library type of tcp_pubsub by injecting the string into the `add_library` call. Can be set to STATIC / SHARED / OBJECT. If set, this will override the regular `BUILD_SHARED_LIBS` CMake option. If not set, CMake will use the default setting, which is controlled by `BUILD_SHARED_LIBS`. | 107 | 108 | ## How to checkout and build 109 | 110 | There are several examples provided that aim to show you the functionality. 111 | 112 | 1. Install cmake and git / git-for-windows 113 | 114 | 2. Checkout this repo and the asio submodule 115 | ```console 116 | git clone https://github.com/eclipse-ecal/tcp_pubsub.git 117 | cd tcp_pubsub 118 | git submodule init 119 | git submodule update 120 | ``` 121 | 122 | 3. CMake the project *(Building as debug will add some debug output)* 123 | ```console 124 | mkdir _build 125 | cd _build 126 | cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=_install 127 | ``` 128 | 129 | 4. Build the project 130 | - Linux: `make` 131 | - Windows: Open `_build\tcp_pubsub.sln` with Visual Studio and build one of the example projects 132 | 133 | 5. Start either of the example pairs on the same machine. 134 | - `hello_world_publisher /.exe` + `hello_world_subscriber /.exe` 135 | *or* 136 | - `performance_publisher /.exe` + `performance_subscriber /.exe` 137 | 138 | ## The Protocol (Version 0) 139 | 140 | When using this library, you do not need to know how the protocol works. Both the subscriber and receiver are completely implemented and ready for you to use. This section is meant for advanced users that are interested in the underlying protocol. 141 | 142 |
143 | Show 144 | 145 | ### Message flow 146 | 147 | The Protocol is quite simple: 148 | 149 | 1. The **Subsriber** connects to the publisher and sends a ProtocolHandshakeRequest. This Message contains the maximum protocol Version the Subscriber supports 150 | 151 | 2. The **Publisher** returns a ProtocolHandshakeResponse. This message contains the protocol version that will be used from now on. The version must not be higher than the version sent by the subsriber. 152 | 153 | 3. The **Publisher** starts sending data to the subsriber. 154 | 155 | _The ProtocolHandshake is meant to provide future-proof expansions. At the moment the only available protocol version is 0._ 156 | 157 | ``` 158 | Subscriber Publisher 159 | | | 160 | | -> ProtocolHandshakeReq -> | 161 | | | 162 | | <- ProtocolHandshakeResp <- | 163 | | | 164 | | <--------- DATA <---------- | 165 | | <--------- DATA <---------- | 166 | | <--------- DATA <---------- | 167 | | ... | 168 | ``` 169 | 170 | ### Message layout 171 | 172 | The protocol uses the following message layout. Values that are not sent by the sender are to be interpreted as 0. 173 | 174 | - **General Message header** 175 | Each message will have a message header as follows. Values are to be interpreted little-endian. 176 | This header is defined in [tcp_pubsub/src/tcp_header.h](tcp_pubsub/src/tcp_header.h) 177 | 178 | - 16 bit: Header size 179 | - 8 bit: Type 180 | - 0 = Regular Payload 181 | - 1 = Handshake Message 182 | - 8 bit: Reserved 183 | - Must be 0 184 | - 64bit: Payload size 185 | 186 | 2. **ProtocolHandshakeReq & ProtocolHandshakeResp** 187 | The layout of ProtocolHandshakeReq / ProtocolHandshakeResp is the same. Values are to be interpreted little-endian 188 | This message is defined in [tcp_pubsub/src/protocol_handshake_message.h](tcp_pubsub/src/protocol_handshake_message.h) 189 | 190 | - Message Header (size given in the first 16 bit) 191 | - 8 bit: Protocol Version 192 | 193 |
194 | -------------------------------------------------------------------------------- /cpack_config.cmake: -------------------------------------------------------------------------------- 1 | set(CPACK_PACKAGE_NAME ${PROJECT_NAME}) 2 | set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A low-level publish-subscribe library operating on TCP/IP") 3 | set(CPACK_PACKAGE_VENDOR "Eclipse eCAL") 4 | set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) 5 | set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) 6 | set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) 7 | set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) 8 | set(CPACK_PACKAGE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/_package") 9 | 10 | set(CPACK_PACKAGE_CONTACT "florian.reimold@continental-corporation.com") 11 | 12 | set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Florian Reimold ") 13 | set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://github.com/eclipse-ecal/tcp_pubsub") 14 | set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) 15 | set(CPACK_DEBIAN_PACKAGE_GENERATE_SHLIBS ON) 16 | 17 | set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_LIST_DIR}/LICENSE") 18 | set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_LIST_DIR}/README.md") 19 | 20 | include(CPack) 21 | -------------------------------------------------------------------------------- /samples/ecal_to_tcp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5.1...4.0) 2 | set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) 3 | 4 | project(ecal_to_tcp) 5 | 6 | set(CMAKE_CXX_STANDARD 14) 7 | 8 | set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) 9 | find_package(tcp_pubsub REQUIRED) 10 | find_package(eCAL REQUIRED) 11 | find_package(Threads REQUIRED) 12 | 13 | set(sources 14 | src/main.cpp 15 | ) 16 | 17 | add_executable (${PROJECT_NAME} 18 | ${sources} 19 | ) 20 | 21 | target_link_libraries (${PROJECT_NAME} 22 | tcp_pubsub::tcp_pubsub 23 | eCAL::core 24 | Threads::Threads 25 | ) 26 | -------------------------------------------------------------------------------- /samples/ecal_to_tcp/src/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | std::map> messages_sent; 17 | 18 | void printLog() 19 | { 20 | for (;;) 21 | { 22 | std::cout << "Messages sent:" << std::endl; 23 | for (auto& message_sent_pair : messages_sent) 24 | { 25 | int messages_sent_temp = message_sent_pair.second.exchange(0); 26 | std::cout << " " << message_sent_pair.first << ": " << messages_sent_temp << std::endl; 27 | } 28 | std::this_thread::sleep_for(std::chrono::seconds(1)); 29 | } 30 | } 31 | 32 | int main(int argc, char** argv) 33 | { 34 | uint16_t start_port; 35 | std::vector topics; 36 | std::vector ecal_subscribers_; 37 | std::vector tcp_publishers_; 38 | 39 | if (argc < 3) 40 | { 41 | std::cerr << "ecal_to_tcp [topic_2 topic_3...]" << std::endl; 42 | std::cerr << std::endl; 43 | std::cerr << " start_port: The port that will be used for the first topic. It will then be increased by 1 for each additional topic." << std::endl; 44 | std::cerr << " topic_x: The eCAL topics this appliation shall subsribe to and forward via TCP." << std::endl; 45 | std::cerr << std::endl; 46 | return 1; 47 | } 48 | 49 | eCAL::Initialize(0, nullptr, "ecal2tcp"); 50 | 51 | // Parse command line 52 | start_port = static_cast(std::stoul(argv[1])); 53 | topics.reserve(argc - 1); 54 | for (int i = 2; i < argc; i++) 55 | { 56 | topics.push_back(std::string(argv[i])); 57 | } 58 | 59 | // Reserve space for publishers and subsribers 60 | ecal_subscribers_.reserve(topics.size()); 61 | tcp_publishers_ .reserve(topics.size()); 62 | 63 | // Create executor 64 | auto executor = std::make_shared(2); 65 | 66 | // Create publishers 67 | for (int i = 0; i < topics.size(); i++) 68 | { 69 | std::cout << "Publishing " << topics[i] << " on port " << (start_port + i) << std::endl; 70 | 71 | tcp_publishers_ .emplace_back(executor, start_port + i); 72 | ecal_subscribers_.emplace_back(topics[i]); 73 | 74 | eCAL::ReceiveCallbackT callback 75 | = [&tcp_publisher = tcp_publishers_.back()](const char* topic_name_, const struct eCAL::SReceiveCallbackData* data_) 76 | { 77 | tcp_publisher.send(static_cast(data_->buf), data_->size); 78 | messages_sent[topic_name_]++; 79 | }; 80 | 81 | ecal_subscribers_.back().AddReceiveCallback(callback); 82 | 83 | messages_sent[topics[i]] = 0; 84 | } 85 | 86 | printLog(); 87 | } 88 | -------------------------------------------------------------------------------- /samples/hello_world_publisher/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5.1...4.0) 2 | 3 | project(hello_world_publisher) 4 | 5 | set(CMAKE_CXX_STANDARD 14) 6 | 7 | set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) 8 | find_package(tcp_pubsub REQUIRED) 9 | 10 | set(sources 11 | src/main.cpp 12 | ) 13 | 14 | add_executable (${PROJECT_NAME} 15 | ${sources} 16 | ) 17 | 18 | target_link_libraries (${PROJECT_NAME} 19 | tcp_pubsub::tcp_pubsub 20 | ) 21 | -------------------------------------------------------------------------------- /samples/hello_world_publisher/src/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | int main() 11 | { 12 | const std::shared_ptr executor = std::make_shared(6); 13 | 14 | int counter = 0; 15 | const tcp_pubsub::Publisher hello_world_publisher(executor, 1588); 16 | 17 | for (;;) 18 | { 19 | std::this_thread::sleep_for(std::chrono::milliseconds(500)); 20 | 21 | const std::string data_to_send = "Hello World " + std::to_string(++counter); 22 | auto now = std::chrono::steady_clock::now(); 23 | 24 | std::cout << "Sending " << data_to_send << std::endl; 25 | hello_world_publisher.send(data_to_send.data(), data_to_send.size()); 26 | } 27 | 28 | return 0; 29 | } 30 | -------------------------------------------------------------------------------- /samples/hello_world_subscriber/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5.1...4.0) 2 | 3 | project(hello_world_subscriber) 4 | 5 | set(CMAKE_CXX_STANDARD 14) 6 | 7 | set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) 8 | find_package(tcp_pubsub REQUIRED) 9 | 10 | set(sources 11 | src/main.cpp 12 | ) 13 | 14 | add_executable (${PROJECT_NAME} 15 | ${sources} 16 | ) 17 | 18 | target_link_libraries (${PROJECT_NAME} 19 | tcp_pubsub::tcp_pubsub 20 | ) 21 | -------------------------------------------------------------------------------- /samples/hello_world_subscriber/src/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | int main() 11 | { 12 | const std::shared_ptr executor = std::make_shared(4); 13 | 14 | tcp_pubsub::Subscriber hello_world_subscriber(executor); 15 | auto session1 = hello_world_subscriber.addSession("127.0.0.1", 1588); 16 | 17 | const std::functioncallback_function 18 | = [](const tcp_pubsub::CallbackData& callback_data) -> void 19 | { 20 | const std::string temp_string_representation(callback_data.buffer_->data(), callback_data.buffer_->size()); 21 | std::cout << "Received playload: " << temp_string_representation << std::endl; 22 | }; 23 | 24 | hello_world_subscriber.setCallback(callback_function); 25 | 26 | // Prevent the application from exiting immediatelly 27 | for (;;) std::this_thread::sleep_for(std::chrono::milliseconds(500)); 28 | 29 | return 0; 30 | } 31 | -------------------------------------------------------------------------------- /samples/integration_test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5.1...4.0) 2 | 3 | project(integration_test) 4 | 5 | set(CMAKE_CXX_STANDARD 14) 6 | 7 | set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) 8 | find_package(tcp_pubsub REQUIRED) 9 | 10 | set(sources 11 | src/main.cpp 12 | ) 13 | 14 | add_executable (${PROJECT_NAME} 15 | ${sources} 16 | ) 17 | 18 | target_link_libraries (${PROJECT_NAME} 19 | tcp_pubsub::tcp_pubsub 20 | ) 21 | -------------------------------------------------------------------------------- /samples/integration_test/src/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | int main() 10 | { 11 | { 12 | std::shared_ptr executor = std::make_shared(6); 13 | tcp_pubsub::Publisher hello_world_publisher(executor, 1588); 14 | } 15 | 16 | std::this_thread::sleep_for(std::chrono::milliseconds(10)); 17 | 18 | return 0; 19 | } 20 | -------------------------------------------------------------------------------- /samples/performance_publisher/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5.1...4.0) 2 | 3 | project(performance_publisher) 4 | 5 | set(CMAKE_CXX_STANDARD 14) 6 | 7 | set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) 8 | find_package(tcp_pubsub REQUIRED) 9 | find_package(Threads REQUIRED) 10 | 11 | set(sources 12 | src/main.cpp 13 | ) 14 | 15 | add_executable (${PROJECT_NAME} 16 | ${sources} 17 | ) 18 | 19 | target_link_libraries (${PROJECT_NAME} 20 | tcp_pubsub::tcp_pubsub 21 | Threads::Threads 22 | ) 23 | -------------------------------------------------------------------------------- /samples/performance_publisher/src/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | std::atomic messages_sent; 13 | 14 | void printLog() 15 | { 16 | for (;;) 17 | { 18 | const int messages_sent_temp = messages_sent.exchange(0); 19 | std::cout << "Sent " << messages_sent_temp << " messages in 1 second" << std::endl; 20 | std::this_thread::sleep_for(std::chrono::seconds(1)); 21 | } 22 | } 23 | 24 | int main() { 25 | const std::shared_ptr executor = std::make_shared(6, tcp_pubsub::logger::logger_no_verbose_debug); 26 | 27 | const tcp_pubsub::Publisher publisher(executor, "0.0.0.0", 1588); 28 | 29 | const std::thread print_thread(printLog); 30 | 31 | std::vector big_buffer; 32 | big_buffer.resize(16 * 1024 * 1024); 33 | 34 | auto next_send_time = std::chrono::steady_clock::time_point(std::chrono::steady_clock::duration(0)); 35 | auto delay_between_sending = std::chrono::milliseconds(10); 36 | 37 | for (;;) 38 | { 39 | { 40 | auto now = std::chrono::steady_clock::now(); 41 | 42 | if (now >= next_send_time) 43 | { 44 | next_send_time = now + delay_between_sending; 45 | } 46 | else 47 | { 48 | auto time_to_sleep = next_send_time - now; 49 | next_send_time = next_send_time + delay_between_sending; 50 | std::this_thread::sleep_for(time_to_sleep); 51 | } 52 | } 53 | { 54 | auto now = std::chrono::steady_clock::now(); 55 | 56 | publisher.send(big_buffer.data(), big_buffer.size()); 57 | messages_sent++; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /samples/performance_subscriber/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5.1...4.0) 2 | 3 | project(performance_subscriber) 4 | 5 | set(CMAKE_CXX_STANDARD 14) 6 | 7 | set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) 8 | find_package(tcp_pubsub REQUIRED) 9 | find_package(Threads REQUIRED) 10 | 11 | set(sources 12 | src/main.cpp 13 | ) 14 | 15 | add_executable (${PROJECT_NAME} 16 | ${sources} 17 | ) 18 | 19 | target_link_libraries (${PROJECT_NAME} 20 | tcp_pubsub::tcp_pubsub 21 | Threads::Threads 22 | ) 23 | -------------------------------------------------------------------------------- /samples/performance_subscriber/src/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | std::atomic messages_received; 13 | 14 | void printLog() 15 | { 16 | for (;;) 17 | { 18 | const int messages_received_temp = messages_received.exchange(0); 19 | std::cout << "Received " << messages_received_temp << " in 1 second" << std::endl; 20 | std::this_thread::sleep_for(std::chrono::seconds(1)); 21 | } 22 | } 23 | 24 | int main() { 25 | const std::shared_ptr executor = std::make_shared(6, tcp_pubsub::logger::logger_no_verbose_debug); 26 | 27 | tcp_pubsub::Subscriber performance_subscriber(executor); 28 | performance_subscriber.addSession("127.0.0.1", 1588); 29 | 30 | const std::thread print_thread(printLog); 31 | 32 | performance_subscriber.setCallback([](const auto& /*callback_data*/) ->void 33 | { 34 | messages_received++; 35 | }); 36 | 37 | 38 | // Prevent the application from exiting immediatelly 39 | for (;;) 40 | { 41 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /samples/tcp_to_ecal/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5.1...4.0) 2 | set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) 3 | 4 | project(tcp_to_ecal) 5 | 6 | set(CMAKE_CXX_STANDARD 14) 7 | 8 | set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) 9 | find_package(tcp_pubsub REQUIRED) 10 | find_package(eCAL REQUIRED) 11 | find_package(Threads REQUIRED) 12 | 13 | set(sources 14 | src/main.cpp 15 | ) 16 | 17 | add_executable (${PROJECT_NAME} 18 | ${sources} 19 | ) 20 | 21 | target_link_libraries (${PROJECT_NAME} 22 | tcp_pubsub::tcp_pubsub 23 | eCAL::core 24 | Threads::Threads 25 | ) 26 | -------------------------------------------------------------------------------- /samples/tcp_to_ecal/src/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | std::map> messages_received; 17 | 18 | void printLog() 19 | { 20 | for (;;) 21 | { 22 | std::cout << "Messages received:" << std::endl; 23 | for (auto& message_sent_pair : messages_received) 24 | { 25 | int messages_sent_temp = message_sent_pair.second.exchange(0); 26 | std::cout << " " << message_sent_pair.first << ": " << messages_sent_temp << std::endl; 27 | } 28 | std::this_thread::sleep_for(std::chrono::seconds(1)); 29 | } 30 | } 31 | 32 | int main(int argc, char** argv) 33 | { 34 | std::string ip; 35 | uint16_t start_port; 36 | std::vector topics; 37 | std::vector tcp_subscribers; 38 | std::vector ecal_publishers; 39 | 40 | if (argc < 4) 41 | { 42 | std::cerr << "tcp_to_ecal [topic_2 topic_3...]" << std::endl; 43 | std::cerr << std::endl; 44 | std::cerr << " ip: The IP address (or hostname) that the ecal_to_tcp application is running on." << std::endl; 45 | std::cerr << " start_port: The port that will be used for the first topic. It will then be increased by 1 for each additional topic." << std::endl; 46 | std::cerr << " topic_x: The eCAL topic to publish the received data to. Tip: Change the topic name to something different, e.g. append \"_fromtcp\", so you don't receive dupliate messages late." << std::endl; 47 | std::cerr << std::endl; 48 | return 1; 49 | } 50 | 51 | eCAL::Initialize(0, nullptr, "tcp2ecal"); 52 | 53 | // Parse command line 54 | ip = argv[1]; 55 | start_port = static_cast(std::stoul(argv[2])); 56 | topics.reserve(argc - 1); 57 | for (int i = 3; i < argc; i++) 58 | { 59 | topics.push_back(std::string(argv[i])); 60 | messages_received[argv[i]] = 0; 61 | } 62 | 63 | // Reserver space for publishers and subsribers 64 | tcp_subscribers.reserve(topics.size()); 65 | ecal_publishers.reserve(topics.size()); 66 | 67 | auto executor = std::make_shared(2); 68 | 69 | // Create publishers 70 | for (int i = 0; i < topics.size(); i++) 71 | { 72 | 73 | std::cout << "Subscribing " << topics[i] << " on " << ip << ":" << (start_port + i) << std::endl; 74 | 75 | ecal_publishers.emplace_back(topics[i]); 76 | 77 | tcp_subscribers.emplace_back(executor); 78 | tcp_subscribers.back().addSession(ip, start_port + i); 79 | tcp_subscribers.back().setCallback( 80 | [&ecal_publisher = ecal_publishers.back(), topic_name = topics[i]](const tcp_pubsub::CallbackData& callback_data) 81 | { 82 | ecal_publisher.Send(callback_data.buffer_->data(), callback_data.buffer_->size()); 83 | messages_received[topic_name]++; 84 | }); 85 | } 86 | 87 | printLog(); 88 | } 89 | -------------------------------------------------------------------------------- /tcp_pubsub/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5.1...4.0) 2 | 3 | include("${CMAKE_CURRENT_LIST_DIR}/version.cmake") 4 | project(tcp_pubsub VERSION ${TCP_PUBSUB_VERSION_MAJOR}.${TCP_PUBSUB_VERSION_MINOR}.${TCP_PUBSUB_VERSION_PATCH}) 5 | 6 | set(CMAKE_POSITION_INDEPENDENT_CODE ON) 7 | 8 | # Disable default export of symbols 9 | set(CMAKE_CXX_VISIBILITY_PRESET hidden) 10 | set(CMAKE_VISIBILITY_INLINES_HIDDEN 1) 11 | 12 | find_package(Threads REQUIRED) 13 | find_package(asio REQUIRED) 14 | find_package(recycle REQUIRED) 15 | 16 | # Include GenerateExportHeader that will create export macros for us 17 | include(GenerateExportHeader) 18 | 19 | # Public API include directory 20 | set (includes 21 | include/tcp_pubsub/callback_data.h 22 | include/tcp_pubsub/executor.h 23 | include/tcp_pubsub/publisher.h 24 | include/tcp_pubsub/subscriber.h 25 | include/tcp_pubsub/subscriber_session.h 26 | include/tcp_pubsub/tcp_pubsub_logger.h 27 | ) 28 | 29 | # Private source files 30 | set(sources 31 | src/executor.cpp 32 | src/executor_impl.cpp 33 | src/executor_impl.h 34 | src/portable_endian.h 35 | src/protocol_handshake_message.h 36 | src/publisher.cpp 37 | src/publisher_impl.cpp 38 | src/publisher_impl.h 39 | src/publisher_session.cpp 40 | src/publisher_session.h 41 | src/subscriber.cpp 42 | src/subscriber_impl.cpp 43 | src/subscriber_impl.h 44 | src/subscriber_session.cpp 45 | src/subscriber_session_impl.cpp 46 | src/subscriber_session_impl.h 47 | src/tcp_header.h 48 | src/tcp_pubsub_logger_abstraction.h 49 | ) 50 | 51 | add_library (${PROJECT_NAME} ${TCP_PUBSUB_LIBRARY_TYPE} 52 | ${includes} 53 | ${sources} 54 | ) 55 | 56 | # Generate version defines 57 | configure_file("tcp_pubsub_version.h.in" "${PROJECT_BINARY_DIR}/include/tcp_pubsub/tcp_pubsub_version.h" @ONLY) 58 | 59 | # Generate header with export macros 60 | generate_export_header(${PROJECT_NAME} 61 | EXPORT_FILE_NAME ${PROJECT_BINARY_DIR}/include/tcp_pubsub/tcp_pubsub_export.h 62 | BASE_NAME TCP_PUBSUB 63 | ) 64 | 65 | add_library (tcp_pubsub::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) 66 | 67 | target_link_libraries(${PROJECT_NAME} 68 | PRIVATE 69 | Threads::Threads 70 | $<$:ws2_32> 71 | $<$:wsock32> 72 | 73 | # Link header-only libs (asio & recycle) as described in this workaround: 74 | # https://gitlab.kitware.com/cmake/cmake/-/issues/15415#note_633938 75 | $ 76 | $ 77 | ) 78 | 79 | target_compile_definitions(${PROJECT_NAME} 80 | PRIVATE 81 | ASIO_STANDALONE 82 | ASIO_DISABLE_VISIBILITY 83 | _WIN32_WINNT=0x0601 84 | ) 85 | 86 | # Check if tcp_pubsub is an object library. If so, define TCP_PUBSUB_STATIC_DEFINE, 87 | # so the CMake generated export header will be correct. 88 | get_target_property(tcp_pubsub_target_type tcp_pubsub TYPE) 89 | if (tcp_pubsub_target_type STREQUAL OBJECT_LIBRARY) 90 | target_compile_definitions(${PROJECT_NAME} 91 | PUBLIC 92 | TCP_PUBSUB_STATIC_DEFINE 93 | ) 94 | endif() 95 | 96 | target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_14) 97 | 98 | target_compile_options(${PROJECT_NAME} PRIVATE 99 | $<$,$,$>: 100 | -Wall -Wextra> 101 | $<$: 102 | /W4>) 103 | 104 | 105 | # Add own public include directory 106 | target_include_directories(${PROJECT_NAME} 107 | PUBLIC 108 | $ 109 | $ # To find the export file generated by generate_export_header 110 | $ 111 | PRIVATE 112 | src/ 113 | ) 114 | 115 | set_target_properties(${PROJECT_NAME} PROPERTIES 116 | VERSION ${PROJECT_VERSION} 117 | SOVERSION ${PROJECT_VERSION_MAJOR} 118 | OUTPUT_NAME ${PROJECT_NAME} 119 | ) 120 | 121 | ################################## 122 | 123 | source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES 124 | ${includes} 125 | ${sources} 126 | ) 127 | 128 | ################################################################################ 129 | ### Installation rules 130 | ################################################################################ 131 | 132 | set(TCP_PUBSUB_INSTALL_CMAKE_DIR "lib/cmake/tcp_pubsub") 133 | 134 | # Install Runtime & Libs 135 | install( 136 | TARGETS ${PROJECT_NAME} 137 | EXPORT tcp_pubsubTargets 138 | 139 | RUNTIME 140 | DESTINATION "bin" 141 | COMPONENT tcp_pubsub_runtime 142 | 143 | LIBRARY 144 | DESTINATION "lib" 145 | COMPONENT tcp_pubsub_runtime 146 | 147 | ARCHIVE 148 | DESTINATION "lib" 149 | COMPONENT tcp_pubsub_dev 150 | ) 151 | 152 | # Install public header files (-> dev package) 153 | install( 154 | DIRECTORY "include/tcp_pubsub" 155 | DESTINATION "include" 156 | COMPONENT tcp_pubsub_dev 157 | FILES_MATCHING PATTERN "*.h" 158 | ) 159 | 160 | # Install the auto-generated header with the export macros (-> dev package) 161 | install( 162 | DIRECTORY "${PROJECT_BINARY_DIR}/include/tcp_pubsub" 163 | DESTINATION "include" 164 | COMPONENT tcp_pubsub_dev 165 | FILES_MATCHING PATTERN "*.h" 166 | ) 167 | 168 | # Install Target.cmake file (-> dev packag) 169 | install( 170 | EXPORT tcp_pubsubTargets 171 | FILE tcp_pubsubTargets.cmake 172 | DESTINATION ${TCP_PUBSUB_INSTALL_CMAKE_DIR} 173 | NAMESPACE tcp_pubsub:: 174 | COMPONENT tcp_pubsub_dev 175 | ) 176 | 177 | # Create and install Config.cmake file (-> dev package) 178 | 179 | include(CMakePackageConfigHelpers) 180 | 181 | configure_package_config_file( 182 | "cmake/tcp_pubsubConfig.cmake.in" 183 | "${CMAKE_CURRENT_BINARY_DIR}/cmake_/tcp_pubsubConfig.cmake" 184 | INSTALL_DESTINATION ${TCP_PUBSUB_INSTALL_CMAKE_DIR} 185 | PATH_VARS TCP_PUBSUB_INSTALL_CMAKE_DIR 186 | ) 187 | write_basic_package_version_file( 188 | "${CMAKE_CURRENT_BINARY_DIR}/cmake_/tcp_pubsubConfigVersion.cmake" 189 | VERSION ${PROJECT_VERSION} 190 | COMPATIBILITY SameMajorVersion 191 | ) 192 | install( 193 | FILES 194 | "${CMAKE_CURRENT_BINARY_DIR}/cmake_/tcp_pubsubConfig.cmake" 195 | "${CMAKE_CURRENT_BINARY_DIR}/cmake_/tcp_pubsubConfigVersion.cmake" 196 | DESTINATION ${TCP_PUBSUB_INSTALL_CMAKE_DIR} 197 | COMPONENT tcp_pubsub_dev 198 | ) 199 | 200 | -------------------------------------------------------------------------------- /tcp_pubsub/cmake/tcp_pubsubConfig.cmake.in: -------------------------------------------------------------------------------- 1 | @PACKAGE_INIT@ 2 | 3 | include(CMakeFindDependencyMacro) 4 | find_dependency(Threads) 5 | 6 | include("${CMAKE_CURRENT_LIST_DIR}/tcp_pubsubTargets.cmake") 7 | -------------------------------------------------------------------------------- /tcp_pubsub/include/tcp_pubsub/callback_data.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include // IWYU pragma: keep 10 | 11 | namespace tcp_pubsub 12 | { 13 | struct CallbackData 14 | { 15 | // At the moment, this callback data only holds the buffer. But just in case 16 | // The tcp subsriber would want to return more than that in a later version, 17 | // we use this struct to improve API stability. 18 | 19 | std::shared_ptr> buffer_; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /tcp_pubsub/include/tcp_pubsub/executor.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include "tcp_pubsub_logger.h" 10 | 11 | #include // IWYU pragma: keep 12 | #include 13 | 14 | namespace tcp_pubsub 15 | { 16 | // Foward-declare implementation 17 | class Executor_Impl; 18 | 19 | // Forward-declare friend classes 20 | class Publisher_Impl; 21 | class Subscriber_Impl; 22 | 23 | /** 24 | * @brief The Executor executes publisher and subscriber workload using a thread pool. 25 | * 26 | * The Exectur has to be passed as shared_ptr to the constructor of any 27 | * Publisher and Subscriber. It contains two things: 28 | * 29 | * - A thread pool that executes workload generated by Publisher and 30 | * Subsriber connection (e.g. sending and receiving data) 31 | * - A logging function that gets used when output has to be logged. 32 | * 33 | */ 34 | class Executor 35 | { 36 | public: 37 | /** 38 | * @brief Creates a new executor 39 | * 40 | * @param[in] thread_count 41 | * The amount of threads that shall execute workload 42 | * 43 | * @param[in] log_function 44 | * A function (LogLevel, string)->void that is used for logging. 45 | */ 46 | TCP_PUBSUB_EXPORT Executor(size_t thread_count, const tcp_pubsub::logger::logger_t& log_function = tcp_pubsub::logger::default_logger); 47 | TCP_PUBSUB_EXPORT ~Executor(); 48 | 49 | // Copy 50 | Executor(const Executor&) = delete; 51 | Executor& operator=(const Executor&) = delete; 52 | 53 | // Move 54 | TCP_PUBSUB_EXPORT Executor& operator=(Executor&&) noexcept; 55 | TCP_PUBSUB_EXPORT Executor(Executor&&) noexcept; 56 | 57 | private: 58 | friend ::tcp_pubsub::Publisher_Impl; 59 | friend ::tcp_pubsub::Subscriber_Impl; 60 | std::shared_ptr executor_impl_; 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /tcp_pubsub/include/tcp_pubsub/publisher.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "executor.h" 14 | 15 | #include // IWYU pragma: keep 16 | #include 17 | 18 | namespace tcp_pubsub 19 | { 20 | class Publisher_Impl; 21 | 22 | /** 23 | * @brief The Publisher side of tcp_pubsub 24 | * 25 | * Once created, a Publisher waits for connections on a port. You can either 26 | * choose a port to listen on or let that decision to the operating system. 27 | * See the constructor for more details. 28 | * 29 | * A Publisher uses a 1-element send-queue: 30 | * 31 | * - 1 message is being sent to a subscriber. As each Subscriber connects 32 | * individually and with it's own link speed, this message will not be the 33 | * same for each subscription. 34 | * 35 | * - One message is being kept as next-message. Once a Session has finished 36 | * sending its message to its subscriber, it will take that message as next 37 | * one. If messages are provided faster than the link speed can handle, only 38 | * the last message is kept and other messages will be dropped. 39 | * 40 | */ 41 | class Publisher 42 | { 43 | public: 44 | /** 45 | * @brief Creates a new publisher 46 | * 47 | * You can either choose a specific port, or leave it to the OS to choose a 48 | * free port for you: 49 | * 50 | * - port = 0: The operating system will choose a free port. You can 51 | * retrieve that port by Publisher::getPort(). 52 | * 53 | * - port != 0: The given port will be used. When this port is already 54 | * opened by another application or the OS denies opening the 55 | * port, isRunning() will return false. 56 | * 57 | * When the port can be opened, the Publisher will immediatelly accept 58 | * connections from Subscribers. Whether the Publisher is running can be 59 | * checked with isRunning(). 60 | * 61 | * @param[in] executor 62 | * The (global) executor that shall execute the workload and be 63 | * used for logging. 64 | * 65 | * @param[in] address 66 | * The IP address to bind to. When setting this to "::", 67 | * connections from any IPv6 & IPv4 are accepted. 68 | * Tip: use an IPv6 IP here to accept both IPv4 and IPv6 69 | * addresses. If "0.0.0.0" is used, the connections will be 70 | * limited to IPv4 connections. 71 | * 72 | * @param[in] port 73 | * The port to accept connections from. When setting to "0", 74 | * the operating system will usually autoamtically chooose a 75 | * free port. 76 | */ 77 | TCP_PUBSUB_EXPORT Publisher(const std::shared_ptr& executor, const std::string& address, uint16_t port); 78 | 79 | /** 80 | * @brief Creates a new publisher 81 | * 82 | * You can either choose a specific port, or leave it to the OS to choose a 83 | * free port for you: 84 | * 85 | * - port = 0: The operating system will choose a free port. You can 86 | * retrieve that port by Publisher::getPort(). 87 | * 88 | * - port != 0: The given port will be used. When this port is already 89 | * opened by another application or the OS denies opening the 90 | * port, isRunning() will return false. 91 | * 92 | * When the port can be opened, the Publisher will immediatelly accept 93 | * connections from Subscribers. It is bound to "::", meaning that it will 94 | * accept connections from any IPv6 and IPv4 connection. Whether the 95 | * Publisher is running can be checked with isRunning(). 96 | * 97 | * @param[in] executor 98 | * The (global) executor that shall execute the workload and be 99 | * used for logging. 100 | * 101 | * @param[in] port 102 | * The port to accept connections from. When omitting this 103 | * parameter, the operating system will usually autoamtically 104 | * chooose a free port. 105 | */ 106 | TCP_PUBSUB_EXPORT Publisher(const std::shared_ptr& executor, uint16_t port = 0); 107 | 108 | // Copy 109 | TCP_PUBSUB_EXPORT Publisher(const Publisher&) = default; 110 | TCP_PUBSUB_EXPORT Publisher& operator=(const Publisher&) = default; 111 | 112 | // Move 113 | TCP_PUBSUB_EXPORT Publisher& operator=(Publisher&&) = default; 114 | TCP_PUBSUB_EXPORT Publisher(Publisher&&) = default; 115 | 116 | // Destructor 117 | TCP_PUBSUB_EXPORT ~Publisher(); 118 | 119 | public: 120 | /** 121 | * @brief Get the port that this publisher is listening to for new connections 122 | * 123 | * - If the publisher has failed to open the given port, 0 is returned 124 | * 125 | * - If the publisher has been created with a specific port, this port is 126 | * returned 127 | * 128 | * - If the publsiher has been created with port=0, the port chosen from the 129 | * operating system is returned 130 | * 131 | * @return The port of this publisher 132 | */ 133 | TCP_PUBSUB_EXPORT uint16_t getPort() const; 134 | 135 | /** 136 | * @brief Get the amount of subscriptions to this publisher 137 | * 138 | * This method is thread-safe 139 | * 140 | * @return The number of active subscribers 141 | */ 142 | TCP_PUBSUB_EXPORT size_t getSubscriberCount() const; 143 | 144 | /** 145 | * @brief Check whether the publisher is running 146 | * 147 | * A publisher is running, iff: 148 | * - The publishers port could be opened and the publisher could bind to 149 | * the given IP address (if provided) 150 | * - cancel() has not been called 151 | * 152 | * This method is thread-safe. 153 | * 154 | * @return Whether the publisher is running 155 | */ 156 | TCP_PUBSUB_EXPORT bool isRunning() const; 157 | 158 | /** 159 | * @brief Send data to all subscribers 160 | * 161 | * Sends one buffer to all subsribers (if possbile). The data will be copied 162 | * to an internal buffer. 163 | * 164 | * If you need to send multiple buffers as one element 165 | * (e.g. header + payload), you should use the send(std::vector buffers) 166 | * function, instead of copying the data into continuous memory in your own 167 | * code. 168 | * 169 | * See send(std::vector buffers) for more details. 170 | * 171 | * This function is thread safe. 172 | * 173 | * @param[in] data 174 | * Pointer to the data to send 175 | * 176 | * @param[in] size 177 | * Size of the data to send in number-of-bytes 178 | * 179 | * @return Whether sending has been successfull (i.e. the publisher is running) 180 | */ 181 | TCP_PUBSUB_EXPORT bool send(const char* data, size_t size) const; 182 | 183 | /** 184 | * @brief Send data to all subscribers 185 | * 186 | * Sends the given data to all subsribers (if possible). This function 187 | * supports sending an indefinite amount of sub-buffers, that will be copied 188 | * into one internal binary buffer. 189 | * You may release your memory after this function has returned. 190 | * 191 | * Tip: 192 | * 193 | * Use this to send your custom Header and Payload as 1 buffer, without 194 | * having to copy everything into one continuous memory chunk yourself! 195 | * This method will copy the memory anyways (and only if there are active 196 | * subsriptions), so copying in your could would be a waste of resources. 197 | * 198 | * A call could look like this: 199 | * 200 | * send({{header.data(), header.size()}, {payload.data(), payload.size()}}); 201 | * 202 | * The subscriber will receive the data as 1 binary blob containing all 203 | * sub-buffers in the same order they have been provided to this function. 204 | * So you need to make sure that your subscriber can recover the information 205 | * about the length of each sub-buffer, if you require to divide the data, 206 | * again. 207 | * 208 | * Note that calling this function does not guarantee that the data will 209 | * physically be sent to the subsribers. Internally, a 1-element-queue is used: 210 | * 211 | * - 1 Element is currently sent to the subscriber. As TCP works with 1->1 212 | * connections, this element may be a different one for all connections, 213 | * if e.g. the link speed is different and some connections send their 214 | * data faster than others. 215 | * 216 | * - 1 Element is kept in queue as next element. It will be sent once the 217 | * current element has been processed. If you call send too fast, this 218 | * buffer will be dropped. tcp_pubsub will always assume that the last 219 | * element is the only important one. 220 | * 221 | * If there are no active subscriptions, this function returns immediatelly 222 | * without doing anything. You don't need to check with getSubsriberCount() 223 | * in that case. 224 | * 225 | * This method is thread-safe. 226 | * 227 | * @param[in] buffers 228 | * List of (sub-)buffers to send to all subscribers 229 | * 230 | * @return True if sending was successfull (i.e. the publisher is running) 231 | */ 232 | TCP_PUBSUB_EXPORT bool send(const std::vector>& buffers) const; 233 | 234 | /** 235 | * @brief Close all connections 236 | * 237 | * Closes all connections to all subscribers. New subscribers are not 238 | * accepted any more. Data cannot be sent any more. 239 | * 240 | * It is *not* required to call this before destroying the publisher object. 241 | * It is done automatically in the destructor. 242 | */ 243 | TCP_PUBSUB_EXPORT void cancel(); 244 | 245 | private: 246 | std::shared_ptr publisher_impl_; 247 | }; 248 | } 249 | -------------------------------------------------------------------------------- /tcp_pubsub/include/tcp_pubsub/subscriber.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "executor.h" 14 | #include "subscriber_session.h" 15 | #include "callback_data.h" 16 | 17 | #include 18 | #include // IWYU pragma: keep 19 | 20 | namespace tcp_pubsub 21 | { 22 | class Subscriber_Impl; 23 | 24 | /** 25 | * @brief The Subscriber side of tcp_pubsub 26 | * 27 | * A subscriber can connect to one or multiple publishers and receive data 28 | * from them. As TCP actually works with 1:1 connections, you need to create a 29 | * SubscriberSession for each connection to a publisher. 30 | * 31 | * Once received, you will get the data via a callback. So after creating a 32 | * Subsriber, you should also call Subscriber::setCallback(). By default, Callbacks are asynchronous (and is is highly recommended that you keep it this way, unless you really know what you are doing!). Subscribers use a 1-element queue: 33 | * 34 | * - 1 message is currently being processed by the callback you set. This 35 | * callback runs in it's own thread, so you are allowed to do time-consuming 36 | * computations in a callback. 37 | * 38 | * - 1 message is kept as next-message. Once your callback has finished, it 39 | * will automatically grab the next message, if available. If messages 40 | * arrive too fast and the callback cannot process them, messages are being 41 | * dropped, so your callback will always receive the latest data available. 42 | * 43 | * All SubscriberSessions will share the same 1-message queue. It is assumend, 44 | * that the SubscriberSessions that are created in the same Subscriber are of 45 | * same "type" (whatever that means for you). 46 | */ 47 | class Subscriber 48 | { 49 | public: 50 | /** 51 | * @brief creates a new Subscriber 52 | * 53 | * A new subscriber will not do anything by default. In order to actually 54 | * receive data with it, you will have to add SubscriberSessions. Each 55 | * SubscriberSession is a connection to a publisher. 56 | * All Sessions in a Subscriber are assumed to receive data of similar 57 | * "type". All Sessions will share a single callback function and callback 58 | * thread. 59 | * 60 | * @param[in] executor 61 | * The (global) executor that shall execute the workload and be 62 | * used for logging. 63 | */ 64 | TCP_PUBSUB_EXPORT Subscriber(const std::shared_ptr& executor); 65 | 66 | // Copy 67 | TCP_PUBSUB_EXPORT Subscriber(const Subscriber&) = default; 68 | TCP_PUBSUB_EXPORT Subscriber& operator=(const Subscriber&) = default; 69 | 70 | // Move 71 | TCP_PUBSUB_EXPORT Subscriber& operator=(Subscriber&&) = default; 72 | TCP_PUBSUB_EXPORT Subscriber(Subscriber&&) = default; 73 | 74 | // Destructor 75 | TCP_PUBSUB_EXPORT ~Subscriber(); 76 | 77 | public: 78 | /** 79 | * @brief Add a new connection to a publisher 80 | * 81 | * Adds a new connection to a publisher. As a publisher is identified by its 82 | * address and port, those parameters are mandatory. The address can both be 83 | * an IP Address or a URL / Hostname / etc. (something the Operating System 84 | * can resolve). 85 | * 86 | * By default, a Session will try to reconnect, when anything failes. How 87 | * often this shall happen until the Session will delete itself from the 88 | * Subscriber can be controlled by the max_reconnection_attempts parameter. 89 | * A negative value will cause the Session to retry indefinitively, so you 90 | * will probably have to write your own algorithm code that cancels Sessions 91 | * that will not recover any more. 92 | * Between reconnection attemps the Session will wait 1 second. 93 | * 94 | * Even though it may usually not make sense, you can add multiple 95 | * sessions to a single publisher. 96 | * 97 | * This function is thread-safe. 98 | * 99 | * @param[in] address 100 | * IP or Hostname of the publisher 101 | * 102 | * @param[in] port 103 | * Port the publisher is listening on 104 | * 105 | * @param[in] max_reconnection_attempts 106 | * How often the Session will try to reconnect in case of an 107 | * issue. A negative value means infinite reconnection attemps. 108 | * 109 | * @return A shared pointer to the session. You don't need to store it. 110 | */ 111 | TCP_PUBSUB_EXPORT std::shared_ptr addSession(const std::string& address, uint16_t port, int max_reconnection_attempts = -1); 112 | 113 | /** 114 | * @brief Add a new connection to a publisher 115 | * 116 | * Adds a new connection to a publisher. In cases where it is not clear how 117 | * the publisher can be reached (e.g. multiple IP addresses), you can 118 | * provide a list of pairs of address and port. The Subscriber will try to 119 | * connect to all of them. The first one that works will be used. This can 120 | * e.g. be used to connect both to "HOSTNAME" and "HOSTNAME.local" (i.e. the 121 | * mDNS variant) or to an IPv4 and an IPv6 address. The server list must 122 | * have at least one entry. 123 | * 124 | * By default, a Session will try to reconnect, when anything failes. How 125 | * often this shall happen until the Session will delete itself from the 126 | * Subscriber can be controlled by the max_reconnection_attempts parameter. 127 | * A negative value will cause the Session to retry indefinitively, so you 128 | * will probably have to write your own algorithm code that cancels Sessions 129 | * that will not recover any more. 130 | * Between reconnection attemps the Session will wait 1 second. 131 | * 132 | * Even though it may usually not make sense, you can add multiple 133 | * sessions to a single publisher. 134 | * 135 | * This function is thread-safe. 136 | * 137 | * @param[in] publisher_list 138 | * A list of [address, port] pairs. The first pair that works 139 | * will be used. The list must have at least one entry. 140 | * 141 | * @param[in] max_reconnection_attempts 142 | * How often the Session will try to reconnect in case of an 143 | * issue. A negative value means infinite reconnection attemps. 144 | * 145 | * @return A shared pointer to the session. You don't need to store it. 146 | */ 147 | TCP_PUBSUB_EXPORT std::shared_ptr addSession(const std::vector>& publisher_list, int max_reconnection_attempts = -1); 148 | 149 | /** 150 | * @brief Get a list of all Sessions. 151 | * 152 | * Get a list of all Sessions. 153 | * 154 | * This function is thread-safe. 155 | * 156 | * @return a list of all sessions. 157 | */ 158 | TCP_PUBSUB_EXPORT std::vector> getSessions() const; 159 | 160 | /** 161 | * @brief Set a receive-data-callback 162 | * 163 | * Set a callback that is called when new data has been received. By 164 | * default, a thread is created that will execute your callback 165 | * (=> synchronous_execution = false). You usually do not want to change 166 | * that. You may do heavy computation in this callback. However, while your 167 | * callback is running, there only exists a 1-message-queue. This makes sure 168 | * that your callback always operates on the latest data available, but if 169 | * your callback needs too long to process, data may be dropped. 170 | * 171 | * It is possible to set a callback from within a running callback. 172 | * 173 | * Synchronous callbacks (DANGEROUS): 174 | * - Synchronous means synchronous in a session-manner. If you are having 175 | * multiple sessions, the callbacks will run in parallel. 176 | * - While a synchronous callback is running, no new data can be read from 177 | * the socket of the corresponding SubscriberSession. This may cause data 178 | * to stack up in the Sockets buffer, if your callback consumes too much 179 | * time. 180 | * - A synchronous callback is exeucted by the Executor's thread-pool. If 181 | * your callback needs too long to process, you can cause all Publishers 182 | * and Subscriber sessions to block, because the thread pool is busy 183 | * processing callbacks and cannot work on the TCP sockets any more 184 | * - Internally in tcp_pubsub, a synchronous callback is used to feed the 185 | * asynchronous callback with new data. This is what it is meant for. So 186 | * if you only want to pass the received shared_ptr to your own 187 | * queue or worker thread, you may actually want to use a synchronous 188 | * callback. 189 | * 190 | * This function is thread-safe 191 | * 192 | * @param callback_function 193 | * @param synchronous_execution 194 | */ 195 | TCP_PUBSUB_EXPORT void setCallback (const std::function& callback_function, bool synchronous_execution = false); 196 | 197 | /** 198 | * @brief Clears the callback and removes all references kept internally. 199 | */ 200 | TCP_PUBSUB_EXPORT void clearCallback(); 201 | 202 | /** 203 | * @brief Shuts down the Subscriber and all Sessions 204 | * 205 | * After cancelling, no new Data will be received. Callbacks will not be fed 206 | * data any more. 207 | * 208 | * Cancelling will wait until the running callback has finished processing, 209 | * before returning. 210 | * 211 | * Cancelling from a callback is supported, but in that case cancel() 212 | * obviously will return before the callback has finished. 213 | * 214 | */ 215 | TCP_PUBSUB_EXPORT void cancel(); 216 | 217 | private: 218 | std::shared_ptr subscriber_impl_; 219 | }; 220 | } 221 | -------------------------------------------------------------------------------- /tcp_pubsub/include/tcp_pubsub/subscriber_session.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include // IWYU pragma: keep 13 | #include 14 | 15 | namespace tcp_pubsub 16 | { 17 | // Forward-declare Implementation 18 | class SubscriberSession_Impl; 19 | 20 | // Friend class 21 | class Subscriber_Impl; 22 | 23 | /** 24 | * @brief A Single connection to a publisher 25 | * 26 | * A SubscriberSessions represents a single connection to a single Publisher. 27 | * SubscriberSessions never exist on their own; they always belong to a 28 | * Subscriber. 29 | * 30 | * A SubscriberSession is created by Subscriber::addSession(). 31 | */ 32 | class SubscriberSession 33 | { 34 | friend Subscriber_Impl; 35 | 36 | private: 37 | TCP_PUBSUB_EXPORT SubscriberSession(const std::shared_ptr& impl); 38 | 39 | public: 40 | // Copy 41 | TCP_PUBSUB_EXPORT SubscriberSession(const SubscriberSession&) = default; 42 | TCP_PUBSUB_EXPORT SubscriberSession& operator=(const SubscriberSession&) = default; 43 | 44 | // Move 45 | TCP_PUBSUB_EXPORT SubscriberSession& operator=(SubscriberSession&&) = default; 46 | TCP_PUBSUB_EXPORT SubscriberSession(SubscriberSession&&) = default; 47 | 48 | // Destructor 49 | TCP_PUBSUB_EXPORT ~SubscriberSession(); 50 | 51 | public: 52 | /** 53 | * @brief Get the list of publishers used for creating the session. 54 | * 55 | * The session will try to connect to the publishers in the order they are 56 | * present in that list. If a connection fails, it will try the next one. 57 | * 58 | * For checking which publisher is currently connected, use getConnectedPublisher(). 59 | * 60 | * @return The list of publishers used for creating the session 61 | */ 62 | TCP_PUBSUB_EXPORT std::vector> getPublisherList() const; 63 | 64 | /** 65 | * @brief Get the currently connected publisher address 66 | * 67 | * @return The address and port of the currently connected publisher 68 | */ 69 | TCP_PUBSUB_EXPORT std::pair getConnectedPublisher() const; 70 | 71 | /** 72 | * @brief Cancels the Session 73 | * 74 | * This cancels the Session and closes the connection to the Publisher. 75 | * If will automatically cause the SubscriberSession to remove itself from 76 | * the Subscriber it was created from. Once you release the shared_ptr to 77 | * it, the object will be deleted. 78 | */ 79 | TCP_PUBSUB_EXPORT void cancel(); 80 | 81 | /** 82 | * @brief Returns whether this Session is connected to a Publisher 83 | * 84 | * @return True, if the Session is connected to a publisher. 85 | */ 86 | TCP_PUBSUB_EXPORT bool isConnected() const; 87 | 88 | private: 89 | std::shared_ptr subscriber_session_impl_; 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /tcp_pubsub/include/tcp_pubsub/tcp_pubsub_logger.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include // IWYU pragma: keep 11 | 12 | namespace tcp_pubsub 13 | { 14 | namespace logger 15 | { 16 | enum class LogLevel 17 | { 18 | DebugVerbose, 19 | Debug, 20 | Info, 21 | Warning, 22 | Error, 23 | Fatal, 24 | }; 25 | 26 | using logger_t = std::function; 27 | 28 | static const logger_t default_logger 29 | = [](const LogLevel log_level, const std::string& message) 30 | { 31 | switch (log_level) 32 | { 33 | case LogLevel::DebugVerbose: 34 | std::cout << "[TCP ps] [Debug+] " + message + "\n"; 35 | break; 36 | case LogLevel::Debug: 37 | std::cout << "[TCP ps] [Debug] " + message + "\n"; 38 | break; 39 | case LogLevel::Info: 40 | std::cout << "[TCP ps] [Info] " + message + "\n"; 41 | break; 42 | case LogLevel::Warning: 43 | std::cerr << "[TCP ps] [Warning] " + message + "\n"; 44 | break; 45 | case LogLevel::Error: 46 | std::cerr << "[TCP ps] [Error] " + message + "\n"; 47 | break; 48 | case LogLevel::Fatal: 49 | std::cerr << "[TCP ps] [Fatal] " + message + "\n"; 50 | break; 51 | default: 52 | break; 53 | } 54 | }; 55 | 56 | static const logger_t logger_no_verbose_debug 57 | = [](const LogLevel log_level, const std::string& message) 58 | { 59 | switch (log_level) 60 | { 61 | case LogLevel::DebugVerbose: 62 | break; 63 | case LogLevel::Debug: 64 | std::cout << "[TCP ps] [Debug] " + message + "\n"; 65 | break; 66 | case LogLevel::Info: 67 | std::cout << "[TCP ps] [Info] " + message + "\n"; 68 | break; 69 | case LogLevel::Warning: 70 | std::cerr << "[TCP ps] [Warning] " + message + "\n"; 71 | break; 72 | case LogLevel::Error: 73 | std::cerr << "[TCP ps] [Error] " + message + "\n"; 74 | break; 75 | case LogLevel::Fatal: 76 | std::cerr << "[TCP ps] [Fatal] " + message + "\n"; 77 | break; 78 | default: 79 | break; 80 | } 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tcp_pubsub/src/executor.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include "executor_impl.h" 10 | #include "tcp_pubsub/tcp_pubsub_logger.h" 11 | 12 | namespace tcp_pubsub 13 | { 14 | Executor::Executor(size_t thread_count, const tcp_pubsub::logger::logger_t & log_function) 15 | : executor_impl_(std::make_shared(log_function)) 16 | { 17 | executor_impl_->start(thread_count); 18 | } 19 | 20 | Executor::~Executor() 21 | { 22 | executor_impl_->stop(); 23 | } 24 | 25 | // Move 26 | Executor& Executor::operator=(Executor&&) noexcept = default; 27 | Executor::Executor(Executor&&) noexcept = default; 28 | } -------------------------------------------------------------------------------- /tcp_pubsub/src/executor_impl.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include "executor_impl.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include "tcp_pubsub/tcp_pubsub_logger.h" 15 | #include "tcp_pubsub_logger_abstraction.h" 16 | 17 | namespace tcp_pubsub 18 | { 19 | Executor_Impl::Executor_Impl(const logger::logger_t& log_function) 20 | : log_(log_function) 21 | , io_context_(std::make_shared()) 22 | , dummy_work_(std::make_shared(io_context_->get_executor())) 23 | { 24 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 25 | log_(logger::LogLevel::Debug, "Executor: Creating Executor."); 26 | #endif 27 | } 28 | 29 | Executor_Impl::~Executor_Impl() 30 | { 31 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 32 | std::stringstream ss; 33 | ss << std::this_thread::get_id(); 34 | const std::string thread_id = ss.str(); 35 | log_(logger::LogLevel::DebugVerbose, "Executor: Deleting from thread " + thread_id + "..."); 36 | #endif 37 | 38 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 39 | log_(logger::LogLevel::Debug, "Executor: Waiting for IoService threads to shut down..."); 40 | #endif 41 | 42 | // Detach all threads and clear the thread pool 43 | // The threads hold a shared_ptr to this object and therefore manage their 44 | // own lifecycle. So when the threads terminate, the last destructor 45 | // detaches the terminating threads. 46 | for (std::thread& thread : thread_pool_) 47 | { 48 | thread.detach(); 49 | } 50 | thread_pool_.clear(); 51 | 52 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 53 | log_(logger::LogLevel::Debug, "Executor: All IoService threads have shut down successfully."); 54 | #endif 55 | 56 | 57 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 58 | log_(logger::LogLevel::Debug, "Executor: Deleted."); 59 | #endif 60 | } 61 | 62 | void Executor_Impl::start(size_t thread_count) 63 | { 64 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 65 | log_(logger::LogLevel::Debug, "Executor: Starting Executor with " + std::to_string(thread_count) + " threads."); 66 | #endif 67 | for (size_t i = 0; i < thread_count; i++) 68 | { 69 | thread_pool_.emplace_back([me = shared_from_this()]() 70 | { 71 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 72 | std::stringstream ss; 73 | ss << std::this_thread::get_id(); 74 | const std::string thread_id = ss.str(); 75 | 76 | me->log_(logger::LogLevel::Debug, "Executor: IoService::Run() in thread " + thread_id); 77 | #endif 78 | 79 | me->io_context_->run(); 80 | 81 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 82 | me->log_(logger::LogLevel::Debug, "Executor: IoService: Shutdown of thread " + thread_id); 83 | #endif 84 | }); 85 | } 86 | } 87 | 88 | void Executor_Impl::stop() 89 | { 90 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 91 | log_(logger::LogLevel::Debug, "Executor::stop()"); 92 | #endif 93 | 94 | // Delete the dummy work 95 | dummy_work_.reset(); 96 | 97 | // Stop the IO Service 98 | io_context_->stop(); 99 | } 100 | 101 | std::shared_ptr Executor_Impl::ioService() const 102 | { 103 | return io_context_; 104 | } 105 | 106 | logger::logger_t Executor_Impl::logFunction() const 107 | { 108 | return log_; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /tcp_pubsub/src/executor_impl.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include // IWYU pragma: keep 12 | 13 | #include "tcp_pubsub/tcp_pubsub_logger.h" 14 | 15 | namespace tcp_pubsub 16 | { 17 | class Executor_Impl : public std::enable_shared_from_this 18 | { 19 | public: 20 | Executor_Impl(const logger::logger_t& log_function); 21 | ~Executor_Impl(); 22 | 23 | // Copy 24 | Executor_Impl(const Executor_Impl&) = delete; 25 | Executor_Impl& operator=(const Executor_Impl&) = delete; 26 | 27 | // Move 28 | Executor_Impl& operator=(Executor_Impl&&) = delete; 29 | Executor_Impl(Executor_Impl&&) = delete; 30 | 31 | public: 32 | void start(size_t thread_count); 33 | void stop(); 34 | 35 | std::shared_ptr ioService() const; 36 | logger::logger_t logFunction() const; 37 | 38 | 39 | 40 | ///////////////////////////////////////// 41 | // Member variables 42 | //////////////////////////////////////// 43 | 44 | private: 45 | const logger::logger_t log_; /// Logger 46 | std::shared_ptr io_context_; /// global io service 47 | 48 | std::vector thread_pool_; /// Asio threadpool executing the io servic 49 | using work_guard_t = asio::executor_work_guard; 50 | std::shared_ptr dummy_work_; /// Dummy work, so the io_context will never run out of work and shut down, even if there is no publisher or subscriber at the moment 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /tcp_pubsub/src/portable_endian.h: -------------------------------------------------------------------------------- 1 | // This file has been taken from: 2 | // https://gist.github.com/panzi/6856583#file-portable_endian-h 3 | // It includes manual changes for the QNX platform 4 | // 5 | // "License": Public Domain 6 | // I, Mathias Panzenböck, place this file hereby into the public domain. Use it at your own risk for whatever you like. 7 | // In case there are jurisdictions that don't support putting things in the public domain you can also consider it to 8 | // be "dual licensed" under the BSD, MIT and Apache licenses, if you want to. This code is trivial anyway. Consider it 9 | // an example on how to get the endian conversion functions on different platforms. 10 | 11 | #ifndef PORTABLE_ENDIAN_H_ 12 | #define PORTABLE_ENDIAN_H_ 13 | 14 | #if (defined(_WIN16) || defined(_WIN32) || defined(_WIN64)) && !defined(__WINDOWS__) 15 | 16 | # define __WINDOWS__ 17 | 18 | #endif 19 | 20 | #if defined(__linux__) || defined(__CYGWIN__) 21 | 22 | # include // IWYU pragma: export 23 | 24 | #elif defined(__APPLE__) 25 | 26 | # include // IWYU pragma: export 27 | 28 | # define htobe16(x) OSSwapHostToBigInt16(x) 29 | # define htole16(x) OSSwapHostToLittleInt16(x) 30 | # define be16toh(x) OSSwapBigToHostInt16(x) 31 | # define le16toh(x) OSSwapLittleToHostInt16(x) 32 | 33 | # define htobe32(x) OSSwapHostToBigInt32(x) 34 | # define htole32(x) OSSwapHostToLittleInt32(x) 35 | # define be32toh(x) OSSwapBigToHostInt32(x) 36 | # define le32toh(x) OSSwapLittleToHostInt32(x) 37 | 38 | # define htobe64(x) OSSwapHostToBigInt64(x) 39 | # define htole64(x) OSSwapHostToLittleInt64(x) 40 | # define be64toh(x) OSSwapBigToHostInt64(x) 41 | # define le64toh(x) OSSwapLittleToHostInt64(x) 42 | 43 | # define __BYTE_ORDER BYTE_ORDER 44 | # define __BIG_ENDIAN BIG_ENDIAN 45 | # define __LITTLE_ENDIAN LITTLE_ENDIAN 46 | # define __PDP_ENDIAN PDP_ENDIAN 47 | 48 | #elif defined(__OpenBSD__) 49 | 50 | # include // IWYU pragma: export 51 | 52 | # define __BYTE_ORDER BYTE_ORDER 53 | # define __BIG_ENDIAN BIG_ENDIAN 54 | # define __LITTLE_ENDIAN LITTLE_ENDIAN 55 | # define __PDP_ENDIAN PDP_ENDIAN 56 | 57 | #elif defined(__NetBSD__) || defined(__FreeBSD__) || defined(__DragonFly__) 58 | 59 | # include // IWYU pragma: export 60 | 61 | # define be16toh(x) betoh16(x) 62 | # define le16toh(x) letoh16(x) 63 | 64 | # define be32toh(x) betoh32(x) 65 | # define le32toh(x) letoh32(x) 66 | 67 | # define be64toh(x) betoh64(x) 68 | # define le64toh(x) letoh64(x) 69 | 70 | #elif defined(__WINDOWS__) 71 | 72 | # include // IWYU pragma: export 73 | # ifdef __GNUC__ 74 | # include // IWYU pragma: export 75 | # endif 76 | 77 | # if BYTE_ORDER == LITTLE_ENDIAN 78 | 79 | # define htobe16(x) htons(x) 80 | # define htole16(x) (x) 81 | # define be16toh(x) ntohs(x) 82 | # define le16toh(x) (x) 83 | 84 | # define htobe32(x) htonl(x) 85 | # define htole32(x) (x) 86 | # define be32toh(x) ntohl(x) 87 | # define le32toh(x) (x) 88 | 89 | # define htobe64(x) htonll(x) 90 | # define htole64(x) (x) 91 | # define be64toh(x) ntohll(x) 92 | # define le64toh(x) (x) 93 | 94 | # elif BYTE_ORDER == BIG_ENDIAN 95 | 96 | /* that would be xbox 360 */ 97 | # define htobe16(x) (x) 98 | # define htole16(x) __builtin_bswap16(x) 99 | # define be16toh(x) (x) 100 | # define le16toh(x) __builtin_bswap16(x) 101 | 102 | # define htobe32(x) (x) 103 | # define htole32(x) __builtin_bswap32(x) 104 | # define be32toh(x) (x) 105 | # define le32toh(x) __builtin_bswap32(x) 106 | 107 | # define htobe64(x) (x) 108 | # define htole64(x) __builtin_bswap64(x) 109 | # define be64toh(x) (x) 110 | # define le64toh(x) __builtin_bswap64(x) 111 | 112 | # else 113 | 114 | # error byte order not supported 115 | 116 | # endif 117 | 118 | # define __BYTE_ORDER BYTE_ORDER 119 | # define __BIG_ENDIAN BIG_ENDIAN 120 | # define __LITTLE_ENDIAN LITTLE_ENDIAN 121 | # define __PDP_ENDIAN PDP_ENDIAN 122 | 123 | #elif defined(__QNXNTO__) 124 | 125 | # include // IWYU pragma: export 126 | # include // IWYU pragma: export 127 | 128 | #else 129 | 130 | # error platform not supported 131 | 132 | #endif 133 | 134 | #endif 135 | -------------------------------------------------------------------------------- /tcp_pubsub/src/protocol_handshake_message.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | namespace tcp_pubsub 9 | { 10 | #pragma pack(push,1) 11 | // This message shall always contain little endian numbers. 12 | struct ProtocolHandshakeMessage 13 | { 14 | uint8_t protocol_version = 0; 15 | }; 16 | #pragma pack(pop) 17 | } 18 | -------------------------------------------------------------------------------- /tcp_pubsub/src/publisher.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include "publisher_impl.h" 16 | 17 | namespace tcp_pubsub 18 | { 19 | Publisher::Publisher(const std::shared_ptr& executor, const std::string& address, uint16_t port) 20 | : publisher_impl_(std::make_shared(executor)) 21 | { 22 | publisher_impl_->start(address, port); 23 | } 24 | 25 | Publisher::Publisher(const std::shared_ptr& executor, uint16_t port) 26 | : Publisher(executor, "::", port) 27 | {} 28 | 29 | Publisher::~Publisher() 30 | { publisher_impl_->cancel(); } 31 | 32 | uint16_t Publisher::getPort() const 33 | { return publisher_impl_->getPort(); } 34 | 35 | size_t Publisher::getSubscriberCount() const 36 | { return publisher_impl_->getSubscriberCount(); } 37 | 38 | bool Publisher::isRunning() const 39 | { return publisher_impl_->isRunning(); } 40 | 41 | bool Publisher::send(const char* const data, size_t size) const 42 | { return this->send({ {data, size} }); } 43 | 44 | bool Publisher::send(const std::vector>& buffers) const 45 | { return publisher_impl_->send(buffers); } 46 | 47 | void Publisher::cancel() 48 | { publisher_impl_->cancel(); } 49 | } 50 | -------------------------------------------------------------------------------- /tcp_pubsub/src/publisher_impl.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include "publisher_impl.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include "executor_impl.h" // IWYU pragma: keep 22 | #include "portable_endian.h" 23 | #include "publisher_session.h" 24 | #include "tcp_header.h" 25 | #include "tcp_pubsub/tcp_pubsub_logger.h" 26 | #include "tcp_pubsub_logger_abstraction.h" 27 | #include 28 | 29 | 30 | namespace tcp_pubsub 31 | { 32 | 33 | //////////////////////////////////////////////// 34 | // Constructor & Destructor 35 | //////////////////////////////////////////////// 36 | 37 | // Constructor 38 | Publisher_Impl::Publisher_Impl(const std::shared_ptr& executor) 39 | : is_running_ (false) 40 | , executor_ (executor) 41 | , acceptor_ (*executor_->executor_impl_->ioService()) 42 | , log_ (executor_->executor_impl_->logFunction()) 43 | {} 44 | 45 | // Destructor 46 | Publisher_Impl::~Publisher_Impl() 47 | { 48 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 49 | std::stringstream ss; 50 | ss << std::this_thread::get_id(); 51 | const std::string thread_id = ss.str(); 52 | log_(logger::LogLevel::DebugVerbose, "Publisher " + localEndpointToString() + ": Deleting from thread " + thread_id + "..."); 53 | #endif 54 | 55 | if (is_running_) 56 | { 57 | cancel(); 58 | } 59 | 60 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 61 | log_(logger::LogLevel::DebugVerbose, "Publisher " + localEndpointToString() + ": Deleted."); 62 | #endif 63 | } 64 | 65 | //////////////////////////////////////////////// 66 | // Start & Stop 67 | //////////////////////////////////////////////// 68 | 69 | bool Publisher_Impl::start(const std::string& address, uint16_t port) 70 | { 71 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 72 | log_(logger::LogLevel::DebugVerbose, "Publisher: Parsing address " + address + ":" + std::to_string(port) + "."); 73 | #endif 74 | 75 | // set up the acceptor to listen on the tcp port 76 | asio::error_code make_address_ec; 77 | const asio::ip::tcp::endpoint endpoint(asio::ip::make_address(address, make_address_ec), port); 78 | if (make_address_ec) 79 | { 80 | log_(logger::LogLevel::Error, "Publisher: Error parsing address \"" + address + ":" + std::to_string(port) + "\": " + make_address_ec.message()); 81 | return false; 82 | } 83 | 84 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 85 | log_(logger::LogLevel::DebugVerbose, "Publisher " + toString(endpoint) + ": Opening acceptor."); 86 | #endif 87 | 88 | { 89 | asio::error_code ec; 90 | acceptor_.open(endpoint.protocol(), ec); 91 | if (ec) 92 | { 93 | log_(logger::LogLevel::Error, "Publisher " + toString(endpoint) + ": Error opening acceptor: " + ec.message()); 94 | return false; 95 | } 96 | } 97 | 98 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 99 | log_(logger::LogLevel::DebugVerbose, "Publisher " + toString(endpoint) + ": Setting \"reuse_address\" option."); 100 | #endif 101 | 102 | { 103 | asio::error_code ec; 104 | acceptor_.set_option(asio::ip::tcp::acceptor::reuse_address(true), ec); 105 | if (ec) 106 | { 107 | log_(logger::LogLevel::Error, "Publisher " + toString(endpoint) + ": Error setting reuse_address option : " + ec.message()); 108 | return false; 109 | } 110 | } 111 | 112 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 113 | log_(logger::LogLevel::DebugVerbose, "Publisher " + toString(endpoint) + ": Binding acceptor to the endpoint."); 114 | #endif 115 | 116 | { 117 | asio::error_code ec; 118 | acceptor_.bind(endpoint, ec); 119 | if (ec) 120 | { 121 | log_(logger::LogLevel::Error, "Publisher " + toString(endpoint) + ": Error binding acceptor: " + ec.message()); 122 | return false; 123 | } 124 | } 125 | 126 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 127 | log_(logger::LogLevel::DebugVerbose, "Publisher " + toString(endpoint) + ": Listening on acceptor."); 128 | #endif 129 | 130 | { 131 | asio::error_code ec; 132 | acceptor_.listen(asio::socket_base::max_listen_connections, ec); 133 | if (ec) 134 | { 135 | log_(logger::LogLevel::Error, "Publisher " + toString(endpoint) + ": Error listening on acceptor: " + ec.message()); 136 | return false; 137 | } 138 | } 139 | 140 | log_(logger::LogLevel::Info, "Publisher " + toString(endpoint) + ": Created publisher and waiting for clients."); 141 | 142 | is_running_ = true; 143 | 144 | acceptClient(); 145 | 146 | return true; 147 | } 148 | 149 | void Publisher_Impl::cancel() 150 | { 151 | 152 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 153 | log_(logger::LogLevel::Debug, "Publisher " + localEndpointToString() + ": Shutting down"); 154 | #endif 155 | 156 | { 157 | asio::error_code ec; 158 | acceptor_.close(ec); 159 | acceptor_.cancel(ec); 160 | } 161 | 162 | is_running_ = false; 163 | 164 | std::vector> publisher_sessions; 165 | { 166 | // Copy the list, so we can safely iterate over it without locking the mutex 167 | const std::lock_guard publisher_sessions_lock(publisher_sessions_mutex_); 168 | publisher_sessions = publisher_sessions_; 169 | } 170 | for (const auto& session : publisher_sessions) 171 | { 172 | session->cancel(); 173 | } 174 | } 175 | 176 | void Publisher_Impl::acceptClient() 177 | { 178 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 179 | log_(logger::LogLevel::Debug, "Publisher " + localEndpointToString() + ": Waiting for new client..."); 180 | #endif 181 | 182 | const std::function&)> publisher_session_closed_handler 183 | = [me = shared_from_this()](const std::shared_ptr& session) -> void 184 | { 185 | const std::lock_guard publisher_sessions_lock(me->publisher_sessions_mutex_); 186 | 187 | auto session_it = std::find(me->publisher_sessions_.begin(), me->publisher_sessions_.end(), session); 188 | if (session_it != me->publisher_sessions_.end()) 189 | { 190 | me->publisher_sessions_.erase(session_it); 191 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 192 | me->log_(logger::LogLevel::Debug, "Publisher " + me->localEndpointToString() + ": Successfully removed Session to subscriber " + session->remoteEndpointToString() + ". Current subscriber count: " + std::to_string(me->publisher_sessions_.size()) + "."); 193 | #endif 194 | } 195 | else 196 | { 197 | me->log_(logger::LogLevel::Error, "Publisher " + me->localEndpointToString() + ": Trying to delete a non-existing publisher session."); 198 | } 199 | }; 200 | 201 | // Create a new session 202 | auto session = std::make_shared(executor_->executor_impl_->ioService(), publisher_session_closed_handler, log_); 203 | acceptor_.async_accept(session->getSocket() 204 | , [session, me = shared_from_this()](asio::error_code ec) 205 | { 206 | if (ec) 207 | { 208 | me->log_(logger::LogLevel::Error, "Publisher " + me->localEndpointToString() + ": Error while waiting for subsriber: " + ec.message()); 209 | return; 210 | } 211 | else 212 | { 213 | me->log_(logger::LogLevel::Info, "Publisher " + me->localEndpointToString() + ": Subscriber " + session->remoteEndpointToString() + " has connected."); 214 | } 215 | 216 | session->start(); 217 | 218 | // Add the session to the session list 219 | { 220 | const std::lock_guard publisher_sessions_lock_(me->publisher_sessions_mutex_); 221 | me->publisher_sessions_.push_back(session); 222 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 223 | me->log_(logger::LogLevel::Debug, "Publisher " + me->localEndpointToString() + ": Current subscriber count: " + std::to_string(me->publisher_sessions_.size())); 224 | #endif 225 | } 226 | 227 | // Accept the next session 228 | me->acceptClient(); 229 | }); 230 | } 231 | 232 | //////////////////////////////////////////////// 233 | // Send data 234 | //////////////////////////////////////////////// 235 | 236 | bool Publisher_Impl::send(const std::vector>& payloads) 237 | { 238 | if (!is_running_) 239 | { 240 | log_(logger::LogLevel::Error, "Publisher::send " + localEndpointToString() + ": Tried to send data to a non-running publisher."); 241 | return false; 242 | } 243 | 244 | // Don' send data if no subscriber is connected 245 | { 246 | const std::lock_guard publisher_sessions_lock(publisher_sessions_mutex_); 247 | if (publisher_sessions_.empty()) 248 | { 249 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 250 | log_(logger::LogLevel::DebugVerbose, "Publisher::send " + localEndpointToString() + ": No connection to any subscriber. Skip sending data."); 251 | #endif 252 | return true; 253 | } 254 | } 255 | 256 | // If a subsriber is connected, we need to initialize a buffer. 257 | const std::shared_ptr> buffer = buffer_pool.allocate(); 258 | 259 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 260 | std::stringstream buffer_pointer_ss; 261 | buffer_pointer_ss << "0x" << std::hex << buffer.get(); 262 | const std::string buffer_pointer_string = buffer_pointer_ss.str(); 263 | #endif 264 | 265 | // Check the size of the buffer and resize it 266 | { 267 | // Size of header 268 | const size_t header_size = sizeof(TcpHeader); 269 | 270 | // Size of user data, i.e. all payload(s) 271 | size_t entire_payload_size = 0; 272 | for (const auto& payload : payloads) 273 | { 274 | entire_payload_size += payload.second; 275 | } 276 | 277 | // Size of header and user data 278 | const size_t complete_size = header_size + entire_payload_size; 279 | 280 | if (buffer->capacity() < complete_size) 281 | { 282 | buffer->reserve(static_cast(complete_size * 1.1)); // Reserve 10% more bytes for later! 283 | } 284 | buffer->resize(complete_size); 285 | 286 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 287 | log_(logger::LogLevel::DebugVerbose, "Publisher::send " + localEndpointToString() + ": Filling buffer " + buffer_pointer_string + " with header and data.Entire buffer size is " + std::to_string(buffer->size()) + " bytes."); 288 | #endif 289 | 290 | // Fill header and copy the given data to the buffer 291 | auto *header = reinterpret_cast(buffer->data()); 292 | header->header_size = htole16(sizeof(TcpHeader)); 293 | header->type = MessageContentType::RegularPayload; 294 | header->reserved = 0; 295 | header->data_size = htole64(entire_payload_size); 296 | 297 | // copy the data into the buffer right after the header 298 | size_t current_position = header_size; 299 | for (const auto& payload : payloads) 300 | { 301 | if ((payload.first != nullptr) && (payload.second > 0)) 302 | { 303 | memcpy(&((*buffer)[current_position]), payload.first, payload.second); 304 | current_position += payload.second; 305 | } 306 | } 307 | } 308 | 309 | // Lock the sessions mutex again and send out the prepared buffer. All publisher sessions will operate on the same buffer! 310 | { 311 | const std::lock_guard publisher_sessions_lock(publisher_sessions_mutex_); 312 | 313 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 314 | log_(logger::LogLevel::DebugVerbose, "Publisher::send " + localEndpointToString() + ": Sending buffer " + buffer_pointer_string + " to " + std::to_string(publisher_sessions_.size()) + " subsribers."); 315 | #endif 316 | 317 | for (const auto& publisher_session : publisher_sessions_) 318 | { 319 | publisher_session->sendDataBuffer(buffer); 320 | } 321 | } 322 | 323 | return true; 324 | } 325 | 326 | //////////////////////////////////////////////// 327 | // (Status-) getters 328 | //////////////////////////////////////////////// 329 | 330 | uint16_t Publisher_Impl::getPort() const 331 | { 332 | if (is_running_) 333 | { 334 | asio::error_code ec; 335 | auto local_endpoint = acceptor_.local_endpoint(ec); 336 | if (!ec) 337 | return local_endpoint.port(); 338 | else 339 | return 0; 340 | } 341 | else 342 | { 343 | return 0; 344 | } 345 | } 346 | 347 | size_t Publisher_Impl::getSubscriberCount() const 348 | { 349 | const std::lock_guard publisher_sessions_lock(publisher_sessions_mutex_); 350 | return publisher_sessions_.size(); 351 | } 352 | 353 | bool Publisher_Impl::isRunning() const 354 | { 355 | return is_running_; 356 | } 357 | 358 | std::string Publisher_Impl::toString(const asio::ip::tcp::endpoint& endpoint) const 359 | { 360 | return endpoint.address().to_string() + ":" + std::to_string(endpoint.port()); 361 | } 362 | 363 | std::string Publisher_Impl::localEndpointToString() const 364 | { 365 | asio::error_code ec; 366 | auto local_endpoint = acceptor_.local_endpoint(ec); 367 | if (!ec) 368 | return toString(local_endpoint); 369 | else 370 | return "?"; 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /tcp_pubsub/src/publisher_impl.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | 18 | #include "publisher_session.h" 19 | #include "tcp_pubsub/tcp_pubsub_logger.h" 20 | #include 21 | 22 | namespace tcp_pubsub 23 | { 24 | class Publisher_Impl : public std::enable_shared_from_this 25 | { 26 | 27 | //////////////////////////////////////////////// 28 | // Constructor & Destructor 29 | //////////////////////////////////////////////// 30 | 31 | public: 32 | // Constructor 33 | Publisher_Impl(const std::shared_ptr& executor); 34 | 35 | // Copy 36 | Publisher_Impl(const Publisher_Impl&) = delete; 37 | Publisher_Impl& operator=(const Publisher_Impl&) = delete; 38 | 39 | // Move 40 | Publisher_Impl& operator=(Publisher_Impl&&) = delete; 41 | Publisher_Impl(Publisher_Impl&&) = delete; 42 | 43 | // Destructor 44 | ~Publisher_Impl(); 45 | 46 | //////////////////////////////////////////////// 47 | // Start & Stop 48 | //////////////////////////////////////////////// 49 | 50 | public: 51 | bool start(const std::string& address, uint16_t port); 52 | void cancel(); 53 | 54 | private: 55 | void acceptClient(); 56 | 57 | //////////////////////////////////////////////// 58 | // Send data 59 | //////////////////////////////////////////////// 60 | 61 | public: 62 | bool send(const std::vector>& payloads); 63 | 64 | //////////////////////////////////////////////// 65 | // (Status-) getters 66 | //////////////////////////////////////////////// 67 | 68 | public: 69 | uint16_t getPort() const; 70 | size_t getSubscriberCount() const; 71 | 72 | bool isRunning() const; 73 | 74 | private: 75 | std::string toString(const asio::ip::tcp::endpoint& endpoint) const; 76 | std::string localEndpointToString() const; 77 | 78 | //////////////////////////////////////////////// 79 | // Member variables 80 | //////////////////////////////////////////////// 81 | 82 | private: 83 | std::atomic is_running_; /// Indicates whether this publisher is running and can send data. May be false, if e.g. binding to the given address has failed. 84 | 85 | // Asio 86 | const std::shared_ptr executor_; /// Global Executor (holding the io_context and thread pool) 87 | asio::ip::tcp::acceptor acceptor_; /// Acceptor used for waiting for clients (i.e. subscribers) 88 | 89 | // Logger 90 | const logger::logger_t log_; /// Function for logging 91 | 92 | // Sessions 93 | mutable std::mutex publisher_sessions_mutex_; 94 | std::vector> publisher_sessions_; /// List of all sessions (i.e. connections to subsribers) 95 | 96 | // Buffer pool 97 | struct buffer_pool_lock_policy_ 98 | { 99 | using mutex_type = std::mutex; 100 | using lock_type = std::lock_guard; 101 | }; 102 | recycle::shared_pool, buffer_pool_lock_policy_> buffer_pool; /// Buffer pool that let's us reuse memory chunks 103 | }; 104 | } 105 | -------------------------------------------------------------------------------- /tcp_pubsub/src/publisher_session.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include "publisher_session.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | 21 | #include "portable_endian.h" 22 | #include "protocol_handshake_message.h" 23 | #include "tcp_header.h" 24 | #include "tcp_pubsub/tcp_pubsub_logger.h" 25 | #include "tcp_pubsub_logger_abstraction.h" 26 | 27 | namespace tcp_pubsub 28 | { 29 | ////////////////////////////////////////////// 30 | /// Constructor & Destructor 31 | ////////////////////////////////////////////// 32 | 33 | PublisherSession::PublisherSession(const std::shared_ptr& io_context 34 | , const std::function&)>& session_closed_handler 35 | , const tcp_pubsub::logger::logger_t& log_function) 36 | : io_context_ (io_context) 37 | , state_ (State::NotStarted) 38 | , session_closed_handler_ (session_closed_handler) 39 | , log_ (log_function) 40 | , data_socket_ (*io_context_) 41 | , data_strand_ (*io_context_) 42 | , sending_in_progress_ (false) 43 | { 44 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 45 | log_(logger::LogLevel::DebugVerbose, "PublisherSession " + endpointToString() + ": Created."); 46 | #endif 47 | } 48 | 49 | PublisherSession::~PublisherSession() 50 | { 51 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 52 | std::stringstream ss; 53 | ss << std::this_thread::get_id(); 54 | const std::string thread_id = ss.str(); 55 | log_(logger::LogLevel::DebugVerbose, "PublisherSession " + endpointToString() + ": Deleting from thread " + thread_id + "..."); 56 | #endif 57 | 58 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 59 | log_(logger::LogLevel::Debug, "PublisherSession " + endpointToString() + ": Deleted."); 60 | #endif 61 | } 62 | 63 | ////////////////////////////////////////////// 64 | /// Start & Stop 65 | ////////////////////////////////////////////// 66 | 67 | void PublisherSession::start() 68 | { 69 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 70 | log_(logger::LogLevel::DebugVerbose, "PublisherSession " + endpointToString() + ": Setting tcp::no_delay option."); 71 | #endif 72 | // Disable Nagle's algorithm. Nagles Algorithm will otherwise cause the 73 | // Socket to wait for more data, if it encounters a frame that can still 74 | // fit more data. Obviously, this is an awfull default behaviour, if we 75 | // want to transmit our data in a timely fashion. 76 | { 77 | asio::error_code ec; 78 | data_socket_.set_option(asio::ip::tcp::no_delay(true), ec); 79 | if (ec) log_(logger::LogLevel::Warning, "PublisherSession " + endpointToString() + ": Failed setting tcp::no_delay option. The performance may suffer."); 80 | } 81 | 82 | state_ = State::Handshaking; 83 | 84 | receiveTcpPacket(); 85 | } 86 | 87 | void PublisherSession::cancel() 88 | { 89 | sessionClosedHandler(); 90 | } 91 | 92 | void PublisherSession::sessionClosedHandler() 93 | { 94 | // Check if this session has already been canceled while at the same time 95 | // setting it to the CANCELED. This ensures, that the handler will only be 96 | // run once. 97 | const State previous_state = (state_.exchange(State::Canceled)); 98 | if (previous_state == State::Canceled) 99 | return; 100 | 101 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 102 | log_(logger::LogLevel::Debug, "PublisherSession " + endpointToString() + ": Closing session."); 103 | #endif 104 | 105 | { 106 | asio::error_code ec; 107 | data_socket_.close(ec); // Even if ec indicates an error, the socket is closed now (according to the documentation) 108 | } 109 | 110 | session_closed_handler_(shared_from_this()); // Run the completion handler 111 | } 112 | 113 | ////////////////////////////////////////////// 114 | /// ProtocolHandshake 115 | ////////////////////////////////////////////// 116 | void PublisherSession::receiveTcpPacket() 117 | { 118 | readHeaderLength(); 119 | } 120 | 121 | void PublisherSession::readHeaderLength() 122 | { 123 | if (state_ == State::Canceled) 124 | return; 125 | 126 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 127 | log_(logger::LogLevel::DebugVerbose, "PublisherSession " + endpointToString() + ": Waiting for data..."); 128 | #endif 129 | 130 | const std::shared_ptr header = std::make_shared(); 131 | 132 | asio::async_read(data_socket_ 133 | , asio::buffer(&(header->header_size), sizeof(header->header_size)) 134 | , asio::transfer_at_least(sizeof(header->header_size)) 135 | , asio::bind_executor(data_strand_, [me = shared_from_this(), header](asio::error_code ec, std::size_t /*length*/) 136 | { 137 | if (ec) 138 | { 139 | me->log_(logger::LogLevel::Error, "PublisherSession " + me->endpointToString() + ": Error reading header length: " + ec.message()); 140 | me->sessionClosedHandler();; 141 | return; 142 | } 143 | me->readHeaderContent(header); 144 | })); 145 | } 146 | 147 | void PublisherSession::readHeaderContent(const std::shared_ptr& header) 148 | { 149 | if (state_ == State::Canceled) 150 | return; 151 | 152 | if (header->header_size < sizeof(header->header_size)) 153 | { 154 | log_(logger::LogLevel::Error, "PublisherSession " + endpointToString() + ": Received header length of " + std::to_string(header->header_size) + ", which is less than the minimal header size."); 155 | sessionClosedHandler(); 156 | return; 157 | } 158 | 159 | const uint16_t remote_header_size = le16toh(header->header_size); 160 | const uint16_t my_header_size = sizeof(*header); 161 | 162 | const uint16_t bytes_to_read_from_socket = std::min(remote_header_size, my_header_size) - sizeof(header->header_size); 163 | const uint16_t bytes_to_discard_from_socket = (remote_header_size > my_header_size ? (remote_header_size - my_header_size) : 0); 164 | 165 | asio::async_read(data_socket_ 166 | , asio::buffer(&reinterpret_cast(header.get())[sizeof(header->header_size)], bytes_to_read_from_socket) 167 | , asio::transfer_at_least(bytes_to_read_from_socket) 168 | , asio::bind_executor(data_strand_, [me = shared_from_this(), header, bytes_to_discard_from_socket](asio::error_code ec, std::size_t /*length*/) 169 | { 170 | if (ec) 171 | { 172 | me->log_(logger::LogLevel::Error, "PublisherSession " + me->endpointToString() + ": Error reading header content: " + ec.message()); 173 | me->sessionClosedHandler();; 174 | return; 175 | } 176 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 177 | me->log_(logger::LogLevel::DebugVerbose 178 | , "PublisherSession " + me->endpointToString() 179 | + ": Received header content: " 180 | + "data_size: " + std::to_string(le64toh(header->data_size))); 181 | #endif 182 | 183 | if (bytes_to_discard_from_socket > 0) 184 | { 185 | me->discardDataBetweenHeaderAndPayload(header, bytes_to_discard_from_socket); 186 | } 187 | else 188 | { 189 | me->readPayload(header); 190 | } 191 | })); 192 | } 193 | 194 | void PublisherSession::discardDataBetweenHeaderAndPayload(const std::shared_ptr& header, uint16_t bytes_to_discard) 195 | { 196 | if (state_ == State::Canceled) 197 | return; 198 | 199 | // This vector is temporary and will be deleted right after we read the data into it 200 | std::vector data_to_discard; 201 | data_to_discard.resize(bytes_to_discard); 202 | 203 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 204 | log_(logger::LogLevel::DebugVerbose, "PublisherSession " + endpointToString() + ": Discarding " + std::to_string(bytes_to_discard) + " bytes after the header."); 205 | #endif 206 | 207 | asio::async_read(data_socket_ 208 | , asio::buffer(data_to_discard.data(), bytes_to_discard) 209 | , asio::transfer_at_least(bytes_to_discard) 210 | , asio::bind_executor(data_strand_, [me = shared_from_this(), header](asio::error_code ec, std::size_t /*length*/) 211 | { 212 | if (ec) 213 | { 214 | me->log_(logger::LogLevel::Error, "PublisherSession " + me->endpointToString() + ": Error discarding bytes after header: " + ec.message()); 215 | me->sessionClosedHandler(); 216 | return; 217 | } 218 | me->readPayload(header); 219 | })); 220 | } 221 | 222 | void PublisherSession::readPayload(const std::shared_ptr& header) 223 | { 224 | if (state_ == State::Canceled) 225 | return; 226 | 227 | if (header->data_size == 0) 228 | { 229 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 230 | log_(logger::LogLevel::Debug, "PublisherSession " + endpointToString() + ": Received data size of 0."); 231 | #endif 232 | sessionClosedHandler(); 233 | return; 234 | } 235 | 236 | // Create a buffer for the playload 237 | const std::shared_ptr> data_buffer = std::make_shared>(); 238 | data_buffer->resize(le64toh(header->data_size)); 239 | 240 | asio::async_read(data_socket_ 241 | , asio::buffer(data_buffer->data(), le64toh(header->data_size)) 242 | , asio::transfer_at_least(le64toh(header->data_size)) 243 | , asio::bind_executor(data_strand_, [me = shared_from_this(), header, data_buffer](asio::error_code ec, std::size_t /*length*/) 244 | { 245 | if (ec) 246 | { 247 | me->log_(logger::LogLevel::Error, "PublisherSession " + me->endpointToString() + ": Error reading payload: " + ec.message()); 248 | me->sessionClosedHandler();; 249 | return; 250 | } 251 | 252 | // Handle payload 253 | if (header->type == MessageContentType::ProtocolHandshake) 254 | { 255 | ProtocolHandshakeMessage handshake_message; 256 | const size_t bytes_to_copy = std::min(data_buffer->size(), sizeof(ProtocolHandshakeMessage)); 257 | std::memcpy(&handshake_message, data_buffer->data(), bytes_to_copy); 258 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 259 | me->log_(logger::LogLevel::Debug, "PublisherSession " + me->endpointToString() + ": Received Handshake message. Maximum supported protocol version from subsriber: v" + std::to_string(handshake_message.protocol_version)); 260 | #endif 261 | me->sendProtocolHandshakeResponse(); 262 | } 263 | else 264 | { 265 | me->log_(logger::LogLevel::Warning, "PublisherSession " + me->endpointToString() + ": Received message is not a handshake message (Type is " + std::to_string(static_cast(header->type)) + ")."); 266 | me->sessionClosedHandler(); 267 | } 268 | })); 269 | } 270 | 271 | void PublisherSession::sendProtocolHandshakeResponse() 272 | { 273 | if (state_ == State::Canceled) 274 | return; 275 | 276 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 277 | log_(logger::LogLevel::Debug, "PublisherSession " + endpointToString() + ": Sending ProtocolHandshakeResponse."); 278 | #endif 279 | 280 | const std::shared_ptr> buffer = std::make_shared>(); 281 | buffer->resize(sizeof(TcpHeader) + sizeof(ProtocolHandshakeMessage)); 282 | 283 | TcpHeader* header = reinterpret_cast(buffer->data()); 284 | header->header_size = htole16(sizeof(TcpHeader)); 285 | header->type = MessageContentType::ProtocolHandshake; 286 | header->reserved = 0; 287 | header->data_size = htole64(sizeof(ProtocolHandshakeMessage)); 288 | 289 | ProtocolHandshakeMessage* handshake_message = reinterpret_cast(&(buffer->operator[](sizeof(TcpHeader)))); 290 | handshake_message->protocol_version = 0; // At the moment, we only support Version 0. 291 | 292 | // Send the buffer directly to the client 293 | sendBufferToClient(buffer); 294 | const State old_state = state_.exchange(State::Running); 295 | if (old_state != State::Handshaking) 296 | state_ = old_state; 297 | } 298 | 299 | ////////////////////////////////////////////// 300 | /// Send Data 301 | ////////////////////////////////////////////// 302 | 303 | void PublisherSession::sendDataBuffer(const std::shared_ptr>& buffer) 304 | { 305 | if (state_ == State::Canceled) 306 | return; 307 | 308 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 309 | std::stringstream buffer_pointer_ss; 310 | buffer_pointer_ss << "0x" << std::hex << buffer.get(); 311 | const std::string buffer_pointer_string = buffer_pointer_ss.str(); 312 | #endif 313 | 314 | { 315 | const std::lock_guard next_buffer_lock(next_buffer_mutex_); 316 | 317 | if ((state_ == State::Running) && !sending_in_progress_) 318 | { 319 | // If we are not sending a buffer at the moment, we can directly trigger sending the given buffer 320 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 321 | log_(logger::LogLevel::DebugVerbose, "PublisherSession " + endpointToString() + ": Trigger sending buffer " + buffer_pointer_string + "."); 322 | #endif 323 | sending_in_progress_ = true; 324 | sendBufferToClient(buffer); 325 | } 326 | else 327 | { 328 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 329 | log_(logger::LogLevel::DebugVerbose, "PublisherSession " + endpointToString() + ": Saved buffer " + buffer_pointer_string + " as next buffer."); 330 | #endif 331 | // Store the new buffer as next buffer 332 | next_buffer_to_send_ = buffer; 333 | } 334 | } 335 | } 336 | 337 | void PublisherSession::sendBufferToClient(const std::shared_ptr>& buffer) 338 | { 339 | if (state_ == State::Canceled) 340 | return; 341 | 342 | asio::async_write(data_socket_ 343 | , asio::buffer(*buffer) 344 | , asio::bind_executor(data_strand_, 345 | [me = shared_from_this(), buffer](asio::error_code ec, std::size_t /*bytes_to_transfer*/) 346 | { 347 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 348 | std::stringstream buffer_pointer_ss; 349 | buffer_pointer_ss << "0x" << std::hex << buffer.get(); 350 | const std::string buffer_pointer_string = buffer_pointer_ss.str(); 351 | #endif 352 | if (ec) 353 | { 354 | me->log_(logger::LogLevel::Warning, "PublisherSession " + me->endpointToString() + ": Failed sending data: " + ec.message()); 355 | me->sessionClosedHandler(); 356 | return; 357 | } 358 | 359 | if (me->state_ == State::Canceled) 360 | return; 361 | 362 | { 363 | const std::lock_guard next_buffer_lock(me->next_buffer_mutex_); 364 | 365 | if (me->next_buffer_to_send_) 366 | { 367 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 368 | me->log_(logger::LogLevel::DebugVerbose, "PublisherSession " + me->endpointToString() + ": Successfully sent buffer " + buffer_pointer_string + ". Next buffer is available, trigger sending it."); 369 | #endif 370 | // We have a next buffer! 371 | 372 | // Copy the next buffer to send from the member variable 373 | // to a temporary variable. Then delete the member variable, 374 | // so when adding a new buffer as next buffer, it is clear 375 | // that we now have taken ownership of that buffer. 376 | auto next_buffer_tmp = me->next_buffer_to_send_; 377 | 378 | me->next_buffer_to_send_ = nullptr; 379 | 380 | // Send the next buffer to the client 381 | me->sendBufferToClient(next_buffer_tmp); 382 | } 383 | else 384 | { 385 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 386 | me->log_(logger::LogLevel::DebugVerbose, "PublisherSession " + me->endpointToString() + ": Successfully sent buffer " + buffer_pointer_string + ". No next buffer available."); 387 | #endif 388 | me->sending_in_progress_ = false; 389 | } 390 | } 391 | } 392 | )); 393 | } 394 | 395 | ////////////////////////////////////////////// 396 | /// (Status-) getters 397 | ////////////////////////////////////////////// 398 | 399 | asio::ip::tcp::socket& PublisherSession::getSocket() 400 | { 401 | return data_socket_; 402 | } 403 | 404 | std::string PublisherSession::localEndpointToString() const 405 | { 406 | asio::error_code ec; 407 | auto local_endpoint = data_socket_.local_endpoint(ec); 408 | if (!ec) 409 | return local_endpoint.address().to_string() + ":" + std::to_string(local_endpoint.port()); 410 | else 411 | return "?"; 412 | } 413 | 414 | std::string PublisherSession::remoteEndpointToString() const 415 | { 416 | asio::error_code ec; 417 | auto remote_endpoint = data_socket_.remote_endpoint(ec); 418 | if (!ec) 419 | return remote_endpoint.address().to_string() + ":" + std::to_string(remote_endpoint.port()); 420 | else 421 | return "?"; 422 | } 423 | 424 | std::string PublisherSession::endpointToString() const 425 | { 426 | return localEndpointToString() + "->" + remoteEndpointToString(); 427 | } 428 | 429 | } 430 | -------------------------------------------------------------------------------- /tcp_pubsub/src/publisher_session.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "tcp_header.h" 16 | #include "tcp_pubsub/tcp_pubsub_logger.h" 17 | 18 | namespace tcp_pubsub 19 | { 20 | class PublisherSession 21 | : public std::enable_shared_from_this 22 | { 23 | ////////////////////////////////////////////// 24 | /// Nested classes 25 | ////////////////////////////////////////////// 26 | private: 27 | enum class State 28 | { 29 | NotStarted, 30 | Handshaking, 31 | Running, 32 | Canceled 33 | }; 34 | 35 | ////////////////////////////////////////////// 36 | /// Constructor & Destructor 37 | ////////////////////////////////////////////// 38 | public: 39 | PublisherSession(const std::shared_ptr& io_context 40 | , const std::function&)>& session_closed_handler 41 | , const tcp_pubsub::logger::logger_t& log_function); 42 | 43 | // Copy 44 | PublisherSession(const PublisherSession&) = delete; 45 | PublisherSession& operator=(const PublisherSession&) = delete; 46 | 47 | // Move 48 | PublisherSession& operator=(PublisherSession&&) = delete; 49 | PublisherSession(PublisherSession&&) = delete; 50 | 51 | ~PublisherSession(); 52 | 53 | ////////////////////////////////////////////// 54 | /// Start & Stop 55 | ////////////////////////////////////////////// 56 | 57 | public: 58 | void start(); 59 | void cancel(); 60 | 61 | private: 62 | void sessionClosedHandler(); 63 | 64 | ////////////////////////////////////////////// 65 | /// ProtocolHandshake 66 | ////////////////////////////////////////////// 67 | private: 68 | void receiveTcpPacket(); 69 | void readHeaderLength (); 70 | void discardDataBetweenHeaderAndPayload(const std::shared_ptr& header, uint16_t bytes_to_discard); 71 | void readHeaderContent(const std::shared_ptr& header); 72 | void readPayload(const std::shared_ptr& header); 73 | 74 | 75 | void sendProtocolHandshakeResponse(); 76 | 77 | ////////////////////////////////////////////// 78 | /// Send Data 79 | ////////////////////////////////////////////// 80 | public: 81 | void sendDataBuffer(const std::shared_ptr>& buffer); 82 | private: 83 | void sendBufferToClient(const std::shared_ptr>& buffer); 84 | 85 | ////////////////////////////////////////////// 86 | /// (Status-) getters 87 | ////////////////////////////////////////////// 88 | 89 | public: 90 | asio::ip::tcp::socket& getSocket(); 91 | std::string localEndpointToString() const; 92 | std::string remoteEndpointToString() const; 93 | std::string endpointToString() const; 94 | 95 | ////////////////////////////////////////////// 96 | /// Member variables 97 | ////////////////////////////////////////////// 98 | private: 99 | // Asio IO Service 100 | std::shared_ptr io_context_; 101 | 102 | // Whether the session has been canceled 103 | std::atomic state_; 104 | 105 | // Handlers 106 | const std::function&)> session_closed_handler_; 107 | // Logger 108 | const logger::logger_t log_; /// Function for logging 109 | 110 | // TCP Socket & Queue (protected by the strand!) 111 | asio::ip::tcp::socket data_socket_; 112 | asio::io_context::strand data_strand_; 113 | 114 | // Variable holding if we are currently sending any data and what data to send next 115 | std::mutex next_buffer_mutex_; 116 | bool sending_in_progress_; 117 | std::shared_ptr> next_buffer_to_send_; 118 | }; 119 | } 120 | -------------------------------------------------------------------------------- /tcp_pubsub/src/subscriber.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "subscriber_impl.h" 14 | #include "tcp_pubsub/callback_data.h" 15 | #include "tcp_pubsub/subscriber_session.h" 16 | #include 17 | 18 | namespace tcp_pubsub 19 | { 20 | Subscriber::Subscriber(const std::shared_ptr& executor) 21 | : subscriber_impl_(std::make_shared(executor)) 22 | {} 23 | 24 | Subscriber::~Subscriber() 25 | { 26 | subscriber_impl_->cancel(); 27 | } 28 | 29 | std::shared_ptr Subscriber::addSession(const std::string& address, uint16_t port, int max_reconnection_attempts) 30 | { return subscriber_impl_->addSession(std::vector>{{address, port}}, max_reconnection_attempts); } 31 | 32 | std::shared_ptr Subscriber::addSession(const std::vector>& publisher_list, int max_reconnection_attempts) 33 | { return subscriber_impl_->addSession(publisher_list, max_reconnection_attempts); } 34 | 35 | 36 | std::vector> Subscriber::getSessions() const 37 | { return subscriber_impl_->getSessions(); } 38 | 39 | void Subscriber::setCallback(const std::function& callback_function, bool synchronous_execution) 40 | { subscriber_impl_->setCallback(callback_function, synchronous_execution); } 41 | 42 | void Subscriber::clearCallback() 43 | { subscriber_impl_->setCallback([](const auto&){}, true); } 44 | 45 | void Subscriber::cancel() 46 | { subscriber_impl_->cancel(); } 47 | } 48 | -------------------------------------------------------------------------------- /tcp_pubsub/src/subscriber_impl.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include "subscriber_impl.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include "executor_impl.h" // IWYU pragma: keep 19 | #include "subscriber_session_impl.h" 20 | #include "tcp_header.h" 21 | #include "tcp_pubsub/callback_data.h" 22 | #include "tcp_pubsub/executor.h" 23 | #include "tcp_pubsub/subscriber_session.h" 24 | #include "tcp_pubsub/tcp_pubsub_logger.h" 25 | #include "tcp_pubsub_logger_abstraction.h" 26 | 27 | namespace tcp_pubsub 28 | { 29 | //////////////////////////////////////////////// 30 | // Constructor & Destructor 31 | //////////////////////////////////////////////// 32 | Subscriber_Impl::Subscriber_Impl(const std::shared_ptr& executor) 33 | : executor_ (executor) 34 | , user_callback_is_synchronous_(true) 35 | , synchronous_user_callback_ ([](const auto&){}) 36 | , callback_thread_stop_ (true) 37 | , log_ (executor_->executor_impl_->logFunction()) 38 | {} 39 | 40 | // Destructor 41 | Subscriber_Impl::~Subscriber_Impl() 42 | { 43 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 44 | std::stringstream ss; 45 | ss << std::this_thread::get_id(); 46 | const std::string thread_id = ss.str(); 47 | log_(logger::LogLevel::DebugVerbose, "Subscriber " + subscriberIdString() + ": Deleting from thread " + thread_id + "..."); 48 | #endif 49 | 50 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 51 | log_(logger::LogLevel::Debug, "Subscriber " + subscriberIdString() + ": Deleted."); 52 | #endif 53 | } 54 | 55 | //////////////////////////////////////////////// 56 | // Session Management 57 | //////////////////////////////////////////////// 58 | std::shared_ptr Subscriber_Impl::addSession(const std::vector>& publisher_list, int max_reconnection_attempts) 59 | { 60 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 61 | // Create a list of all publishers as string 62 | std::string publisher_list_string; 63 | for (const auto& publisher : publisher_list) 64 | { 65 | publisher_list_string += publisher.first + ":" + std::to_string(publisher.second); 66 | if (&publisher != &publisher_list.back()) 67 | publisher_list_string += ", "; 68 | } 69 | 70 | log_(logger::LogLevel::DebugVerbose, "Subscriber " + subscriberIdString() + ": Adding session for endpoints {" + publisher_list_string + "}."); 71 | #endif 72 | 73 | // Function for getting a free buffer 74 | const std::function>()> get_free_buffer_handler 75 | = [me = shared_from_this()]() -> std::shared_ptr> 76 | { 77 | return me->buffer_pool_.allocate(); 78 | }; 79 | 80 | // Function for cleaning up 81 | const std::function&)> subscriber_session_closed_handler 82 | = [me = shared_from_this()](const std::shared_ptr& subscriber_session_impl) -> void 83 | { 84 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 85 | me->log_(logger::LogLevel::Debug, "Subscriber " + me->subscriberIdString() + ": Removing session " + subscriber_session_impl->remoteEndpointToString() + "."); 86 | #endif 87 | const std::lock_guardsession_list_lock(me->session_list_mutex_); 88 | 89 | // Look up the current subscriber session and remove it from the list 90 | auto session_it = std::find_if(me->session_list_.begin() 91 | , me->session_list_.end() 92 | , [&subscriber_session_impl] (const std::shared_ptr& session) ->bool 93 | { return subscriber_session_impl == session->subscriber_session_impl_; }); 94 | if (session_it != me->session_list_.end()) 95 | { 96 | me->session_list_.erase(session_it); 97 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 98 | me->log_(logger::LogLevel::Debug, "Subscriber " + me->subscriberIdString() + ": Current number of sessions: " + std::to_string(me->session_list_.size())); 99 | #endif 100 | } 101 | else 102 | { 103 | // This can never happen, unless I screwed up while implementing this 104 | me->log_(logger::LogLevel::Fatal, "Subscriber " + me->subscriberIdString() + ": Error removing subscriber: The subscriber does not exist"); 105 | } 106 | }; 107 | 108 | // Create a new Subscriber Session. Unfortunatelly we cannot use 109 | // ::std::make_shared here, as the constructor is private and make_shared 110 | // cannot access it. Thus, we crate the object manually with new. 111 | std::shared_ptr subscriber_session( 112 | new SubscriberSession(std::make_shared(executor_->executor_impl_->ioService() 113 | , publisher_list 114 | , max_reconnection_attempts 115 | , get_free_buffer_handler 116 | , subscriber_session_closed_handler 117 | , log_))); 118 | 119 | setCallbackToSession(subscriber_session); 120 | 121 | { 122 | const std::lock_guard session_list_lock(session_list_mutex_); 123 | session_list_.push_back(subscriber_session); 124 | subscriber_session->subscriber_session_impl_->start(); 125 | } 126 | 127 | return subscriber_session; 128 | } 129 | 130 | std::vector> Subscriber_Impl::getSessions() const 131 | { 132 | const std::lock_guard session_list_lock(session_list_mutex_); 133 | return session_list_; 134 | } 135 | 136 | void Subscriber_Impl::setCallback(const std::function& callback_function, bool synchronous_execution) 137 | { 138 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 139 | log_(logger::LogLevel::Debug, "Subscriber " + subscriberIdString() + ": Setting new " + (synchronous_execution ? "synchronous" : "asynchronous") + " callback."); 140 | #endif 141 | 142 | // Stop and remove the old callback thread at first 143 | if (callback_thread_) 144 | { 145 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 146 | log_(logger::LogLevel::DebugVerbose, "Subscriber " + subscriberIdString() + ": Stopping old callback thread..."); 147 | #endif 148 | callback_thread_stop_ = true; 149 | last_callback_data_cv_.notify_all(); 150 | 151 | // Join or detach the old thread. We cannot join a thread from it's own 152 | // thread, so we detach the thread in that case. 153 | if (std::this_thread::get_id() == callback_thread_->get_id()) 154 | callback_thread_->detach(); 155 | else 156 | callback_thread_->join(); 157 | 158 | callback_thread_.reset(); 159 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 160 | log_(logger::LogLevel::DebugVerbose, "Subscriber " + subscriberIdString() + ": Old callback thread has terminated."); 161 | #endif 162 | } 163 | 164 | const bool renew_synchronous_callbacks = (synchronous_execution || user_callback_is_synchronous_); 165 | 166 | if (synchronous_execution) 167 | { 168 | // Save the callback as member variable. We need to pass it to all new sessions. 169 | synchronous_user_callback_ = callback_function; 170 | user_callback_is_synchronous_ = synchronous_execution; 171 | 172 | // Clean the last callback data, so any buffer in there is freed 173 | const std::unique_lock callback_lock(last_callback_data_mutex_); 174 | last_callback_data_ = CallbackData(); 175 | } 176 | if (!synchronous_execution) 177 | { 178 | synchronous_user_callback_ = [](const auto&) {}; 179 | user_callback_is_synchronous_ = synchronous_execution; 180 | 181 | // Create a new callback thread with the new callback from the function parameter 182 | callback_thread_stop_ = false; 183 | callback_thread_ = std::make_unique( 184 | [me = shared_from_this(), callback_function]() 185 | { 186 | for (;;) 187 | { 188 | CallbackData this_callback_data; // create empty callback data 189 | 190 | { 191 | // Lock callback mutex and wait for valid data. Wake up if the callback data contains valid data or the user set a synchronous callback. In the latter case, we exit the thread. 192 | std::unique_lock callback_lock(me->last_callback_data_mutex_); 193 | me->last_callback_data_cv_.wait(callback_lock, [&me]() -> bool { return bool(me->last_callback_data_.buffer_) || me->callback_thread_stop_; }); 194 | 195 | // Exit if the user has set a synchronous callback 196 | if (me->callback_thread_stop_) return; 197 | 198 | std::swap(this_callback_data, me->last_callback_data_); // Now an empty callback data is in "last_callback_data" again 199 | } 200 | 201 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 202 | me->log_(logger::LogLevel::DebugVerbose, "Subscriber " + me->subscriberIdString() + ": Executing asynchronous callback"); 203 | #endif 204 | // Execute the user callback. Note that the callback mutex is not locked any more, so while the expensive user callback is executed, our tcp sessions can already store new data. 205 | callback_function(this_callback_data); 206 | } 207 | }); 208 | } 209 | 210 | //std::lock_guard callback_lock(callback_mutex_); 211 | if (renew_synchronous_callbacks) 212 | { 213 | const std::lock_guard session_list_lock(session_list_mutex_); 214 | for (const auto& session : session_list_) 215 | { 216 | setCallbackToSession(session); 217 | } 218 | } 219 | } 220 | 221 | void Subscriber_Impl::setCallbackToSession(const std::shared_ptr& session) 222 | { 223 | if (user_callback_is_synchronous_) 224 | { 225 | session->subscriber_session_impl_->setSynchronousCallback( 226 | [callback = synchronous_user_callback_, me = shared_from_this()](const std::shared_ptr>& buffer, const std::shared_ptr& /*header*/)->void 227 | { 228 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 229 | me->log_(logger::LogLevel::DebugVerbose, "Subscriber " + me->subscriberIdString() + ": Executing synchronous callback"); 230 | #endif 231 | const std::lock_guard callback_lock(me->last_callback_data_mutex_); 232 | if (me->user_callback_is_synchronous_) 233 | { 234 | CallbackData callback_data; 235 | callback_data.buffer_ = buffer; 236 | callback(callback_data); 237 | } 238 | }); 239 | } 240 | else 241 | { 242 | session->subscriber_session_impl_->setSynchronousCallback( 243 | [me = shared_from_this()](const std::shared_ptr>& buffer, const std::shared_ptr& /*header*/)->void 244 | { 245 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 246 | me->log_(logger::LogLevel::DebugVerbose, "Subscriber " + me->subscriberIdString() + ": Storing data for asynchronous callback"); 247 | #endif 248 | const std::lock_guard callback_lock(me->last_callback_data_mutex_); 249 | if (!me->user_callback_is_synchronous_) 250 | { 251 | me->last_callback_data_.buffer_ = buffer; 252 | 253 | me->last_callback_data_cv_.notify_all(); 254 | } 255 | }); 256 | } 257 | } 258 | 259 | void Subscriber_Impl::cancel() 260 | { 261 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 262 | log_(logger::LogLevel::Debug, "Subscriber " + subscriberIdString() + ": Cancelling..."); 263 | #endif 264 | 265 | { 266 | const std::lock_guard session_list_lock(session_list_mutex_); 267 | for (const auto& session : session_list_) 268 | { 269 | session->cancel(); 270 | } 271 | } 272 | 273 | // Stop and remove the old callback thread at first 274 | if (callback_thread_) 275 | { 276 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 277 | log_(logger::LogLevel::DebugVerbose, "Subscriber " + subscriberIdString() + ": Stopping callback thread..."); 278 | #endif 279 | callback_thread_stop_ = true; 280 | last_callback_data_cv_.notify_all(); 281 | 282 | // Join or detach the old thread. We cannot join a thread from it's own 283 | // thread, so we detach the thread in that case. 284 | if (std::this_thread::get_id() == callback_thread_->get_id()) 285 | callback_thread_->detach(); 286 | else 287 | callback_thread_->join(); 288 | 289 | callback_thread_.reset(); 290 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 291 | log_(logger::LogLevel::DebugVerbose, "Subscriber " + subscriberIdString() + ": Callback thread has terminated."); 292 | #endif 293 | } 294 | 295 | // Delete the user callback 296 | synchronous_user_callback_ = [](const auto&){}; 297 | user_callback_is_synchronous_ = true; 298 | } 299 | 300 | std::string Subscriber_Impl::subscriberIdString() const 301 | { 302 | std::stringstream ss; 303 | ss << "0x" << std::hex << this; 304 | return ss.str(); 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /tcp_pubsub/src/subscriber_impl.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | namespace tcp_pubsub { 25 | class Subscriber_Impl : public std::enable_shared_from_this 26 | { 27 | //////////////////////////////////////////////// 28 | // Constructor & Destructor 29 | //////////////////////////////////////////////// 30 | public: 31 | // Constructor 32 | Subscriber_Impl(const std::shared_ptr& executor); 33 | 34 | // Copy 35 | Subscriber_Impl(const Subscriber_Impl&) = delete; 36 | Subscriber_Impl& operator=(const Subscriber_Impl&) = delete; 37 | 38 | // Move 39 | Subscriber_Impl& operator=(Subscriber_Impl&&) = delete; 40 | Subscriber_Impl(Subscriber_Impl&&) = delete; 41 | 42 | // Destructor 43 | ~Subscriber_Impl(); 44 | 45 | //////////////////////////////////////////////// 46 | // Session Management 47 | //////////////////////////////////////////////// 48 | public: 49 | std::shared_ptr addSession(const std::vector>& publisher_list, int max_reconnection_attempts); 50 | std::vector> getSessions() const; 51 | 52 | void setCallback(const std::function& callback_function, bool synchronous_execution); 53 | private: 54 | void setCallbackToSession(const std::shared_ptr& session); 55 | 56 | public: 57 | void cancel(); 58 | 59 | private: 60 | std::string subscriberIdString() const; 61 | 62 | //////////////////////////////////////////////// 63 | // Member variables 64 | //////////////////////////////////////////////// 65 | private: 66 | // Asio 67 | const std::shared_ptr executor_; /// Global Executor 68 | 69 | // List of all Sessions 70 | mutable std::mutex session_list_mutex_; 71 | std::vector> session_list_; 72 | 73 | // Callback 74 | mutable std::mutex last_callback_data_mutex_; 75 | std::condition_variable last_callback_data_cv_; 76 | CallbackData last_callback_data_; 77 | 78 | std::atomic user_callback_is_synchronous_; 79 | std::function synchronous_user_callback_; 80 | 81 | std::unique_ptr callback_thread_; 82 | std::atomic callback_thread_stop_; 83 | 84 | // Buffer pool 85 | struct buffer_pool_lock_policy_ 86 | { 87 | using mutex_type = std::mutex; 88 | using lock_type = std::lock_guard; 89 | }; 90 | recycle::shared_pool, buffer_pool_lock_policy_> buffer_pool_; /// Buffer pool that let's us reuse memory chunks 91 | 92 | // Log function 93 | const tcp_pubsub::logger::logger_t log_; 94 | }; 95 | } // namespace tcp_pubsub 96 | -------------------------------------------------------------------------------- /tcp_pubsub/src/subscriber_session.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "subscriber_session_impl.h" 13 | 14 | namespace tcp_pubsub 15 | { 16 | SubscriberSession::SubscriberSession(const std::shared_ptr& impl) 17 | : subscriber_session_impl_(impl) 18 | {} 19 | 20 | SubscriberSession::~SubscriberSession() 21 | { 22 | subscriber_session_impl_->cancel(); 23 | } 24 | 25 | std::vector> SubscriberSession::getPublisherList() const 26 | { return subscriber_session_impl_->getPublisherList(); } 27 | 28 | std::pair SubscriberSession::getConnectedPublisher() const 29 | { return subscriber_session_impl_->getConnectedPublisher(); } 30 | 31 | void SubscriberSession::cancel() 32 | { subscriber_session_impl_->cancel(); } 33 | 34 | bool SubscriberSession::isConnected() const 35 | { return subscriber_session_impl_->isConnected(); } 36 | } 37 | -------------------------------------------------------------------------------- /tcp_pubsub/src/subscriber_session_impl.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #include "subscriber_session_impl.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include 22 | 23 | #include "portable_endian.h" 24 | #include "protocol_handshake_message.h" 25 | #include "tcp_header.h" 26 | #include "tcp_pubsub/tcp_pubsub_logger.h" 27 | #include "tcp_pubsub_logger_abstraction.h" 28 | 29 | namespace tcp_pubsub 30 | { 31 | ////////////////////////////////////////////// 32 | /// Constructor & Destructor 33 | ////////////////////////////////////////////// 34 | 35 | SubscriberSession_Impl::SubscriberSession_Impl(const std::shared_ptr& io_context 36 | , const std::vector>& publisher_list 37 | , int max_reconnection_attempts 38 | , const std::function>()>& get_buffer_handler 39 | , const std::function&)>& session_closed_handler 40 | , const tcp_pubsub::logger::logger_t& log_function) 41 | : publisher_list_ (publisher_list) 42 | , resolver_ (*io_context) 43 | , max_reconnection_attempts_(max_reconnection_attempts) 44 | , retries_left_ (max_reconnection_attempts) 45 | , retry_timer_ (*io_context, std::chrono::seconds(1)) 46 | , canceled_ (false) 47 | , data_socket_ (*io_context) 48 | , data_strand_ (*io_context) 49 | , get_buffer_handler_ (get_buffer_handler) 50 | , session_closed_handler_ (session_closed_handler) 51 | , log_ (log_function) 52 | { 53 | // Throw an exception if the publisher list is empty 54 | if (publisher_list_.empty()) 55 | { 56 | throw std::invalid_argument("SubscriberSession_Impl: Publisher list is empty."); 57 | } 58 | } 59 | 60 | // Destructor 61 | SubscriberSession_Impl::~SubscriberSession_Impl() 62 | { 63 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 64 | std::stringstream ss; 65 | ss << std::this_thread::get_id(); 66 | const std::string thread_id = ss.str(); 67 | log_(logger::LogLevel::DebugVerbose, "SubscriberSession " + endpointToString() + ": Deleting from thread " + thread_id + "..."); 68 | #endif 69 | 70 | cancel(); 71 | 72 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 73 | log_(logger::LogLevel::Debug, "SubscriberSession " + endpointToString() + ": Deleted."); 74 | #endif 75 | } 76 | 77 | ////////////////////////////////////////////// 78 | /// Connect to publisher 79 | ////////////////////////////////////////////// 80 | 81 | void SubscriberSession_Impl::start() 82 | { 83 | if (canceled_) return; 84 | 85 | // Start resolving the endpoint given in the constructor 86 | resolveEndpoint(0); 87 | } 88 | 89 | void SubscriberSession_Impl::resolveEndpoint(size_t publisher_list_index) 90 | { 91 | if (canceled_) 92 | { 93 | connectionFailedHandler(); 94 | return; 95 | } 96 | 97 | resolver_.async_resolve(publisher_list_[publisher_list_index].first 98 | , std::to_string(publisher_list_[publisher_list_index].second) 99 | , [me = shared_from_this(), publisher_list_index](asio::error_code ec, const asio::ip::tcp::resolver::results_type& resolved_endpoints) 100 | { 101 | if (ec) 102 | { 103 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 104 | { 105 | const std::string message = "Failed resolving endpoint [" + me->publisher_list_[publisher_list_index].first + ":" + std::to_string(me->publisher_list_[publisher_list_index].second) + "]: " + ec.message(); 106 | me->log_(logger::LogLevel::Debug, "SubscriberSession " + me->endpointToString() + ": " + message); 107 | } 108 | #endif // 109 | if (publisher_list_index + 1 < me->publisher_list_.size()) 110 | { 111 | // Try next possible endpoint 112 | me->resolveEndpoint(publisher_list_index + 1); 113 | } 114 | else 115 | { 116 | // Log warning 117 | std::string message = "Failed resolving any endpoint: "; 118 | for (size_t i = 0; i < me->publisher_list_.size(); i++) 119 | { 120 | message += me->publisher_list_[i].first + ":" + std::to_string(me->publisher_list_[i].second); 121 | if (i + 1 < me->publisher_list_.size()) 122 | message += ", "; 123 | } 124 | me->log_(logger::LogLevel::Warning, "SubscriberSession " + me->endpointToString() + ": " + message); 125 | 126 | // Execute connection Failed handler 127 | me->connectionFailedHandler(); 128 | return; 129 | } 130 | } 131 | else 132 | { 133 | me->connectToEndpoint(resolved_endpoints, publisher_list_index); 134 | } 135 | }); 136 | } 137 | 138 | void SubscriberSession_Impl::connectToEndpoint(const asio::ip::tcp::resolver::results_type& resolved_endpoints, size_t publisher_list_index) 139 | { 140 | if (canceled_) 141 | { 142 | connectionFailedHandler(); 143 | return; 144 | } 145 | 146 | // Convert the resolved_endpoints iterator to an endpoint sequence 147 | // (i.e. a vector of endpoints) 148 | auto endpoint_sequence = std::make_shared>(); 149 | for (const auto& endpoint : resolved_endpoints) 150 | { 151 | endpoint_sequence->push_back(endpoint); 152 | } 153 | 154 | asio::async_connect(data_socket_ 155 | , *endpoint_sequence 156 | , [me = shared_from_this(), publisher_list_index](asio::error_code ec, const asio::ip::tcp::endpoint& /*endpoint*/) 157 | { 158 | if (ec) 159 | { 160 | me->log_(logger::LogLevel::Warning, "SubscriberSession " + me->localEndpointToString() + ": Failed connecting to publisher " + me->publisher_list_[publisher_list_index].first + ":" + std::to_string(me->publisher_list_[publisher_list_index].second) + ": " + ec.message()); 161 | me->connectionFailedHandler(); 162 | return; 163 | } 164 | else 165 | { 166 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 167 | me->log_(logger::LogLevel::Debug, "SubscriberSession " + me->endpointToString() + ": Successfully connected to publisher " + me->publisher_list_[publisher_list_index].first + ":" + std::to_string(me->publisher_list_[publisher_list_index].second)); 168 | #endif 169 | 170 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 171 | me->log_(logger::LogLevel::DebugVerbose, "SubscriberSession " + me->endpointToString() + ": Setting tcp::no_delay option."); 172 | #endif 173 | // Disable Nagle's algorithm. Nagles Algorithm will otherwise cause the 174 | // Socket to wait for more data, if it encounters a frame that can still 175 | // fit more data. Obviously, this is an awfull default behaviour, if we 176 | // want to transmit our data in a timely fashion. 177 | { 178 | asio::error_code nodelay_ec; 179 | me->data_socket_.set_option(asio::ip::tcp::no_delay(true), nodelay_ec); 180 | if (nodelay_ec) me->log_(logger::LogLevel::Warning, "SubscriberSession " + me->endpointToString() + ": Failed setting tcp::no_delay option. The performance may suffer."); 181 | } 182 | 183 | // Store the connected publisher endpoint 184 | { 185 | const std::lock_guard lock(me->connected_publisher_endpoint_mutex_); 186 | me->connected_publisher_endpoint_ = me->publisher_list_[publisher_list_index]; 187 | } 188 | 189 | // Start reading a package by reading the header length. Everything will 190 | // unfold from there automatically. 191 | me->sendProtokolHandshakeRequest(); 192 | } 193 | }); 194 | } 195 | 196 | void SubscriberSession_Impl::sendProtokolHandshakeRequest() 197 | { 198 | if (canceled_) 199 | { 200 | connectionFailedHandler(); 201 | return; 202 | } 203 | 204 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 205 | log_(logger::LogLevel::Debug, "SubscriberSession " + endpointToString() + ": Sending ProtocolHandshakeRequest."); 206 | #endif 207 | 208 | const std::shared_ptr> buffer = std::make_shared>(); 209 | buffer->resize(sizeof(TcpHeader) + sizeof(ProtocolHandshakeMessage)); 210 | 211 | TcpHeader* header = reinterpret_cast(buffer->data()); 212 | header->header_size = htole16(sizeof(TcpHeader)); 213 | header->type = MessageContentType::ProtocolHandshake; 214 | header->reserved = 0; 215 | header->data_size = htole64(sizeof(ProtocolHandshakeMessage)); 216 | 217 | ProtocolHandshakeMessage* handshake_message = reinterpret_cast(&(buffer->operator[](sizeof(TcpHeader)))); 218 | handshake_message->protocol_version = 0; // At the moment, we only support Version 0. 219 | 220 | asio::async_write(data_socket_ 221 | , asio::buffer(*buffer) 222 | , asio::bind_executor(data_strand_, 223 | [me = shared_from_this(), buffer](asio::error_code ec, std::size_t /*bytes_to_transfer*/) 224 | { 225 | if (ec) 226 | { 227 | me->log_(logger::LogLevel::Warning, "SubscriberSession " + me->endpointToString() + ": Failed sending ProtocolHandshakeRequest: " + ec.message()); 228 | me->connectionFailedHandler(); 229 | return; 230 | } 231 | me->readHeaderLength(); 232 | })); 233 | } 234 | 235 | 236 | void SubscriberSession_Impl::connectionFailedHandler() 237 | { 238 | // Reset the connected publisher endpoint 239 | { 240 | const std::lock_guard lock(connected_publisher_endpoint_mutex_); 241 | connected_publisher_endpoint_ = std::make_pair("", 0); 242 | } 243 | 244 | { 245 | asio::error_code ec; 246 | data_socket_.shutdown(asio::ip::tcp::socket::shutdown_both, ec); 247 | } 248 | 249 | { 250 | asio::error_code ec; 251 | data_socket_.close(ec); // Even if ec indicates an error, the socket is closed now (according to the documentation) 252 | } 253 | 254 | if (!canceled_ && (retries_left_ < 0 || retries_left_ > 0)) 255 | { 256 | // Decrement the number of retries we have left 257 | if (retries_left_ > 0) 258 | retries_left_--; 259 | 260 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 261 | log_(logger::LogLevel::Debug, "SubscriberSession " + endpointToString() + ": Waiting and retrying to connect"); 262 | #endif 263 | 264 | // Retry connection after a short time 265 | retry_timer_.async_wait([me = shared_from_this()](asio::error_code ec) 266 | { 267 | if (ec) 268 | { 269 | me->log_(logger::LogLevel::Warning, "SubscriberSession " + me->endpointToString() + ": Waiting to reconnect failed: " + ec.message()); 270 | me->session_closed_handler_(me); 271 | return; 272 | } 273 | me->resolveEndpoint(0); 274 | }); 275 | } 276 | else 277 | { 278 | session_closed_handler_(shared_from_this()); 279 | } 280 | } 281 | 282 | ///////////////////////////////////////////// 283 | // Data receiving 284 | ///////////////////////////////////////////// 285 | 286 | void SubscriberSession_Impl::readHeaderLength() 287 | { 288 | if (canceled_) 289 | { 290 | connectionFailedHandler(); 291 | return; 292 | } 293 | 294 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 295 | log_(logger::LogLevel::DebugVerbose, "SubscriberSession " + endpointToString() + ": Waiting for data..."); 296 | #endif 297 | 298 | const std::shared_ptr header = std::make_shared(); 299 | 300 | asio::async_read(data_socket_ 301 | , asio::buffer(&(header->header_size), sizeof(header->header_size)) 302 | , asio::transfer_at_least(sizeof(header->header_size)) 303 | , asio::bind_executor(data_strand_, [me = shared_from_this(), header](asio::error_code ec, std::size_t /*length*/) 304 | { 305 | if (ec) 306 | { 307 | me->log_(logger::LogLevel::Error, "SubscriberSession " + me->endpointToString() + ": Error reading header length: " + ec.message()); 308 | me->connectionFailedHandler();; 309 | return; 310 | } 311 | me->readHeaderContent(header); 312 | })); 313 | } 314 | 315 | void SubscriberSession_Impl::readHeaderContent(const std::shared_ptr& header) 316 | { 317 | if (canceled_) 318 | { 319 | connectionFailedHandler(); 320 | return; 321 | } 322 | 323 | if (header->header_size < sizeof(header->header_size)) 324 | { 325 | log_(logger::LogLevel::Error, "SubscriberSession " + endpointToString() + ": Received header length of " + std::to_string(header->header_size) + ", which is less than the minimal header size."); 326 | connectionFailedHandler(); 327 | return; 328 | } 329 | 330 | const uint16_t remote_header_size = le16toh(header->header_size); 331 | const uint16_t my_header_size = sizeof(*header); 332 | 333 | const uint16_t bytes_to_read_from_socket = std::min(remote_header_size, my_header_size) - sizeof(header->header_size); 334 | const uint16_t bytes_to_discard_from_socket = (remote_header_size > my_header_size ? (remote_header_size - my_header_size) : 0); 335 | 336 | asio::async_read(data_socket_ 337 | , asio::buffer(&reinterpret_cast(header.get())[sizeof(header->header_size)], bytes_to_read_from_socket) 338 | , asio::transfer_at_least(bytes_to_read_from_socket) 339 | , asio::bind_executor(data_strand_, [me = shared_from_this(), header, bytes_to_discard_from_socket](asio::error_code ec, std::size_t /*length*/) 340 | { 341 | if (ec) 342 | { 343 | me->log_(logger::LogLevel::Error, "SubscriberSession " + me->endpointToString() + ": Error reading header content: " + ec.message()); 344 | me->connectionFailedHandler();; 345 | return; 346 | } 347 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 348 | me->log_(logger::LogLevel::DebugVerbose, "SubscriberSession " + me->endpointToString() 349 | + ": Received header content: " 350 | + "data_size: " + std::to_string(le64toh(header->data_size))); 351 | #endif 352 | 353 | if (bytes_to_discard_from_socket > 0) 354 | { 355 | me->discardDataBetweenHeaderAndPayload(header, bytes_to_discard_from_socket); 356 | } 357 | else 358 | { 359 | me->readPayload(header); 360 | } 361 | })); 362 | } 363 | 364 | void SubscriberSession_Impl::discardDataBetweenHeaderAndPayload(const std::shared_ptr& header, uint16_t bytes_to_discard) 365 | { 366 | if (canceled_) 367 | { 368 | connectionFailedHandler(); 369 | return; 370 | } 371 | 372 | std::vector data_to_discard; 373 | data_to_discard.resize(bytes_to_discard); 374 | 375 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 376 | log_(logger::LogLevel::DebugVerbose, "SubscriberSession " + endpointToString() + ": Discarding " + std::to_string(bytes_to_discard) + " bytes after the header."); 377 | #endif 378 | 379 | asio::async_read(data_socket_ 380 | , asio::buffer(data_to_discard.data(), bytes_to_discard) 381 | , asio::transfer_at_least(bytes_to_discard) 382 | , asio::bind_executor(data_strand_, [me = shared_from_this(), header](asio::error_code ec, std::size_t /*length*/) 383 | { 384 | if (ec) 385 | { 386 | me->log_(logger::LogLevel::Error, "SubscriberSession " + me->endpointToString() + ": Error discarding bytes after header: " + ec.message()); 387 | me->connectionFailedHandler();; 388 | return; 389 | } 390 | me->readPayload(header); 391 | })); 392 | } 393 | 394 | void SubscriberSession_Impl::readPayload(const std::shared_ptr& header) 395 | { 396 | if (canceled_) 397 | { 398 | connectionFailedHandler(); 399 | return; 400 | } 401 | 402 | if (header->data_size == 0) 403 | { 404 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 405 | log_(logger::LogLevel::Debug, "SubscriberSession " + endpointToString() + ": Received data size of 0."); 406 | #endif 407 | readHeaderLength(); 408 | return; 409 | } 410 | 411 | // Get a buffer. This may be a used or a new one. 412 | const std::shared_ptr> data_buffer = get_buffer_handler_(); 413 | 414 | if (data_buffer->capacity() < le64toh(header->data_size)) 415 | { 416 | // Reserve 10% extra memory 417 | data_buffer->reserve(static_cast(le64toh(header->data_size) * 1.1)); 418 | } 419 | 420 | // Resize the buffer to the required size 421 | data_buffer->resize(le64toh(header->data_size)); 422 | 423 | asio::async_read(data_socket_ 424 | , asio::buffer(data_buffer->data(), le64toh(header->data_size)) 425 | , asio::transfer_at_least(le64toh(header->data_size)) 426 | , asio::bind_executor(data_strand_, [me = shared_from_this(), header, data_buffer](asio::error_code ec, std::size_t /*length*/) 427 | { 428 | if (ec) 429 | { 430 | me->log_(logger::LogLevel::Error, "SubscriberSession " + me->endpointToString() + ": Error reading payload: " + ec.message()); 431 | me->connectionFailedHandler();; 432 | return; 433 | } 434 | 435 | // Reset the max amount of reconnects 436 | me->retries_left_ = me->max_reconnection_attempts_; 437 | 438 | if (header->type == MessageContentType::ProtocolHandshake) 439 | { 440 | ProtocolHandshakeMessage handshake_message; 441 | const size_t bytes_to_copy = std::min(data_buffer->size(), sizeof(ProtocolHandshakeMessage)); 442 | std::memcpy(&handshake_message, data_buffer->data(), bytes_to_copy); 443 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 444 | me->log_(logger::LogLevel::Debug, "SubscriberSession " + me->endpointToString() + ": Received Handshake message. Using Protocol version v" + std::to_string(handshake_message.protocol_version)); 445 | #endif 446 | if (handshake_message.protocol_version > 0) 447 | { 448 | me->log_(logger::LogLevel::Error, "SubscriberSession " + me->endpointToString() + ": Publisher set protocol version to v" + std::to_string(handshake_message.protocol_version) + ". This protocol is not supported."); 449 | me->connectionFailedHandler(); 450 | return; 451 | } 452 | } 453 | else if (header->type == MessageContentType::RegularPayload) 454 | { 455 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 456 | me->log_(logger::LogLevel::DebugVerbose, "SubscriberSession " + me->endpointToString() + ": Received message of type \"RegularPayload\""); 457 | #endif 458 | // Call the callback first, ... 459 | asio::post(me->data_strand_, [me, data_buffer, header]() 460 | { 461 | if (me->canceled_) 462 | { 463 | me->connectionFailedHandler(); 464 | return; 465 | } 466 | me->synchronous_callback_(data_buffer, header); 467 | }); 468 | 469 | } 470 | else 471 | { 472 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 473 | me->log_(logger::LogLevel::DebugVerbose, "SubscriberSession " + me->endpointToString() + ": Received message has unknow type: " + std::to_string(static_cast(header->type))); 474 | #endif 475 | } 476 | 477 | // ... then start reading the next message 478 | asio::post(me->data_strand_, [me]() { 479 | me->readHeaderLength(); 480 | }); 481 | })); 482 | } 483 | 484 | ////////////////////////////////////////////// 485 | /// Public API 486 | ////////////////////////////////////////////// 487 | 488 | void SubscriberSession_Impl::setSynchronousCallback(const std::function>&, const std::shared_ptr&)>& callback) 489 | { 490 | if (canceled_) return; 491 | 492 | // We let asio set the callback for the following reasons: 493 | // - We can protect the variable with the data_strand => If the callback is currently running, the new callback will be applied afterwards 494 | // - We don't need an additional mutex, so a synchronous callback should actually be able to set another callback that gets activated once the current callback call ends 495 | // - Reading the next message will start once the callback call is finished. Therefore, read and callback are synchronized and the callback calls don't start stacking up 496 | asio::post(data_strand_, [me = shared_from_this(), callback]() 497 | { 498 | me->synchronous_callback_ = callback; 499 | }); 500 | } 501 | 502 | std::vector> SubscriberSession_Impl::getPublisherList() const 503 | { 504 | return publisher_list_; 505 | } 506 | 507 | std::pair SubscriberSession_Impl::getConnectedPublisher() const 508 | { 509 | const std::lock_guard lock(connected_publisher_endpoint_mutex_); 510 | return connected_publisher_endpoint_; 511 | } 512 | 513 | void SubscriberSession_Impl::cancel() 514 | { 515 | const bool already_canceled = canceled_.exchange(true); 516 | 517 | if (already_canceled) return; 518 | 519 | #if (TCP_PUBSUB_LOG_DEBUG_ENABLED) 520 | log_(logger::LogLevel::Debug, "SubscriberSession " + endpointToString() + ": Cancelling..."); 521 | #endif 522 | 523 | { 524 | asio::error_code ec; 525 | data_socket_.close(ec); 526 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 527 | if (ec) 528 | log_(logger::LogLevel::DebugVerbose, "SubscriberSession " + endpointToString() + ": Failed closing socket: " + ec.message()); 529 | else 530 | log_(logger::LogLevel::DebugVerbose, "SubscriberSession " + endpointToString() + ": Successfully closed socket."); 531 | #endif 532 | } 533 | 534 | { 535 | asio::error_code ec; 536 | data_socket_.cancel(ec); // Even if ec indicates an error, the socket is closed now (according to the documentation) 537 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 538 | if (ec) 539 | log_(logger::LogLevel::DebugVerbose, "SubscriberSession " + endpointToString() + ": Failed cancelling socket: " + ec.message()); 540 | else 541 | log_(logger::LogLevel::DebugVerbose, "SubscriberSession " + endpointToString() + ": Successfully canceled socket."); 542 | #endif 543 | } 544 | 545 | { 546 | try { 547 | static_cast(retry_timer_.cancel()); 548 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 549 | log_(logger::LogLevel::DebugVerbose, "SubscriberSession " + endpointToString() + ": Successfully canceled retry timer."); 550 | #endif 551 | } catch (asio::system_error& err){ 552 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 553 | log_(logger::LogLevel::DebugVerbose, "SubscriberSession " + endpointToString() + ": Failed canceling retry timer: " + err.what()); 554 | #endif 555 | } 556 | } 557 | 558 | resolver_.cancel(); 559 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 560 | log_(logger::LogLevel::DebugVerbose, "SubscriberSession " + endpointToString() + ": Successfully canceled resovler."); 561 | #endif 562 | } 563 | 564 | bool SubscriberSession_Impl::isConnected() const 565 | { 566 | asio::error_code ec; 567 | data_socket_.remote_endpoint(ec); 568 | 569 | // If we can get the remote endpoint, we consider the socket as connected. 570 | // Otherwise it is not connected. 571 | 572 | return !static_cast(ec); 573 | } 574 | 575 | std::string SubscriberSession_Impl::remoteEndpointToString() const 576 | { 577 | const std::lock_guard lock(connected_publisher_endpoint_mutex_); 578 | return (connected_publisher_endpoint_.first.empty() ? "?" : connected_publisher_endpoint_.first) 579 | + ":" 580 | + std::to_string(connected_publisher_endpoint_.second); 581 | } 582 | 583 | std::string SubscriberSession_Impl::localEndpointToString() const 584 | { 585 | asio::error_code ec; 586 | auto local_endpoint = data_socket_.local_endpoint(ec); 587 | if (ec) 588 | return "?"; 589 | else 590 | return local_endpoint.address().to_string() + ":" + std::to_string(local_endpoint.port()); 591 | } 592 | 593 | std::string SubscriberSession_Impl::endpointToString() const 594 | { 595 | return localEndpointToString() + "->" + remoteEndpointToString(); 596 | } 597 | } 598 | -------------------------------------------------------------------------------- /tcp_pubsub/src/subscriber_session_impl.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | #include "tcp_header.h" 19 | #include "tcp_pubsub/tcp_pubsub_logger.h" 20 | 21 | namespace tcp_pubsub { 22 | class SubscriberSession_Impl : public std::enable_shared_from_this 23 | { 24 | ////////////////////////////////////////////// 25 | /// Constructor & Destructor 26 | ////////////////////////////////////////////// 27 | public: 28 | SubscriberSession_Impl(const std::shared_ptr& io_context 29 | , const std::vector>& publisher_list 30 | , int max_reconnection_attempts 31 | , const std::function>()>& get_buffer_handler 32 | , const std::function&)>& session_closed_handler 33 | , const tcp_pubsub::logger::logger_t& log_function); 34 | 35 | 36 | // Copy 37 | SubscriberSession_Impl(const SubscriberSession_Impl&) = delete; 38 | SubscriberSession_Impl& operator=(const SubscriberSession_Impl&) = delete; 39 | 40 | // Move 41 | SubscriberSession_Impl& operator=(SubscriberSession_Impl&&) = delete; 42 | SubscriberSession_Impl(SubscriberSession_Impl&&) = delete; 43 | 44 | // Destructor 45 | ~SubscriberSession_Impl(); 46 | 47 | ////////////////////////////////////////////// 48 | /// Connect to publisher 49 | ////////////////////////////////////////////// 50 | public: 51 | void start(); 52 | 53 | private: 54 | void resolveEndpoint(size_t publisher_list_index); 55 | void connectToEndpoint(const asio::ip::tcp::resolver::results_type& resolved_endpoints, size_t publisher_list_index); 56 | 57 | void sendProtokolHandshakeRequest(); 58 | 59 | void connectionFailedHandler(); 60 | 61 | ///////////////////////////////////////////// 62 | // Data receiving 63 | ///////////////////////////////////////////// 64 | private: 65 | void readHeaderLength(); 66 | void readHeaderContent(const std::shared_ptr& header); 67 | void discardDataBetweenHeaderAndPayload(const std::shared_ptr& header, uint16_t bytes_to_discard); 68 | void readPayload(const std::shared_ptr& header); 69 | 70 | ////////////////////////////////////////////// 71 | /// Public API 72 | ////////////////////////////////////////////// 73 | public: 74 | void setSynchronousCallback(const std::function>&, const std::shared_ptr&)>& callback); 75 | 76 | void cancel(); 77 | bool isConnected() const; 78 | 79 | std::vector> getPublisherList() const; 80 | std::pair getConnectedPublisher() const; 81 | 82 | std::string remoteEndpointToString() const; 83 | std::string localEndpointToString() const; 84 | std::string endpointToString() const; 85 | 86 | ////////////////////////////////////////////// 87 | /// Member variables 88 | ////////////////////////////////////////////// 89 | private: 90 | // Endpoint and resolver given by / constructed by the constructor 91 | std::vector> publisher_list_; ///< The list of endpoints that this session will connect to. The first reachable will be used 92 | asio::ip::tcp::resolver resolver_; 93 | asio::ip::tcp::endpoint endpoint_; 94 | 95 | mutable std::mutex connected_publisher_endpoint_mutex_; ///< Mutex for the connected_publisher_endpoint_ 96 | std::pair connected_publisher_endpoint_; ///< The endpoint of the publisher we are connected to 97 | 98 | // Amount of retries left 99 | int max_reconnection_attempts_; 100 | int retries_left_; 101 | asio::steady_timer retry_timer_; 102 | std::atomic canceled_; 103 | 104 | // TCP Socket & Queue (protected by the strand!) 105 | asio::ip::tcp::socket data_socket_; 106 | asio::io_context::strand data_strand_; // Used for socket operations and the callback. This is done so messages don't queue up in the asio stack. We only start receiving new messages, after we have delivered the current one. 107 | 108 | // Handlers 109 | const std::function>()> get_buffer_handler_; ///< Function for retrieving / constructing an empty buffer 110 | const std::function&)> session_closed_handler_; ///< Handler that is called when the session is closed 111 | std::function>&, const std::shared_ptr&)> synchronous_callback_; ///< [PROTECTED BY data_strand_!] Callback that is called when a complete message has been received. Executed in the asio constext, so this must be cheap! 112 | 113 | // Logger 114 | const tcp_pubsub::logger::logger_t log_; 115 | }; 116 | } // namespace tcp_pubsub 117 | -------------------------------------------------------------------------------- /tcp_pubsub/src/tcp_header.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | namespace tcp_pubsub 9 | { 10 | enum class MessageContentType : uint8_t 11 | { 12 | RegularPayload = 0, // The Content is a user-defined payload that shall be given to the user code 13 | ProtocolHandshake = 1, // The contnet is a handshake message that defines which protocol version shall be used 14 | 15 | // This is meant for future use. At the moment, received messages that don't 16 | // have the type set to "RegularPayload" are discarded. So in the future, 17 | // e.g. when we decide to send meta information or anything else over the 18 | // same socket, old subscribers will not try to interpret those messages 19 | // as payload. 20 | }; 21 | 22 | #pragma pack(push,1) 23 | 24 | // This Header shall always contain little endian numbers. 25 | struct TcpHeader 26 | { 27 | uint16_t header_size = 0; 28 | MessageContentType type = MessageContentType::RegularPayload; 29 | uint8_t reserved = 0; // Added for 32bit-alignment. Can later be reused e.g. as a flag field or similar. 30 | uint64_t data_size = 0; 31 | }; 32 | 33 | #pragma pack(pop) 34 | 35 | } -------------------------------------------------------------------------------- /tcp_pubsub/src/tcp_pubsub_logger_abstraction.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | #pragma once 5 | 6 | // 7 | // If DEBUG_VERBOSE is not set by the user, we enable it when compiling in 8 | // Debug mode. The user can still omit those verbose messages by setting a 9 | // logger function that drops the verbose messages. 10 | // 11 | #ifndef TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED 12 | #ifdef NDEBUG 13 | #define TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED 0 14 | #else 15 | #define TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED 1 16 | #endif // NDEBUG 17 | #endif // !TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED 18 | 19 | // 20 | // If the DEBUG_VERBOSE is enabled, we also enable the normal DEBUG logging 21 | // 22 | #if (TCP_PUBSUB_LOG_DEBUG_VERBOSE_ENABLED) 23 | #define TCP_PUBSUB_LOG_DEBUG_ENABLED 1 24 | #endif 25 | 26 | // 27 | // If we haven't decided yet whether DEBUG logging shall be enabled, we enable 28 | // it when compiling in Debug mode and disable it otherwise. 29 | // 30 | #ifndef TCP_PUBSUB_LOG_DEBUG_ENABLED 31 | #ifdef NDEBUG 32 | #define TCP_PUBSUB_LOG_DEBUG_ENABLED 0 33 | #else 34 | #define TCP_PUBSUB_LOG_DEBUG_ENABLED 1 35 | #endif // NDEBUG 36 | #endif // !TCP_PUBSUB_LOG_DEBUG_ENABLED 37 | -------------------------------------------------------------------------------- /tcp_pubsub/tcp_pubsub_version.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define TCP_PUBSUB_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ 4 | #define TCP_PUBSUB_VERSION_MINOR @PROJECT_VERSION_MINOR@ 5 | #define TCP_PUBSUB_VERSION_PATCH @PROJECT_VERSION_PATCH@ -------------------------------------------------------------------------------- /tcp_pubsub/version.cmake: -------------------------------------------------------------------------------- 1 | set(TCP_PUBSUB_VERSION_MAJOR 2) 2 | set(TCP_PUBSUB_VERSION_MINOR 0) 3 | set(TCP_PUBSUB_VERSION_PATCH 1) 4 | -------------------------------------------------------------------------------- /tests/tcp_pubsub_test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Copyright (c) Continental. All rights reserved. 3 | # Licensed under the MIT license. See LICENSE file in the project root for details. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | ################################################################################ 7 | 8 | cmake_minimum_required(VERSION 3.13...4.0) 9 | 10 | project(tcp_pubsub_test) 11 | 12 | set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) 13 | 14 | find_package(tcp_pubsub REQUIRED) 15 | find_package(GTest REQUIRED) 16 | 17 | set(sources 18 | src/atomic_signalable.h 19 | src/tcp_pubsub_test.cpp 20 | ) 21 | 22 | add_executable (${PROJECT_NAME} 23 | ${sources} 24 | ) 25 | 26 | target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_14) 27 | 28 | target_link_libraries (${PROJECT_NAME} 29 | tcp_pubsub::tcp_pubsub 30 | GTest::gtest_main 31 | ) 32 | 33 | source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${sources}) 34 | 35 | include(GoogleTest) 36 | gtest_discover_tests(${PROJECT_NAME}) 37 | -------------------------------------------------------------------------------- /tests/tcp_pubsub_test/src/atomic_signalable.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | template 12 | class atomic_signalable 13 | { 14 | public: 15 | atomic_signalable(T initial_value) : value(initial_value) {} 16 | 17 | atomic_signalable& operator=(const T new_value) 18 | { 19 | const std::lock_guard lock(mutex); 20 | value = new_value; 21 | cv.notify_all(); 22 | return *this; 23 | } 24 | 25 | T operator++() 26 | { 27 | const std::lock_guard lock(mutex); 28 | T newValue = ++value; 29 | cv.notify_all(); 30 | return newValue; 31 | } 32 | 33 | T operator++(T) 34 | { 35 | const std::lock_guard lock(mutex); 36 | T oldValue = value++; 37 | cv.notify_all(); 38 | return oldValue; 39 | } 40 | 41 | T operator--() 42 | { 43 | const std::lock_guard lock(mutex); 44 | T newValue = --value; 45 | cv.notify_all(); 46 | return newValue; 47 | } 48 | 49 | T operator--(T) 50 | { 51 | const std::lock_guard lock(mutex); 52 | T oldValue = value--; 53 | cv.notify_all(); 54 | return oldValue; 55 | } 56 | 57 | T operator+=(const T& other) 58 | { 59 | const std::lock_guard lock(mutex); 60 | value += other; 61 | cv.notify_all(); 62 | return value; 63 | } 64 | 65 | T operator-=(const T& other) 66 | { 67 | const std::lock_guard lock(mutex); 68 | value -= other; 69 | cv.notify_all(); 70 | return value; 71 | } 72 | 73 | T operator*=(const T& other) 74 | { 75 | const std::lock_guard lock(mutex); 76 | value *= other; 77 | cv.notify_all(); 78 | return value; 79 | } 80 | 81 | T operator/=(const T& other) 82 | { 83 | const std::lock_guard lock(mutex); 84 | value /= other; 85 | cv.notify_all(); 86 | return value; 87 | } 88 | 89 | T operator%=(const T& other) 90 | { 91 | const std::lock_guard lock(mutex); 92 | value %= other; 93 | cv.notify_all(); 94 | return value; 95 | } 96 | 97 | template 98 | bool wait_for(Predicate predicate, std::chrono::milliseconds timeout) 99 | { 100 | std::unique_lock lock(mutex); 101 | return cv.wait_for(lock, timeout, [&]() { return predicate(value); }); 102 | } 103 | 104 | T get() const 105 | { 106 | const std::lock_guard lock(mutex); 107 | return value; 108 | } 109 | 110 | bool operator==(T other) const 111 | { 112 | const std::lock_guard lock(mutex); 113 | return value == other; 114 | } 115 | 116 | bool operator==(const atomic_signalable& other) const 117 | { 118 | std::lock_guard lock_this(mutex); 119 | std::lock_guard lock_other(other.mutex); 120 | return value == other.value; 121 | } 122 | 123 | bool operator!=(T other) const 124 | { 125 | const std::lock_guard lock(mutex); 126 | return value != other; 127 | } 128 | 129 | bool operator<(T other) const 130 | { 131 | const std::lock_guard lock(mutex); 132 | return value < other; 133 | } 134 | 135 | bool operator<=(T other) const 136 | { 137 | const std::lock_guard lock(mutex); 138 | return value <= other; 139 | } 140 | 141 | bool operator>(T other) const 142 | { 143 | const std::lock_guard lock(mutex); 144 | return value > other; 145 | } 146 | 147 | bool operator>=(T other) const 148 | { 149 | const std::lock_guard lock(mutex); 150 | return value >= other; 151 | } 152 | 153 | private: 154 | T value; 155 | std::condition_variable cv; 156 | mutable std::mutex mutex; 157 | }; 158 | 159 | 160 | template 161 | bool operator==(const T& other, const atomic_signalable& atomic) 162 | { 163 | return atomic == other; 164 | } 165 | 166 | template 167 | bool operator!=(const T& other, const atomic_signalable& atomic) 168 | { 169 | return atomic != other; 170 | } 171 | 172 | template 173 | bool operator<(const T& other, const atomic_signalable& atomic) 174 | { 175 | return atomic > other; 176 | } 177 | 178 | template 179 | bool operator<=(const T& other, const atomic_signalable& atomic) 180 | { 181 | return atomic >= other; 182 | } 183 | 184 | template 185 | bool operator>(const T& other, const atomic_signalable& atomic) 186 | { 187 | return atomic < other; 188 | } 189 | 190 | template 191 | bool operator>=(const T& other, const atomic_signalable& atomic) 192 | { 193 | return atomic <= other; 194 | } 195 | -------------------------------------------------------------------------------- /tests/tcp_pubsub_test/src/tcp_pubsub_test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Continental. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include "atomic_signalable.h" 17 | 18 | #include 19 | 20 | // Basic test that sends two messages from a publisher to a subscriber 21 | TEST(tcp_pubsub, basic_test) 22 | { 23 | atomic_signalable num_messages_received(0); 24 | 25 | // Create executor 26 | std::shared_ptr executor = std::make_shared(1, tcp_pubsub::logger::logger_no_verbose_debug); 27 | 28 | // Create publisher 29 | tcp_pubsub::Publisher hello_world_publisher(executor, 1588); 30 | 31 | // Create subscriber 32 | tcp_pubsub::Subscriber hello_world_subscriber(executor); 33 | 34 | // Subscribe to localhost on port 1588 35 | hello_world_subscriber.addSession("127.0.0.1", 1588); 36 | 37 | std::string received_message; 38 | 39 | // Create a callback that will be called when a message is received 40 | std::function callback_function = 41 | [&received_message, &num_messages_received](const tcp_pubsub::CallbackData& callback_data) 42 | { 43 | received_message = std::string(callback_data.buffer_->data(), callback_data.buffer_->size()); 44 | ++num_messages_received; 45 | }; 46 | 47 | // Register the callback 48 | hello_world_subscriber.setCallback(callback_function); 49 | 50 | // Wait up to 1 second for the subscriber to connect 51 | for (int i = 0; i < 10; ++i) 52 | { 53 | if (hello_world_subscriber.getSessions().at(0)->isConnected() 54 | && hello_world_publisher.getSubscriberCount() >= 1) 55 | { 56 | break; 57 | } 58 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 59 | } 60 | 61 | // Check that the subscriber is connected 62 | EXPECT_TRUE(hello_world_subscriber.getSessions().at(0)->isConnected()); 63 | EXPECT_EQ(hello_world_publisher.getSubscriberCount(), 1); 64 | 65 | // Publish "Hello World 1" 66 | { 67 | const std::string message = "Hello World 1"; 68 | hello_world_publisher.send(message.data(), message.size()); 69 | } 70 | 71 | // wait for message to be received 72 | num_messages_received.wait_for([](int value) { return value > 0; }, std::chrono::seconds(1)); 73 | 74 | // Check that the message was received 75 | EXPECT_EQ(received_message, "Hello World 1"); 76 | EXPECT_EQ(num_messages_received.get(), 1); 77 | 78 | // Publish "Hello World 2" 79 | { 80 | const std::string message = "Hello World 2"; 81 | hello_world_publisher.send(message.data(), message.size()); 82 | } 83 | 84 | // wait for message to be received 85 | num_messages_received.wait_for([](int value) { return value > 1; }, std::chrono::seconds(1)); 86 | 87 | // Check that the message was received 88 | EXPECT_EQ(received_message, "Hello World 2"); 89 | EXPECT_EQ(num_messages_received.get(), 2); 90 | } 91 | 92 | // Test that sends a very large message from a publisher to a subscriber 93 | TEST(tcp_pubsub, large_message_test) 94 | { 95 | constexpr size_t message_size = 1024 * 1024 * 16; 96 | 97 | atomic_signalable num_messages_received(0); 98 | 99 | // Create executor 100 | std::shared_ptr executor = std::make_shared(1, tcp_pubsub::logger::logger_no_verbose_debug); 101 | 102 | // Create publisher 103 | tcp_pubsub::Publisher hello_world_publisher(executor, 1588); 104 | 105 | // Create subscriber 106 | tcp_pubsub::Subscriber hello_world_subscriber(executor); 107 | 108 | // Subscribe to localhost on port 1588 109 | hello_world_subscriber.addSession("127.0.0.1", 1588); 110 | 111 | std::string received_message; 112 | 113 | // Create a callback that will be called when a message is received 114 | std::function callback_function = 115 | [&received_message, &num_messages_received](const tcp_pubsub::CallbackData& callback_data) 116 | { 117 | received_message = std::string(callback_data.buffer_->data(), callback_data.buffer_->size()); 118 | ++num_messages_received; 119 | }; 120 | 121 | // Register the callback 122 | hello_world_subscriber.setCallback(callback_function); 123 | 124 | // Wait up to 1 second for the subscriber to connect 125 | for (int i = 0; i < 10; ++i) 126 | { 127 | if (hello_world_subscriber.getSessions().at(0)->isConnected() 128 | && hello_world_publisher.getSubscriberCount() >= 1) 129 | { 130 | break; 131 | } 132 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 133 | } 134 | 135 | // Check that the subscriber is connected 136 | EXPECT_TRUE(hello_world_subscriber.getSessions().at(0)->isConnected()); 137 | EXPECT_EQ(hello_world_publisher.getSubscriberCount(), 1); 138 | 139 | // Create a large message consisting of random bytes 140 | std::string message; 141 | message.resize(message_size); 142 | for (size_t i = 0; i < message_size; ++i) 143 | { 144 | message[i] = static_cast(rand() % 256); 145 | } 146 | 147 | // Publish the large message 148 | hello_world_publisher.send(message.data(), message.size()); 149 | 150 | // wait for message to be received 151 | num_messages_received.wait_for([](int value) { return value > 0; }, std::chrono::seconds(1)); 152 | 153 | // Check that the message was received 154 | EXPECT_EQ(received_message, message); 155 | EXPECT_EQ(num_messages_received.get(), 1); 156 | } 157 | 158 | // Test that sends messages from 2 publishers to a single subscriber 159 | TEST(tcp_pubsub, multiple_publishers_test) 160 | { 161 | atomic_signalable num_messages_received(0); 162 | 163 | // Create executor 164 | std::shared_ptr executor = std::make_shared(1, tcp_pubsub::logger::logger_no_verbose_debug); 165 | 166 | // Create publisher 1 167 | tcp_pubsub::Publisher hello_world_publisher1(executor, 1588); 168 | 169 | // Create publisher 2 170 | tcp_pubsub::Publisher hello_world_publisher2(executor, 1589); 171 | 172 | // Create subscriber 173 | tcp_pubsub::Subscriber hello_world_subscriber(executor); 174 | 175 | // Subscribe to localhost on port 1588 176 | hello_world_subscriber.addSession("127.0.0.1", 1588); 177 | hello_world_subscriber.addSession("127.0.0.1", 1589); 178 | 179 | std::string received_message; 180 | 181 | // Create a callback that will be called when a message is received 182 | std::function callback_function = 183 | [&received_message, &num_messages_received](const tcp_pubsub::CallbackData& callback_data) 184 | { 185 | received_message = std::string(callback_data.buffer_->data(), callback_data.buffer_->size()); 186 | ++num_messages_received; 187 | }; 188 | 189 | // Register the callback 190 | hello_world_subscriber.setCallback(callback_function); 191 | 192 | // Wait up to 1 second for the subscriber to connect 193 | for (int i = 0; i < 10; ++i) 194 | { 195 | if (hello_world_subscriber.getSessions().at(0)->isConnected() 196 | && hello_world_subscriber.getSessions().at(1)->isConnected() 197 | && hello_world_publisher1.getSubscriberCount() >= 1 198 | && hello_world_publisher2.getSubscriberCount() >= 1) 199 | { 200 | break; 201 | } 202 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 203 | } 204 | 205 | // Check that the subscriber is connected 206 | EXPECT_TRUE(hello_world_subscriber.getSessions().at(0)->isConnected()); 207 | EXPECT_TRUE(hello_world_subscriber.getSessions().at(1)->isConnected()); 208 | EXPECT_EQ(hello_world_publisher1.getSubscriberCount(), 1); 209 | EXPECT_EQ(hello_world_publisher2.getSubscriberCount(), 1); 210 | 211 | // Publish "Hello World 1" 212 | { 213 | const std::string message = "Hello World 1"; 214 | hello_world_publisher1.send(message.data(), message.size()); 215 | } 216 | 217 | // wait for message to be received 218 | num_messages_received.wait_for([](int value) { return value > 0; }, std::chrono::seconds(1)); 219 | 220 | // Check that the message was received 221 | EXPECT_EQ(received_message, "Hello World 1"); 222 | EXPECT_EQ(num_messages_received.get(), 1); 223 | 224 | // Publish "Hello World 2" 225 | { 226 | const std::string message = "Hello World 2"; 227 | hello_world_publisher2.send(message.data(), message.size()); 228 | } 229 | 230 | // wait for message to be received 231 | num_messages_received.wait_for([](int value) { return value > 1; }, std::chrono::seconds(1)); 232 | 233 | // Check that the message was received 234 | EXPECT_EQ(received_message, "Hello World 2"); 235 | EXPECT_EQ(num_messages_received.get(), 2); 236 | } 237 | 238 | // Test that sends messages from a single publisher to 2 subscribers 239 | TEST(tcp_pubsub, multiple_subscribers_test) 240 | { 241 | atomic_signalable num_messages_received1(0); 242 | atomic_signalable num_messages_received2(0); 243 | 244 | // Create executor 245 | std::shared_ptr executor = std::make_shared(1, tcp_pubsub::logger::logger_no_verbose_debug); 246 | 247 | // Create publisher 248 | tcp_pubsub::Publisher hello_world_publisher(executor, 1588); 249 | 250 | // Create subscriber 1 251 | tcp_pubsub::Subscriber hello_world_subscriber1(executor); 252 | 253 | // Create subscriber 2 254 | tcp_pubsub::Subscriber hello_world_subscriber2(executor); 255 | 256 | // Subscribe to localhost on port 1588 257 | hello_world_subscriber1.addSession("127.0.0.1", 1588); 258 | hello_world_subscriber2.addSession("127.0.0.1", 1588); 259 | 260 | std::string received_message1; 261 | std::string received_message2; 262 | 263 | // Create a callback that will be called when a message is received 264 | std::function callback_function1 = 265 | [&received_message1, &num_messages_received1](const tcp_pubsub::CallbackData& callback_data) 266 | { 267 | received_message1 = std::string(callback_data.buffer_->data(), callback_data.buffer_->size()); 268 | ++num_messages_received1; 269 | }; 270 | 271 | std::function callback_function2 = 272 | [&received_message2, &num_messages_received2](const tcp_pubsub::CallbackData& callback_data) 273 | { 274 | received_message2 = std::string(callback_data.buffer_->data(), callback_data.buffer_->size()); 275 | ++num_messages_received2; 276 | }; 277 | 278 | // Register the callback 279 | hello_world_subscriber1.setCallback(callback_function1); 280 | hello_world_subscriber2.setCallback(callback_function2); 281 | 282 | // Wait up to 1 second for the subscriber to connect 283 | for (int i = 0; i < 10; ++i) 284 | { 285 | if (hello_world_subscriber1.getSessions().at(0)->isConnected() 286 | && hello_world_subscriber2.getSessions().at(0)->isConnected() 287 | && hello_world_publisher.getSubscriberCount() >= 2) 288 | { 289 | break; 290 | } 291 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 292 | } 293 | 294 | // Check that the subscriber is connected 295 | EXPECT_TRUE(hello_world_subscriber1.getSessions().at(0)->isConnected()); 296 | EXPECT_TRUE(hello_world_subscriber2.getSessions().at(0)->isConnected()); 297 | EXPECT_EQ(hello_world_publisher.getSubscriberCount(), 2); 298 | 299 | // Publish "Hello World 1" 300 | { 301 | const std::string message = "Hello World 1"; 302 | hello_world_publisher.send(message.data(), message.size()); 303 | } 304 | 305 | // wait for message to be received 306 | num_messages_received1.wait_for([](int value) { return value > 0; }, std::chrono::seconds(1)); 307 | num_messages_received2.wait_for([](int value) { return value > 0; }, std::chrono::seconds(1)); 308 | 309 | // Check that the message was received 310 | EXPECT_EQ(received_message1, "Hello World 1"); 311 | EXPECT_EQ(num_messages_received1.get(), 1); 312 | EXPECT_EQ(received_message2, "Hello World 1"); 313 | EXPECT_EQ(num_messages_received2.get(), 1); 314 | } 315 | 316 | // Test connecting to a list of possible publishers. The subscriber will connect to the first available publisher. 317 | TEST(tcp_pubsub, publisher_list_test) 318 | { 319 | atomic_signalable num_messages_received(0); 320 | 321 | // Create executor 322 | std::shared_ptr executor = std::make_shared(1, tcp_pubsub::logger::logger_no_verbose_debug); 323 | 324 | // Create publisher 325 | tcp_pubsub::Publisher hello_world_publisher(executor, 1588); 326 | 327 | // Create subscriber 328 | tcp_pubsub::Subscriber hello_world_subscriber(executor); 329 | 330 | // Subscribe to localhost on port 1588 331 | std::vector> publishers = { { "NonExistentPublisher", 1800 }, { "localhost", 1588 } }; 332 | hello_world_subscriber.addSession(publishers); 333 | 334 | std::string received_message; 335 | 336 | // Create a callback that will be called when a message is received 337 | std::function callback_function = 338 | [&received_message, &num_messages_received](const tcp_pubsub::CallbackData& callback_data) 339 | { 340 | received_message = std::string(callback_data.buffer_->data(), callback_data.buffer_->size()); 341 | ++num_messages_received; 342 | }; 343 | 344 | // Register the callback 345 | hello_world_subscriber.setCallback(callback_function); 346 | 347 | // Wait up to 10 seconds for the subscriber to connect 348 | for (int i = 0; i < 100; ++i) 349 | { 350 | if (hello_world_subscriber.getSessions().at(0)->isConnected() 351 | && hello_world_publisher.getSubscriberCount() >= 1) 352 | { 353 | break; 354 | } 355 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 356 | } 357 | 358 | // Check that the subscriber is connected 359 | EXPECT_TRUE(hello_world_subscriber.getSessions().at(0)->isConnected()); 360 | EXPECT_EQ(hello_world_publisher.getSubscriberCount(), 1); 361 | 362 | // Check the address and port hat the subscriber connected to 363 | EXPECT_EQ(hello_world_subscriber.getSessions().at(0)->getConnectedPublisher().first, "localhost"); 364 | EXPECT_EQ(hello_world_subscriber.getSessions().at(0)->getConnectedPublisher().second, 1588); 365 | 366 | // Publish "Hello World 1" 367 | { 368 | const std::string message = "Hello World 1"; 369 | hello_world_publisher.send(message.data(), message.size()); 370 | } 371 | 372 | // wait for message to be received 373 | num_messages_received.wait_for([](int value) { return value > 0; }, std::chrono::seconds(1)); 374 | 375 | // Check that the message was received 376 | EXPECT_EQ(received_message, "Hello World 1"); 377 | EXPECT_EQ(num_messages_received.get(), 1); 378 | } 379 | -------------------------------------------------------------------------------- /thirdparty/asio/Module/Findasio.cmake: -------------------------------------------------------------------------------- 1 | find_path(asio_INCLUDE_DIR 2 | NAMES asio.hpp 3 | HINTS 4 | "${CMAKE_CURRENT_LIST_DIR}/../asio/asio/include" 5 | NO_DEFAULT_PATH 6 | NO_CMAKE_FIND_ROOT_PATH 7 | ) 8 | 9 | if(asio_INCLUDE_DIR-NOTFOUND) 10 | message(FATAL_ERROR "Could not find asio library") 11 | set(asio_FOUND FALSE) 12 | else() 13 | set(asio_FOUND TRUE) 14 | set(ASIO_INCLUDE_DIR ${asio_INCLUDE_DIR}) 15 | endif() 16 | 17 | if(asio_FOUND) 18 | include(FindPackageHandleStandardArgs) 19 | find_package_handle_standard_args(asio 20 | REQUIRED_VARS asio_INCLUDE_DIR) 21 | 22 | if(NOT TARGET asio::asio) 23 | set(asio_INCLUDE_DIRS ${asio_INCLUDE_DIR}) 24 | add_library(asio::asio INTERFACE IMPORTED) 25 | set_target_properties(asio::asio PROPERTIES 26 | INTERFACE_INCLUDE_DIRECTORIES ${asio_INCLUDE_DIR} 27 | INTERFACE_COMPILE_DEFINITIONS ASIO_STANDALONE) 28 | mark_as_advanced(asio_INCLUDE_DIR) 29 | endif() 30 | endif() 31 | -------------------------------------------------------------------------------- /thirdparty/asio/build-asio.cmake: -------------------------------------------------------------------------------- 1 | list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_LIST_DIR}/Module") -------------------------------------------------------------------------------- /thirdparty/gtest/Module/FindGTest.cmake: -------------------------------------------------------------------------------- 1 | set(GTest_FOUND TRUE CACHE BOOL "Found Google Test" FORCE) -------------------------------------------------------------------------------- /thirdparty/gtest/build-gtest.cmake: -------------------------------------------------------------------------------- 1 | # Googletest automatically forces MT instead of MD if we do not set this option. 2 | if(MSVC) 3 | set(gtest_force_shared_crt ON CACHE BOOL "My option" FORCE) 4 | set(BUILD_GMOCK OFF CACHE BOOL "My option" FORCE) 5 | set(INSTALL_GTEST OFF CACHE BOOL "My option" FORCE) 6 | endif() 7 | 8 | add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/googletest" EXCLUDE_FROM_ALL) 9 | 10 | if(NOT TARGET GTest::gtest) 11 | add_library(GTest::gtest ALIAS gtest) 12 | endif() 13 | 14 | if(NOT TARGET GTest::gtest_main) 15 | add_library(GTest::gtest_main ALIAS gtest_main) 16 | endif() 17 | 18 | # Prepend googletest-module/FindGTest.cmake to Module Path 19 | list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_LIST_DIR}/Module") -------------------------------------------------------------------------------- /thirdparty/recycle/Module/Findrecycle.cmake: -------------------------------------------------------------------------------- 1 | set(recycle_FOUND TRUE CACHE BOOL "Found steinwurf::recycle" FORCE) -------------------------------------------------------------------------------- /thirdparty/recycle/build-recycle.cmake: -------------------------------------------------------------------------------- 1 | add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/recycle" EXCLUDE_FROM_ALL) 2 | 3 | # Prepend asio-module/Findrecycle.cmake to the module path 4 | list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_LIST_DIR}/Module") 5 | -------------------------------------------------------------------------------- /thirdparty/tcp_pubsub/Module/Findtcp_pubsub.cmake: -------------------------------------------------------------------------------- 1 | # Stub find script for in-source build of samples 2 | set(tcp_pubsub_FOUND True) --------------------------------------------------------------------------------