mirror of
https://github.com/actix/examples
synced 2024-11-23 14:31:07 +01:00
update all websocket examples to v4
This commit is contained in:
parent
1b23e3ff3d
commit
4d8573c3fe
1119
Cargo.lock
generated
1119
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -53,8 +53,8 @@ members = [
|
|||||||
"template_engines/yarte",
|
"template_engines/yarte",
|
||||||
"template_engines/sailfish",
|
"template_engines/sailfish",
|
||||||
"websockets/autobahn",
|
"websockets/autobahn",
|
||||||
"websockets/chat-broker",
|
|
||||||
"websockets/chat",
|
"websockets/chat",
|
||||||
"websockets/tcp-chat",
|
"websockets/chat-broker",
|
||||||
"websockets/websocket",
|
"websockets/chat-tcp",
|
||||||
|
"websockets/echo",
|
||||||
]
|
]
|
||||||
|
@ -55,7 +55,7 @@ async fn main() -> io::Result<()> {
|
|||||||
.service(web::resource("/").route(web::get().to(api::index)))
|
.service(web::resource("/").route(web::get().to(api::index)))
|
||||||
.service(web::resource("/todo").route(web::post().to(api::create)))
|
.service(web::resource("/todo").route(web::post().to(api::create)))
|
||||||
.service(web::resource("/todo/{id}").route(web::post().to(api::update)))
|
.service(web::resource("/todo/{id}").route(web::post().to(api::update)))
|
||||||
.service(Files::new("/static", "./static/"))
|
.service(Files::new("/static", "./static"))
|
||||||
})
|
})
|
||||||
.bind(("127.0.0.1", 8080))?
|
.bind(("127.0.0.1", 8080))?
|
||||||
.workers(2)
|
.workers(2)
|
||||||
|
@ -8,7 +8,9 @@ name = "websocket-autobahn-server"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix = "0.10"
|
actix = "0.12"
|
||||||
actix-web = "3"
|
actix-web = "4.0.0-rc.3"
|
||||||
actix-web-actors = "3"
|
actix-web-actors = "4.0.0-beta.12"
|
||||||
env_logger = "0.8"
|
|
||||||
|
env_logger = "0.9"
|
||||||
|
log = "0.4"
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
# websocket
|
# WebSocket Autobahn Test Server
|
||||||
|
|
||||||
Websocket server for autobahn suite testing.
|
WebSocket server for the [Autobahn WebSocket protocol testsuite](https://github.com/crossbario/autobahn-testsuite).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### server
|
### Server
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd websockets/autobahn
|
cd websockets/autobahn
|
||||||
cargo run --bin websocket-autobahn-server
|
cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running Autobahn Test Suite
|
### Running Autobahn Test Suite
|
||||||
|
|
||||||
Running the autobahn test suite is easiest using the docker image
|
Running the autobahn test suite is easiest using the docker image as explained on the [autobahn test suite repo](https://github.com/crossbario/autobahn-testsuite#using-the-testsuite-docker-image).
|
||||||
as explained on the [autobahn-testsuite repo](https://github.com/crossbario/autobahn-testsuite#using-the-testsuite-docker-image).
|
|
||||||
|
|
||||||
First, start a server (see above). Then, run the test suite in fuzzingclient mode:
|
After starting the server, in the same directory, run the test suite in "fuzzing client" mode:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
],
|
],
|
||||||
"cases": ["*"],
|
"cases": ["*"],
|
||||||
"exclude-cases": [
|
"exclude-cases": [
|
||||||
|
"9.*",
|
||||||
"12.*",
|
"12.*",
|
||||||
"13.*"
|
"13.*"
|
||||||
],
|
],
|
||||||
|
@ -3,18 +3,17 @@ use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServ
|
|||||||
use actix_web_actors::ws;
|
use actix_web_actors::ws;
|
||||||
|
|
||||||
async fn ws_index(r: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
|
async fn ws_index(r: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
|
||||||
ws::start(WebSocket::new(), &r, stream)
|
ws::start(AutobahnWebSocket::default(), &r, stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WebSocket {}
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct AutobahnWebSocket;
|
||||||
|
|
||||||
impl Actor for WebSocket {
|
impl Actor for AutobahnWebSocket {
|
||||||
type Context = ws::WebsocketContext<Self>;
|
type Context = ws::WebsocketContext<Self>;
|
||||||
|
|
||||||
fn started(&mut self, _ctx: &mut Self::Context) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocket {
|
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for AutobahnWebSocket {
|
||||||
fn handle(
|
fn handle(
|
||||||
&mut self,
|
&mut self,
|
||||||
msg: Result<ws::Message, ws::ProtocolError>,
|
msg: Result<ws::Message, ws::ProtocolError>,
|
||||||
@ -37,23 +36,19 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebSocket {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
env_logger::init();
|
|
||||||
|
log::info!("starting HTTP server at http://localhost:9001");
|
||||||
|
|
||||||
HttpServer::new(|| {
|
HttpServer::new(|| {
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(middleware::Logger::default())
|
.wrap(middleware::Logger::default())
|
||||||
.service(web::resource("/").route(web::get().to(ws_index)))
|
.service(web::resource("/").route(web::get().to(ws_index)))
|
||||||
})
|
})
|
||||||
.bind("127.0.0.1:9001")?
|
.workers(2)
|
||||||
|
.bind(("127.0.0.1", 9001))?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -8,13 +8,12 @@ name = "server"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix = "0.10"
|
actix = "0.12"
|
||||||
actix-broker = "0.3.1"
|
actix-broker = "0.4"
|
||||||
actix-files = "0.4"
|
actix-files = "0.6.0-beta.16"
|
||||||
actix-rt = "1"
|
actix-web = "4.0.0-rc.3"
|
||||||
actix-web = "3"
|
actix-web-actors = "4.0.0-beta.12"
|
||||||
actix-web-actors = "3"
|
|
||||||
env_logger = "0.8"
|
env_logger = "0.9"
|
||||||
futures = "0.3"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
rand = "0.7"
|
rand = "0.8"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use log::info;
|
use actix_files::{Files, NamedFile};
|
||||||
|
use actix_web::{
|
||||||
use actix_files::Files;
|
middleware::Logger, web, App, Error, HttpRequest, HttpServer, Responder,
|
||||||
use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer};
|
};
|
||||||
use actix_web_actors::ws;
|
use actix_web_actors::ws;
|
||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
@ -10,28 +10,32 @@ mod session;
|
|||||||
|
|
||||||
use session::WsChatSession;
|
use session::WsChatSession;
|
||||||
|
|
||||||
async fn chat_route(
|
async fn index() -> impl Responder {
|
||||||
|
NamedFile::open_async("./static/index.html").await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn chat_ws(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
stream: web::Payload,
|
stream: web::Payload,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<impl Responder, Error> {
|
||||||
ws::start(WsChatSession::default(), &req, stream)
|
ws::start(WsChatSession::default(), &req, stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
.init();
|
|
||||||
|
|
||||||
let addr = ("127.0.0.1", 8080);
|
log::info!("starting HTTP server at http://localhost:8080");
|
||||||
|
|
||||||
let srv = HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.service(web::resource("/ws/").to(chat_route))
|
.service(web::resource("/").to(index))
|
||||||
.service(Files::new("/", "./static/").index_file("index.html"))
|
.service(web::resource("/ws").to(chat_ws))
|
||||||
|
.service(Files::new("/static", "./static"))
|
||||||
|
.wrap(Logger::default())
|
||||||
})
|
})
|
||||||
.bind(&addr)?;
|
.workers(2)
|
||||||
|
.bind(("127.0.0.1", 8080))?
|
||||||
info!("Starting http server: {}", &addr);
|
.run()
|
||||||
|
.await
|
||||||
srv.run().await
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use actix_broker::BrokerSubscribe;
|
use actix_broker::BrokerSubscribe;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use crate::message::{ChatMessage, JoinRoom, LeaveRoom, ListRooms, SendMessage};
|
use crate::message::{ChatMessage, JoinRoom, LeaveRoom, ListRooms, SendMessage};
|
||||||
|
|
||||||
type Client = Recipient<ChatMessage>;
|
type Client = Recipient<ChatMessage>;
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
use log::{debug, info};
|
use actix::{fut, prelude::*};
|
||||||
|
|
||||||
use actix::fut;
|
|
||||||
use actix::prelude::*;
|
|
||||||
use actix_broker::BrokerIssue;
|
use actix_broker::BrokerIssue;
|
||||||
use actix_web_actors::ws;
|
use actix_web_actors::ws;
|
||||||
|
|
||||||
use crate::message::{ChatMessage, JoinRoom, LeaveRoom, ListRooms, SendMessage};
|
use crate::{
|
||||||
use crate::server::WsChatServer;
|
message::{ChatMessage, JoinRoom, LeaveRoom, ListRooms, SendMessage},
|
||||||
|
server::WsChatServer,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct WsChatSession {
|
pub struct WsChatSession {
|
||||||
@ -84,7 +83,7 @@ impl Actor for WsChatSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
||||||
info!(
|
log::info!(
|
||||||
"WsChatSession closed for {}({}) in room {}",
|
"WsChatSession closed for {}({}) in room {}",
|
||||||
self.name.clone().unwrap_or_else(|| "anon".to_string()),
|
self.name.clone().unwrap_or_else(|| "anon".to_string()),
|
||||||
self.id,
|
self.id,
|
||||||
@ -115,7 +114,7 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsChatSession {
|
|||||||
Ok(msg) => msg,
|
Ok(msg) => msg,
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("WEBSOCKET MESSAGE: {:?}", msg);
|
log::debug!("WEBSOCKET MESSAGE: {:?}", msg);
|
||||||
|
|
||||||
match msg {
|
match msg {
|
||||||
ws::Message::Text(text) => {
|
ws::Message::Text(text) => {
|
||||||
|
@ -130,7 +130,7 @@
|
|||||||
const { location } = window
|
const { location } = window
|
||||||
|
|
||||||
const proto = location.protocol.startsWith('https') ? 'wss' : 'ws'
|
const proto = location.protocol.startsWith('https') ? 'wss' : 'ws'
|
||||||
const wsUri = `${proto}://${location.host}/ws/`
|
const wsUri = `${proto}://${location.host}/ws`
|
||||||
|
|
||||||
log('Connecting...')
|
log('Connecting...')
|
||||||
socket = new WebSocket(wsUri)
|
socket = new WebSocket(wsUri)
|
||||||
|
@ -26,6 +26,6 @@ log = "0.4"
|
|||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1.13.1", features = ["full"] }
|
||||||
tokio-util = { version = "0.6", features = ["codec"] }
|
tokio-util = { version = "0.6", features = ["codec"] }
|
||||||
tokio-stream = "0.1.8"
|
tokio-stream = "0.1.8"
|
@ -1,8 +1,10 @@
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use actix::*;
|
use actix::prelude::*;
|
||||||
use actix_files as fs;
|
use actix_files::NamedFile;
|
||||||
use actix_web::{http::header, web, App, Error, HttpRequest, HttpResponse, HttpServer};
|
use actix_web::{
|
||||||
|
middleware::Logger, web, App, Error, HttpRequest, HttpServer, Responder,
|
||||||
|
};
|
||||||
use actix_web_actors::ws;
|
use actix_web_actors::ws;
|
||||||
|
|
||||||
mod codec;
|
mod codec;
|
||||||
@ -11,15 +13,20 @@ mod session;
|
|||||||
|
|
||||||
/// How often heartbeat pings are sent
|
/// How often heartbeat pings are sent
|
||||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
/// How long before lack of client response causes a timeout
|
/// How long before lack of client response causes a timeout
|
||||||
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
async fn index() -> impl Responder {
|
||||||
|
NamedFile::open_async("./static/index.html").await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
/// Entry point for our route
|
/// Entry point for our route
|
||||||
async fn chat_route(
|
async fn chat_route(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
stream: web::Payload,
|
stream: web::Payload,
|
||||||
srv: web::Data<Addr<server::ChatServer>>,
|
srv: web::Data<Addr<server::ChatServer>>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<impl Responder, Error> {
|
||||||
ws::start(
|
ws::start(
|
||||||
WsChatSession {
|
WsChatSession {
|
||||||
id: 0,
|
id: 0,
|
||||||
@ -109,7 +116,7 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsChatSession {
|
|||||||
Ok(msg) => msg,
|
Ok(msg) => msg,
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("WEBSOCKET MESSAGE: {:?}", msg);
|
log::debug!("WEBSOCKET MESSAGE: {:?}", msg);
|
||||||
match msg {
|
match msg {
|
||||||
ws::Message::Ping(msg) => {
|
ws::Message::Ping(msg) => {
|
||||||
self.hb = Instant::now();
|
self.hb = Instant::now();
|
||||||
@ -235,19 +242,14 @@ async fn main() -> std::io::Result<()> {
|
|||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(web::Data::new(server.clone()))
|
.app_data(web::Data::new(server.clone()))
|
||||||
// redirect to websocket.html
|
// WebSocket UI HTML file
|
||||||
.service(web::resource("/").route(web::get().to(|| async {
|
.service(web::resource("/").to(index))
|
||||||
HttpResponse::Found()
|
|
||||||
.insert_header((header::LOCATION, "/static/websocket.html"))
|
|
||||||
.finish()
|
|
||||||
})))
|
|
||||||
// websocket
|
// websocket
|
||||||
.service(web::resource("/ws/").to(chat_route))
|
.service(web::resource("/ws").to(chat_route))
|
||||||
// static resources
|
.wrap(Logger::default())
|
||||||
.service(fs::Files::new("/static/", "static/"))
|
|
||||||
})
|
})
|
||||||
.bind(("127.0.0.1", 8080))?
|
.bind(("127.0.0.1", 8080))?
|
||||||
.workers(1)
|
.workers(2)
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
204
websockets/chat-tcp/static/index.html
Normal file
204
websockets/chat-tcp/static/index.html
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Websocket Chat Broker</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'] {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#log {
|
||||||
|
width: 30em;
|
||||||
|
height: 20em;
|
||||||
|
overflow: auto;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status {
|
||||||
|
padding: 0 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text {
|
||||||
|
width: 17em;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg--status {
|
||||||
|
/* a light yellow */
|
||||||
|
background-color: #ffffc9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg--message {
|
||||||
|
/* a light blue */
|
||||||
|
background-color: #d2f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg--error {
|
||||||
|
background-color: pink;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Chat!</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button id="connect">Connect</button>
|
||||||
|
<span>Status:</span>
|
||||||
|
<span id="status">disconnected</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="log"></div>
|
||||||
|
|
||||||
|
<form id="chatform">
|
||||||
|
<input type="text" id="text" />
|
||||||
|
<input type="submit" id="send" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Commands</h2>
|
||||||
|
<table style="border-spacing: 0.5em;">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>/list</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
list all available rooms
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>/join name</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
join room, if room does not exist, create new one
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>/name name</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
set session name
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>some message</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
just string, send message to all peers in same room
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const $status = document.querySelector('#status')
|
||||||
|
const $connectButton = document.querySelector('#connect')
|
||||||
|
const $log = document.querySelector('#log')
|
||||||
|
const $form = document.querySelector('#chatform')
|
||||||
|
const $input = document.querySelector('#text')
|
||||||
|
|
||||||
|
/** @type {WebSocket | null} */
|
||||||
|
var socket = null
|
||||||
|
|
||||||
|
function log(msg, type = 'status') {
|
||||||
|
$log.innerHTML += `<p class="msg msg--${type}">${msg}</p>`
|
||||||
|
$log.scrollTop += 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
disconnect()
|
||||||
|
|
||||||
|
const { location } = window
|
||||||
|
|
||||||
|
const proto = location.protocol.startsWith('https') ? 'wss' : 'ws'
|
||||||
|
const wsUri = `${proto}://${location.host}/ws`
|
||||||
|
|
||||||
|
log('Connecting...')
|
||||||
|
socket = new WebSocket(wsUri)
|
||||||
|
|
||||||
|
socket.onopen = () => {
|
||||||
|
log('Connected')
|
||||||
|
updateConnectionStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.onmessage = (ev) => {
|
||||||
|
log('Received: ' + ev.data, 'message')
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.onclose = () => {
|
||||||
|
log('Disconnected')
|
||||||
|
socket = null
|
||||||
|
updateConnectionStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
if (socket) {
|
||||||
|
log('Disconnecting...')
|
||||||
|
socket.close()
|
||||||
|
socket = null
|
||||||
|
|
||||||
|
updateConnectionStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConnectionStatus() {
|
||||||
|
if (socket) {
|
||||||
|
$status.style.backgroundColor = 'transparent'
|
||||||
|
$status.style.color = 'green'
|
||||||
|
$status.textContent = `connected`
|
||||||
|
$connectButton.innerHTML = 'Disconnect'
|
||||||
|
$input.focus()
|
||||||
|
} else {
|
||||||
|
$status.style.backgroundColor = 'red'
|
||||||
|
$status.style.color = 'white'
|
||||||
|
$status.textContent = 'disconnected'
|
||||||
|
$connectButton.textContent = 'Connect'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$connectButton.addEventListener('click', () => {
|
||||||
|
if (socket) {
|
||||||
|
disconnect()
|
||||||
|
} else {
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConnectionStatus()
|
||||||
|
})
|
||||||
|
|
||||||
|
$form.addEventListener('submit', (ev) => {
|
||||||
|
ev.preventDefault()
|
||||||
|
|
||||||
|
const text = $input.value
|
||||||
|
|
||||||
|
log('Sending: ' + text)
|
||||||
|
socket.send(text)
|
||||||
|
|
||||||
|
$input.value = ''
|
||||||
|
$input.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
updateConnectionStatus()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -8,15 +8,13 @@ name = "websocket-chat-server"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix = "0.10"
|
actix = "0.12"
|
||||||
actix-web = "3"
|
actix-files = "0.6.0-beta.16"
|
||||||
actix-web-actors = "3"
|
actix-web = "4.0.0-rc.3"
|
||||||
actix-files = "0.3"
|
actix-web-actors = "4.0.0-beta.12"
|
||||||
|
|
||||||
rand = "0.7"
|
env_logger = "0.9"
|
||||||
bytes = "0.5"
|
log = "0.4"
|
||||||
byteorder = "1.3"
|
rand = "0.8"
|
||||||
futures = "0.3"
|
|
||||||
env_logger = "0.8"
|
|
||||||
serde = "1"
|
serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
use std::sync::{
|
use std::{
|
||||||
atomic::{AtomicUsize, Ordering},
|
sync::{
|
||||||
Arc,
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
time::Instant,
|
||||||
};
|
};
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use actix::*;
|
use actix::*;
|
||||||
use actix_files as fs;
|
use actix_files::{Files, NamedFile};
|
||||||
use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder};
|
use actix_web::{
|
||||||
|
middleware::Logger, web, App, Error, HttpRequest, HttpResponse, HttpServer,
|
||||||
|
Responder,
|
||||||
|
};
|
||||||
use actix_web_actors::ws;
|
use actix_web_actors::ws;
|
||||||
|
|
||||||
mod server;
|
mod server;
|
||||||
|
mod session;
|
||||||
|
|
||||||
/// How often heartbeat pings are sent
|
async fn index() -> impl Responder {
|
||||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
NamedFile::open_async("./static/index.html").await.unwrap()
|
||||||
/// How long before lack of client response causes a timeout
|
}
|
||||||
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
|
||||||
|
|
||||||
/// Entry point for our websocket route
|
/// Entry point for our websocket route
|
||||||
async fn chat_route(
|
async fn chat_route(
|
||||||
@ -23,7 +28,7 @@ async fn chat_route(
|
|||||||
srv: web::Data<Addr<server::ChatServer>>,
|
srv: web::Data<Addr<server::ChatServer>>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
ws::start(
|
ws::start(
|
||||||
WsChatSession {
|
session::WsChatSession {
|
||||||
id: 0,
|
id: 0,
|
||||||
hb: Instant::now(),
|
hb: Instant::now(),
|
||||||
room: "Main".to_owned(),
|
room: "Main".to_owned(),
|
||||||
@ -35,229 +40,36 @@ async fn chat_route(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Displays and affects state
|
/// Displays state
|
||||||
async fn get_count(count: web::Data<Arc<AtomicUsize>>) -> impl Responder {
|
async fn get_count(count: web::Data<AtomicUsize>) -> impl Responder {
|
||||||
let current_count = count.fetch_add(1, Ordering::SeqCst);
|
let current_count = count.load(Ordering::SeqCst);
|
||||||
format!("Visitors: {}", current_count)
|
format!("Visitors: {}", current_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WsChatSession {
|
|
||||||
/// unique session id
|
|
||||||
id: usize,
|
|
||||||
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
|
||||||
/// otherwise we drop connection.
|
|
||||||
hb: Instant,
|
|
||||||
/// joined room
|
|
||||||
room: String,
|
|
||||||
/// peer name
|
|
||||||
name: Option<String>,
|
|
||||||
/// Chat server
|
|
||||||
addr: Addr<server::ChatServer>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Actor for WsChatSession {
|
|
||||||
type Context = ws::WebsocketContext<Self>;
|
|
||||||
|
|
||||||
/// Method is called on actor start.
|
|
||||||
/// We register ws session with ChatServer
|
|
||||||
fn started(&mut self, ctx: &mut Self::Context) {
|
|
||||||
// we'll start heartbeat process on session start.
|
|
||||||
self.hb(ctx);
|
|
||||||
|
|
||||||
// register self in chat server. `AsyncContext::wait` register
|
|
||||||
// future within context, but context waits until this future resolves
|
|
||||||
// before processing any other events.
|
|
||||||
// HttpContext::state() is instance of WsChatSessionState, state is shared
|
|
||||||
// across all routes within application
|
|
||||||
let addr = ctx.address();
|
|
||||||
self.addr
|
|
||||||
.send(server::Connect {
|
|
||||||
addr: addr.recipient(),
|
|
||||||
})
|
|
||||||
.into_actor(self)
|
|
||||||
.then(|res, act, ctx| {
|
|
||||||
match res {
|
|
||||||
Ok(res) => act.id = res,
|
|
||||||
// something is wrong with chat server
|
|
||||||
_ => ctx.stop(),
|
|
||||||
}
|
|
||||||
fut::ready(())
|
|
||||||
})
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stopping(&mut self, _: &mut Self::Context) -> Running {
|
|
||||||
// notify chat server
|
|
||||||
self.addr.do_send(server::Disconnect { id: self.id });
|
|
||||||
Running::Stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle messages from chat server, we simply send it to peer websocket
|
|
||||||
impl Handler<server::Message> for WsChatSession {
|
|
||||||
type Result = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: server::Message, ctx: &mut Self::Context) {
|
|
||||||
ctx.text(msg.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// WebSocket message handler
|
|
||||||
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsChatSession {
|
|
||||||
fn handle(
|
|
||||||
&mut self,
|
|
||||||
msg: Result<ws::Message, ws::ProtocolError>,
|
|
||||||
ctx: &mut Self::Context,
|
|
||||||
) {
|
|
||||||
let msg = match msg {
|
|
||||||
Err(_) => {
|
|
||||||
ctx.stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Ok(msg) => msg,
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("WEBSOCKET MESSAGE: {:?}", msg);
|
|
||||||
match msg {
|
|
||||||
ws::Message::Ping(msg) => {
|
|
||||||
self.hb = Instant::now();
|
|
||||||
ctx.pong(&msg);
|
|
||||||
}
|
|
||||||
ws::Message::Pong(_) => {
|
|
||||||
self.hb = Instant::now();
|
|
||||||
}
|
|
||||||
ws::Message::Text(text) => {
|
|
||||||
let m = text.trim();
|
|
||||||
// we check for /sss type of messages
|
|
||||||
if m.starts_with('/') {
|
|
||||||
let v: Vec<&str> = m.splitn(2, ' ').collect();
|
|
||||||
match v[0] {
|
|
||||||
"/list" => {
|
|
||||||
// Send ListRooms message to chat server and wait for
|
|
||||||
// response
|
|
||||||
println!("List rooms");
|
|
||||||
self.addr
|
|
||||||
.send(server::ListRooms)
|
|
||||||
.into_actor(self)
|
|
||||||
.then(|res, _, ctx| {
|
|
||||||
match res {
|
|
||||||
Ok(rooms) => {
|
|
||||||
for room in rooms {
|
|
||||||
ctx.text(room);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => println!("Something is wrong"),
|
|
||||||
}
|
|
||||||
fut::ready(())
|
|
||||||
})
|
|
||||||
.wait(ctx)
|
|
||||||
// .wait(ctx) pauses all events in context,
|
|
||||||
// so actor wont receive any new messages until it get list
|
|
||||||
// of rooms back
|
|
||||||
}
|
|
||||||
"/join" => {
|
|
||||||
if v.len() == 2 {
|
|
||||||
self.room = v[1].to_owned();
|
|
||||||
self.addr.do_send(server::Join {
|
|
||||||
id: self.id,
|
|
||||||
name: self.room.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.text("joined");
|
|
||||||
} else {
|
|
||||||
ctx.text("!!! room name is required");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"/name" => {
|
|
||||||
if v.len() == 2 {
|
|
||||||
self.name = Some(v[1].to_owned());
|
|
||||||
} else {
|
|
||||||
ctx.text("!!! name is required");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => ctx.text(format!("!!! unknown command: {:?}", m)),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let msg = if let Some(ref name) = self.name {
|
|
||||||
format!("{}: {}", name, m)
|
|
||||||
} else {
|
|
||||||
m.to_owned()
|
|
||||||
};
|
|
||||||
// send message to chat server
|
|
||||||
self.addr.do_send(server::ClientMessage {
|
|
||||||
id: self.id,
|
|
||||||
msg,
|
|
||||||
room: self.room.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ws::Message::Binary(_) => println!("Unexpected binary"),
|
|
||||||
ws::Message::Close(reason) => {
|
|
||||||
ctx.close(reason);
|
|
||||||
ctx.stop();
|
|
||||||
}
|
|
||||||
ws::Message::Continuation(_) => {
|
|
||||||
ctx.stop();
|
|
||||||
}
|
|
||||||
ws::Message::Nop => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WsChatSession {
|
|
||||||
/// helper method that sends ping to client every second.
|
|
||||||
///
|
|
||||||
/// also this method checks heartbeats from client
|
|
||||||
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
|
|
||||||
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
|
||||||
// check client heartbeats
|
|
||||||
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
|
||||||
// heartbeat timed out
|
|
||||||
println!("Websocket Client heartbeat failed, disconnecting!");
|
|
||||||
|
|
||||||
// notify chat server
|
|
||||||
act.addr.do_send(server::Disconnect { id: act.id });
|
|
||||||
|
|
||||||
// stop actor
|
|
||||||
ctx.stop();
|
|
||||||
|
|
||||||
// don't try to send a ping
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.ping(b"");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env_logger::init();
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
|
|
||||||
// App state
|
// set up applications state
|
||||||
// We are keeping a count of the number of visitors
|
// keep a count of the number of visitors
|
||||||
let app_state = Arc::new(AtomicUsize::new(0));
|
let app_state = Arc::new(AtomicUsize::new(0));
|
||||||
|
|
||||||
// Start chat server actor
|
// start chat server actor
|
||||||
let server = server::ChatServer::new(app_state.clone()).start();
|
let server = server::ChatServer::new(app_state.clone()).start();
|
||||||
|
|
||||||
// Create Http server with websocket support
|
log::info!("starting HTTP server at http://localhost:8080");
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.data(app_state.clone())
|
.app_data(web::Data::from(app_state.clone()))
|
||||||
.data(server.clone())
|
.app_data(web::Data::new(server.clone()))
|
||||||
// redirect to websocket.html
|
.service(web::resource("/").to(index))
|
||||||
.service(web::resource("/").route(web::get().to(|| {
|
.route("/count", web::get().to(get_count))
|
||||||
HttpResponse::Found()
|
.route("/ws", web::get().to(chat_route))
|
||||||
.header("LOCATION", "/static/websocket.html")
|
.service(Files::new("/static", "./static"))
|
||||||
.finish()
|
.wrap(Logger::default())
|
||||||
})))
|
|
||||||
.route("/count/", web::get().to(get_count))
|
|
||||||
// websocket
|
|
||||||
.service(web::resource("/ws/").to(chat_route))
|
|
||||||
// static resources
|
|
||||||
.service(fs::Files::new("/static/", "static/"))
|
|
||||||
})
|
})
|
||||||
|
.workers(2)
|
||||||
.bind(("127.0.0.1", 8080))?
|
.bind(("127.0.0.1", 8080))?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
|
@ -2,15 +2,16 @@
|
|||||||
//! And manages available rooms. Peers send messages to other peers in same
|
//! And manages available rooms. Peers send messages to other peers in same
|
||||||
//! room through `ChatServer`.
|
//! room through `ChatServer`.
|
||||||
|
|
||||||
use actix::prelude::*;
|
use std::{
|
||||||
use rand::{self, rngs::ThreadRng, Rng};
|
collections::{HashMap, HashSet},
|
||||||
|
sync::{
|
||||||
use std::sync::{
|
atomic::{AtomicUsize, Ordering},
|
||||||
atomic::{AtomicUsize, Ordering},
|
Arc,
|
||||||
Arc,
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use actix::prelude::*;
|
||||||
|
use rand::{self, rngs::ThreadRng, Rng};
|
||||||
|
|
||||||
/// Chat server sends this messages to session
|
/// Chat server sends this messages to session
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
@ -56,14 +57,17 @@ impl actix::Message for ListRooms {
|
|||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
#[rtype(result = "()")]
|
#[rtype(result = "()")]
|
||||||
pub struct Join {
|
pub struct Join {
|
||||||
/// Client id
|
/// Client ID
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
|
|
||||||
/// Room name
|
/// Room name
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `ChatServer` manages chat rooms and responsible for coordinating chat
|
/// `ChatServer` manages chat rooms and responsible for coordinating chat session.
|
||||||
/// session. implementation is super primitive
|
///
|
||||||
|
/// Implementation is very naïve.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct ChatServer {
|
pub struct ChatServer {
|
||||||
sessions: HashMap<usize, Recipient<Message>>,
|
sessions: HashMap<usize, Recipient<Message>>,
|
||||||
rooms: HashMap<String, HashSet<usize>>,
|
rooms: HashMap<String, HashSet<usize>>,
|
||||||
@ -118,7 +122,7 @@ impl Handler<Connect> for ChatServer {
|
|||||||
println!("Someone joined");
|
println!("Someone joined");
|
||||||
|
|
||||||
// notify all users in same room
|
// notify all users in same room
|
||||||
self.send_message(&"Main".to_owned(), "Someone joined", 0);
|
self.send_message("Main", "Someone joined", 0);
|
||||||
|
|
||||||
// register session with random id
|
// register session with random id
|
||||||
let id = self.rng.gen::<usize>();
|
let id = self.rng.gen::<usize>();
|
||||||
|
206
websockets/chat/src/session.rs
Normal file
206
websockets/chat/src/session.rs
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use actix::prelude::*;
|
||||||
|
use actix_web_actors::ws;
|
||||||
|
|
||||||
|
use crate::server;
|
||||||
|
|
||||||
|
/// How often heartbeat pings are sent
|
||||||
|
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// How long before lack of client response causes a timeout
|
||||||
|
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct WsChatSession {
|
||||||
|
/// unique session id
|
||||||
|
pub id: usize,
|
||||||
|
|
||||||
|
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
||||||
|
/// otherwise we drop connection.
|
||||||
|
pub hb: Instant,
|
||||||
|
|
||||||
|
/// joined room
|
||||||
|
pub room: String,
|
||||||
|
|
||||||
|
/// peer name
|
||||||
|
pub name: Option<String>,
|
||||||
|
|
||||||
|
/// Chat server
|
||||||
|
pub addr: Addr<server::ChatServer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsChatSession {
|
||||||
|
/// helper method that sends ping to client every second.
|
||||||
|
///
|
||||||
|
/// also this method checks heartbeats from client
|
||||||
|
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
|
||||||
|
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
||||||
|
// check client heartbeats
|
||||||
|
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
||||||
|
// heartbeat timed out
|
||||||
|
println!("Websocket Client heartbeat failed, disconnecting!");
|
||||||
|
|
||||||
|
// notify chat server
|
||||||
|
act.addr.do_send(server::Disconnect { id: act.id });
|
||||||
|
|
||||||
|
// stop actor
|
||||||
|
ctx.stop();
|
||||||
|
|
||||||
|
// don't try to send a ping
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ping(b"");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for WsChatSession {
|
||||||
|
type Context = ws::WebsocketContext<Self>;
|
||||||
|
|
||||||
|
/// Method is called on actor start.
|
||||||
|
/// We register ws session with ChatServer
|
||||||
|
fn started(&mut self, ctx: &mut Self::Context) {
|
||||||
|
// we'll start heartbeat process on session start.
|
||||||
|
self.hb(ctx);
|
||||||
|
|
||||||
|
// register self in chat server. `AsyncContext::wait` register
|
||||||
|
// future within context, but context waits until this future resolves
|
||||||
|
// before processing any other events.
|
||||||
|
// HttpContext::state() is instance of WsChatSessionState, state is shared
|
||||||
|
// across all routes within application
|
||||||
|
let addr = ctx.address();
|
||||||
|
self.addr
|
||||||
|
.send(server::Connect {
|
||||||
|
addr: addr.recipient(),
|
||||||
|
})
|
||||||
|
.into_actor(self)
|
||||||
|
.then(|res, act, ctx| {
|
||||||
|
match res {
|
||||||
|
Ok(res) => act.id = res,
|
||||||
|
// something is wrong with chat server
|
||||||
|
_ => ctx.stop(),
|
||||||
|
}
|
||||||
|
fut::ready(())
|
||||||
|
})
|
||||||
|
.wait(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stopping(&mut self, _: &mut Self::Context) -> Running {
|
||||||
|
// notify chat server
|
||||||
|
self.addr.do_send(server::Disconnect { id: self.id });
|
||||||
|
Running::Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle messages from chat server, we simply send it to peer websocket
|
||||||
|
impl Handler<server::Message> for WsChatSession {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: server::Message, ctx: &mut Self::Context) {
|
||||||
|
ctx.text(msg.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WebSocket message handler
|
||||||
|
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsChatSession {
|
||||||
|
fn handle(
|
||||||
|
&mut self,
|
||||||
|
msg: Result<ws::Message, ws::ProtocolError>,
|
||||||
|
ctx: &mut Self::Context,
|
||||||
|
) {
|
||||||
|
let msg = match msg {
|
||||||
|
Err(_) => {
|
||||||
|
ctx.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(msg) => msg,
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!("WEBSOCKET MESSAGE: {:?}", msg);
|
||||||
|
match msg {
|
||||||
|
ws::Message::Ping(msg) => {
|
||||||
|
self.hb = Instant::now();
|
||||||
|
ctx.pong(&msg);
|
||||||
|
}
|
||||||
|
ws::Message::Pong(_) => {
|
||||||
|
self.hb = Instant::now();
|
||||||
|
}
|
||||||
|
ws::Message::Text(text) => {
|
||||||
|
let m = text.trim();
|
||||||
|
// we check for /sss type of messages
|
||||||
|
if m.starts_with('/') {
|
||||||
|
let v: Vec<&str> = m.splitn(2, ' ').collect();
|
||||||
|
match v[0] {
|
||||||
|
"/list" => {
|
||||||
|
// Send ListRooms message to chat server and wait for
|
||||||
|
// response
|
||||||
|
println!("List rooms");
|
||||||
|
self.addr
|
||||||
|
.send(server::ListRooms)
|
||||||
|
.into_actor(self)
|
||||||
|
.then(|res, _, ctx| {
|
||||||
|
match res {
|
||||||
|
Ok(rooms) => {
|
||||||
|
for room in rooms {
|
||||||
|
ctx.text(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => println!("Something is wrong"),
|
||||||
|
}
|
||||||
|
fut::ready(())
|
||||||
|
})
|
||||||
|
.wait(ctx)
|
||||||
|
// .wait(ctx) pauses all events in context,
|
||||||
|
// so actor wont receive any new messages until it get list
|
||||||
|
// of rooms back
|
||||||
|
}
|
||||||
|
"/join" => {
|
||||||
|
if v.len() == 2 {
|
||||||
|
self.room = v[1].to_owned();
|
||||||
|
self.addr.do_send(server::Join {
|
||||||
|
id: self.id,
|
||||||
|
name: self.room.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.text("joined");
|
||||||
|
} else {
|
||||||
|
ctx.text("!!! room name is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"/name" => {
|
||||||
|
if v.len() == 2 {
|
||||||
|
self.name = Some(v[1].to_owned());
|
||||||
|
} else {
|
||||||
|
ctx.text("!!! name is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => ctx.text(format!("!!! unknown command: {:?}", m)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let msg = if let Some(ref name) = self.name {
|
||||||
|
format!("{}: {}", name, m)
|
||||||
|
} else {
|
||||||
|
m.to_owned()
|
||||||
|
};
|
||||||
|
// send message to chat server
|
||||||
|
self.addr.do_send(server::ClientMessage {
|
||||||
|
id: self.id,
|
||||||
|
msg,
|
||||||
|
room: self.room.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ws::Message::Binary(_) => println!("Unexpected binary"),
|
||||||
|
ws::Message::Close(reason) => {
|
||||||
|
ctx.close(reason);
|
||||||
|
ctx.stop();
|
||||||
|
}
|
||||||
|
ws::Message::Continuation(_) => {
|
||||||
|
ctx.stop();
|
||||||
|
}
|
||||||
|
ws::Message::Nop => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
198
websockets/chat/static/index.html
Normal file
198
websockets/chat/static/index.html
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Chat!</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'] {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#log {
|
||||||
|
width: 30em;
|
||||||
|
height: 20em;
|
||||||
|
overflow: auto;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status {
|
||||||
|
padding: 0 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text {
|
||||||
|
width: 17em;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg--status {
|
||||||
|
/* a light yellow */
|
||||||
|
background-color: #ffffc9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg--message {
|
||||||
|
/* a light blue */
|
||||||
|
background-color: #d2f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg--error {
|
||||||
|
background-color: pink;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Chat!</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button id="connect">Connect</button>
|
||||||
|
<span>Status:</span>
|
||||||
|
<span id="status">disconnected</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="log"></div>
|
||||||
|
|
||||||
|
<form id="chatform">
|
||||||
|
<input type="text" id="text" />
|
||||||
|
<input type="submit" id="send" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Commands</h2>
|
||||||
|
<table style="border-spacing: 0.5em">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>/list</code>
|
||||||
|
</td>
|
||||||
|
<td>list all available rooms</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>/join name</code>
|
||||||
|
</td>
|
||||||
|
<td>join room, if room does not exist, create new one</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>/name name</code>
|
||||||
|
</td>
|
||||||
|
<td>set session name</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>some message</code>
|
||||||
|
</td>
|
||||||
|
<td>just string, send message to all peers in same room</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const $status = document.querySelector('#status')
|
||||||
|
const $connectButton = document.querySelector('#connect')
|
||||||
|
const $log = document.querySelector('#log')
|
||||||
|
const $form = document.querySelector('#chatform')
|
||||||
|
const $input = document.querySelector('#text')
|
||||||
|
|
||||||
|
/** @type {WebSocket | null} */
|
||||||
|
var socket = null
|
||||||
|
|
||||||
|
function log(msg, type = 'status') {
|
||||||
|
$log.innerHTML += `<p class="msg msg--${type}">${msg}</p>`
|
||||||
|
$log.scrollTop += 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
disconnect()
|
||||||
|
|
||||||
|
const { location } = window
|
||||||
|
|
||||||
|
const proto = location.protocol.startsWith('https') ? 'wss' : 'ws'
|
||||||
|
const wsUri = `${proto}://${location.host}/ws`
|
||||||
|
|
||||||
|
log('Connecting...')
|
||||||
|
socket = new WebSocket(wsUri)
|
||||||
|
|
||||||
|
socket.onopen = () => {
|
||||||
|
log('Connected')
|
||||||
|
updateConnectionStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.onmessage = (ev) => {
|
||||||
|
log('Received: ' + ev.data, 'message')
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.onclose = () => {
|
||||||
|
log('Disconnected')
|
||||||
|
socket = null
|
||||||
|
updateConnectionStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
if (socket) {
|
||||||
|
log('Disconnecting...')
|
||||||
|
socket.close()
|
||||||
|
socket = null
|
||||||
|
|
||||||
|
updateConnectionStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConnectionStatus() {
|
||||||
|
if (socket) {
|
||||||
|
$status.style.backgroundColor = 'transparent'
|
||||||
|
$status.style.color = 'green'
|
||||||
|
$status.textContent = `connected`
|
||||||
|
$connectButton.innerHTML = 'Disconnect'
|
||||||
|
$input.focus()
|
||||||
|
} else {
|
||||||
|
$status.style.backgroundColor = 'red'
|
||||||
|
$status.style.color = 'white'
|
||||||
|
$status.textContent = 'disconnected'
|
||||||
|
$connectButton.textContent = 'Connect'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$connectButton.addEventListener('click', () => {
|
||||||
|
if (socket) {
|
||||||
|
disconnect()
|
||||||
|
} else {
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConnectionStatus()
|
||||||
|
})
|
||||||
|
|
||||||
|
$form.addEventListener('submit', (ev) => {
|
||||||
|
ev.preventDefault()
|
||||||
|
|
||||||
|
const text = $input.value
|
||||||
|
|
||||||
|
log('Sending: ' + text)
|
||||||
|
socket.send(text)
|
||||||
|
|
||||||
|
$input.value = ''
|
||||||
|
$input.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
updateConnectionStatus()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,112 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Chat!</title>
|
|
||||||
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
|
|
||||||
<script>
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
window.onload = () => {
|
|
||||||
let conn = null
|
|
||||||
|
|
||||||
const log = (msg) => {
|
|
||||||
div_log.innerHTML += msg + '<br>'
|
|
||||||
div_log.scroll(0, div_log.scrollTop + 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const connect = () => {
|
|
||||||
disconnect()
|
|
||||||
|
|
||||||
const wsUri =
|
|
||||||
(window.location.protocol === 'https:' ? 'wss://' : 'ws://') +
|
|
||||||
window.location.host +
|
|
||||||
'/ws/'
|
|
||||||
|
|
||||||
conn = new WebSocket(wsUri)
|
|
||||||
log('Connecting...')
|
|
||||||
|
|
||||||
conn.onopen = function () {
|
|
||||||
log('Connected.')
|
|
||||||
update_ui()
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.onmessage = function (e) {
|
|
||||||
log('Received: ' + e.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.onclose = function () {
|
|
||||||
log('Disconnected.')
|
|
||||||
conn = null
|
|
||||||
|
|
||||||
update_ui()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const disconnect = () => {
|
|
||||||
if (conn) {
|
|
||||||
log('Disconnecting...')
|
|
||||||
conn.close()
|
|
||||||
conn = null
|
|
||||||
|
|
||||||
update_ui()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const update_ui = () => {
|
|
||||||
if (!conn) {
|
|
||||||
span_status.textContent = 'disconnected'
|
|
||||||
btn_connect.textContent = 'Connect'
|
|
||||||
} else {
|
|
||||||
span_status.textContent = `connected (${conn.protocol})`
|
|
||||||
btn_connect.textContent = 'Disconnect'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
btn_connect.onclick = () => {
|
|
||||||
if (!conn) {
|
|
||||||
connect()
|
|
||||||
} else {
|
|
||||||
disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
update_ui()
|
|
||||||
}
|
|
||||||
|
|
||||||
btn_send.onclick = () => {
|
|
||||||
if (!conn) return
|
|
||||||
|
|
||||||
const text = input_text.value
|
|
||||||
log('Sending: ' + text)
|
|
||||||
conn.send(text)
|
|
||||||
|
|
||||||
input_text.value = ''
|
|
||||||
input_text.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
input_text.onkeyup = (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
btn_send.click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h3>Chat!</h3>
|
|
||||||
<div>
|
|
||||||
<button id="btn_connect">Connect</button>
|
|
||||||
Status: <span id="span_status">disconnected</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="div_log"
|
|
||||||
style="width: 20em; height: 15em; overflow: auto; border: 1px solid black"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<input id="input_text" type="text" />
|
|
||||||
<input id="btn_send" type="button" value="Send" />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
27
websockets/echo/Cargo.toml
Normal file
27
websockets/echo/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "websocket"
|
||||||
|
version = "1.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "websocket-server"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "websocket-client"
|
||||||
|
path = "src/client.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix = "0.12"
|
||||||
|
actix-codec = "0.5"
|
||||||
|
actix-files = "0.6.0-beta.16"
|
||||||
|
actix-rt = "2"
|
||||||
|
actix-web = "4.0.0-rc.3"
|
||||||
|
actix-web-actors = "4.0.0-beta.12"
|
||||||
|
awc = "3.0.0-beta.21"
|
||||||
|
|
||||||
|
env_logger = "0.9"
|
||||||
|
log = "0.4"
|
||||||
|
futures = "0.3.7"
|
||||||
|
tokio = { version = "1.13.1", features = ["full"] }
|
||||||
|
tokio-stream = "0.1.8"
|
72
websockets/echo/src/client.rs
Normal file
72
websockets/echo/src/client.rs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
//! Simple websocket client.
|
||||||
|
|
||||||
|
use std::{io, thread};
|
||||||
|
|
||||||
|
use actix_web::web::Bytes;
|
||||||
|
use awc::ws;
|
||||||
|
use futures::{SinkExt as _, StreamExt as _};
|
||||||
|
use tokio::{select, sync::mpsc};
|
||||||
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
|
|
||||||
|
log::info!("starting echo WebSocket client");
|
||||||
|
|
||||||
|
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
|
||||||
|
let mut cmd_rx = UnboundedReceiverStream::new(cmd_rx);
|
||||||
|
|
||||||
|
// run blocking terminal input reader on separate thread
|
||||||
|
let input_thread = thread::spawn(move || loop {
|
||||||
|
let mut cmd = String::with_capacity(32);
|
||||||
|
|
||||||
|
if io::stdin().read_line(&mut cmd).is_err() {
|
||||||
|
log::error!("error reading line");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_tx.send(cmd).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let (res, mut ws) = awc::Client::new()
|
||||||
|
.ws("ws://127.0.0.1:8080/ws")
|
||||||
|
.connect()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
log::debug!("response: {res:?}");
|
||||||
|
log::info!("connected; server will echo messages sent");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
select! {
|
||||||
|
Some(msg) = ws.next() => {
|
||||||
|
match msg {
|
||||||
|
Ok(ws::Frame::Text(txt)) => {
|
||||||
|
// log echoed messages from server
|
||||||
|
log::info!("Server: {:?}", txt)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ws::Frame::Ping(_)) => {
|
||||||
|
// respond to ping probes
|
||||||
|
ws.send(ws::Message::Pong(Bytes::new())).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(cmd) = cmd_rx.next() => {
|
||||||
|
if cmd.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.send(ws::Message::Text(cmd.into())).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
else => break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input_thread.join().unwrap();
|
||||||
|
}
|
42
websockets/echo/src/main.rs
Normal file
42
websockets/echo/src/main.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
//! Simple echo websocket server.
|
||||||
|
//!
|
||||||
|
//! Open `http://localhost:8080/` in browser to test.
|
||||||
|
|
||||||
|
use actix_files::NamedFile;
|
||||||
|
use actix_web::{
|
||||||
|
middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder,
|
||||||
|
};
|
||||||
|
use actix_web_actors::ws;
|
||||||
|
|
||||||
|
mod server;
|
||||||
|
use self::server::MyWebSocket;
|
||||||
|
|
||||||
|
async fn index() -> impl Responder {
|
||||||
|
NamedFile::open_async("./static/index.html").await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WebSocket handshake and start `MyWebSocket` actor.
|
||||||
|
async fn echo_ws(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
|
||||||
|
ws::start(MyWebSocket::new(), &req, stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
|
|
||||||
|
log::info!("starting HTTP server at http://localhost:8080");
|
||||||
|
|
||||||
|
HttpServer::new(|| {
|
||||||
|
App::new()
|
||||||
|
// WebSocket UI HTML file
|
||||||
|
.service(web::resource("/").to(index))
|
||||||
|
// websocket route
|
||||||
|
.service(web::resource("/ws").route(web::get().to(echo_ws)))
|
||||||
|
// enable logger
|
||||||
|
.wrap(middleware::Logger::default())
|
||||||
|
})
|
||||||
|
.workers(2)
|
||||||
|
.bind(("127.0.0.1", 8080))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
@ -1,36 +1,49 @@
|
|||||||
//! Simple echo websocket server.
|
|
||||||
//! Open `http://localhost:8080/index.html` in browser
|
|
||||||
//! or [python console client](https://github.com/actix/examples/blob/master/websocket/websocket-client.py)
|
|
||||||
//! could be used for testing.
|
|
||||||
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use actix_files as fs;
|
|
||||||
use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer};
|
|
||||||
use actix_web_actors::ws;
|
use actix_web_actors::ws;
|
||||||
|
|
||||||
/// How often heartbeat pings are sent
|
/// How often heartbeat pings are sent
|
||||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
/// How long before lack of client response causes a timeout
|
/// How long before lack of client response causes a timeout
|
||||||
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
/// do websocket handshake and start `MyWebSocket` actor
|
|
||||||
async fn ws_index(r: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
|
|
||||||
println!("{:?}", r);
|
|
||||||
let res = ws::start(MyWebSocket::new(), &r, stream);
|
|
||||||
println!("{:?}", res);
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// websocket connection is long running connection, it easier
|
/// websocket connection is long running connection, it easier
|
||||||
/// to handle with an actor
|
/// to handle with an actor
|
||||||
struct MyWebSocket {
|
pub struct MyWebSocket {
|
||||||
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
||||||
/// otherwise we drop connection.
|
/// otherwise we drop connection.
|
||||||
hb: Instant,
|
hb: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MyWebSocket {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { hb: Instant::now() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// helper method that sends ping to client every second.
|
||||||
|
///
|
||||||
|
/// also this method checks heartbeats from client
|
||||||
|
fn hb(&self, ctx: &mut <Self as Actor>::Context) {
|
||||||
|
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
||||||
|
// check client heartbeats
|
||||||
|
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
||||||
|
// heartbeat timed out
|
||||||
|
println!("Websocket Client heartbeat failed, disconnecting!");
|
||||||
|
|
||||||
|
// stop actor
|
||||||
|
ctx.stop();
|
||||||
|
|
||||||
|
// don't try to send a ping
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ping(b"");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Actor for MyWebSocket {
|
impl Actor for MyWebSocket {
|
||||||
type Context = ws::WebsocketContext<Self>;
|
type Context = ws::WebsocketContext<Self>;
|
||||||
|
|
||||||
@ -67,50 +80,3 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWebSocket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MyWebSocket {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self { hb: Instant::now() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// helper method that sends ping to client every second.
|
|
||||||
///
|
|
||||||
/// also this method checks heartbeats from client
|
|
||||||
fn hb(&self, ctx: &mut <Self as Actor>::Context) {
|
|
||||||
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
|
||||||
// check client heartbeats
|
|
||||||
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
|
||||||
// heartbeat timed out
|
|
||||||
println!("Websocket Client heartbeat failed, disconnecting!");
|
|
||||||
|
|
||||||
// stop actor
|
|
||||||
ctx.stop();
|
|
||||||
|
|
||||||
// don't try to send a ping
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.ping(b"");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_web::main]
|
|
||||||
async fn main() -> std::io::Result<()> {
|
|
||||||
std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
|
|
||||||
env_logger::init();
|
|
||||||
|
|
||||||
HttpServer::new(|| {
|
|
||||||
App::new()
|
|
||||||
// enable logger
|
|
||||||
.wrap(middleware::Logger::default())
|
|
||||||
// websocket route
|
|
||||||
.service(web::resource("/ws/").route(web::get().to(ws_index)))
|
|
||||||
// static files
|
|
||||||
.service(fs::Files::new("/", "static/").index_file("index.html"))
|
|
||||||
})
|
|
||||||
// start http server on 127.0.0.1:8080
|
|
||||||
.bind(("127.0.0.1", 8080))?
|
|
||||||
.run()
|
|
||||||
.await
|
|
||||||
}
|
|
171
websockets/echo/static/index.html
Normal file
171
websockets/echo/static/index.html
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Websocket Echo</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'] {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#log {
|
||||||
|
width: 30em;
|
||||||
|
height: 20em;
|
||||||
|
overflow: auto;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status {
|
||||||
|
padding: 0 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text {
|
||||||
|
width: 17em;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg--status {
|
||||||
|
/* a light yellow */
|
||||||
|
background-color: #ffffc9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg--message {
|
||||||
|
/* a light blue */
|
||||||
|
background-color: #d2f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg--error {
|
||||||
|
background-color: pink;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Chat!</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button id="connect">Connect</button>
|
||||||
|
<span>Status:</span>
|
||||||
|
<span id="status">disconnected</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="log"></div>
|
||||||
|
|
||||||
|
<form id="chatform">
|
||||||
|
<input type="text" id="text" />
|
||||||
|
<input type="submit" id="send" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Usage</h2>
|
||||||
|
<p>After connecting, type into the text box and the server will echo your message.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const $status = document.querySelector('#status')
|
||||||
|
const $connectButton = document.querySelector('#connect')
|
||||||
|
const $log = document.querySelector('#log')
|
||||||
|
const $form = document.querySelector('#chatform')
|
||||||
|
const $input = document.querySelector('#text')
|
||||||
|
|
||||||
|
/** @type {WebSocket | null} */
|
||||||
|
var socket = null
|
||||||
|
|
||||||
|
function log(msg, type = 'status') {
|
||||||
|
$log.innerHTML += `<p class="msg msg--${type}">${msg}</p>`
|
||||||
|
$log.scrollTop += 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
disconnect()
|
||||||
|
|
||||||
|
const { location } = window
|
||||||
|
|
||||||
|
const proto = location.protocol.startsWith('https') ? 'wss' : 'ws'
|
||||||
|
const wsUri = `${proto}://${location.host}/ws`
|
||||||
|
|
||||||
|
log('Connecting...')
|
||||||
|
socket = new WebSocket(wsUri)
|
||||||
|
|
||||||
|
socket.onopen = () => {
|
||||||
|
log('Connected')
|
||||||
|
updateConnectionStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.onmessage = (ev) => {
|
||||||
|
log('Received: ' + ev.data, 'message')
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.onclose = () => {
|
||||||
|
log('Disconnected')
|
||||||
|
socket = null
|
||||||
|
updateConnectionStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
if (socket) {
|
||||||
|
log('Disconnecting...')
|
||||||
|
socket.close()
|
||||||
|
socket = null
|
||||||
|
|
||||||
|
updateConnectionStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConnectionStatus() {
|
||||||
|
if (socket) {
|
||||||
|
$status.style.backgroundColor = 'transparent'
|
||||||
|
$status.style.color = 'green'
|
||||||
|
$status.textContent = `connected`
|
||||||
|
$connectButton.innerHTML = 'Disconnect'
|
||||||
|
$input.focus()
|
||||||
|
} else {
|
||||||
|
$status.style.backgroundColor = 'red'
|
||||||
|
$status.style.color = 'white'
|
||||||
|
$status.textContent = 'disconnected'
|
||||||
|
$connectButton.textContent = 'Connect'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$connectButton.addEventListener('click', () => {
|
||||||
|
if (socket) {
|
||||||
|
disconnect()
|
||||||
|
} else {
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConnectionStatus()
|
||||||
|
})
|
||||||
|
|
||||||
|
$form.addEventListener('submit', (ev) => {
|
||||||
|
ev.preventDefault()
|
||||||
|
|
||||||
|
const text = $input.value
|
||||||
|
|
||||||
|
log('Sending: ' + text)
|
||||||
|
socket.send(text)
|
||||||
|
|
||||||
|
$input.value = ''
|
||||||
|
$input.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
updateConnectionStatus()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,90 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js">
|
|
||||||
</script>
|
|
||||||
<script language="javascript" type="text/javascript">
|
|
||||||
$(function() {
|
|
||||||
var conn = null;
|
|
||||||
function log(msg) {
|
|
||||||
var control = $('#log');
|
|
||||||
control.html(control.html() + msg + '<br/>');
|
|
||||||
control.scrollTop(control.scrollTop() + 1000);
|
|
||||||
}
|
|
||||||
function connect() {
|
|
||||||
disconnect();
|
|
||||||
var wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+window.location.host + '/ws/';
|
|
||||||
conn = new WebSocket(wsUri);
|
|
||||||
log('Connecting...');
|
|
||||||
conn.onopen = function() {
|
|
||||||
log('Connected.');
|
|
||||||
update_ui();
|
|
||||||
};
|
|
||||||
conn.onmessage = function(e) {
|
|
||||||
log('Received: ' + e.data);
|
|
||||||
};
|
|
||||||
conn.onclose = function() {
|
|
||||||
log('Disconnected.');
|
|
||||||
conn = null;
|
|
||||||
update_ui();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function disconnect() {
|
|
||||||
if (conn != null) {
|
|
||||||
log('Disconnecting...');
|
|
||||||
conn.close();
|
|
||||||
conn = null;
|
|
||||||
update_ui();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function update_ui() {
|
|
||||||
var msg = '';
|
|
||||||
if (conn == null) {
|
|
||||||
$('#status').text('disconnected');
|
|
||||||
$('#connect').html('Connect');
|
|
||||||
} else {
|
|
||||||
$('#status').text('connected (' + conn.protocol + ')');
|
|
||||||
$('#connect').html('Disconnect');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$('#connect').click(function() {
|
|
||||||
if (conn == null) {
|
|
||||||
connect();
|
|
||||||
} else {
|
|
||||||
disconnect();
|
|
||||||
}
|
|
||||||
update_ui();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
$('#send').click(function() {
|
|
||||||
var text = $('#text').val();
|
|
||||||
log('Sending: ' + text);
|
|
||||||
conn.send(text);
|
|
||||||
$('#text').val('').focus();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
$('#text').keyup(function(e) {
|
|
||||||
if (e.keyCode === 13) {
|
|
||||||
$('#send').click();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h3>Chat!</h3>
|
|
||||||
<div>
|
|
||||||
<button id="connect">Connect</button> | Status:
|
|
||||||
<span id="status">disconnected</span>
|
|
||||||
</div>
|
|
||||||
<div id="log"
|
|
||||||
style="width:20em;height:15em;overflow:auto;border:1px solid black">
|
|
||||||
</div>
|
|
||||||
<form id="chatform" onsubmit="return false;">
|
|
||||||
<input id="text" type="text" />
|
|
||||||
<input id="send" type="button" value="Send" />
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,23 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "websocket"
|
|
||||||
version = "1.0.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "websocket-server"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "websocket-client"
|
|
||||||
path = "src/client.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
actix = "0.10"
|
|
||||||
actix-codec = "0.3"
|
|
||||||
actix-web = "3"
|
|
||||||
actix-web-actors = "3"
|
|
||||||
actix-files = "0.3"
|
|
||||||
awc = "2"
|
|
||||||
env_logger = "0.8"
|
|
||||||
futures = "0.3.1"
|
|
||||||
bytes = "0.5.3"
|
|
@ -1,113 +0,0 @@
|
|||||||
//! Simple websocket client.
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::{io, thread};
|
|
||||||
|
|
||||||
use actix::io::SinkWrite;
|
|
||||||
use actix::*;
|
|
||||||
use actix_codec::Framed;
|
|
||||||
use awc::{
|
|
||||||
error::WsProtocolError,
|
|
||||||
ws::{Codec, Frame, Message},
|
|
||||||
BoxedSocket, Client,
|
|
||||||
};
|
|
||||||
use bytes::Bytes;
|
|
||||||
use futures::stream::{SplitSink, StreamExt};
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
|
||||||
env_logger::init();
|
|
||||||
|
|
||||||
let sys = System::new("websocket-client");
|
|
||||||
|
|
||||||
Arbiter::spawn(async {
|
|
||||||
let (response, framed) = Client::new()
|
|
||||||
.ws("http://127.0.0.1:8080/ws/")
|
|
||||||
.connect()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
println!("Error: {}", e);
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
println!("{:?}", response);
|
|
||||||
let (sink, stream) = framed.split();
|
|
||||||
let addr = ChatClient::create(|ctx| {
|
|
||||||
ChatClient::add_stream(stream, ctx);
|
|
||||||
ChatClient(SinkWrite::new(sink, ctx))
|
|
||||||
});
|
|
||||||
|
|
||||||
// start console loop
|
|
||||||
thread::spawn(move || loop {
|
|
||||||
let mut cmd = String::new();
|
|
||||||
if io::stdin().read_line(&mut cmd).is_err() {
|
|
||||||
println!("error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addr.do_send(ClientCommand(cmd));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
sys.run().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ChatClient(SinkWrite<Message, SplitSink<Framed<BoxedSocket, Codec>, Message>>);
|
|
||||||
|
|
||||||
#[derive(Message)]
|
|
||||||
#[rtype(result = "()")]
|
|
||||||
struct ClientCommand(String);
|
|
||||||
|
|
||||||
impl Actor for ChatClient {
|
|
||||||
type Context = Context<Self>;
|
|
||||||
|
|
||||||
fn started(&mut self, ctx: &mut Context<Self>) {
|
|
||||||
// start heartbeats otherwise server will disconnect after 10 seconds
|
|
||||||
self.hb(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stopped(&mut self, _: &mut Context<Self>) {
|
|
||||||
println!("Disconnected");
|
|
||||||
|
|
||||||
// Stop application on disconnect
|
|
||||||
System::current().stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChatClient {
|
|
||||||
fn hb(&self, ctx: &mut Context<Self>) {
|
|
||||||
ctx.run_later(Duration::new(1, 0), |act, ctx| {
|
|
||||||
act.0.write(Message::Ping(Bytes::from_static(b"")));
|
|
||||||
act.hb(ctx);
|
|
||||||
|
|
||||||
// client should also check for a timeout here, similar to the
|
|
||||||
// server code
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle stdin commands
|
|
||||||
impl Handler<ClientCommand> for ChatClient {
|
|
||||||
type Result = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: ClientCommand, _ctx: &mut Context<Self>) {
|
|
||||||
self.0.write(Message::Text(msg.0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle server websocket messages
|
|
||||||
impl StreamHandler<Result<Frame, WsProtocolError>> for ChatClient {
|
|
||||||
fn handle(&mut self, msg: Result<Frame, WsProtocolError>, _: &mut Context<Self>) {
|
|
||||||
if let Ok(Frame::Text(txt)) = msg {
|
|
||||||
println!("Server: {:?}", txt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn started(&mut self, _ctx: &mut Context<Self>) {
|
|
||||||
println!("Connected");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finished(&mut self, ctx: &mut Context<Self>) {
|
|
||||||
println!("Server disconnected");
|
|
||||||
ctx.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl actix::io::WriteHandler<WsProtocolError> for ChatClient {}
|
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
Before Width: | Height: | Size: 32 KiB |
@ -1,90 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js">
|
|
||||||
</script>
|
|
||||||
<script language="javascript" type="text/javascript">
|
|
||||||
$(function() {
|
|
||||||
var conn = null;
|
|
||||||
function log(msg) {
|
|
||||||
var control = $('#log');
|
|
||||||
control.html(control.html() + msg + '<br/>');
|
|
||||||
control.scrollTop(control.scrollTop() + 1000);
|
|
||||||
}
|
|
||||||
function connect() {
|
|
||||||
disconnect();
|
|
||||||
var wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+window.location.host + '/ws/';
|
|
||||||
conn = new WebSocket(wsUri);
|
|
||||||
log('Connecting...');
|
|
||||||
conn.onopen = function() {
|
|
||||||
log('Connected.');
|
|
||||||
update_ui();
|
|
||||||
};
|
|
||||||
conn.onmessage = function(e) {
|
|
||||||
log('Received: ' + e.data);
|
|
||||||
};
|
|
||||||
conn.onclose = function() {
|
|
||||||
log('Disconnected.');
|
|
||||||
conn = null;
|
|
||||||
update_ui();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function disconnect() {
|
|
||||||
if (conn != null) {
|
|
||||||
log('Disconnecting...');
|
|
||||||
conn.close();
|
|
||||||
conn = null;
|
|
||||||
update_ui();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function update_ui() {
|
|
||||||
var msg = '';
|
|
||||||
if (conn == null) {
|
|
||||||
$('#status').text('disconnected');
|
|
||||||
$('#connect').html('Connect');
|
|
||||||
} else {
|
|
||||||
$('#status').text('connected (' + conn.protocol + ')');
|
|
||||||
$('#connect').html('Disconnect');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$('#connect').click(function() {
|
|
||||||
if (conn == null) {
|
|
||||||
connect();
|
|
||||||
} else {
|
|
||||||
disconnect();
|
|
||||||
}
|
|
||||||
update_ui();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
$('#send').click(function() {
|
|
||||||
var text = $('#text').val();
|
|
||||||
log('Sending: ' + text);
|
|
||||||
conn.send(text);
|
|
||||||
$('#text').val('').focus();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
$('#text').keyup(function(e) {
|
|
||||||
if (e.keyCode === 13) {
|
|
||||||
$('#send').click();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h3>Chat!</h3>
|
|
||||||
<div>
|
|
||||||
<button id="connect">Connect</button> | Status:
|
|
||||||
<span id="status">disconnected</span>
|
|
||||||
</div>
|
|
||||||
<div id="log"
|
|
||||||
style="width:20em;height:15em;overflow:auto;border:1px solid black">
|
|
||||||
</div>
|
|
||||||
<form id="chatform" onsubmit="return false;">
|
|
||||||
<input id="text" type="text" />
|
|
||||||
<input id="send" type="button" value="Send" />
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Loading…
Reference in New Issue
Block a user