Init
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<h1>Oxspeak</h1>
|
<h1>Oxspeak</h1>
|
||||||
|
<ws />
|
||||||
<server_list />
|
<server_list />
|
||||||
|
|
||||||
<category_list />
|
<category_list />
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
import Server_list from "@/components/server/server_list.vue";
|
import Server_list from "@/components/server/server_list.vue";
|
||||||
import Category_list from "@/components/category/category_list.vue";
|
import Category_list from "@/components/category/category_list.vue";
|
||||||
import Channel_list from "@/components/channel/channel_list.vue";
|
import Channel_list from "@/components/channel/channel_list.vue";
|
||||||
|
import Ws from "@/components/ws.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
17
frontend/src/components/ws.vue
Normal file
17
frontend/src/components/ws.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup>
|
||||||
|
import {onMounted, ref} from "vue";
|
||||||
|
|
||||||
|
const messages = ref([])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
let ws = new WebSocket("ws://localhost:7000/handler/ws/");
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p></p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,8 @@ use crate::app::AppState;
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
use crate::event_bus::EventBus;
|
use crate::event_bus::EventBus;
|
||||||
use crate::network::http::{HTTPServer, WSHub};
|
use crate::hub::Clients;
|
||||||
|
use crate::network::http::HTTPServer;
|
||||||
use crate::network::udp::UDPServer;
|
use crate::network::udp::UDPServer;
|
||||||
use crate::repositories::Repositories;
|
use crate::repositories::Repositories;
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ impl App {
|
|||||||
db: db.clone(),
|
db: db.clone(),
|
||||||
event_bus: event_bus.clone(),
|
event_bus: event_bus.clone(),
|
||||||
repositories: repositories.clone(),
|
repositories: repositories.clone(),
|
||||||
ws_hub: WSHub::new()
|
clients: Clients::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let udp_server = UDPServer::new(config.bind_addr());
|
let udp_server = UDPServer::new(config.bind_addr());
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
use crate::event_bus::EventBus;
|
use crate::event_bus::EventBus;
|
||||||
use crate::network::http::WSHub;
|
use crate::hub::Clients;
|
||||||
use crate::repositories::Repositories;
|
use crate::repositories::Repositories;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -8,7 +8,7 @@ pub struct AppState {
|
|||||||
pub db: Database,
|
pub db: Database,
|
||||||
pub event_bus: EventBus,
|
pub event_bus: EventBus,
|
||||||
pub repositories: Repositories,
|
pub repositories: Repositories,
|
||||||
pub ws_hub: WSHub,
|
pub clients: Clients,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
|
|||||||
84
src/hub/client.rs
Normal file
84
src/hub/client.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use axum::extract::ws::Message;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use crate::app::AppState;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Client {
|
||||||
|
pub user_id: uuid::Uuid,
|
||||||
|
pub connection_id: uuid::Uuid,
|
||||||
|
pub connected_at: DateTime<Utc>,
|
||||||
|
|
||||||
|
// tools
|
||||||
|
app_state: AppState,
|
||||||
|
sender: mpsc::Sender<Message>,
|
||||||
|
tasks: Arc<Mutex<HashMap<String, JoinHandle<()>>>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub fn new(
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
connection_id: uuid::Uuid,
|
||||||
|
app_state: AppState,
|
||||||
|
sender: mpsc::Sender<Message>
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
user_id,
|
||||||
|
connection_id,
|
||||||
|
connected_at: Utc::now(),
|
||||||
|
app_state,
|
||||||
|
sender,
|
||||||
|
tasks: Arc::new(Mutex::new(HashMap::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send(&self, message: Message) {
|
||||||
|
let _ = self.sender.send(message).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn on_message(&self, message: Message) {
|
||||||
|
if let Message::Text(t) = message {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn on_disconnect(&self) {
|
||||||
|
self.clear_tasks().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_task(&self, name: &str, task: JoinHandle<()>) {
|
||||||
|
self.tasks.lock().insert(name.to_string(), task);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_task(&self, name: &str) {
|
||||||
|
let task = self.tasks.lock().remove(name);
|
||||||
|
|
||||||
|
if let Some(task) = task {
|
||||||
|
task.abort();
|
||||||
|
let _ = task.await; // On peut await ici sans bloquer le Mutex
|
||||||
|
println!("Task {} cleaned up", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear_tasks(&self) {
|
||||||
|
// 1. On vide la map et on récupère toutes les tâches d'un coup
|
||||||
|
let mut tasks_to_clean = Vec::new();
|
||||||
|
{
|
||||||
|
let mut lock = self.tasks.lock();
|
||||||
|
for (name, task) in lock.drain() {
|
||||||
|
tasks_to_clean.push((name, task));
|
||||||
|
}
|
||||||
|
} // Le verrou est relâché ICI
|
||||||
|
|
||||||
|
// 2. On les nettoie une par une de manière asynchrone
|
||||||
|
for (name, task) in tasks_to_clean {
|
||||||
|
task.abort();
|
||||||
|
let _ = task.await;
|
||||||
|
println!("Task {} cleaned up", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/hub/mod.rs
Normal file
28
src/hub/mod.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
mod client;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use uuid::Uuid;
|
||||||
|
pub use client::Client;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Clients {
|
||||||
|
pub connections: Arc<RwLock<HashMap<Uuid, Client>>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clients {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
connections: Arc::new(RwLock::new(HashMap::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_client(&self, client: Client) {
|
||||||
|
self.connections.write().insert(client.connection_id, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_client(&self, connection_id: Uuid) {
|
||||||
|
self.connections.write().remove(&connection_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,4 +7,5 @@ pub mod database;
|
|||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod serializers;
|
pub mod serializers;
|
||||||
pub mod repositories;
|
pub mod repositories;
|
||||||
pub mod event_bus;
|
pub mod event_bus;
|
||||||
|
pub mod hub;
|
||||||
@@ -11,6 +11,5 @@ mod context;
|
|||||||
pub use server::HTTPServer;
|
pub use server::HTTPServer;
|
||||||
pub use error::HTTPError;
|
pub use error::HTTPError;
|
||||||
pub use context::RequestContext;
|
pub use context::RequestContext;
|
||||||
pub use web::WSHub;
|
|
||||||
|
|
||||||
pub type AppRouter = Router<AppState>;
|
pub type AppRouter = Router<AppState>;
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use axum::extract::{State, WebSocketUpgrade};
|
|
||||||
use axum::extract::ws::{Message, WebSocket};
|
|
||||||
use axum::response::IntoResponse;
|
|
||||||
use futures_util::{SinkExt, StreamExt};
|
|
||||||
use parking_lot::RwLock;
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use uuid::Uuid;
|
|
||||||
use crate::app::AppState;
|
|
||||||
|
|
||||||
async fn ws_handler(
|
|
||||||
ws: WebSocketUpgrade,
|
|
||||||
State(app_state): State<AppState>
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
// todo : récupérer le vrai id de l'utilisateur
|
|
||||||
let user_id = Uuid::new_v4();
|
|
||||||
ws.on_upgrade(move |socket| handle_socket(socket, app_state, user_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_socket(socket: WebSocket, app_state: AppState, user_id: Uuid) {
|
|
||||||
// .split() sépare la lecture de l'écriture
|
|
||||||
let (mut ws_sender, mut ws_receiver) = socket.split();
|
|
||||||
let (tx, mut rx) = mpsc::channel::<Message>(100);
|
|
||||||
|
|
||||||
// ID unique pour CETTE connexion spécifique (un utilisateur peut en avoir plusieurs)
|
|
||||||
let connection_id = Uuid::new_v4();
|
|
||||||
|
|
||||||
// 1. Enregistrement du client (Gestion du Vec)
|
|
||||||
let client = ConnectedClient {
|
|
||||||
user_id,
|
|
||||||
connection_id,
|
|
||||||
sender: tx,
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut clients = app_state.ws_hub.clients.write();
|
|
||||||
clients.entry(user_id).or_insert_with(Vec::new).push(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Tâche d'envoi (Hub -> Browser)
|
|
||||||
let mut send_task = tokio::spawn(async move {
|
|
||||||
while let Some(msg) = rx.recv().await {
|
|
||||||
if ws_sender.send(msg).await.is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Tâche de réception (Browser -> Server)
|
|
||||||
let mut recv_task = tokio::spawn(async move {
|
|
||||||
while let Some(result) = ws_receiver.next().await {
|
|
||||||
if let Ok(msg) = result {
|
|
||||||
if let Message::Close(_) = msg { break; }
|
|
||||||
// Ici vous pourriez dispatcher les messages vers l'event_bus
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Attente de fin
|
|
||||||
tokio::select! {
|
|
||||||
_ = (&mut send_task) => recv_task.abort(),
|
|
||||||
_ = (&mut recv_task) => send_task.abort(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// 5. Nettoyage propre : on retire seulement CETTE connexion
|
|
||||||
let mut clients = app_state.ws_hub.clients.write();
|
|
||||||
if let Some(user_conns) = clients.get_mut(&user_id) {
|
|
||||||
user_conns.retain(|c| c.connection_id != connection_id);
|
|
||||||
// Optionnel : si le Vec est vide, on peut supprimer l'entrée pour libérer de la mémoire
|
|
||||||
if user_conns.is_empty() {
|
|
||||||
clients.remove(&user_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Connexion {} de l'utilisateur {} fermée.", connection_id, user_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Représente une connexion active
|
|
||||||
pub struct ConnectedClient {
|
|
||||||
pub user_id: Uuid,
|
|
||||||
pub connection_id: Uuid,
|
|
||||||
pub sender: mpsc::Sender<Message>,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Gestionnaire global des connexions WebSocket
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct WSHub {
|
|
||||||
pub clients: Arc<RwLock<HashMap<Uuid, Vec<ConnectedClient>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WSHub {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
clients: Arc::new(RwLock::new(HashMap::new()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,11 +3,10 @@ use crate::app::AppState;
|
|||||||
use crate::network::http::AppRouter;
|
use crate::network::http::AppRouter;
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
mod hub;
|
mod ws_handler;
|
||||||
|
|
||||||
pub use hub::WSHub;
|
|
||||||
|
|
||||||
pub fn setup_route() -> AppRouter {
|
pub fn setup_route() -> AppRouter {
|
||||||
AppRouter::new()
|
AppRouter::new()
|
||||||
.nest("/api", api::setup_route())
|
.nest("/api", api::setup_route())
|
||||||
|
.nest("/handler", ws_handler::setup_route())
|
||||||
}
|
}
|
||||||
78
src/network/http/web/ws_handler.rs
Normal file
78
src/network/http/web/ws_handler.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use axum::extract::{State, WebSocketUpgrade};
|
||||||
|
use axum::extract::ws::{Message, WebSocket};
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::routing::{delete, get, post, put};
|
||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::app::AppState;
|
||||||
|
use crate::hub::Client;
|
||||||
|
use crate::network::http::AppRouter;
|
||||||
|
|
||||||
|
pub fn setup_route() -> AppRouter {
|
||||||
|
AppRouter::new()
|
||||||
|
.route("/ws/", get(ws_handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ws_handler(
|
||||||
|
ws: WebSocketUpgrade,
|
||||||
|
State(app_state): State<AppState>
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
// todo : récupérer le vrai id de l'utilisateur
|
||||||
|
println!("Nouvelle connexion WebSocket !");
|
||||||
|
let user_id = Uuid::new_v4();
|
||||||
|
ws.on_upgrade(move |socket| handle_socket(socket, user_id, app_state))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_socket(socket: WebSocket, user_id: Uuid, app_state: AppState) {
|
||||||
|
println!("1Nouvelle session pour le client {}", user_id);
|
||||||
|
// .split() sépare la lecture de l'écriture
|
||||||
|
let (mut ws_sender, mut ws_receiver) = socket.split();
|
||||||
|
let (tx, mut rx) = mpsc::channel::<Message>(100);
|
||||||
|
|
||||||
|
// ID unique pour CETTE connexion spécifique (un utilisateur peut en avoir plusieurs)
|
||||||
|
let connection_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
// 1. Création de l'instance Client (ton objet intelligent)
|
||||||
|
let client = Client::new(user_id, connection_id, app_state.clone(), tx);
|
||||||
|
|
||||||
|
// 2. Enregistrement dans le Hub global pour être visible des autres
|
||||||
|
app_state.clients.add_client(client.clone());
|
||||||
|
|
||||||
|
// 3. Tâche d'ENVOI : On écoute la boîte aux lettres et on pousse vers le navigateur
|
||||||
|
let mut send_task = tokio::spawn(async move {
|
||||||
|
while let Some(msg) = rx.recv().await {
|
||||||
|
if ws_sender.send(msg).await.is_err() {
|
||||||
|
break; // Socket fermée
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Tâche de RÉCEPTION : On écoute le navigateur et on délègue au Client
|
||||||
|
let client_for_recv = client.clone();
|
||||||
|
let mut recv_task = tokio::spawn(async move {
|
||||||
|
while let Some(Ok(msg)) = ws_receiver.next().await {
|
||||||
|
if let Message::Close(_) = msg {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// On utilise la méthode métier que tu as définie !
|
||||||
|
client_for_recv.on_message(msg).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("2Nouvelle session pour le client {}", connection_id);
|
||||||
|
|
||||||
|
// 5. Attente de la fin de session (Bloque ici jusqu'à la déco)
|
||||||
|
tokio::select! {
|
||||||
|
_ = (&mut send_task) => (),
|
||||||
|
_ = (&mut recv_task) => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 6. Nettoyage
|
||||||
|
client.on_disconnect().await;
|
||||||
|
app_state.clients.remove_client(connection_id);
|
||||||
|
println!("Session terminée pour le client {}", connection_id);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user