Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ Cargo.lock

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb


# Added by cargo

/target
17 changes: 17 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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]
9 changes: 9 additions & 0 deletions client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "client"
version = "0.1.0"
edition = "2024"

[dependencies]
tokio.workspace = true
rand.workspace = true
clap.workspace = true
28 changes: 28 additions & 0 deletions client/src/cli.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
},
/// Leave the current server room
Leave,
/// Exit the application
Exit,
}
84 changes: 84 additions & 0 deletions client/src/handler.rs
Original file line number Diff line number Diff line change
@@ -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<OwnedWriteHalf>,
pub task: Option<JoinHandle<()>>,
}

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 <address>' 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.");
}
}
}
65 changes: 65 additions & 0 deletions client/src/main.rs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
7 changes: 7 additions & 0 deletions server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "server"
version = "0.1.0"
edition = "2024"

[dependencies]
tokio.workspace = true
61 changes: 61 additions & 0 deletions server/src/handler.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
mut rx: broadcast::Receiver<String>,
) {
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);
}
25 changes: 25 additions & 0 deletions server/src/main.rs
Original file line number Diff line number Diff line change
@@ -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::<String>(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;
});
}
}
18 changes: 18 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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");
}