actix-web使用ws

此代码是在官方示例上改造的、包含多群聊、私聊

用户根据uid进行区分 ws://127.0.0.1:8080/ws?uid=123&name=test1,可以加入多个房间,默认广播是main房间

toml

tokio = { version="1.8.1", features=["sync", "time", "rt-multi-thread","macros"] }
tokio-tungstenite = "0.15.0"
tungstenite = "0.14.0"
futures-util = "0.3.15"
env_logger = "0.8.4"
log = "0.4.14"
url = "2.0"
actix-web = "4"
actix-ws = "*"
actix-files="*"
rand="*"

main.rs

//! 多房间 WebSocket 聊天服务器
//! 在浏览器中打开 `http://localhost:8080/` 进行测试
use std::io;
use actix_files::NamedFile;
use actix_web::{App, Error, HttpRequest, HttpResponse, HttpServer, Responder, middleware, web};
use tokio::{task::{spawn, spawn_local}, try_join, };
mod handler;
mod server_simple;
pub use self::server_simple::{ChatServer, ChatServerHandle};

/// 连接ID类型
pub type ConnId = u64;

/// 房间ID类型
pub type RoomId = String;

/// 发送到房间/客户端的消息类型
pub type Msg = String;

/// 首页路由处理函数
async fn index() -> impl Responder {
    NamedFile::open_async("./static/index.html").await.unwrap()
}

/// 升级http链接成ws
async fn chat_ws(
    req: HttpRequest,
    stream: web::Payload,
    chat_server: web::Data<ChatServerHandle>,
) -> Result<HttpResponse, Error> {
    // 从查询参数中提取 UID 和名称
    let query_string = req.query_string();
    let params: std::collections::HashMap<String, String> = 
        url::form_urlencoded::parse(query_string.as_bytes())
            .into_owned()
            .collect();
    
    let uid = match params.get("uid") {
        Some(uid_str) => uid_str.clone(),
        None => {
            log::warn!("Connection attempt without UID parameter");
            return Err(actix_web::error::ErrorBadRequest("UID parameter is required"));
        }
    };
    
    let name = params.get("name").cloned();

    let (res, session, msg_stream) = actix_ws::handle(&req, stream)?;

    // 异步任务处理聊天
    spawn_local(handler::chat_ws(
        (**chat_server).clone(),
        session,
        msg_stream,
        uid,
        name,
    ));

    Ok(res)
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> io::Result<()> {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    log::info!("starting HTTP server at http://localhost:8080");

    let chat_server = ChatServer::new();
    let server_handle = ChatServerHandle::new(chat_server.clone());
    
    // 这里可以启动任何必要的后台任务
    // 对于简化版,我们不需要额外的运行循环
    
    let http_server = HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(server_handle.clone()))
            // WebSocket UI HTML file
            .service(web::resource("/").to(index))
            // websocket routes
            .service(web::resource("/ws").route(web::get().to(chat_ws)))
            // standard middleware
            .wrap(middleware::NormalizePath::trim())
            .wrap(middleware::Logger::default())
    })
    .workers(1)
    .bind(("127.0.0.1", 8080))?
    .run();

    http_server.await?;

    Ok(())
}

handler.rs

use std::time::{Duration, Instant};
use actix_ws::AggregatedMessage;
use futures_util::StreamExt as _;
use tokio::{time::interval};
use crate::{ChatServerHandle, ConnId};
/// 心跳检测间隔时间
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30);

/// 客户端无响应超时时间
const CLIENT_TIMEOUT: Duration = Duration::from_secs(60);

