mirror of
https://github.com/actix/examples
synced 2024-11-23 22:41: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/sailfish",
|
||||
"websockets/autobahn",
|
||||
"websockets/chat-broker",
|
||||
"websockets/chat",
|
||||
"websockets/tcp-chat",
|
||||
"websockets/websocket",
|
||||
"websockets/chat-broker",
|
||||
"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("/todo").route(web::post().to(api::create)))
|
||||
.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))?
|
||||
.workers(2)
|
||||
|
@ -8,7 +8,9 @@ name = "websocket-autobahn-server"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
actix = "0.10"
|
||||
actix-web = "3"
|
||||
actix-web-actors = "3"
|
||||
env_logger = "0.8"
|
||||
actix = "0.12"
|
||||
actix-web = "4.0.0-rc.3"
|
||||
actix-web-actors = "4.0.0-beta.12"
|
||||
|
||||
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
|
||||
|
||||
### server
|
||||
### Server
|
||||
|
||||
```bash
|
||||
cd websockets/autobahn
|
||||
cargo run --bin websocket-autobahn-server
|
||||
cargo run
|
||||
```
|
||||
|
||||
### Running Autobahn Test Suite
|
||||
|
||||
Running the autobahn test suite is easiest using the docker image
|
||||
as explained on the [autobahn-testsuite repo](https://github.com/crossbario/autobahn-testsuite#using-the-testsuite-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).
|
||||
|
||||
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
|
||||
docker run -it --rm \
|
||||
|
@ -8,6 +8,7 @@
|
||||
],
|
||||
"cases": ["*"],
|
||||
"exclude-cases": [
|
||||
"9.*",
|
||||
"12.*",
|
||||
"13.*"
|
||||
],
|
||||
|
@ -3,18 +3,17 @@ use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServ
|
||||
use actix_web_actors::ws;
|
||||
|
||||
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>;
|
||||
|
||||
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(
|
||||
&mut self,
|
||||
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]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
|
||||
env_logger::init();
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
log::info!("starting HTTP server at http://localhost:9001");
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.wrap(middleware::Logger::default())
|
||||
.service(web::resource("/").route(web::get().to(ws_index)))
|
||||
})
|
||||
.bind("127.0.0.1:9001")?
|
||||
.workers(2)
|
||||
.bind(("127.0.0.1", 9001))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
@ -8,13 +8,12 @@ name = "server"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
actix = "0.10"
|
||||
actix-broker = "0.3.1"
|
||||
actix-files = "0.4"
|
||||
actix-rt = "1"
|
||||
actix-web = "3"
|
||||
actix-web-actors = "3"
|
||||
env_logger = "0.8"
|
||||
futures = "0.3"
|
||||
actix = "0.12"
|
||||
actix-broker = "0.4"
|
||||
actix-files = "0.6.0-beta.16"
|
||||
actix-web = "4.0.0-rc.3"
|
||||
actix-web-actors = "4.0.0-beta.12"
|
||||
|
||||
env_logger = "0.9"
|
||||
log = "0.4"
|
||||
rand = "0.7"
|
||||
rand = "0.8"
|
||||
|
@ -1,7 +1,7 @@
|
||||
use log::info;
|
||||
|
||||
use actix_files::Files;
|
||||
use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer};
|
||||
use actix_files::{Files, NamedFile};
|
||||
use actix_web::{
|
||||
middleware::Logger, web, App, Error, HttpRequest, HttpServer, Responder,
|
||||
};
|
||||
use actix_web_actors::ws;
|
||||
|
||||
mod message;
|
||||
@ -10,28 +10,32 @@ mod session;
|
||||
|
||||
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,
|
||||
stream: web::Payload,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
) -> Result<impl Responder, Error> {
|
||||
ws::start(WsChatSession::default(), &req, stream)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
||||
.init();
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
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()
|
||||
.service(web::resource("/ws/").to(chat_route))
|
||||
.service(Files::new("/", "./static/").index_file("index.html"))
|
||||
.service(web::resource("/").to(index))
|
||||
.service(web::resource("/ws").to(chat_ws))
|
||||
.service(Files::new("/static", "./static"))
|
||||
.wrap(Logger::default())
|
||||
})
|
||||
.bind(&addr)?;
|
||||
|
||||
info!("Starting http server: {}", &addr);
|
||||
|
||||
srv.run().await
|
||||
.workers(2)
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix::prelude::*;
|
||||
use actix_broker::BrokerSubscribe;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::message::{ChatMessage, JoinRoom, LeaveRoom, ListRooms, SendMessage};
|
||||
|
||||
type Client = Recipient<ChatMessage>;
|
||||
|
@ -1,12 +1,11 @@
|
||||
use log::{debug, info};
|
||||
|
||||
use actix::fut;
|
||||
use actix::prelude::*;
|
||||
use actix::{fut, prelude::*};
|
||||
use actix_broker::BrokerIssue;
|
||||
use actix_web_actors::ws;
|
||||
|
||||
use crate::message::{ChatMessage, JoinRoom, LeaveRoom, ListRooms, SendMessage};
|
||||
use crate::server::WsChatServer;
|
||||
use crate::{
|
||||
message::{ChatMessage, JoinRoom, LeaveRoom, ListRooms, SendMessage},
|
||||
server::WsChatServer,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WsChatSession {
|
||||
@ -84,7 +83,7 @@ impl Actor for WsChatSession {
|
||||
}
|
||||
|
||||
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
||||
info!(
|
||||
log::info!(
|
||||
"WsChatSession closed for {}({}) in room {}",
|
||||
self.name.clone().unwrap_or_else(|| "anon".to_string()),
|
||||
self.id,
|
||||
@ -115,7 +114,7 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsChatSession {
|
||||
Ok(msg) => msg,
|
||||
};
|
||||
|
||||
debug!("WEBSOCKET MESSAGE: {:?}", msg);
|
||||
log::debug!("WEBSOCKET MESSAGE: {:?}", msg);
|
||||
|
||||
match msg {
|
||||
ws::Message::Text(text) => {
|
||||
|
@ -130,7 +130,7 @@
|
||||
const { location } = window
|
||||
|
||||
const proto = location.protocol.startsWith('https') ? 'wss' : 'ws'
|
||||
const wsUri = `${proto}://${location.host}/ws/`
|
||||
const wsUri = `${proto}://${location.host}/ws`
|
||||
|
||||
log('Connecting...')
|
||||
socket = new WebSocket(wsUri)
|
||||
|
@ -26,6 +26,6 @@ log = "0.4"
|
||||
rand = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio = { version = "1.13.1", features = ["full"] }
|
||||
tokio-util = { version = "0.6", features = ["codec"] }
|
||||
tokio-stream = "0.1.8"
|
@ -1,8 +1,10 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use actix::*;
|
||||
use actix_files as fs;
|
||||
use actix_web::{http::header, web, App, Error, HttpRequest, HttpResponse, HttpServer};
|
||||
use actix::prelude::*;
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::{
|
||||
middleware::Logger, web, App, Error, HttpRequest, HttpServer, Responder,
|
||||
};
|
||||
use actix_web_actors::ws;
|
||||
|
||||
mod codec;
|
||||
@ -11,15 +13,20 @@ mod session;
|
||||
|
||||
/// 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);
|
||||
|
||||
async fn index() -> impl Responder {
|
||||
NamedFile::open_async("./static/index.html").await.unwrap()
|
||||
}
|
||||
|
||||
/// Entry point for our route
|
||||
async fn chat_route(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
srv: web::Data<Addr<server::ChatServer>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
) -> Result<impl Responder, Error> {
|
||||
ws::start(
|
||||
WsChatSession {
|
||||
id: 0,
|
||||
@ -109,7 +116,7 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsChatSession {
|
||||
Ok(msg) => msg,
|
||||
};
|
||||
|
||||
println!("WEBSOCKET MESSAGE: {:?}", msg);
|
||||
log::debug!("WEBSOCKET MESSAGE: {:?}", msg);
|
||||
match msg {
|
||||
ws::Message::Ping(msg) => {
|
||||
self.hb = Instant::now();
|
||||
@ -235,19 +242,14 @@ async fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(web::Data::new(server.clone()))
|
||||
// redirect to websocket.html
|
||||
.service(web::resource("/").route(web::get().to(|| async {
|
||||
HttpResponse::Found()
|
||||
.insert_header((header::LOCATION, "/static/websocket.html"))
|
||||
.finish()
|
||||
})))
|
||||
// WebSocket UI HTML file
|
||||
.service(web::resource("/").to(index))
|
||||
// websocket
|
||||
.service(web::resource("/ws/").to(chat_route))
|
||||
// static resources
|
||||
.service(fs::Files::new("/static/", "static/"))
|
||||
.service(web::resource("/ws").to(chat_route))
|
||||
.wrap(Logger::default())
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
.workers(1)
|
||||
.workers(2)
|
||||
.run()
|
||||
.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"
|
||||
|
||||
[dependencies]
|
||||
actix = "0.10"
|
||||
actix-web = "3"
|
||||
actix-web-actors = "3"
|
||||
actix-files = "0.3"
|
||||
actix = "0.12"
|
||||
actix-files = "0.6.0-beta.16"
|
||||
actix-web = "4.0.0-rc.3"
|
||||
actix-web-actors = "4.0.0-beta.12"
|
||||
|
||||
rand = "0.7"
|
||||
bytes = "0.5"
|
||||
byteorder = "1.3"
|
||||
futures = "0.3"
|
||||
env_logger = "0.8"
|
||||
env_logger = "0.9"
|
||||
log = "0.4"
|
||||
rand = "0.8"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
|
@ -1,20 +1,25 @@
|
||||
use std::sync::{
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Instant,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use actix::*;
|
||||
use actix_files as fs;
|
||||
use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder};
|
||||
use actix_files::{Files, NamedFile};
|
||||
use actix_web::{
|
||||
middleware::Logger, web, App, Error, HttpRequest, HttpResponse, HttpServer,
|
||||
Responder,
|
||||
};
|
||||
use actix_web_actors::ws;
|
||||
|
||||
mod server;
|
||||
mod session;
|
||||
|
||||
/// 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);
|
||||
async fn index() -> impl Responder {
|
||||
NamedFile::open_async("./static/index.html").await.unwrap()
|
||||
}
|
||||
|
||||
/// Entry point for our websocket route
|
||||
async fn chat_route(
|
||||
@ -23,7 +28,7 @@ async fn chat_route(
|
||||
srv: web::Data<Addr<server::ChatServer>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
ws::start(
|
||||
WsChatSession {
|
||||
session::WsChatSession {
|
||||
id: 0,
|
||||
hb: Instant::now(),
|
||||
room: "Main".to_owned(),
|
||||
@ -35,229 +40,36 @@ async fn chat_route(
|
||||
)
|
||||
}
|
||||
|
||||
/// Displays and affects state
|
||||
async fn get_count(count: web::Data<Arc<AtomicUsize>>) -> impl Responder {
|
||||
let current_count = count.fetch_add(1, Ordering::SeqCst);
|
||||
/// Displays state
|
||||
async fn get_count(count: web::Data<AtomicUsize>) -> impl Responder {
|
||||
let current_count = count.load(Ordering::SeqCst);
|
||||
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]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
env_logger::init();
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
// App state
|
||||
// We are keeping a count of the number of visitors
|
||||
// set up applications state
|
||||
// keep a count of the number of visitors
|
||||
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();
|
||||
|
||||
// Create Http server with websocket support
|
||||
log::info!("starting HTTP server at http://localhost:8080");
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(app_state.clone())
|
||||
.data(server.clone())
|
||||
// redirect to websocket.html
|
||||
.service(web::resource("/").route(web::get().to(|| {
|
||||
HttpResponse::Found()
|
||||
.header("LOCATION", "/static/websocket.html")
|
||||
.finish()
|
||||
})))
|
||||
.route("/count/", web::get().to(get_count))
|
||||
// websocket
|
||||
.service(web::resource("/ws/").to(chat_route))
|
||||
// static resources
|
||||
.service(fs::Files::new("/static/", "static/"))
|
||||
.app_data(web::Data::from(app_state.clone()))
|
||||
.app_data(web::Data::new(server.clone()))
|
||||
.service(web::resource("/").to(index))
|
||||
.route("/count", web::get().to(get_count))
|
||||
.route("/ws", web::get().to(chat_route))
|
||||
.service(Files::new("/static", "./static"))
|
||||
.wrap(Logger::default())
|
||||
})
|
||||
.workers(2)
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
.run()
|
||||
.await
|
||||
|
@ -2,15 +2,16 @@
|
||||
//! And manages available rooms. Peers send messages to other peers in same
|
||||
//! room through `ChatServer`.
|
||||
|
||||
use actix::prelude::*;
|
||||
use rand::{self, rngs::ThreadRng, Rng};
|
||||
|
||||
use std::sync::{
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use actix::prelude::*;
|
||||
use rand::{self, rngs::ThreadRng, Rng};
|
||||
|
||||
/// Chat server sends this messages to session
|
||||
#[derive(Message)]
|
||||
@ -56,14 +57,17 @@ impl actix::Message for ListRooms {
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct Join {
|
||||
/// Client id
|
||||
/// Client ID
|
||||
pub id: usize,
|
||||
|
||||
/// Room name
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// `ChatServer` manages chat rooms and responsible for coordinating chat
|
||||
/// session. implementation is super primitive
|
||||
/// `ChatServer` manages chat rooms and responsible for coordinating chat session.
|
||||
///
|
||||
/// Implementation is very naïve.
|
||||
#[derive(Debug)]
|
||||
pub struct ChatServer {
|
||||
sessions: HashMap<usize, Recipient<Message>>,
|
||||
rooms: HashMap<String, HashSet<usize>>,
|
||||
@ -118,7 +122,7 @@ impl Handler<Connect> for ChatServer {
|
||||
println!("Someone joined");
|
||||
|
||||
// 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
|
||||
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 actix::prelude::*;
|
||||
use actix_files as fs;
|
||||
use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer};
|
||||
use actix_web_actors::ws;
|
||||
|
||||
/// 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);
|
||||
|
||||
/// 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
|
||||
/// to handle with an actor
|
||||
struct MyWebSocket {
|
||||
pub struct MyWebSocket {
|
||||
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
||||
/// otherwise we drop connection.
|
||||
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 {
|
||||
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