mirror of
https://codeberg.org/JasterV/chat_rooms.rs.git
synced 2026-04-26 18:20:03 +00:00
first commit
This commit is contained in:
commit
a104cef2a5
12 changed files with 529 additions and 0 deletions
116
client/.gitignore
vendored
Normal file
116
client/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env*.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node
|
||||
28
client/package-lock.json
generated
Normal file
28
client/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "client",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"bootstrap": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.3.tgz",
|
||||
"integrity": "sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ=="
|
||||
},
|
||||
"jquery": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
|
||||
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
|
||||
},
|
||||
"net": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz",
|
||||
"integrity": "sha1-0XV+yaf7I3HYPPR1XOPifhCCk4g="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
|
||||
}
|
||||
}
|
||||
}
|
||||
18
client/package.json
Normal file
18
client/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "client",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bootstrap": "^4.5.3",
|
||||
"jquery": "^3.5.1",
|
||||
"net": "^1.0.2",
|
||||
"node-fetch": "^2.6.1"
|
||||
}
|
||||
}
|
||||
38
client/src/index.html
Normal file
38
client/src/index.html
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Chat</title>
|
||||
<script src="main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<section class="init-room">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="exampleInputEmail1">Room Id</label>
|
||||
<input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp">
|
||||
<small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="exampleInputPassword1">Password</label>
|
||||
<input type="password" class="form-control" id="exampleInputPassword1">
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" id="exampleCheck1">
|
||||
<label class="form-check-label" for="exampleCheck1">Check me out</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</section>
|
||||
<section style="display: none;">
|
||||
<div class="chat">
|
||||
<div class="chat-header"></div>
|
||||
<div class="chat-body">
|
||||
|
||||
</div>
|
||||
<div class="chat-input"></div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
12
client/src/main.js
Normal file
12
client/src/main.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
require('bootstrap');
|
||||
require('bootstrap/dist/css/bootstrap.css')
|
||||
const net = require('net');
|
||||
|
||||
// const client = new net.Socket();
|
||||
// connector.connect(PORT, HOST, function () {
|
||||
// console.log('CONNECTED TO: ' + HOST + ':' + PORT);
|
||||
// connector.write()
|
||||
// });
|
||||
// connector.on('data', function (addr) {
|
||||
// console.log('DATA: ' + data);
|
||||
// });
|
||||
13
server/.gitignore
vendored
Normal file
13
server/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Created by https://www.toptal.com/developers/gitignore/api/rust
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=rust
|
||||
|
||||
### Rust ###
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/rust
|
||||
24
server/Cargo.toml
Normal file
24
server/Cargo.toml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "server"
|
||||
version = "0.1.0"
|
||||
authors = ["JasterV <victorcoder2@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
name = "lib"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "rest-api"
|
||||
path = "src/bin.rs"
|
||||
|
||||
[dependencies]
|
||||
rocket = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
|
||||
[dependencies.rocket_contrib]
|
||||
version = "0.4"
|
||||
default-features = false
|
||||
features = ["json"]
|
||||
89
server/src/bin.rs
Normal file
89
server/src/bin.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#![feature(proc_macro_hygiene, decl_macro)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
extern crate uuid;
|
||||
|
||||
use lib::rooms::rooms_map::RoomsMap;
|
||||
use rocket::{
|
||||
http::{RawStr, Status},
|
||||
response::status::Custom,
|
||||
State,
|
||||
};
|
||||
use rocket_contrib::json::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RoomInfo {
|
||||
addr: SocketAddr,
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[get("/rooms/<id>")]
|
||||
fn get_addr(id: &RawStr, rooms: State<Arc<Mutex<RoomsMap>>>) -> Result<String, Custom<String>> {
|
||||
let arc_clone = _get_state_arc(&rooms);
|
||||
let rooms = arc_clone.lock().unwrap();
|
||||
match rooms.get_addr(id) {
|
||||
Ok(addr) => Ok(addr.to_string()),
|
||||
Err(_) => Err(Custom(
|
||||
Status::ImATeapot,
|
||||
"Can't get the address".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/rooms?create")]
|
||||
fn create_room(state: State<Arc<Mutex<RoomsMap>>>) -> Result<Json<RoomInfo>, Custom<String>> {
|
||||
let arc_clone = _get_state_arc(&state);
|
||||
let mut rooms = arc_clone.lock().unwrap();
|
||||
let id = Uuid::new_v4().to_string();
|
||||
match rooms.start_room(id.clone()) {
|
||||
Ok(addr) => {
|
||||
_close_timeout(id.clone(), &state);
|
||||
Ok(Json(RoomInfo { addr, id }))
|
||||
}
|
||||
Err(message) => Err(Custom(Status::Locked, message)),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/rooms/<id>")]
|
||||
fn close_room(id: String, state: State<Arc<Mutex<RoomsMap>>>) -> Result<String, Custom<String>> {
|
||||
let arc_clone = _get_state_arc(&state);
|
||||
let mut rooms = arc_clone.lock().unwrap();
|
||||
match rooms.close_room(id.clone()) {
|
||||
Ok(_) => Ok(id),
|
||||
Err(message) => Err(Custom(Status::Gone, message)),
|
||||
}
|
||||
}
|
||||
|
||||
fn _get_state_arc<T: Send + Sync>(state: &State<Arc<Mutex<T>>>) -> Arc<Mutex<T>> {
|
||||
let arc = state.inner();
|
||||
let arc_clone = Arc::clone(arc);
|
||||
arc_clone
|
||||
}
|
||||
|
||||
fn _close_timeout(id: String, state: &State<Arc<Mutex<RoomsMap>>>) {
|
||||
let arc_clone = _get_state_arc(state);
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_secs(RoomsMap::ROOMS_TIMEOUT));
|
||||
let mut rooms = arc_clone.lock().unwrap();
|
||||
rooms.close_room(id).ok();
|
||||
});
|
||||
}
|
||||
|
||||
fn main() {
|
||||
rocket::ignite()
|
||||
.mount("/", routes![get_addr, close_room, create_room])
|
||||
.manage(Arc::new(Mutex::new(RoomsMap::new())))
|
||||
.launch();
|
||||
}
|
||||
1
server/src/lib.rs
Normal file
1
server/src/lib.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod rooms;
|
||||
2
server/src/rooms/mod.rs
Normal file
2
server/src/rooms/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod room;
|
||||
pub mod rooms_map;
|
||||
132
server/src/rooms/room.rs
Normal file
132
server/src/rooms/room.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::net::{SocketAddr, TcpListener, TcpStream};
|
||||
use std::thread;
|
||||
use std::{
|
||||
string::FromUtf8Error,
|
||||
sync::mpsc::{channel, sync_channel, Receiver, SendError, Sender, SyncSender},
|
||||
};
|
||||
|
||||
pub struct Room {
|
||||
rx: Receiver<(String, TcpStream)>,
|
||||
tx: Sender<(String, TcpStream)>,
|
||||
clients: Vec<TcpStream>,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
pub const MAX_CLIENTS: usize = 15;
|
||||
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = channel();
|
||||
Room {
|
||||
tx,
|
||||
rx,
|
||||
clients: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_broadcast(&mut self, message: String, addr: SocketAddr) {
|
||||
self.clients = self
|
||||
.clients
|
||||
.iter()
|
||||
.filter_map(|client| {
|
||||
let buff = message.clone().into_bytes();
|
||||
if addr == client.local_addr().unwrap() {
|
||||
let mut client = client.try_clone().unwrap();
|
||||
client.write_all(&buff).map(|_| client).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
pub fn start(mut self) -> Result<RoomController, String> {
|
||||
match TcpListener::bind("127.0.0.1:0") {
|
||||
Ok(listener) => {
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.expect("Error setting non blocking");
|
||||
let (tx, rx) = sync_channel(1);
|
||||
let listener = listener.try_clone().unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
thread::spawn(move || {
|
||||
self._start(listener, rx);
|
||||
});
|
||||
Ok(RoomController { addr, closer: tx })
|
||||
}
|
||||
Err(_) => Err(String::from("Error binding the socket")),
|
||||
}
|
||||
}
|
||||
|
||||
fn _shutdown(&mut self) {
|
||||
self.clients.iter().for_each(|client| {
|
||||
client.shutdown(std::net::Shutdown::Both).unwrap();
|
||||
});
|
||||
self.clients = vec![];
|
||||
}
|
||||
|
||||
fn _start(&mut self, listener: TcpListener, closer: Receiver<()>) {
|
||||
loop {
|
||||
//ON ACCEPT
|
||||
if let Ok((mut socket, addr)) = listener.accept() {
|
||||
let tx = Sender::clone(&self.tx);
|
||||
if self.clients.len() < Room::MAX_CLIENTS {
|
||||
println!("Client {} connected to room!", addr);
|
||||
self.clients.push(socket.try_clone().unwrap());
|
||||
thread::spawn(move || Self::_listen_child(socket, tx));
|
||||
} else {
|
||||
socket.write_all(b"The room is full").unwrap();
|
||||
}
|
||||
}
|
||||
//ON CLOSE
|
||||
if let Ok(_) = closer.try_recv() {
|
||||
self._shutdown();
|
||||
break;
|
||||
}
|
||||
// ON MESSAGE
|
||||
if let Ok((msg, socket)) = self.rx.try_recv() {
|
||||
self.send_broadcast(msg, socket.local_addr().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _listen_child(mut socket: TcpStream, tx: Sender<(String, TcpStream)>) {
|
||||
loop {
|
||||
let mut buffer = vec![0; 1024];
|
||||
match socket.read(&mut buffer) {
|
||||
Ok(_) => {
|
||||
if let Ok(message) = Self::parse_request(buffer) {
|
||||
if message.len() <= 0 {
|
||||
break;
|
||||
}
|
||||
tx.send((message, socket.try_clone().unwrap())).unwrap();
|
||||
}
|
||||
}
|
||||
Err(ref err) if err.kind() == ErrorKind::WouldBlock => (),
|
||||
Err(_) => {
|
||||
println!("closing connection with: {}", socket.local_addr().unwrap());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_request(req: Vec<u8>) -> Result<String, FromUtf8Error> {
|
||||
String::from_utf8(req.into_iter().take_while(|&x| x != 0).collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RoomController {
|
||||
addr: SocketAddr,
|
||||
closer: SyncSender<()>,
|
||||
}
|
||||
|
||||
impl RoomController {
|
||||
pub fn addr(&self) -> SocketAddr {
|
||||
self.addr
|
||||
}
|
||||
|
||||
pub fn shutdown_room(&self) -> Result<(), SendError<()>> {
|
||||
Ok(self.closer.send(())?)
|
||||
}
|
||||
}
|
||||
56
server/src/rooms/rooms_map.rs
Normal file
56
server/src/rooms/rooms_map.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use std::collections::HashMap;
|
||||
use std::io::{self, ErrorKind};
|
||||
use std::net::{SocketAddr};
|
||||
use super::room::{Room, RoomController};
|
||||
|
||||
pub struct RoomsMap(HashMap<String, RoomController>);
|
||||
|
||||
impl RoomsMap {
|
||||
pub const MAX_ROOMS: usize = 10;
|
||||
pub const ROOMS_TIMEOUT: u64 = 60 * 10; // timeout in seconds
|
||||
|
||||
pub fn new() -> Self {
|
||||
RoomsMap(HashMap::new())
|
||||
}
|
||||
|
||||
pub fn get_addr(&self, id: &str) -> io::Result<SocketAddr> {
|
||||
match self.0.get(id) {
|
||||
Some(controller) => Ok(controller.addr()),
|
||||
None => Err(io::Error::new(ErrorKind::AddrNotAvailable, ":(")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_room(&mut self, id: String) -> Result<SocketAddr, String> {
|
||||
if self.0.contains_key(&id) {
|
||||
Err(String::from("Room already exists"))
|
||||
} else if self.0.len() >= Self::MAX_ROOMS {
|
||||
Err(format!("Can't create more than {} rooms", Self::MAX_ROOMS))
|
||||
} else {
|
||||
let room = Room::new();
|
||||
match room.start() {
|
||||
Ok(controller) => {
|
||||
let addr = controller.addr();
|
||||
self.0.insert(id, controller);
|
||||
Ok(addr)
|
||||
}
|
||||
Err(msg) => Err(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_room(&mut self, id: String) -> Result<(), String> {
|
||||
match self.0.get(&id) {
|
||||
Some(controller) => {
|
||||
match controller.shutdown_room() {
|
||||
Ok(_) => {
|
||||
self.0.remove_entry(&id);
|
||||
Ok(())
|
||||
},
|
||||
Err(_) => Err(String::from("Can't close the room"))
|
||||
}
|
||||
}
|
||||
None => Err(String::from("The room doesnt exists")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in a new issue