/// 处理从客户端接收到的文本和二进制消息,响应ping消息,并监控
/// 连接健康状况以检测网络问题并释放资源。
pub async fn chat_ws(
    chat_server: ChatServerHandle,
    mut session: actix_ws::Session,
    msg_stream: actix_ws::MessageStream,
    uid: String,
    initial_name: Option<String>,
) {
    let mut name: Option<String> = initial_name.clone();
    let mut last_heartbeat = Instant::now();
    let mut interval = interval(HEARTBEAT_INTERVAL);

    let conn_id = rand::random::<ConnId>();
    
    // 连接服务器并获取消息接收器
    log::info!("正在连接服务器,conn_id: {}, uid: {}, name: {:?}", conn_id, uid, initial_name);
    let mut msg_rx = chat_server.connect(conn_id, uid.clone(), initial_name.clone()).await;
    log::info!("服务器连接成功,开始接收消息");

    let mut msg_stream = msg_stream
        .max_frame_size(128 * 1024)
        .aggregate_continuations()
        .max_continuation_size(2 * 1024 * 1024);

    let close_reason = loop {
        // 开始tokio的select
        tokio::select! {
            // 等待客户端消息
            Some(result) = msg_stream.next() => {
                //处理消息
                match result {
                    Ok(msg) => {  //处理消息成功
                        log::info!("接收到消息类型: {:?}", msg);
                        match msg {
                            AggregatedMessage::Ping(bytes) => { // ping消息、浏览器协议处理
                                last_heartbeat = Instant::now();
                                let _ = session.pong(&bytes).await;
                            }

                            AggregatedMessage::Pong(_) => {    //pong消息、浏览器协议处理
                                last_heartbeat = Instant::now();
                            }

                            AggregatedMessage::Text(text) => { //文本消息
                                log::info!("收到文本消息: {}", text);
                                process_text_msg(&chat_server, &mut session, &text, conn_id, &mut name).await;
                                log::info!("消息处理完成");
                            }

                            AggregatedMessage::Binary(_bin) => {      //二进制消息,如果是protobuf的话,会走这
                                log::warn!("unexpected binary message");
                            }

                            AggregatedMessage::Close(reason) => { //关闭
                                log::info!("收到客户端关闭消息,原因: {:?}", reason);
                                break reason;
                            },
                        }
                    }
                    Err(e) => { // 处理消息失败
                        log::error!("WebSocket消息流错误: {:?}", e);
                        break None;
                    }
                }
            }
            // 等待聊天服务器的消息
            Some(chat_msg) = msg_rx.recv() => {
                // 如果发送消息失败(连接已关闭),则跳出循环
                if session.text(chat_msg).await.is_err() {
                    break None;
                }
            }
            // 定时器
            _ = interval.tick() => {
                if Instant::now().duration_since(last_heartbeat) > CLIENT_TIMEOUT {
                    log::info!("客户端心跳超时,断开连接");
                    break None;
                }
                if session.ping(b"").await.is_err() {
                    log::info!("发送心跳ping失败,断开连接");
                    break None;
                }
            }
        }
    };

    log::info!("WebSocket连接即将断开,原因: {:?}", close_reason);
    // 处理断开消息
    chat_server.disconnect(conn_id).await;

    // 关闭
    let _ = session.close(close_reason).await;
}

