From da43b479c3907be5c6b40836390dbd4d3bc3e4a6 Mon Sep 17 00:00:00 2001 From: Jayant Date: Fri, 20 Mar 2026 00:09:23 +0530 Subject: [PATCH] Working app --- .gitignore | 5 +++ Cargo.toml | 17 +++++++++ client/Cargo.toml | 9 +++++ client/src/cli.rs | 28 +++++++++++++++ client/src/handler.rs | 84 +++++++++++++++++++++++++++++++++++++++++++ client/src/main.rs | 65 +++++++++++++++++++++++++++++++++ server/Cargo.toml | 7 ++++ server/src/handler.rs | 61 +++++++++++++++++++++++++++++++ server/src/main.rs | 25 +++++++++++++ src/main.rs | 18 ++++++++++ 10 files changed, 319 insertions(+) create mode 100644 Cargo.toml create mode 100644 client/Cargo.toml create mode 100644 client/src/cli.rs create mode 100644 client/src/handler.rs create mode 100644 client/src/main.rs create mode 100644 server/Cargo.toml create mode 100644 server/src/handler.rs create mode 100644 server/src/main.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore index 6985cf1..196e176 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + + +# Added by cargo + +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d43e7c0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "simple-chat" +version = "0.1.0" +edition = "2024" + +[workspace] +members = [ + "server", + "client", +] + +[workspace.dependencies] +tokio = { version = "1.50.0", features = ["full"] } +rand = "0.10.0" +clap = { version = "4.5.31", features = ["derive"] } + +[dependencies] diff --git a/client/Cargo.toml b/client/Cargo.toml new file mode 100644 index 0000000..62c9905 --- /dev/null +++ b/client/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "client" +version = "0.1.0" +edition = "2024" + +[dependencies] +tokio.workspace = true +rand.workspace = true +clap.workspace = true diff --git a/client/src/cli.rs b/client/src/cli.rs new file mode 100644 index 0000000..381c287 --- /dev/null +++ b/client/src/cli.rs @@ -0,0 +1,28 @@ +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[command(name = "chat-client")] +#[command(about = "A simple chat client CLI", long_about = None)] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Connect to a server + Connect { + /// The IP address and port (e.g., 127.0.0.1:8080) + address: String, + }, + /// Send a message to the server + Send { + /// The message to send + #[arg(num_args = 1..)] + message: Vec, + }, + /// Leave the current server room + Leave, + /// Exit the application + Exit, +} diff --git a/client/src/handler.rs b/client/src/handler.rs new file mode 100644 index 0000000..7d5d2bd --- /dev/null +++ b/client/src/handler.rs @@ -0,0 +1,84 @@ +use tokio::net::TcpStream; +use tokio::net::tcp::OwnedWriteHalf; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use std::io::{self, Write}; +use tokio::task::JoinHandle; + +pub struct ChatHandler { + pub writer: Option, + pub task: Option>, +} + +impl ChatHandler { + pub fn new() -> Self { + Self { + writer: None, + task: None, + } + } + + pub async fn connect(&mut self, address: &str, client_user: &str) -> io::Result<()> { + if self.writer.is_some() { + println!("Already connected. Use 'leave' first."); + return Ok(()); + } + + let mut stream = TcpStream::connect(address).await?; + println!("Connected to {}", address); + + // Send client ID immediately + stream.write_all(format!("{}\n", client_user).as_bytes()).await?; + + let (reader, writer) = stream.into_split(); + self.writer = Some(writer); + + // Spawn task to handle incoming messages + let handle = tokio::spawn(async move { + let mut server_reader = BufReader::new(reader); + let mut server_line = String::new(); + loop { + server_line.clear(); + match server_reader.read_line(&mut server_line).await { + Ok(0) => { + println!("\nDisconnected from server."); + break; + } + Ok(_) => { + print!("\r{}", server_line); + print!("> "); + io::stdout().flush().unwrap(); + } + Err(_) => break, + } + } + }); + self.task = Some(handle); + Ok(()) + } + + pub async fn send(&mut self, message: &str) -> io::Result<()> { + if let Some(ref mut w) = self.writer { + if let Err(e) = w.write_all(format!("{}\n", message).as_bytes()).await { + println!("Failed to send message: {}", e); + self.leave(); + return Err(e); + } + Ok(()) + } else { + println!("Not connected. Use 'connect
' first."); + Ok(()) + } + } + + pub fn leave(&mut self) { + if self.writer.is_some() { + println!("Left server room"); + self.writer = None; + if let Some(h) = self.task.take() { + h.abort(); + } + } else { + println!("Not connected."); + } + } +} diff --git a/client/src/main.rs b/client/src/main.rs new file mode 100644 index 0000000..effba82 --- /dev/null +++ b/client/src/main.rs @@ -0,0 +1,65 @@ +mod cli; +mod handler; + +use clap::Parser; +use rand::distr::{Alphanumeric, SampleString}; +use tokio::io::{AsyncBufReadExt, BufReader}; +use std::io::{self, Write}; +use cli::{Cli, Commands}; +use handler::ChatHandler; + +#[tokio::main] +async fn main() { + let client_user = Alphanumeric.sample_string(&mut rand::rng(), 16); + println!("Chat Client Started."); + println!("User ID: {}", client_user); + println!("Type 'help' for available commands."); + + let mut stdin = BufReader::new(tokio::io::stdin()); + let mut stdin_line = String::new(); + let mut handler = ChatHandler::new(); + + loop { + print!("> "); + io::stdout().flush().unwrap(); + + stdin_line.clear(); + if stdin.read_line(&mut stdin_line).await.unwrap() == 0 { + break; + } + + let input = stdin_line.trim(); + if input.is_empty() { + continue; + } + + let args = std::iter::once("chat-client").chain(input.split_whitespace()); + + let cli = match Cli::try_parse_from(args) { + Ok(cli) => cli, + Err(e) => { + println!("{}", e); + continue; + } + }; + + match cli.command { + Commands::Connect { address } => { + if let Err(e) = handler.connect(&address, &client_user).await { + println!("Connection failed: {}", e); + } + } + Commands::Send { message } => { + let full_message = message.join(" "); + let _ = handler.send(&full_message).await; + } + Commands::Leave => { + handler.leave(); + } + Commands::Exit => { + println!("Exiting..."); + break; + } + } + } +} \ No newline at end of file diff --git a/server/Cargo.toml b/server/Cargo.toml new file mode 100644 index 0000000..e9bd2d9 --- /dev/null +++ b/server/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "server" +version = "0.1.0" +edition = "2024" + +[dependencies] +tokio.workspace = true diff --git a/server/src/handler.rs b/server/src/handler.rs new file mode 100644 index 0000000..4db3429 --- /dev/null +++ b/server/src/handler.rs @@ -0,0 +1,61 @@ +use tokio::net::TcpStream; +use tokio::sync::broadcast; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; + + +pub async fn handle_client( + mut socket: TcpStream, + tx: broadcast::Sender, + mut rx: broadcast::Receiver, +) { + let (reader, mut writer) = socket.split(); + let mut reader = BufReader::new(reader); + let mut line = String::new(); + + // Read the first line as the client's user code + let client_user = match reader.read_line(&mut line).await { + Ok(n) if n > 0 => { + let id = line.trim().to_string(); + println!("Client {} connected to the server room", id); + line.clear(); + id + } + _ => return, + }; + + loop { + tokio::select! { + // Receive from client and broadcast + result = reader.read_line(&mut line) => { + match result { + Ok(0) => break, + Ok(_) => { + let broadcast_msg = format!("{} > {}", client_user, line); + if let Err(e) = tx.send(broadcast_msg) { + eprintln!("Broadcast error: {}", e); + } + line.clear(); + } + Err(_) => break, + } + } + // Receive from broadcast and send to client + result = rx.recv() => { + match result { + Ok(msg) => { + if let Err(e) = writer.write_all(msg.as_bytes()).await { + eprintln!("Write error: {}", e); + break; + } + } + Err(broadcast::error::RecvError::Lagged(_)) => { + continue; + } + Err(_) => break, + } + } + } + } + + println!("Client {} left server room", client_user); +} diff --git a/server/src/main.rs b/server/src/main.rs new file mode 100644 index 0000000..4328120 --- /dev/null +++ b/server/src/main.rs @@ -0,0 +1,25 @@ +mod handler; + +use tokio::net::TcpListener; +use tokio::sync::broadcast; +use handler::handle_client; + +#[tokio::main] +async fn main() { + let addr = "127.0.0.1:8080"; + let listener = TcpListener::bind(addr).await.unwrap(); + + let (tx, _rx) = broadcast::channel::(10); + + println!("Server running on {}", addr); + + loop { + let (socket, _addr) = listener.accept().await.unwrap(); + let tx = tx.clone(); + let rx = tx.subscribe(); + + tokio::spawn(async move { + handle_client(socket, tx, rx).await; + }); + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b28ba6d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,18 @@ +fn main() { + println!("Simple Chat - Project Usage Guide"); + println!("====================================\n"); + + println!("To get the chat system running, follow these steps in separate terminals:\n"); + + println!("1. Start the Server:"); + println!(" cargo run -p server\n"); + + println!("2. Start the first Client:"); + println!(" cargo run -p client\n"); + + println!("3. Start the second Client:"); + println!(" cargo run -p client\n"); + + println!("Once connected, any message typed in one client will be broadcast to all other connected clients."); + println!("The server runs locally on 127.0.0.1:8080.\n"); +}