-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: websocket implementation (#214)
* feat: websocket implementation * chore: split up http platforms * feat: support binary data from websockets * fix: handle multiple messages on a connection * chore: update lockfile
- Loading branch information
1 parent
0af3403
commit 3134da9
Showing
12 changed files
with
2,106 additions
and
263 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[package] | ||
name = "ngyn-websocket" | ||
version = "0.1.1" | ||
edition = "2021" | ||
description = "Websocket Runtime Platform for ngyn web framework" | ||
license = "MIT" | ||
documentation = "https://ngyn.rs/docs" | ||
repository = "https://github.com/ngyn-rs/ngyn" | ||
homepage = "https://ngyn.rs" | ||
rust-version = "1.75" | ||
keywords = ["ngyn", "run-time", "platform", "websockets", "framework"] | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
ngyn_shared = { version = "0.4", path = "../shared" } | ||
tokio = { version = "1", features = ["full"] } | ||
websocket = "0.27.1" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
use core::fmt; | ||
use ngyn_shared::core::engine::{NgynPlatform, PlatformData}; | ||
use ngyn_shared::core::handler::RouteHandler; | ||
use ngyn_shared::server::response::ReadBytes; | ||
use ngyn_shared::server::NgynRequest; | ||
use std::io::ErrorKind; | ||
use std::net::ToSocketAddrs; | ||
use std::sync::{Arc, Mutex}; | ||
use websocket::sync::Writer; | ||
use websocket::Message; | ||
use websocket::{sync::Server, OwnedMessage}; | ||
|
||
#[derive(Default)] | ||
pub struct WebsocketApplication { | ||
data: PlatformData, | ||
clients: Arc<Mutex<Vec<Writer<std::net::TcpStream>>>>, | ||
} | ||
|
||
impl NgynPlatform for WebsocketApplication { | ||
fn data_mut(&mut self) -> &mut PlatformData { | ||
&mut self.data | ||
} | ||
} | ||
|
||
impl WebsocketApplication { | ||
/// add a route to handle | ||
pub fn route(&mut self, path: &str, handler: impl Into<RouteHandler>) { | ||
self.data_mut().add_route(path, None, handler.into()); | ||
} | ||
|
||
// Broadcast message to all connected clients | ||
pub fn broadcast(&self, message: &str) -> Result<(), websocket::WebSocketError> { | ||
let mut clients = self | ||
.clients | ||
.lock() | ||
.map_err(|_| websocket::WebSocketError::IoError(ErrorKind::InvalidData.into()))?; | ||
|
||
for client in clients.iter_mut() { | ||
client.send_message(&OwnedMessage::Text(message.to_string()))?; | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Listens for incoming connections and serves the application. | ||
/// | ||
/// ### Arguments | ||
/// | ||
/// * `addr` - The address to listen on. | ||
/// | ||
/// ### Returns | ||
/// | ||
/// A `Result` indicating success or failure. | ||
pub fn listen<A: ToSocketAddrs + fmt::Debug>( | ||
self, | ||
addr: A, | ||
) -> Result<(), Box<dyn std::error::Error>> { | ||
let server = Server::bind(addr)?; | ||
let data_handler = Arc::new(self.data); | ||
|
||
for request in server.filter_map(Result::ok) { | ||
let path = request.uri(); | ||
let clients = Arc::clone(&self.clients); | ||
let data_handler = data_handler.clone(); | ||
|
||
tokio::spawn(async move { | ||
if let Ok(client) = request.accept() { | ||
let (mut receiver, mut sender) = client.split().unwrap(); | ||
for message in receiver.incoming_messages() { | ||
match message { | ||
Ok(OwnedMessage::Text(_)) | Ok(OwnedMessage::Binary(_)) => { | ||
// Infallible at this point, so we can safely call `unwrap` | ||
let body = match message.unwrap() { | ||
OwnedMessage::Binary(data) => data, | ||
OwnedMessage::Text(data) => data.into(), | ||
_ => return, | ||
}; | ||
let mut req = NgynRequest::new(body); | ||
// default to index url if parsing fails | ||
*req.uri_mut() = path.parse().unwrap_or_default(); | ||
|
||
let mut response = data_handler.respond(req).await; | ||
|
||
if let Ok(data) = response.read_bytes().await { | ||
let message = | ||
if response.headers().get("Content-Type").is_none() { | ||
Message::text(String::from_utf8_lossy(&data)) | ||
} else { | ||
Message::binary(data.to_vec()) | ||
}; | ||
sender.send_message(&message).unwrap(); | ||
} | ||
} | ||
Ok(OwnedMessage::Close(_)) => { | ||
let message = Message::close(); | ||
sender.send_message(&message).unwrap(); | ||
break; | ||
} | ||
Ok(OwnedMessage::Ping(data)) => { | ||
let message = Message::pong(data); | ||
sender.send_message(&message).unwrap(); | ||
break; | ||
} | ||
Err(_) => break, | ||
_ => {} | ||
} | ||
} | ||
|
||
// Add client to the list of connected clients | ||
if let Ok(mut client_list) = clients.lock() { | ||
client_list.push(sender); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
Ok(()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
[package] | ||
name = "websocket" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[dependencies] | ||
ngyn = { version = "0.4.4", path = "../../crates/core" } | ||
ngyn-websocket = { version = "0.1.0", path = "../../crates/ws" } | ||
tokio = { version = "1", features = ["full"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
use ngyn::prelude::*; | ||
use ngyn_websocket::WebsocketApplication; | ||
|
||
#[tokio::main] | ||
async fn main() { | ||
let mut app = WebsocketApplication::default(); | ||
|
||
app.any("/", handler(|_| "Hello")); | ||
|
||
println!("Starting server at ws://127.0.0.1:8080"); | ||
|
||
let _ = app.listen("0.0.0.0:8080"); | ||
} |