//处理文本消息
async fn process_text_msg(
    chat_server: &ChatServerHandle,
    session: &mut actix_ws::Session,
    text: &str,
    conn: ConnId,
    name: &mut Option<String>,
) {
    let msg = text.trim();
    // 检查 /<cmd> 类型的消息
    if msg.starts_with('/') {
        let mut cmd_args = msg.splitn(2, ' ');
        // 字符串长度不为0
        match cmd_args.next().unwrap() {
            "/list" => {  //房间列表
                let rooms = chat_server.list_rooms().await;
                for room in rooms {
                    if session.text(room).await.is_err() {
                        break;
                    }
                }
            }

            "/join" => match cmd_args.next() {  // 加入房间
                Some(room) => {
                    chat_server.join_room(conn, room).await;
                    let _ = session.text(format!("joined {room}")).await;
                }

                None => {
                    let _ = session.text("!!! room name is required").await;
                }
            },

            "/pm" => match cmd_args.next() {  //私聊
                Some(target_and_msg) => {
                    // 分割目标UID和消息内容,格式为: "target_uid:消息内容"
                    let parts: Vec<&str> = target_and_msg.splitn(2, ':').collect();
                    if parts.len() == 2 {
                        let target_uid = parts[0].trim();
                        let pm_msg = parts[1].trim();
                        
                        // 发送私聊消息
                        let success = chat_server.send_private_message(conn, target_uid, pm_msg).await;
                        
                        if success {
                            let _ = session.text(format!("[系统消息] 私聊消息已发送给用户 {}: {}", target_uid, pm_msg)).await;
                        } else {
                            let _ = session.text(format!("[系统消息] 发送私聊消息失败,用户 {} 可能不在线", target_uid)).await;
                        }
                    } else {
                        let _ = session.text("[系统消息] 私聊命令格式错误,请使用: /pm target_uid:消息内容").await;
                    }
                }
                None => {
                        let _ = session.text("[系统消息] 请输入目标UID和消息内容,格式: /pm target_uid:消息内容").await;
                }
            },
            
            _ => {
                let _ = session
                    .text(format!("!!! unknown command: {msg}"))
                    .await;
            }
        }
    } else {
        // 检查是否为房间消息格式: #房间名 消息内容
        if msg.starts_with('#') {  //某个房间进行广播
            let parts: Vec<&str> = msg.splitn(2, ' ').collect();
            if parts.len() == 2 {
                let room_name = &parts[0][1..]; // 移除 # 符号
                let room_msg = parts[1];
                
                // 添加前缀并发送到指定房间
                let prefixed_msg = match name {
                    Some(name) => format!("{name}: {room_msg}"),
                    None => room_msg.to_string(),
                };
                
                log::info!("发送房间消息到: {}", room_name);
                chat_server.send_message_to_room(conn, room_name, &prefixed_msg).await;
            } else {
                // 如果格式不正确,发送错误消息
                let _ = session.text("[系统消息] 房间消息格式错误,请使用: #房间名 消息内容").await;
            }
        } else {
            // 普通消息,发送到当前房间
            let msg = match name {
                Some(name) => format!("{name}: {msg}"),
                None => msg.to_owned(),
            };

            log::info!("发送普通消息: {}", msg);
            chat_server.send_message(conn, &msg).await;
        }
    }
}

server_simple.rs

//! 简化版聊天服务器
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tokio::sync::RwLock;
use log;
use log::info;

//链接id
pub type ConnId = u64;
//房间名字
pub type RoomId = String;
//消息
pub type Msg = String;

/// 用户信息结构体,整合所有用户相关数据
#[derive(Clone)]
struct UserInfo {
    conn_id: ConnId,
    name: String,
    rooms: HashSet<RoomId>, //可以加入多个房间
    sender: tokio::sync::mpsc::UnboundedSender<Msg>,
}

#[derive(Clone)]
pub struct ChatServer {
    // 以UID为键的用户信息映射
    uid_to_user: Arc<RwLock<HashMap<String, UserInfo>>>,
    // 房间到用户集合的映射
    room_users: Arc<RwLock<HashMap<RoomId, HashSet<ConnId>>>>,
}

impl ChatServer {
    pub fn new() -> Self {
        let mut rooms = HashMap::new();
        // 初始化一个main房间,全部用户都在一个大群
        rooms.insert("main".to_string(), HashSet::new());
        //实例化hash表
        Self {
            uid_to_user: Arc::new(RwLock::new(HashMap::new())),
            room_users: Arc::new(RwLock::new(rooms)),
        }
    }

