Skip to content

Instantly share code, notes, and snippets.

@JacobMJones
Created December 21, 2023 03:28
Show Gist options
  • Save JacobMJones/5cfe20ae3a916f2e1f4deda32738444e to your computer and use it in GitHub Desktop.
Save JacobMJones/5cfe20ae3a916f2e1f4deda32738444e to your computer and use it in GitHub Desktop.
rust server updated commented
//TcpListener: A TCP socket server, listening for connections from TCP clients.
//TcpStream: Represents a TCP connection to a remote server, used for sending and receiving data.
use tokio::net::{TcpListener, TcpStream};
//Connection: Manages a connection to a mini-Redis server, handling the sending and receiving of data frames.
//Frame: Represents a frame in the Redis protocol, which is a basic unit of communication in Redis, containing commands or data.
use mini_redis::{Connection, Frame};
use mini_redis::Command::{self, Get, Set};
// Bytes: A reference-counted slice of bytes, commonly used for efficiently handling and sharing data without copying it.
use bytes::Bytes;
//HashMap: A hash table data structure that stores key-value pairs. It allows for fast retrieval of values based on their unique keys.
use std::collections::HashMap;
//Atomic Reference Counting. It's a thread-safe reference-counting pointer used to share immutable data between multiple threads.
//Its primary purpose is to enable multiple threads to hold references to data
use std::sync::Arc;
//This is a compiler directive that disables the warning for using non-camel case type names. I
#[allow(non_camel_case_types)]
//Type alias to compose several types: Arc, Tokio's mutex, and a Hashmap that maps strings to byte values
//player positions is a hash map. where player position data will be stored.
//Basically Arc and tokio control the access to the hash
type player_positions = Arc<tokio::sync::Mutex<HashMap<String, Bytes>>>;
//TCP server entry point that listens for client connections on a specific port.
//It handles incoming connections concurrently by spawning separate asynchronous tasks for each connected client.
#[tokio::main]
async fn main() {
// Binds a TCP listener to the local address 127.0.0.1 on port 6379.
// This listener will accept incoming TCP connections.
let listener = TcpListener::bind("127.0.0.1:6379").await.unwrap(); // let listener = TcpListener::bind("0.0.0.0:6379").await.unwrap();
println!("Listening");
// Creates a new, shared (Arc-wrapped), thread-safe (Mutex-wrapped) HashMap.
// This HashMap will store player positions, with each player's IP as the key.
let player_positions: Arc<tokio::sync::Mutex<HashMap<String, Bytes>>> = Arc::new(tokio::sync::Mutex::new(HashMap::new()));
//infinite loop to continuously listen for and accept new connections.
loop {
// Accepts a new connection. This will wait (asynchronously) until a new client connects.
// 'socket' is the connected socket, 'addr' is the address of the connecting client.
let (socket, addr) = listener.accept().await.unwrap();
// Checks if the connecting IP address is already connected.
for (ip, _) in player_positions.lock().await.iter() {
if ip == &addr.ip().to_string() {
println!("Already connected");
continue;
}
}
println!("Accepted from: {}", addr.ip());
// Clones the Arc, increasing the reference count, to safely share player_positions
// across multiple threads.
let player_positions = player_positions.clone(); // is vector: ip, position
// Spawns a new asynchronous task for each client connection.
// This allows handling multiple clients concurrently.
tokio::spawn(async move {
// Calls the 'process' function, passing the socket, shared player_positions, and client address.
// Each client will be processed in its own asynchronous task.
process(socket, player_positions, addr).await;
});
}
}
async fn process(socket: TcpStream, player_positions: player_positions, sender: std::net::SocketAddr) {
let screen_size = [300, 300];
let mut connection = Connection::new(socket);
let player_movement: HashMap<&str, [i8; 2]> = HashMap::from([("w", [0, 1]), ("a", [-1, 0]), ("s", [0, -1]), ("d", [1, 0])]);
let mut player_positions = player_positions.lock().await;
while let Some(frame) = connection.read_frame().await.unwrap() {
// println!("GOT: {:?}", frame);
let response = match Command::from_frame(frame).unwrap() {
Set(cmd) => {
//TODO: Make sure when setting a player position, it doesn't go out of bounds
player_positions.insert(sender.ip().to_string(), cmd.value().clone()); //sets player position based on sender ip
//println!("Set command received. Updated player_positions: {:?}", player_positions);
Frame::Simple("OK".to_string()) // throwaway response
}
Get(cmd) => {
if cmd.key() == "PUD" { // returns all the player positions
let mut player_update = String::new();
for (key, value) in player_positions.iter() {
player_update.push_str(&format!("{}:{};", key, String::from_utf8(value.to_vec()).unwrap()));
}
Frame::Bulk(player_update.into())
} else if cmd.key() == "w" || cmd.key() == "a" || cmd.key() == "s" || cmd.key() == "d" { // handles player movement
println!("Get command received: {:?}", cmd);
let mut player_pos_array = split_coords(String::from_utf8(player_positions.get(&sender.ip().to_string()).unwrap().to_vec()).unwrap()).await;
let player_movement = player_movement.get(cmd.key()).unwrap();
if player_movement[0] == 1 { // this ugly shit is because we have to subtract i8 from u8, which is not allowed. Also makes sure bounds are checked.
if player_pos_array[0] < screen_size[0] {
player_pos_array[0] += 1;
}
}
if player_movement[0] == -1 {
if player_pos_array[0] > 0 {
player_pos_array[0] -= 1;
}
}
if player_movement[1] == 1 {
if player_pos_array[1] < screen_size[1] {
player_pos_array[1] += 1;
}
}
if player_movement[1] == -1 {
if player_pos_array[1] > 0 {
player_pos_array[1] -= 1;
}
}
let player_pos = Bytes::from(format!("{},{}", player_pos_array[0], player_pos_array[1]));
player_positions.insert(sender.ip().to_string(), player_pos.clone()); // updates database with new player position
Frame::Bulk(player_pos.clone()) // returns new position
} else {
// here if get bytes do not match to PUD or directional keys
println!("Get command received: {:?}", cmd);
Frame::Null
}
}
cmd => panic!("unimplemented {:?}", cmd),
};
// Write the response to the client
connection.write_frame(&response).await.unwrap();
}
}
async fn split_coords(text: String) -> [u16; 2] { // splits bytes like "0,300" into [0, 300]
let mut split_text = text.split(|x| x == ',');
[split_text.next().unwrap().trim().parse::<u16>().unwrap(), split_text.next().unwrap().trim().parse::<u16>().unwrap()]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment