From d72254c7d0d4c6a64dd4b4c36011d5893d5906a4 Mon Sep 17 00:00:00 2001 From: Nell Date: Tue, 15 Jul 2025 17:05:04 +0200 Subject: [PATCH] init --- .gitignore | 1 + .idea/.gitignore | 8 + .idea/modules.xml | 8 + .idea/ox_speak_server.iml | 11 + .idea/vcs.xml | 6 + Cargo.lock | 639 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 25 ++ profile.json.gz | Bin 0 -> 46260 bytes src/app/app.rs | 66 ++++ src/app/mod.rs | 1 + src/core/mod.rs | 0 src/domain/client.rs | 162 ++++++++++ src/domain/event.rs | 40 +++ src/domain/mod.rs | 3 + src/domain/user.rs | 47 +++ src/lib.rs | 6 + src/main.rs | 19 ++ src/network/mod.rs | 2 + src/network/protocol.rs | 246 +++++++++++++++ src/network/udp.rs | 104 +++++++ src/network/udp_back.rs | 193 ++++++++++++ src/runtime/dispatcher.rs | 87 ++++++ src/runtime/mod.rs | 1 + src/utils/byte_utils.rs | 398 ++++++++++++++++++++++++ src/utils/mod.rs | 1 + 25 files changed, 2074 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/ox_speak_server.iml create mode 100644 .idea/vcs.xml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 profile.json.gz create mode 100644 src/app/app.rs create mode 100644 src/app/mod.rs create mode 100644 src/core/mod.rs create mode 100644 src/domain/client.rs create mode 100644 src/domain/event.rs create mode 100644 src/domain/mod.rs create mode 100644 src/domain/user.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/network/mod.rs create mode 100644 src/network/protocol.rs create mode 100644 src/network/udp.rs create mode 100644 src/network/udp_back.rs create mode 100644 src/runtime/dispatcher.rs create mode 100644 src/runtime/mod.rs create mode 100644 src/utils/byte_utils.rs create mode 100644 src/utils/mod.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5329813 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/ox_speak_server.iml b/.idea/ox_speak_server.iml new file mode 100644 index 0000000..cf84ae4 --- /dev/null +++ b/.idea/ox_speak_server.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8e37241 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,639 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "ox_speak_server" +version = "0.1.0" +dependencies = [ + "bytes", + "dashmap", + "event-listener", + "parking_lot", + "serde", + "serde_json", + "strum", + "tokio", + "uuid", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3a73b3c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "ox_speak_server" +version = "0.1.0" +edition = "2024" + +[lib] +# The `_lib` suffix may seem redundant but it is necessary +# to make the lib name unique and wouldn't conflict with the bin name. +# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 +name = "ox_speak_server_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[profile.release] +debug = true + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +parking_lot = "0.12" +tokio = { version = "1.46", features = ["full"] } +strum = {version = "0.27", features = ["derive"] } +uuid = {version = "1.17", features = ["v4", "serde"] } +event-listener = "5.4" +dashmap = "6.1" +bytes = "1.10" \ No newline at end of file diff --git a/profile.json.gz b/profile.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..20bf1e1c217454b9044139110b361111cfcfb039 GIT binary patch literal 46260 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 x>, + + // Network + udp_server: UdpServer, + + // Clients + client_manager: ClientManager, + +} + +impl App { + pub async fn new() -> Self { + let (event_bus, event_rx) = EventBus::new(); + + let udp_server = UdpServer::new(event_bus.clone(), "127.0.0.1:5000").await; + let client_manager = ClientManager::new(); + let dispatcher = Dispatcher::new(event_bus.clone(), udp_server.clone(), client_manager.clone()).await; + + + Self { + event_bus, + dispatcher, + event_rx: Some(event_rx), + udp_server, + client_manager + } + } + + pub async fn start(&mut self) { + if let Some(event_rx) = self.event_rx.take() { + let dispatcher = self.dispatcher.clone(); + tokio::spawn(async move { + dispatcher.start(event_rx).await; + }); + } + + let _ = self.udp_server.start().await; + let _ = self.tick_tasks().await; + println!("App started"); + } + + async fn tick_tasks(&self) { + let event_bus = self.event_bus.clone(); + tokio::spawn(async move { + let mut interval = interval(Duration::from_secs(1)); + loop { + // println!("Tick"); + interval.tick().await; + let _ = event_bus.emit(Event::TickSeconds).await; + } + }); + } + +} \ No newline at end of file diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..02c0277 --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1 @@ +pub mod app; \ No newline at end of file diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/client.rs b/src/domain/client.rs new file mode 100644 index 0000000..b62abc6 --- /dev/null +++ b/src/domain/client.rs @@ -0,0 +1,162 @@ +//! Gestion des clients pour les connexions UDP +//! +//! Ce module fournit les structures et méthodes pour gérer les clients +//! connectés au serveur UDP, incluant leur tracking et leurs modifications. + +use std::net::SocketAddr; +use std::sync::Arc; +use dashmap::DashMap; +use tokio::time::Instant; +use uuid::Uuid; +use std::hash::{Hash, Hasher}; +use std::time::Duration; + +/// Représente un client connecté au serveur UDP +/// +/// Chaque client est identifié par un UUID unique et contient +/// son adresse réseau ainsi que l'heure de sa dernière activité. +#[derive(Debug)] +pub struct Client { + id: Uuid, + address: SocketAddr, + last_seen: Instant, +} + +/// Gestionnaire threadsafe pour les clients connectés +/// +/// Utilise `DashMap` pour permettre un accès concurrent sécurisé +/// aux clients depuis plusieurs threads. +#[derive(Clone)] +pub struct ClientManager { + clients: Arc>, +} + +impl Client { + /// Crée un nouveau client avec un UUID généré automatiquement + pub fn new(address: SocketAddr) -> Self { + let id = Uuid::new_v4(); + Self { + id, + address, + last_seen: Instant::now(), + } + } + + /// Retourne le UUID unique du client + pub fn id(&self) -> Uuid { + self.id + } + + /// Retourne l'adresse socket du client + pub fn address(&self) -> SocketAddr { + self.address + } + + /// Retourne l'instant de la dernière activité du client + pub fn last_seen(&self) -> Instant { + self.last_seen + } + + /// Met à jour l'heure de dernière activité du client à maintenant + pub fn update_last_seen(&mut self) { + self.last_seen = Instant::now(); + } +} + +impl Hash for Client { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl PartialEq for Client { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Client {} + +impl ClientManager { + /// Crée un nouveau gestionnaire de clients vide + pub fn new() -> Self { + Self { + clients: Arc::new(DashMap::new()), + } + } + + /// Ajoute un client au gestionnaire + pub fn add(&self, client: Client) { + self.clients.insert(client.address(), client); + } + + /// Supprime un client du gestionnaire + pub fn remove(&self, client: Client) { + self.clients.remove(&client.address()); + } + + /// Vérifie si un client existe pour une adresse donnée + pub fn client_exists(&self, address: SocketAddr) -> bool { + self.clients.contains_key(&address) + } + + /// Récupère une référence vers un client par son adresse + pub fn get_client_by_address(&self, address: SocketAddr) -> Option> { + self.clients.get(&address) + } + + /// Récupère toutes les adresses des clients connectés + pub fn get_all_adresses(&self) -> Vec { + self.clients.iter().map(|entry| *entry.key()).collect() + } + + /// Met à jour l'heure de dernière activité d'un client + pub fn update_client_last_seen(&self, address: SocketAddr) { + if let Some(mut client) = self.clients.get_mut(&address) { + client.update_last_seen(); + } + } + + /// Supprimer les clients trop vieux + pub fn cleanup(&self, max_age: Duration) { + let now = Instant::now(); + self.clients.retain(|_, client| now - client.last_seen() < max_age); + } + + /// Modifie un client via une closure + /// + /// # Arguments + /// * `address` - L'adresse du client à modifier + /// * `f` - La closure qui recevra une référence mutable vers le client + /// + /// # Returns + /// `true` si le client a été trouvé et modifié, `false` sinon + /// + /// # Examples + /// ```ignore + /// let client_manager = ClientManager::new(); + /// let addr = "127.0.0.1:8080".parse().unwrap(); + /// + /// // Mise à jour simple + /// client_manager.modify_client(addr, |client| { + /// client.update_last_seen(); + /// }); + /// + /// // Modifications multiples + /// let success = client_manager.modify_client(addr, |client| { + /// client.update_last_seen(); + /// // autres modifications... + /// }); + /// ``` + pub fn modify_client(&self, address: SocketAddr, f: F) -> bool + where + F: FnOnce(&mut Client), + { + if let Some(mut client) = self.clients.get_mut(&address) { + f(&mut *client); + true + } else { + false + } + } +} \ No newline at end of file diff --git a/src/domain/event.rs b/src/domain/event.rs new file mode 100644 index 0000000..ec9a599 --- /dev/null +++ b/src/domain/event.rs @@ -0,0 +1,40 @@ +use std::net::SocketAddr; +use tokio::sync::mpsc; +use crate::network::protocol::{UDPMessage}; + +#[derive(Clone, Debug)] +pub enum Event { + AppStarted, + AppStopped, + + UdpStarted, + UdpStopped, + UdpIn(UDPMessage), + UdpOut(UDPMessage), + + TickSeconds +} + +#[derive(Clone)] +pub struct EventBus { + pub sender: mpsc::Sender, +} + +impl EventBus { + pub fn new() -> (Self, mpsc::Receiver) { + let (sender, receiver) = mpsc::channel(10000); + (Self { sender }, receiver) + } + + pub async fn emit(&self, event: Event) { + let _ = self.sender.send(event).await; + } + + pub fn emit_sync(&self, event: Event) { + let _ = self.sender.try_send(event); + } + + pub fn clone_sender(&self) -> mpsc::Sender { + self.sender.clone() + } +} \ No newline at end of file diff --git a/src/domain/mod.rs b/src/domain/mod.rs new file mode 100644 index 0000000..0f8b8ef --- /dev/null +++ b/src/domain/mod.rs @@ -0,0 +1,3 @@ +pub mod event; +pub mod user; +pub mod client; \ No newline at end of file diff --git a/src/domain/user.rs b/src/domain/user.rs new file mode 100644 index 0000000..bfcf8f1 --- /dev/null +++ b/src/domain/user.rs @@ -0,0 +1,47 @@ +use std::net::SocketAddr; +use std::sync::Arc; +use dashmap::DashMap; +use uuid::Uuid; + +pub struct User { + id: Uuid, + udp_addr: Option, +} + +#[derive(Clone)] +pub struct UserManager { + users: Arc>, +} + +impl User { + pub fn new(id: Uuid) -> Self { + Self { + id, + udp_addr: None, + } + } + + pub fn default() -> Self { + Self::new(Uuid::new_v4()) + } + + pub fn set_udp_addr(&mut self, udp_addr: SocketAddr) { + self.udp_addr = Some(udp_addr); + } +} + +impl UserManager { + pub fn new() -> Self { + Self { + users: Arc::new(DashMap::new()) + } + } + + pub fn add_user(&self, user: User) { + self.users.insert(user.id, user); + } + + pub fn delete_user(&self, user: User) { + self.users.remove(&user.id); + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ad3d89c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +pub mod app; +pub mod core; +pub mod domain; +pub mod network; +pub mod runtime; +pub mod utils; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..fb9efbc --- /dev/null +++ b/src/main.rs @@ -0,0 +1,19 @@ +use tokio::signal; +use ox_speak_server_lib::app::app::App; + +#[tokio::main] +async fn main() { + let mut app = App::new().await; + app.start().await; + + // Attendre le signal Ctrl+C + match signal::ctrl_c().await { + Ok(()) => { + println!("Arrêt du serveur..."); + } + Err(err) => { + eprintln!("Erreur lors de l'écoute du signal: {}", err); + } + } + +} diff --git a/src/network/mod.rs b/src/network/mod.rs new file mode 100644 index 0000000..054820a --- /dev/null +++ b/src/network/mod.rs @@ -0,0 +1,2 @@ +pub mod protocol; +pub mod udp; \ No newline at end of file diff --git a/src/network/protocol.rs b/src/network/protocol.rs new file mode 100644 index 0000000..6eca83a --- /dev/null +++ b/src/network/protocol.rs @@ -0,0 +1,246 @@ +use std::collections::HashSet; +use bytes::{Bytes, BytesMut, Buf, BufMut}; +use std::net::SocketAddr; +use uuid::Uuid; +use strum::{EnumIter, FromRepr}; + +#[repr(u8)] +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, EnumIter, FromRepr)] +pub enum UDPMessageType { + Ping = 0, + Audio = 1, + // Futurs types ici... +} + +#[derive(Debug, Clone, PartialEq)] +pub enum UDPMessageData { + // Client messages - Zero-copy avec Bytes + ClientPing { message_id: Uuid }, + ClientAudio { sequence: u16, data: Bytes }, + + // Server messages + ServerPing { message_id: Uuid }, + ServerAudio { user: Uuid, sequence: u16, data: Bytes }, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct UDPMessage { + pub data: UDPMessageData, + pub address: SocketAddr, + pub size: usize, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct UdpBroadcastMessage { + pub data: UDPMessageData, + pub addresses: HashSet, // ou Vec selon besoins + pub size: usize, +} + + +#[derive(Debug, Clone, PartialEq)] +pub enum ParseError { + EmptyData, + InvalidData, + InvalidMessageType, + InvalidUuid, +} + +impl From for ParseError { + fn from(_: uuid::Error) -> Self { + ParseError::InvalidUuid + } +} + +impl UDPMessageData { + // Parsing zero-copy depuis Bytes + pub fn from_client_bytes(mut data: Bytes) -> Result { + if data.is_empty() { + return Err(ParseError::EmptyData); + } + + let msg_type = data.get_u8(); // Consomme 1 byte + + match msg_type { + 0 => { // Ping + if data.remaining() < 16 { + return Err(ParseError::InvalidData); + } + let uuid_bytes = data.split_to(16); // Zero-copy split + let message_id = Uuid::from_slice(&uuid_bytes)?; + Ok(Self::ClientPing { message_id }) + } + 1 => { // Audio + if data.remaining() < 2 { + return Err(ParseError::InvalidData); + } + let sequence = data.get_u16(); // Big-endian par défaut + let audio_data = data; // Le reste pour l'audio + Ok(Self::ClientAudio { sequence, data: audio_data }) + } + _ => Err(ParseError::InvalidMessageType), + } + } + + // Constructeurs server + pub fn server_ping(message_id: Uuid) -> Self { + Self::ServerPing { message_id } + } + + pub fn server_audio(user: Uuid, sequence: u16, data: Bytes) -> Self { + Self::ServerAudio { user, sequence, data } + } + + // Sérialisation optimisée avec BytesMut + pub fn to_bytes(&self) -> Bytes { + match self { + Self::ServerPing { message_id } => { + let mut buf = BytesMut::with_capacity(17); + buf.put_u8(0); // Message type + buf.put_slice(message_id.as_bytes()); + buf.freeze() + } + Self::ServerAudio { user, sequence, data } => { + let mut buf = BytesMut::with_capacity(19 + data.len()); + buf.put_u8(1); // Message type + buf.put_slice(user.as_bytes()); + buf.put_u16(*sequence); + buf.put_slice(data); + buf.freeze() + } + _ => panic!("Client messages cannot be serialized"), + } + } + + pub fn to_vec(&self) -> Vec { + // pas très optimisé + self.to_bytes().to_vec() + } + + pub fn message_type(&self) -> UDPMessageType { + match self { + Self::ClientPing { .. } | Self::ServerPing { .. } => UDPMessageType::Ping, + Self::ClientAudio { .. } | Self::ServerAudio { .. } => UDPMessageType::Audio, + } + } + + // Calcule la taille du message sérialisé + pub fn size(&self) -> usize { + match self { + Self::ClientPing { .. } | Self::ServerPing { .. } => 17, // 1 + 16 (UUID) + Self::ClientAudio { data, .. } => 3 + data.len(), // 1 + 2 + audio_data + Self::ServerAudio { data, .. } => 19 + data.len(), // 1 + 16 + 2 + audio_data + } + } +} + +impl UDPMessage { + // Parsing depuis slice -> Bytes (zero-copy si possible) + pub fn from_client_bytes(address: SocketAddr, data: &[u8]) -> Result { + let original_size = data.len(); + let bytes = Bytes::copy_from_slice(data); // Seule allocation + let data = UDPMessageData::from_client_bytes(bytes)?; + Ok(Self { + data, + address, + size: original_size + }) + } + + // Constructeurs server + pub fn server_ping(address: SocketAddr, message_id: Uuid) -> Self { + let data = UDPMessageData::server_ping(message_id); + let size = data.size(); + Self { data, address, size } + } + + pub fn server_audio(address: SocketAddr, user: Uuid, sequence: u16, data: Bytes) -> Self { + let msg_data = UDPMessageData::server_audio(user, sequence, data); + let size = msg_data.size(); + Self { data: msg_data, address, size } + } + + // Helpers + pub fn to_bytes(&self) -> Bytes { + self.data.to_bytes() + } + + pub fn to_vec(&self) -> Vec { + self.data.to_vec() + } + + pub fn message_type(&self) -> UDPMessageType { + self.data.message_type() + } + + // Getters + pub fn size(&self) -> usize { + self.size + } + + pub fn address(&self) -> SocketAddr { + self.address + } +} + +impl UDPMessage { + // Helpers pour récupérer certain éléments des messages + pub fn get_message_id(&self) -> Option { + match &self.data { + UDPMessageData::ClientPing { message_id } => Some(*message_id), + UDPMessageData::ServerPing { message_id } => Some(*message_id), + _ => None, + } + } +} + +// Helper pour compatibilité avec UDPMessageType +impl UDPMessageType { + pub fn from_message(data: &[u8]) -> Option { + if data.is_empty() { + return None; + } + Self::from_repr(data[0]) + } +} + +impl UdpBroadcastMessage { + // Constructeurs server + pub fn server_ping(addresses: HashSet, message_id: Uuid) -> Self { + let data = UDPMessageData::server_ping(message_id); + let size = data.size(); + Self { data, addresses, size } + } + + pub fn server_audio(addresses: HashSet, user: Uuid, sequence: u16, data: Bytes) -> Self { + let msg_data = UDPMessageData::server_audio(user, sequence, data); + let size = msg_data.size(); + Self { data: msg_data, addresses, size } + } + + // Conversion vers messages individuels (pour compatibilité) + pub fn to_individual_messages(&self) -> Vec { + self.addresses.iter().map(|&addr| { + UDPMessage { + data: self.data.clone(), + address: addr, + size: self.size, + } + }).collect() + } + + // Helpers + pub fn to_bytes(&self) -> Bytes { + self.data.to_bytes() + } + + pub fn addresses(&self) -> &HashSet { + &self.addresses + } + + pub fn size(&self) -> usize { + self.size + } +} + + diff --git a/src/network/udp.rs b/src/network/udp.rs new file mode 100644 index 0000000..dfb05f1 --- /dev/null +++ b/src/network/udp.rs @@ -0,0 +1,104 @@ +use tokio::net::UdpSocket; +use std::error::Error; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::task::AbortHandle; +use crate::domain::event::{Event, EventBus}; +use crate::network::protocol::{UDPMessage, UdpBroadcastMessage}; + +#[derive(Clone)] +pub struct UdpServer { + event_bus: EventBus, + socket: Arc, + abort_handle: Option, +} + +impl UdpServer { + pub async fn new(event_bus: EventBus, addr: &str) -> Self { + let socket = UdpSocket::bind(addr).await.unwrap(); + let addr = socket.local_addr().unwrap(); + println!("Socket UDP lié avec succès on {}", addr); + + Self { + event_bus, + socket: Arc::new(socket), + abort_handle: None + } + } + + pub async fn start(&mut self) -> Result<(), Box> { + println!("Démarrage du serveur UDP..."); + let event_bus = self.event_bus.clone(); + let socket = self.socket.clone(); + + let recv_task = tokio::spawn(async move { + // Buffer réutilisable pour éviter les allocations + let mut buf = vec![0u8; 1500]; + + loop { + match socket.recv_from(&mut buf).await { + Ok((size, address)) => { + // Slice du buffer pour éviter de copier des données inutiles + if let Ok(message) = UDPMessage::from_client_bytes(address, &buf[..size]) { + event_bus.emit(Event::UdpIn(message)).await; + } + // Sinon, on ignore silencieusement le message malformé + } + Err(e) => { + match e.kind() { + std::io::ErrorKind::ConnectionReset | + std::io::ErrorKind::ConnectionAborted => { + // Silencieux pour les déconnexions normales + continue; + } + _ => { + println!("Erreur UDP: {}", e); + continue; + } + } + } + } + } + }); + + self.abort_handle = Some(recv_task.abort_handle()); + Ok(()) + } + + pub async fn send_udp_message(&self, message: &UDPMessage) -> bool { + match self.socket.send_to(&message.to_bytes(), message.address()).await { + Ok(size) => { + self.event_bus.emit(Event::UdpOut(message.clone())).await; + true + } + Err(e) => { + println!("Erreur lors de l'envoi du message à {}: {}", message.address(), e); + false + } + } + } + + pub async fn broadcast_udp_message(&self, message: &UdpBroadcastMessage) -> bool { + let bytes = message.to_bytes(); + + for &address in message.addresses() { + match self.socket.send_to(&bytes, address).await { + Ok(_) => { + // Emit individual event pour tracking + let individual_msg = UDPMessage { + data: message.data.clone(), + address, + size: message.size, + }; + self.event_bus.emit(Event::UdpOut(individual_msg)).await; + } + Err(e) => { + println!("Erreur broadcast vers {}: {}", address, e); + } + } + } + + true + } + +} \ No newline at end of file diff --git a/src/network/udp_back.rs b/src/network/udp_back.rs new file mode 100644 index 0000000..b5bd8d1 --- /dev/null +++ b/src/network/udp_back.rs @@ -0,0 +1,193 @@ +use tokio::net::UdpSocket; +use tokio::sync::RwLock; +use std::error::Error; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; +use dashmap::DashMap; +use tokio::task::AbortHandle; +use tokio::time::{sleep, Instant}; +use crate::domain::client::Client; +use crate::domain::event::{Event, EventBus}; +use crate::network::protocol::{UdpClientMessage, UdpServerMessage}; + +#[derive(Clone)] +pub struct UdpServer { + event_bus: EventBus, + socket: Arc, + abort_handle: Option, + clients: Arc>, +} + +impl UdpServer { + pub async fn new(event_bus: EventBus, addr: &str) -> Self { + let socket = UdpSocket::bind(addr).await.unwrap(); + let addr = socket.local_addr().unwrap(); + println!("Socket UDP lié avec succès on {}", addr); + + Self { + event_bus, + socket: Arc::new(socket), + abort_handle: None, + clients: Arc::new(DashMap::new()), + } + } + + pub async fn start(&mut self) -> Result<(), Box> { + println!("Démarrage du serveur UDP..."); + let event_bus = self.event_bus.clone(); + let socket = self.socket.clone(); + let clients = self.clients.clone(); + + let recv_task = tokio::spawn(async move { + let mut buf = [0u8; 1500]; + loop { + match socket.recv_from(&mut buf).await { + Ok((size, address)) => { + // Ajouter le client à la liste + // todo : solution vraiment pas idéal, il faudrait vraiment la repenser avec un système helo/bye + if !clients.contains_key(&address) { + let client = Client::new(address); + clients.insert(address, client); + println!("Nouveau client connecté: {}", address); + }else { + let mut client = clients.get_mut(&address).unwrap(); + client.update_last_seen(); + } + + if let Ok(message) = UdpClientMessage::from_bytes(&buf[..size]) { + let event = Event::UdpIn { address, size, message }; + event_bus.emit(event).await; + } else { + println!("Erreur lors du parsing du message de {}: {:?}", address, &buf[..size]); + } + } + Err(e) => { + match e.kind() { + std::io::ErrorKind::ConnectionReset | + std::io::ErrorKind::ConnectionAborted => { + // Silencieux pour les déconnexions normales + continue; + } + _ => { + println!("Erreur UDP: {}", e); + continue; + } + } + } + } + } + }); + + self.abort_handle = Some(recv_task.abort_handle()); + Ok(()) + } + + pub async fn send(&self, address: SocketAddr, message: UdpServerMessage) -> bool { + let event_bus = self.event_bus.clone(); + match self.socket.send_to(&message.to_byte(), address).await { + Ok(size) => { + event_bus.emit(Event::UdpOut { address, size, message }).await; + true + } + Err(e) => { + println!("Erreur lors de l'envoi du message à {}: {}", address, e); + // Optionnel : retirer le client si l'adresse est invalide + self.remove_client(address).await; + false + } + } + } + + pub async fn group_send(&self, addr_list: Vec, message: UdpServerMessage) -> bool { + if addr_list.is_empty() { + return true; + } + + let socket = self.socket.clone(); + let clients = self.clients.clone(); + + let send_tasks: Vec<_> = addr_list.into_iter().map(|address| { + let event_bus = self.event_bus.clone(); + let message_clone = message.clone(); + let socket_clone = socket.clone(); + let clients_clone = clients.clone(); + + tokio::spawn(async move { + match socket_clone.send_to(&message_clone.to_byte(), address).await { + Ok(size) => { + event_bus.emit(Event::UdpOut { address, size, message: message_clone }).await; + true + } + Err(e) => { + println!("Erreur lors de l'envoi du message à {}: {}", address, e); + // Optionnel : retirer le client si l'adresse est invalide + if clients_clone.contains_key(&address) { + clients_clone.remove(&address); + println!("Client {} retiré de la liste", address); + } + false + } + } + }) + }).collect(); + + let mut all_success = true; + for task in send_tasks { + match task.await { + Ok(success) => { + if !success { + all_success = false; + } + } + Err(_) => { + all_success = false; + } + } + } + + all_success + } + + pub async fn all_send(&self, message: UdpServerMessage) -> bool { + let client_addresses = self.get_clients().await; + self.group_send(client_addresses, message).await + } + + pub async fn get_clients(&self) -> Vec { + self.clients.iter() + .map(|entry| *entry.key()) + .collect() + } + + // Nouvelle méthode pour nettoyer les clients déconnectés + async fn remove_client(&self, address: SocketAddr) { + if self.clients.contains_key(&address){ + self.clients.remove(&address); + println!("Client {} retiré de la liste", address); + }else { + println!("Client {} n'est pas dans la liste", address); + } + } + + // Méthode pour nettoyer les clients inactifs + pub async fn cleanup_inactive_clients(&self) { + let timeout = Duration::from_secs(10); + let now = Instant::now(); + let mut to_remove = Vec::new(); + + for entry in self.clients.iter() { + let address = *entry.key(); + let client = entry.value(); + + if now.duration_since(client.last_seen()) > timeout { + to_remove.push(address); + } + } + + for address in &to_remove { + println!("Suppression du client {}", address); + self.clients.remove(address); + } + } +} \ No newline at end of file diff --git a/src/runtime/dispatcher.rs b/src/runtime/dispatcher.rs new file mode 100644 index 0000000..8d8cabd --- /dev/null +++ b/src/runtime/dispatcher.rs @@ -0,0 +1,87 @@ +use std::time::Duration; +use tokio::sync::mpsc; +use tokio::task::AbortHandle; +use crate::domain::client::ClientManager; +use crate::domain::event::{Event, EventBus}; +use crate::network::protocol::{UDPMessageType, UDPMessage}; +use crate::network::udp::UdpServer; + +#[derive(Clone)] +pub struct Dispatcher { + event_bus: EventBus, + + udp_server: UdpServer, + client_manager: ClientManager +} + +impl Dispatcher { + pub async fn new(event_bus: EventBus, udp_server: UdpServer, client_manager: ClientManager) -> Self { + Self { + event_bus, + udp_server, + client_manager, + } + } + + pub async fn start(&self, mut receiver: mpsc::Receiver) { + let (udp_in_abort_handle, udp_in_sender) = self.udp_in_handler().await; + + while let Some(event) = receiver.recv().await { + match event { + Event::UdpIn(message) => { + let _ = udp_in_sender.send(message).await; + // // println!("Message reçu de {}: {:?}", address, message); + // let udp_server = self.udp_server.clone(); + // tokio::spawn(async move { + // match message { + // UdpClientMessage::Ping {message_id} => { + // let send = UdpServerMessage::ping(message_id); + // let _ = udp_server.all_send(send); + // } + // UdpClientMessage::Audio {sequence, data} => { + // let tmp_user_id = Uuid::new_v4(); + // let send = UdpServerMessage::audio(tmp_user_id, sequence, data); + // let _ = udp_server.all_send(send).await; + // } + // } + // }); + } + Event::UdpOut(message) => { + // println!("Message envoyé à {}: {:?}", address, message); + } + Event::TickSeconds => { + self.client_manager.cleanup(Duration::from_secs(10)); + } + _ => { + println!("Event non prit en charge : {:?}", event) + } + } + } + } + + pub async fn udp_in_handler(&self) -> (AbortHandle, mpsc::Sender) { + let (sender, mut consumer) = mpsc::channel::(1024); + let udp_server = self.udp_server.clone(); + + let task = tokio::spawn(async move { + while let Some(message) = consumer.recv().await { + // Traitement direct du message sans double parsing + match message.message_type() { + UDPMessageType::Ping => { + let response_message = UDPMessage::server_ping(message.address, message.get_message_id().unwrap()); + let _ = udp_server.send_udp_message(&response_message); + } + UDPMessageType::Audio => { + // Traiter l'audio + } + } + } + }); + + (task.abort_handle(), sender) + } + + + + +} \ No newline at end of file diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs new file mode 100644 index 0000000..ebffa3f --- /dev/null +++ b/src/runtime/mod.rs @@ -0,0 +1 @@ +pub mod dispatcher; \ No newline at end of file diff --git a/src/utils/byte_utils.rs b/src/utils/byte_utils.rs new file mode 100644 index 0000000..13d5ebe --- /dev/null +++ b/src/utils/byte_utils.rs @@ -0,0 +1,398 @@ +use uuid::Uuid; + +/// Helpers pour la manipulation de bytes - idéal pour les protocoles binaires +/// +/// Cette structure permet de lire séquentiellement des données binaires +/// en maintenant une position de lecture interne. +pub struct ByteReader<'a> { + /// Référence vers les données à lire + data: &'a [u8], + /// Position actuelle dans le buffer de lecture + position: usize, +} + +impl<'a> ByteReader<'a> { + /// Crée un nouveau lecteur de bytes à partir d'un slice + /// + /// # Arguments + /// * `data` - Le slice de bytes à lire + /// + /// # Example + /// ```text + /// let data = &[0x01, 0x02, 0x03, 0x04]; + /// let reader = ByteReader::new(data); + /// ``` + pub fn new(data: &'a [u8]) -> Self { + Self { data, position: 0 } + } + + /// Retourne le nombre de bytes restants à lire + /// + /// Utilise `saturating_sub` pour éviter les débordements + /// si la position dépasse la taille des données + pub fn remaining(&self) -> usize { + self.data.len().saturating_sub(self.position) + } + + /// Vérifie si tous les bytes ont été lus + /// + /// # Returns + /// `true` si il n'y a plus de bytes à lire + pub fn is_empty(&self) -> bool { + self.remaining() == 0 + } + + /// Retourne la position actuelle de lecture + pub fn position(&self) -> usize { + self.position + } + + /// Déplace la position de lecture à l'index spécifié + /// + /// La position est automatiquement limitée à la taille des données + /// pour éviter les débordements + /// + /// # Arguments + /// * `position` - Nouvelle position de lecture + pub fn seek(&mut self, position: usize) { + self.position = position.min(self.data.len()); + } + + /// Lit un byte (u8) à la position actuelle + /// + /// # Returns + /// * `Ok(u8)` - La valeur lue si disponible + /// * `Err(&'static str)` - Si la fin du buffer est atteinte + pub fn read_u8(&mut self) -> Result { + if self.position < self.data.len() { + let value = self.data[self.position]; + self.position += 1; + Ok(value) + } else { + Err("Not enough data for u8") + } + } + + /// Lit un entier 16-bit en big-endian + /// + /// # Returns + /// * `Ok(u16)` - La valeur lue si 2 bytes sont disponibles + /// * `Err(&'static str)` - Si moins de 2 bytes sont disponibles + pub fn read_u16_be(&mut self) -> Result { + if self.remaining() >= 2 { + let value = u16::from_be_bytes([ + self.data[self.position], + self.data[self.position + 1], + ]); + self.position += 2; + Ok(value) + } else { + Err("Not enough data for u16") + } + } + + /// Lit un entier 32-bit en big-endian + /// + /// # Returns + /// * `Ok(u32)` - La valeur lue si 4 bytes sont disponibles + /// * `Err(&'static str)` - Si moins de 4 bytes sont disponibles + pub fn read_u32_be(&mut self) -> Result { + if self.remaining() >= 4 { + let value = u32::from_be_bytes([ + self.data[self.position], + self.data[self.position + 1], + self.data[self.position + 2], + self.data[self.position + 3], + ]); + self.position += 4; + Ok(value) + } else { + Err("Not enough data for u32") + } + } + + /// Lit un entier 64-bit en big-endian + /// + /// # Returns + /// * `Ok(u64)` - La valeur lue si 8 bytes sont disponibles + /// * `Err(&'static str)` - Si moins de 8 bytes sont disponibles + pub fn read_u64_be(&mut self) -> Result { + if self.remaining() >= 8 { + let value = u64::from_be_bytes([ + self.data[self.position], + self.data[self.position + 1], + self.data[self.position + 2], + self.data[self.position + 3], + self.data[self.position + 4], + self.data[self.position + 5], + self.data[self.position + 6], + self.data[self.position + 7], + ]); + self.position += 8; + Ok(value) + } else { + Err("Not enough data for u64") + } + } + + /// Lit un UUID (16 bytes) à la position actuelle + /// + /// Les UUIDs sont stockés sous forme de 16 bytes consécutifs. + /// Cette méthode lit ces 16 bytes et les convertit en UUID. + /// + /// # Returns + /// * `Ok(Uuid)` - L'UUID lu si 16 bytes sont disponibles + /// * `Err(&'static str)` - Si moins de 16 bytes sont disponibles + pub fn read_uuid(&mut self) -> Result { + if self.remaining() >= 16 { + let uuid_bytes = self.read_fixed_bytes::<16>()?; + Ok(Uuid::from_bytes(uuid_bytes)) + } else { + Err("Not enough data for UUID") + } + } + + + /// Lit une séquence de bytes de longueur spécifiée + /// + /// # Arguments + /// * `len` - Nombre de bytes à lire + /// + /// # Returns + /// * `Ok(&[u8])` - Slice des bytes lus si disponibles + /// * `Err(&'static str)` - Si pas assez de bytes disponibles + pub fn read_bytes(&mut self, len: usize) -> Result<&'a [u8], &'static str> { + if self.remaining() >= len { + let slice = &self.data[self.position..self.position + len]; + self.position += len; + Ok(slice) + } else { + Err("Not enough data for bytes") + } + } + + /// Lit un tableau de bytes de taille fixe définie à la compilation + /// + /// Utilise les generics const pour définir la taille du tableau + /// + /// # Returns + /// * `Ok([u8; N])` - Tableau de bytes lu si disponible + /// * `Err(&'static str)` - Si pas assez de bytes disponibles + pub fn read_fixed_bytes(&mut self) -> Result<[u8; N], &'static str> { + if self.remaining() >= N { + let mut array = [0u8; N]; + array.copy_from_slice(&self.data[self.position..self.position + N]); + self.position += N; + Ok(array) + } else { + Err("Not enough data for fixed bytes") + } + } + + /// Lit tous les bytes restants dans le buffer + /// + /// Après cet appel, le reader sera vide (position = taille totale) + /// + /// # Returns + /// Slice contenant tous les bytes restants + pub fn read_remaining(&mut self) -> &'a [u8] { + let slice = &self.data[self.position..]; + self.position = self.data.len(); + slice + } +} + +/// Structure pour construire séquentiellement des données binaires +/// +/// Contrairement à ByteReader, cette structure possède ses propres données +/// et permet d'écrire des valeurs de différents types. +pub struct ByteWriter { + /// Buffer interne pour stocker les données écrites + data: Vec, +} + +impl ByteWriter { + /// Crée un nouveau writer avec un Vec vide + pub fn new() -> Self { + Self { data: Vec::new() } + } + + /// Crée un nouveau writer avec une capacité pré-allouée + /// + /// Utile pour éviter les réallocations si la taille finale + /// est approximativement connue + /// + /// # Arguments + /// * `capacity` - Capacité initiale du buffer + pub fn with_capacity(capacity: usize) -> Self { + Self { + data: Vec::with_capacity(capacity), + } + } + + /// Écrit un byte (u8) dans le buffer + /// + /// # Arguments + /// * `value` - Valeur à écrire + pub fn write_u8(&mut self, value: u8) { + self.data.push(value); + } + + /// Écrit un entier 16-bit en big-endian + /// + /// # Arguments + /// * `value` - Valeur à écrire + pub fn write_u16_be(&mut self, value: u16) { + self.data.extend_from_slice(&value.to_be_bytes()); + } + + /// Écrit un entier 32-bit en big-endian + /// + /// # Arguments + /// * `value` - Valeur à écrire + pub fn write_u32_be(&mut self, value: u32) { + self.data.extend_from_slice(&value.to_be_bytes()); + } + + /// Écrit un entier 64-bit en big-endian + /// + /// # Arguments + /// * `value` - Valeur à écrire + pub fn write_u64_be(&mut self, value: u64) { + self.data.extend_from_slice(&value.to_be_bytes()); + } + + /// Écrit une séquence de bytes dans le buffer + /// + /// # Arguments + /// * `bytes` - Slice de bytes à écrire + pub fn write_bytes(&mut self, bytes: &[u8]) { + self.data.extend_from_slice(bytes); + } + + /// Écrit un tableau de bytes de taille fixe + /// + /// # Arguments + /// * `bytes` - Tableau de bytes à écrire + pub fn write_fixed_bytes(&mut self, bytes: [u8; N]) { + self.data.extend_from_slice(&bytes); + } + + /// Consomme le writer et retourne le Vec contenant les données + /// + /// # Returns + /// Vec contenant toutes les données écrites + pub fn into_vec(self) -> Vec { + self.data + } + + /// Retourne une référence vers les données sous forme de slice + /// + /// # Returns + /// Slice des données écrites + pub fn as_slice(&self) -> &[u8] { + &self.data + } + + /// Retourne la taille actuelle du buffer + pub fn len(&self) -> usize { + self.data.len() + } + + /// Vérifie si le buffer est vide + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } +} + +/// Implémentation du trait Default pour ByteWriter +/// +/// Permet d'utiliser ByteWriter::default() comme équivalent de ByteWriter::new() +impl Default for ByteWriter { + fn default() -> Self { + Self::new() + } +} + +/// Fonctions utilitaires standalone pour la lecture directe sans état +/// +/// Ces fonctions permettent de lire des valeurs à des offsets spécifiques +/// sans avoir besoin de créer un ByteReader. + +/// Lit un byte à l'offset spécifié +/// +/// # Arguments +/// * `data` - Slice de données source +/// * `offset` - Position de lecture +/// +/// # Returns +/// * `Some(u8)` - Valeur lue si l'offset est valide +/// * `None` - Si l'offset dépasse la taille des données +pub fn read_u8_at(data: &[u8], offset: usize) -> Option { + data.get(offset).copied() +} + +/// Lit un entier 16-bit big-endian à l'offset spécifié +/// +/// # Arguments +/// * `data` - Slice de données source +/// * `offset` - Position de lecture +/// +/// # Returns +/// * `Some(u16)` - Valeur lue si 2 bytes sont disponibles à l'offset +/// * `None` - Si pas assez de bytes disponibles +pub fn read_u16_be_at(data: &[u8], offset: usize) -> Option { + if data.len() >= offset + 2 { + Some(u16::from_be_bytes([data[offset], data[offset + 1]])) + } else { + None + } +} + +/// Lit un entier 32-bit big-endian à l'offset spécifié +/// +/// # Arguments +/// * `data` - Slice de données source +/// * `offset` - Position de lecture +/// +/// # Returns +/// * `Some(u32)` - Valeur lue si 4 bytes sont disponibles à l'offset +/// * `None` - Si pas assez de bytes disponibles +pub fn read_u32_be_at(data: &[u8], offset: usize) -> Option { + if data.len() >= offset + 4 { + Some(u32::from_be_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ])) + } else { + None + } +} + +/// Lit un entier 64-bit big-endian à l'offset spécifié +/// +/// # Arguments +/// * `data` - Slice de données source +/// * `offset` - Position de lecture +/// +/// # Returns +/// * `Some(u64)` - Valeur lue si 8 bytes sont disponibles à l'offset +/// * `None` - Si pas assez de bytes disponibles +pub fn read_u64_be_at(data: &[u8], offset: usize) -> Option { + if data.len() >= offset + 8 { + Some(u64::from_be_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + data[offset + 4], + data[offset + 5], + data[offset + 6], + data[offset + 7], + ])) + } else { + None + } +} \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..a238d3f --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod byte_utils; \ No newline at end of file