    // 添加连接
    pub async fn add_connection(&self, conn_id: ConnId, uid: String, name: Option<String>) -> tokio::sync::mpsc::UnboundedReceiver<Msg> {
        info!("添加连接,conn_id: {}, uid: {}, name: {:?}", conn_id, uid, name);
        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
        
        // 设置用户名
        let name = name.unwrap_or_else(|| uid.clone());
        
        // 创建用户信息并存储
        let user_info = UserInfo {
            conn_id,
            name,
            rooms: HashSet::new(),
            sender: tx,
        };
        //写法1
        //self.uid_to_user.write().await.insert(uid.clone(), user_info);
        //写法2
        RwLock::write(&self.uid_to_user).await.insert(uid.clone(), user_info);
        info!("用户信息已存储,uid: {}", uid.clone());
        // 让用户加入默认房间
        self.join_room_internal(conn_id, "main".to_string()).await;
        
        rx
    }

    // 移除连接
    // 内部辅助方法:通过conn_id查找uid
    async fn get_uid_by_conn_id(&self, conn_id: ConnId) -> Option<String> {
        let users = RwLock::read(&self.uid_to_user).await;
        users.iter().find(|(_, user_info)| user_info.conn_id == conn_id).map(|(uid, _)| uid.clone())
    }
    
    // 内部辅助方法:通过conn_id查找用户名
    async fn get_username_by_conn_id(&self, conn_id: ConnId) -> Option<String> {
        let users = RwLock::read(&self.uid_to_user).await;
        users.values().find(|user_info| user_info.conn_id == conn_id).map(|user_info| user_info.name.clone())
    }
    
    // 内部辅助方法:通过conn_id查找用户房间列表
    async fn get_user_rooms_by_conn_id(&self, conn_id: ConnId) -> Vec<String> {
        let users = RwLock::read(&self.uid_to_user).await;
        users.values().find(|user_info| user_info.conn_id == conn_id).map(|user_info| user_info.rooms.iter().cloned().collect()).unwrap_or_default()
    }

    // 离线、同时也退出加入的房间
    pub async fn remove_connection(&self, conn_id: ConnId) {
        info!("移除连接,conn_id: {}", conn_id);
        // 遍历查找对应连接ID的用户
        let uid_to_remove = self.get_uid_by_conn_id(conn_id).await;
        
        info!("查找待移除的用户,conn_id: {}, 找到的uid: {:?}", conn_id, uid_to_remove);
        
        if let Some(uid) = uid_to_remove {
            // 从所有房间中移除用户
            let all_rooms = self.list_rooms().await;
            info!("用户 {} 加入了 {} 个房间,开始清理", uid, all_rooms.len());
            
            // 先收集需要从房间中移除的连接ID,避免在持有锁的同时进行修改
            let rooms_to_clean = {
                let room_users_read = RwLock::read(&self.room_users).await;
                all_rooms.into_iter().filter(|room| {
                    room_users_read.get(room).map_or(false, |users| users.contains(&conn_id))
                }).collect::<Vec<_>>()
            };
            
            info!("开始清理 {} 个房间", rooms_to_clean.len());
            for (i, room) in rooms_to_clean.iter().enumerate() {
                info!("处理第 {} 个房间: {}", i, room);
                
                let mut room_users_lock = RwLock::write(&self.room_users).await;
                info!("获取房间 {} 的写锁成功", room);
                
                if let Some(room_users) = room_users_lock.get_mut(room) {
                    info!("找到房间 {} 的用户列表,准备移除用户 {}", room, uid);
                    room_users.remove(&conn_id);
                    info!("从房间 {} 移除用户 {} 完成", room, uid);
                    
                    // 检查房间是否为空,但保留 main 房间不被删除
                    if room_users.is_empty() && room != "main" {
                        info!("房间 {} 为空,准备删除", room);
                        room_users_lock.remove(room);
                        info!("房间 {} 已删除", room);
                    }
                } else {
                    info!("房间 {} 不存在于房间列表中", room);
                }
                drop(room_users_lock); // 明确释放锁
                info!("房间 {} 处理完成", room);
            }
            info!("所有房间清理完成");
            
            info!("准备移除用户基本信息,uid: {}", uid);
            RwLock::write(&self.uid_to_user).await.remove(&uid);
            info!("用户 {} 信息已移除", uid);
        } else {
            info!("未找到连接ID {} 对应的用户", conn_id);
        }
    }

