commit d72254c7d0d4c6a64dd4b4c36011d5893d5906a4 Author: Nell Date: Tue Jul 15 17:05:04 2025 +0200 init 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 0000000..20bf1e1 Binary files /dev/null and b/profile.json.gz differ diff --git a/src/app/app.rs b/src/app/app.rs new file mode 100644 index 0000000..b682ee5 --- /dev/null +++ b/src/app/app.rs @@ -0,0 +1,66 @@ +use std::time::Duration; +use tokio::sync::mpsc; +use tokio::time::interval; +use crate::domain::client::ClientManager; +use crate::domain::event::{Event, EventBus}; +use crate::network::udp::UdpServer; +use crate::runtime::dispatcher::Dispatcher; + +pub struct App { + // Communication inter-components + event_bus: EventBus, + dispatcher: Dispatcher, + event_rx: Option>, + + // 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