Files
axum/examples/chat/src/main.rs
T

160 lines
5.0 KiB
Rust
Raw Normal View History

2021-08-02 16:42:10 -03:00
//! Example chat application.
//!
//! Run with
//!
2021-08-02 23:09:09 +02:00
//! ```not_rust
//! cargo run -p example-chat
2021-08-02 16:42:10 -03:00
//! ```
2021-10-05 08:50:27 +03:00
use axum::{
extract::{
ws::{Message, WebSocket, WebSocketUpgrade},
2022-08-17 17:13:31 +02:00
State,
2021-10-05 08:50:27 +03:00
},
response::{Html, IntoResponse},
routing::get,
2022-03-01 00:39:22 +01:00
Router,
2021-10-05 08:50:27 +03:00
};
use futures::{sink::SinkExt, stream::StreamExt};
2021-10-05 08:50:27 +03:00
use std::{
collections::HashSet,
sync::{Arc, Mutex},
};
use tokio::sync::broadcast;
2022-03-06 12:37:00 +01:00
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
2021-08-01 23:42:34 +03:00
// Our shared state
struct AppState {
2023-01-08 16:24:19 +01:00
// We require unique usernames. This tracks which usernames have been taken.
2021-08-01 23:42:34 +03:00
user_set: Mutex<HashSet<String>>,
2023-01-08 16:24:19 +01:00
// Channel used to send messages to all connected clients.
2021-08-01 23:42:34 +03:00
tx: broadcast::Sender<String>,
}
#[tokio::main]
async fn main() {
2022-03-06 12:37:00 +01:00
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "example_chat=trace".into()),
)
2022-03-06 12:37:00 +01:00
.with(tracing_subscriber::fmt::layer())
.init();
2022-03-01 17:30:09 +08:00
2023-01-08 16:24:19 +01:00
// Set up application state for use with with_state().
2021-08-01 23:42:34 +03:00
let user_set = Mutex::new(HashSet::new());
let (tx, _rx) = broadcast::channel(100);
let app_state = Arc::new(AppState { user_set, tx });
2022-11-18 12:02:58 +01:00
let app = Router::new()
.route("/", get(index))
2022-11-18 12:02:58 +01:00
.route("/websocket", get(websocket_handler))
.with_state(app_state);
2021-08-01 23:42:34 +03:00
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
2021-08-01 23:42:34 +03:00
.await
.unwrap();
tracing::debug!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
2021-08-01 23:42:34 +03:00
}
async fn websocket_handler(
ws: WebSocketUpgrade,
2022-08-17 17:13:31 +02:00
State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
ws.on_upgrade(|socket| websocket(socket, state))
}
2023-01-08 16:24:19 +01:00
// This function deals with a single websocket connection, i.e., a single
// connected client / user, for which we will spawn two independent tasks (for
// receiving / sending chat messages).
async fn websocket(stream: WebSocket, state: Arc<AppState>) {
2023-01-08 16:24:19 +01:00
// By splitting, we can send and receive at the same time.
2021-08-01 23:42:34 +03:00
let (mut sender, mut receiver) = stream.split();
2021-11-03 20:03:46 +01:00
// Username gets set in the receive loop, if it's valid.
2021-08-01 23:42:34 +03:00
let mut username = String::new();
// Loop until a text message is found.
while let Some(Ok(message)) = receiver.next().await {
if let Message::Text(name) = message {
2021-08-01 23:42:34 +03:00
// If username that is sent by client is not taken, fill username string.
check_username(&state, &mut username, &name);
2021-08-01 23:42:34 +03:00
// If not empty we want to quit the loop else we want to quit function.
if !username.is_empty() {
break;
} else {
// Only send our client that username is taken.
let _ = sender
.send(Message::Text(String::from("Username already taken.")))
.await;
2021-08-01 23:42:34 +03:00
return;
}
}
}
2023-01-08 16:24:19 +01:00
// We subscribe *before* sending the "joined" message, so that we will also
// display it to our client.
2021-08-01 23:42:34 +03:00
let mut rx = state.tx.subscribe();
2023-01-08 16:24:19 +01:00
// Now send the "joined" message to all subscribers.
2023-09-19 02:51:57 -04:00
let msg = format!("{username} joined.");
tracing::debug!("{msg}");
2021-08-01 23:42:34 +03:00
let _ = state.tx.send(msg);
2023-01-08 16:24:19 +01:00
// Spawn the first task that will receive broadcast messages and send text
// messages over the websocket to our client.
2021-08-01 23:42:34 +03:00
let mut send_task = tokio::spawn(async move {
while let Ok(msg) = rx.recv().await {
// In any websocket error, break loop.
if sender.send(Message::Text(msg)).await.is_err() {
2021-08-01 23:42:34 +03:00
break;
}
}
});
2023-01-08 16:24:19 +01:00
// Clone things we want to pass (move) to the receiving task.
2021-08-01 23:42:34 +03:00
let tx = state.tx.clone();
let name = username.clone();
2023-01-08 16:24:19 +01:00
// Spawn a task that takes messages from the websocket, prepends the user
// name, and sends them to all broadcast subscribers.
2021-08-01 23:42:34 +03:00
let mut recv_task = tokio::spawn(async move {
while let Some(Ok(Message::Text(text))) = receiver.next().await {
// Add username before message.
2023-09-19 02:51:57 -04:00
let _ = tx.send(format!("{name}: {text}"));
2021-08-01 23:42:34 +03:00
}
});
2023-01-08 16:24:19 +01:00
// If any one of the tasks run to completion, we abort the other.
2021-08-01 23:42:34 +03:00
tokio::select! {
_ = &mut send_task => recv_task.abort(),
_ = &mut recv_task => send_task.abort(),
2021-08-01 23:42:34 +03:00
};
2023-01-08 16:24:19 +01:00
// Send "user left" message (similar to "joined" above).
2023-09-19 02:51:57 -04:00
let msg = format!("{username} left.");
tracing::debug!("{msg}");
2021-08-01 23:42:34 +03:00
let _ = state.tx.send(msg);
2023-01-08 16:24:19 +01:00
// Remove username from map so new clients can take it again.
2021-08-01 23:42:34 +03:00
state.user_set.lock().unwrap().remove(&username);
}
fn check_username(state: &AppState, string: &mut String, name: &str) {
let mut user_set = state.user_set.lock().unwrap();
if !user_set.contains(name) {
user_set.insert(name.to_owned());
string.push_str(name);
}
}
// Include utf-8 file at **compile** time.
async fn index() -> Html<&'static str> {
Html(std::include_str!("../chat.html"))
2021-08-01 23:42:34 +03:00
}