在Rust中如何高效实现WebSocket? - ahmadrosid

22-03-25 banq

如果您正在学习 Web 套接字,您可能会编写一个聊天服务器,但今天让我们做一些不同的事情。这是我们今天要介绍的内容:学习如何编写 Web 套接字服务器以提高生产力。
 
创建 websocket 服务器:
  • 用于呈现 html 的索引端点。
  • 用于发送事件文件更改的 Websocket 端点。
  • 用于监听来自 websocket 的事件文件更改的 Javascript。

 

Actix Web
在 Rust 世界中,actix web 具有丰富的构建 Web 应用程序的功能。所以让我们将这些库添​​加到我们的项目中。

actix = "0.13"
actix-web = "4"
actix-web-actors = "4.1"


接下来让我们创建用于在索引页面呈现 html 的处理程序。
接下来,让我们创建处理程序,在索引页上渲染html。
在这种情况下,我们将读取index.html文件作为html模板,并替换table.html文件中的内容。

#[get("/")]
async fn index() -> Result<HttpResponse> {
    let content = std::fs::read_to_string("table.html").expect("table not found");
    let body = include_str!("index.html")
        .to_string()
        .replace("{content}", &content);
    Ok(HttpResponse::build(StatusCode::OK)
        .content_type("text/html; charset=utf-8")
        .body(body))
}


并为我们的websocket服务器添加入口点:

use websocket::FileWatcherWebsocket;

async fn echo_ws(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
    ws::start(FileWatcherWebsocket::new(), &req, stream)
}


然后是websocket服务器本身:

const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(1);
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
const FILE_PATH: &'static str = "table.html";

pub struct FileWatcherWebsocket {
    hb: Instant,
    modified: SystemTime,
}

impl FileWatcherWebsocket {
    pub fn new() -> Self {
        let metadata = fs::metadata(FILE_PATH).unwrap();
        let modified = metadata.modified().unwrap();

        Self {
            hb: Instant::now(),
            modified,
        }
    }

    fn hb(&self, ctx: &mut <Self as Actor>::Context) {
        ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
            if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
                println!("Websocket Client heartbeat failed, disconnecting!");

                ctx.stop();

                return;
            }

            let modified = fs::metadata(FILE_PATH).unwrap().modified().unwrap();
            if modified.duration_since(act.modified).unwrap() > Duration::from_millis(0) {
                act.modified = modified;
                println!("Sending file changes event! {}", &FILE_PATH);
                ctx.text("file_changed")
            }

            ctx.ping(b"");
        });
    }
}

impl Actor for FileWatcherWebsocket {
    type Context = ws::WebsocketContext<Self>;

    fn started(&mut self, ctx: &mut Self::Context) {
        self.hb(ctx);
    }
}

impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for FileWatcherWebsocket {
    fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
        match msg {
            Ok(ws::Message::Ping(msg)) => {
                self.hb = Instant::now();
                ctx.pong(&msg);
            }
            Ok(ws::Message::Pong(_)) => {
                self.hb = Instant::now();
            }
            Ok(ws::Message::Text(text)) => ctx.text(text),
            Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
            Ok(ws::Message::Close(reason)) => {
                ctx.close(reason);
                ctx.stop();
            }
            _ => ctx.stop(),
        }
    }
}


这个websocket服务器将每隔一秒Ping一次客户端,然后检查最后修改的文件table.html,如果有任何文件变化,它将发送*file_changed事件给客户端。
 
 

客户端
幸运的是,如今支持websocket客户端开箱即用。因此,这里是我们在index.html文件中包含的客户端的代码。

<script>
  let socket = new WebSocket("ws://localhost:8080/ws");

  socket.onopen = function(e) {
    console.log("[open] Connection established");
    console.log("Sending to server");
    socket.send("start_connection");
  };

  socket.onmessage = function(event) {
    console.log(`[message] Data received from server: ${event.data}`);
    if (event.data == "file_changed") {
      window.location.reload();
    }
  };

  socket.onclose = function(event) {
    if (event.wasClean) {
      console.log(`[close] Connection closed, code=${event.code} reason=${event.reason}`);
    } else {
      console.log('[close] Connection died');
    }
  };

  socket.onerror = function(error) {
    console.log(error)
  };

</script>


客户端将监听每个传入的消息,然后在有任何file_changed事件时重新加载浏览器。

这是一个非常简单的实现,但为我节省了大量的时间。
 

结论
我认为每当我们学习新技术时,用它来解决我们的问题会非常有趣,在这种情况下,我用它来自动重新加载网页,这让我减少了调试工作,我对此感到非常高兴。
如果你想查看完整的源代码,你可以看这里
 

3