    // 加入房间
    pub async fn join_room_internal(&self, conn_id: ConnId, room: RoomId) {
        // 添加到房间用户映射
        RwLock::write(&self.room_users)
            .await
            .entry(room.clone())
            .or_insert_with(HashSet::new)
            .insert(conn_id);
        
        // 通过conn_id查找对应的uid
        let uid_found = self.get_uid_by_conn_id(conn_id).await;
        // 设置用户信息新加入了房间
        if let Some(uid) = uid_found {
            if let Some(user_info) = RwLock::write(&self.uid_to_user).await.get_mut(&uid) {
                user_info.rooms.insert(room);
            }
        }
    }

    // 发送消息到房间--遍历房间内的人
    pub async fn send_message_to_room(&self, conn_id: ConnId, room: &str, msg: &str) {
        info!("准备发送消息,conn_id: {}, room: {}, msg: {}", conn_id, room, msg);
        
        // 通过conn_id查找发送者用户名
        let sender_name = self.get_username_by_conn_id(conn_id).await;
        
        if let Some(ref name) = sender_name {
            info!("找到发送者名称: {}", name);
        }
        
        if let Some(name) = sender_name {
            let message = format!("[{}] {}: {}", room, name, msg);
            info!("构建消息: {}", message);
            
            if let Some(users) = RwLock::read(&self.room_users).await.get(room) {
                info!("房间 {} 有 {} 个用户", room, users.len());
                // 复制一份用户连接ID列表,避免在持有读锁的情况下遍历
                let user_ids: Vec<ConnId> = users.iter().copied().collect();
                
                for user_conn_id in user_ids {
                    info!("向用户 {} 发送消息", user_conn_id);
                    // 通过conn_id查找用户
                    let target_user_info = {
                        let users = RwLock::read(&self.uid_to_user).await;
                        users.values().find(|info| info.conn_id == user_conn_id).cloned()
                    };
                    if let Some(user_info) = target_user_info {
                        info!("找到目标用户,尝试发送消息");
                        // 如果发送失败(接收端已关闭),忽略错误
                        if user_info.sender.send(message.clone()).is_err() {
                            log::warn!("向用户 {} 发送消息失败,连接可能已断开", user_conn_id);
                        } else {
                            info!("消息成功发送到用户 {}", user_conn_id);
                        }
                    } else {
                        log::warn!("未找到用户 {} 的信息", user_conn_id);
                    }
                }
            } else {
                log::warn!("未找到房间 {}", room);
            }
        } else {
            log::warn!("未找到发送者名称,conn_id: {}", conn_id);
        }
    }

    // 发送私聊消息 todo:未判断自己给自己发
    pub async fn send_private_message(&self, sender_conn_id: ConnId, target_uid: &str, msg: &str) -> bool {
        // 先从uid_to_user中获取目标用户信息
        if let Some(target_info) = RwLock::read(&self.uid_to_user).await.get(target_uid) {
            
            // 通过conn_id查找发送者用户名
            let sender_name = self.get_user_name(sender_conn_id).await;
            
            if let Some(name) = sender_name {
                let message = format!("[私聊][{} -> {}]: {}", name, target_uid, msg);
                
                // 如果发送失败(接收端已关闭),忽略错误
                if target_info.sender.send(message).is_err() {
                    // 发送失败,可能是接收方已断开
                    return false;
                }
                return true;
            }
        }
        
        false
    }

    // 获取用户姓名
    pub async fn get_user_name(&self, conn_id: ConnId) -> Option<String> {
        self.get_username_by_conn_id(conn_id).await
    }

    // 获取用户所在房间列表
    pub async fn get_user_rooms(&self, conn_id: ConnId) -> Vec<String> {
        self.get_user_rooms_by_conn_id(conn_id).await
    }

    // 获取房间列表
    pub async fn list_rooms(&self) -> Vec<String> {
        RwLock::read(&self.room_users)
            .await
            .keys()
            .cloned()
            .collect()
    }
}

#[derive(Clone)]
pub struct ChatServerHandle {
    server: ChatServer,
}

impl ChatServerHandle {
    pub fn new(server: ChatServer) -> Self {
        Self { server }
    }

    pub async fn connect(&self, conn_id: ConnId, uid: String, name: Option<String>) -> tokio::sync::mpsc::UnboundedReceiver<Msg> {
        self.server.add_connection(conn_id, uid, name).await
    }

    pub async fn disconnect(&self, conn_id: ConnId) {
        self.server.remove_connection(conn_id).await;
    }

    pub async fn join_room(&self, conn_id: ConnId, room: impl Into<RoomId>) {
        self.server.join_room_internal(conn_id, room.into()).await;
    }

    pub async fn send_message_to_room(&self, conn_id: ConnId, room: &str, msg: &str) {
        self.server.send_message_to_room(conn_id, room, msg).await;
    }

    pub async fn send_message(&self, conn_id: ConnId, msg: &str) {
        self.server.send_message_to_room(conn_id, "main", msg).await;
    }

    pub async fn send_private_message(&self, sender_conn_id: ConnId, target_uid: &str, msg: &str) -> bool {
        self.server.send_private_message(sender_conn_id, target_uid, msg).await
    }

    pub async fn list_rooms(&self) -> Vec<String> {
        self.server.list_rooms().await
    }

    pub async fn get_user_name(&self, conn_id: ConnId) -> Option<String> {
        self.server.get_user_name(conn_id).await
    }

    pub async fn get_user_rooms(&self, conn_id: ConnId) -> Vec<String> {
        self.server.get_user_rooms(conn_id).await
    }
}

index.html

<!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>

    <form id="urlform">
      <label for="url">WebSocket Address</label>
      <br>
      <input style="width: 500px" type="text" id="url" value="ws://127.0.0.1:8080/ws?uid=123&name=testName" />
      <input type="submit" value="确定" />
    </form>

    <div>
      <button id="connect" disabled>连接</button>
      <span>Status:</span>
      <span id="status">disconnected</span>
    </div>

    <div id="log"></div>

    <form id="chatform">
      <input type="text" id="text" autocomplete="off" />
      <input type="submit" id="send" />
    </form>

    <hr />

    <section>
      <h2>Commands</h2>
      <table style="border-spacing: 0.5em">
        <tr>
          <td>
            <code>/list</code>
          </td>
          <td>展示所有房间</td>
        </tr>
        <tr>
          <td>
            <code>/join name</code>
          </td>
          <td>加入房间、如果没有房间,就创建一个,可以加入多个房间</td>
        </tr>
        <tr>
          <td>
            <code>/pm 目标用户id:消息内容</code>
          </td>
          <td>私聊</td>
        </tr>
        <tr>
          <td>
            <code>#房间名字 群发的消息内容</code>
          </td>
          <td>相同房间的用户会进行群发</td>
        </tr>
        <tr>
          <td>
            <code>消息内容</code>
          </td>
          <td>默认会群发main这个房间</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')
      const $urlInput = document.querySelector('#url')

      /** @type {WebSocket | null} */
      var socket = null
      var websocketUrl = '';

      function log(msg, type = 'status') {
        $log.innerHTML += `<p class="msg msg--${type}">${msg}</p>`
        $log.scrollTop += 1000
      }

      function connect() {
        disconnect()

        websocketUrl = $urlInput.value;

        log('Connecting...')
        socket = new WebSocket(websocketUrl)

        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'
        }
      }

      document.querySelector('#urlform').addEventListener('submit', (event) => {
        event.preventDefault()
        $connectButton.disabled = false
        websocketUrl = $urlInput.value
      })

      $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>

效果

ScreenShot_2026-01-14_180355_104

posted @ 2026-01-15 08:13  朝阳1  阅读(2)  评论(0)    收藏  举报