【socketioxide和axum集成-实现websocket实时通信-Rust点滴】

启动socketio依靠examle里的layer

https://github.com/Totodore/socketioxide
原版echo代码

use axum::routing::get;
use serde_json::Value;
use socketioxide::{
    extract::{AckSender, Bin, Data, SocketRef},
    SocketIo,
};
use tracing::info;
use tracing_subscriber::FmtSubscriber;

fn on_connect(socket: SocketRef, Data(data): Data<Value>) {
    info!("Socket.IO connected: {:?} {:?}", socket.ns(), socket.id);
    socket.emit("auth", data).ok();

    socket.on(
        "message",
        |socket: SocketRef, Data::<Value>(data), Bin(bin)| {
            info!("Received event: {:?} {:?}", data, bin);
            socket.bin(bin).emit("message-back", data).ok();
        },
    );

    socket.on(
        "message-with-ack",
        |Data::<Value>(data), ack: AckSender, Bin(bin)| {
            info!("Received event: {:?} {:?}", data, bin);
            ack.bin(bin).send(data).ok();
        },
    );
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing::subscriber::set_global_default(FmtSubscriber::default())?;

    let (layer, io) = SocketIo::new_layer();

    io.ns("/", on_connect);
    io.ns("/custom", on_connect);

    let app = axum::Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .layer(layer);

    info!("Starting server");

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();

    Ok(())
}

通过文档
https://docs.rs/socketioxide/latest/socketioxide/index.html
和一些搜素出来代码,需要

  • 允许跨域
use tower::ServiceBuilder;
use tower_http::{cors::CorsLayer,cors::Any, services::ServeDir,add_extension::AddExtensionLayer };

    let app = axum::Router::new()
    .route("/", get( list_keys))
    .route("/postmsg", post( handl_emit))
        .route("/ioonline", get( list_keys))
        .layer(     ServiceBuilder::new()
        .layer(CorsLayer::permissive())
        // Enable CORS policy
      .layer(layer))

一. 使用可变State依靠axum里的example

all example axum axum github example readme

我要用的:key_vaule_store.rs axum Arc<RWloc>example
使用了,Arc,这个synce机制,有加锁的办法.
主要逻辑代码
声明结构


type SharedState = Arc<RwLock<State>>;

#[derive(Default)]
struct State {
    db: HashMap<String, Bytes>,
}

引用实现初始化

  .layer(
        ServiceBuilder::new()
            .load_shed()
            .concurrency_limit(1024)
            .timeout(Duration::from_secs(10))
            .layer(TraceLayer::new_for_http())
            .layer(AddExtensionLayer::new(SharedState::default()))
            .into_inner(),
    )

在hangdle里使用

async fn list_keys(Extension(state): Extension<SharedState>) -> String {
    let db = &state.read().unwrap().db;

    db.keys()
        .map(|key| key.to_string())
        .collect::<Vec<String>>()
        .join("\n")
}

在IO在namespace, handler中使用

//on main init io
io.ns("/chat", on_connect);
//
...
fn on_connect(socket: SocketRef, Data(data): Data<Value>,HttpExtension(state): HttpExtension<SharedState>) {
    info!(ns = socket.ns(), ?socket.id,   "Socket.IO connected");
    let mut stalock =state.write().unwrap();
    stalock.db.insert(socket.id.to_string(), format!("{}@{}",clientip,day()));
       

axum 的handle参数传递是各种组合,据说可以获得客户IP我也没找到特别合适的,下面介绍.

二.提取client,IP

为了了解每个socketio客户的在线状态,需要提取其IP.作为一个身份标识,最后发现ip不能做id,每个socket client有.自己的id.根据id的connect,disconnect,记录到上面的state.db. id做key, ip@time做value.一个IP可以在刷新时,造成,socket.id的相关的state快速变动,而disconect事件,往往有推后到,新id上线. 如果IP做id, 新的socket,会被旧socket的disconnect搞下线

1. 非代理,tcp,socket对方地址

在建立服务时获得引用,依靠HTTPExtension,这是原始tcp套接字的获取,
日常的app:::server

 axum::serve(listener, app).await.unwrap();

获取地址信息的app::server会携带connect_info::启动:

axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await.unwrap();

完整掩饰

use axum::{
    extract::ConnectInfo,
    routing::get,
    Router,
};
use std::net::SocketAddr;

let app = Router::new().route("/", get(handler));

async fn handler(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String {
    format!("Hello {addr}")
}

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await.unwrap();

信息来自,socketioxide的作者,他提供了很及时的帮助.
https://github.com/Totodore/socketioxide/issues/101
这种办法可行,随axum主版本更新,好过,第三方的extractor
比如这个,现在很新,但是我没有验证
https://github.com/imbolc/axum-client-ip

2.代理情况下socket.req_parts.

适应docket容器,非HOST模式,和反向代理的复杂情况.
在代理模式下
在handle里的缺省参数,socketRef的函数socket.req_parts().headers.get(IPKEY)提取到请求信息其中的headers,中的,x-forwarded-for,代理补充的remote-ip数据 ,具体名字可以在调试后确定下来.这是一个通用协议

static IPKEY:&str="x-forwarded-for";
//static IPKEY:&str="host";
static UP_ON:&str="update_online";
fn on_connect(socket: SocketRef, Data(data): Data<Value>,HttpExtension(state): HttpExtension<SharedState>) {
    info!(ns = socket.ns(), ?socket.id,   "Socket.IO connected");

    let clientip= match socket.req_parts().headers.get(IPKEY){
        Some(ipaddr)=> ipaddr.to_str().unwrap(),
        None=> "127.0.0.1"
    };
 
  

三. axum的handle中使用emit发送消息.

在连接建立时,把socketRef.clone()存入,共享State.然后在get,orpost的axum route handle获取并使用.
主要用于通过url发送广播消息, 不同服务器间的消息传递.
flask–> rust,socketio->socketio client.

因为要完成flask的socketio的解耦.目前只想到了这个办法.
上面的方法,可能造成socketRef的过早释放.不适合做正常的用法.
下面是最终有两种方法可用,

1. io,存入State解决.

#[derive(Default)] 
struct State {
    db: HashMap<String, String>,
    socket:Option<SocketIo>,
    
}
type SharedState = Arc<RwLock<State>>;
   io.ns("/", on_connect);
    io.ns("/chat", on_connect);
    let mut newstate=  SharedState::default();
     newstate.write().unwrap().socket=Some(io);
    let app = axum::Router::new()
    .route("/", get( list_keys))
    .route("/postmsg", post( handl_emit))
        .route("/ioonline", get( list_keys))
        .layer(     ServiceBuilder::new()
        .layer(AddExtensionLayer::new(newstate))
        .layer(CorsLayer::permissive())
           
       // Enable CORS policy
      .layer(layer))
     
      ;

得到io以后, 在handle使用,注意io.of是设定namespace, 就是在io.ns里,第一个参数.锁定某个空间.

async fn handl_emit(Extension(state): Extension<SharedState>,extract::Json(payload): extract::Json<emit_body>)  {
    if let  Some(io)= &state.read().unwrap().socket{
   
        io.of("/chat").unwrap()
        .emit(&payload.room,&payload.msg).ok();
 
       }
  println!("/r.n /postmsg to room:{},msg:{}",&payload.room,&payload.msg); 
}
async fn list_keys(Extension(state): Extension<SharedState>) -> Json<HashMap<String, String>> {
    let db = &state.read().unwrap().db;
    Json(db.clone())
}

2.把io存入初始设定作为唯一单例

在app启动前,初始化 layer,io, 然后,启动app时传入sever,最后在handle中直接使用。

#[derive(  Clone)]
struct OneIo{
   io:Box<SocketIo> ,
   layer:Box<layer::SocketIoLayer>,

}
fn initIO()->OneIo{
    let (layer, io) = SocketIo::new_layer();
    OneIo{io:Box::new(io),layer:Box::new(layer)}
}
lazy_static! {
    static ref APP_CONFIG:OneIo  = initIO();
}
........
#[tokio::main] 
async fn main() -> Result<(), Box<dyn std::error::Error>> {
(*APP_CONFIG).io.ns("/", on_connect);
let app = axum::Router::new()
    .route("/", get( list_keys))
    .route("/postmsg", post( handl_emit))
        .route("/ioonline", get( list_keys))
        .layer(     ServiceBuilder::new()
        .layer(AddExtensionLayer::new(newstate))
        .layer(CorsLayer::permissive())
           
       // Enable CORS policy
      .layer( *APP_CONFIG.layer.clone()))
     // i dont know why layer.clone() ,complier sugest .
      ;

 .......
//in some handle 
async fn handl_emit(extract::Json(payload): extract::Json<emit_body>)  {
   // if let  Some(io)= &state.read().unwrap().socket{
   
        APP_CONFIG.io.of("/chat").unwrap()
        .emit(&payload.room,&payload.msg).ok();
 }
}

3.http-post外部调用方式

这样在另一个web服务,flask里调用

requests.post('http://127.0.0.1:3002/postmsg', json={'room':'backmsg','msg':"asdfsdf"}).text

四.演示几个自己用的消息处理

1 .上线通知、null 未签到提醒、mess人员签到通知

static UP_ON:&str="update_online";
fn on_connect(socket: SocketRef, Data(data): Data<Value>,HttpExtension(state): HttpExtension<SharedState>) {
    info!(ns = socket.ns(), ?socket.id,   "Socket.IO connected");
//得到IP
    let clientip= match socket.req_parts().headers.get(IPKEY){
        Some(ipaddr)=> ipaddr.to_str().unwrap(),
        None=> "127.0.0.1"
    };
    //更新存储 
    let mut stalock =state.write().unwrap();
    stalock.db.insert(socket.id.to_string(), format!("{}@{}",clientip,day()));
//上线提醒
socket.broadcast().emit(UP_ON,&make_stamap(&clientip,true)).ok();
// 对null订阅,发送空值提醒。非空以后网页取消对null在订阅
    let mut map = HashMap::new();
    map.insert("data", INFONULL);
    socket.broadcast().emit("null", &map).ok();  //通知没有签到人员
    socket.on("mess", |socket: SocketRef, Data::<Value>(data)| {
     //转发mess, 更新人名,从客户端到机关. 
        let mut map = HashMap::new();
        map.insert("data", &data);
        socket.broadcast().emit("mess", &map).ok();;
    });    
    ....
}

2.离线通知

update_online 上下线更新.
离线的处理

  //位于 io.ns("/chat", on_connect);定义的on_connect 内部,但是像套娃,需要自己在另外一套一模一样的参数签名
  socket.on_disconnect(|socket: SocketRef, reason: DisconnectReason,HttpExtension(state): HttpExtension<SharedState>| async move {
         
        let clientip= match socket.req_parts().headers.get(IPKEY){
            Some(ipaddr)=> ipaddr.to_str().unwrap(),
            None=> "127.0.0.1"
        };

        state.write().unwrap().db.remove(&socket.id.to_string());
        if let Some((key, value)) =state.read().unwrap().db.iter().find(|(_, v)| (**v).contains(&clientip))
         {
            println!("找到了第一个符合条件的元素: key={}, value={}", key, value);
        } else {
            println!("没有找到符合条件的元素");
       
        socket.broadcast().emit(UP_ON,&make_stamap(&clientip,false)).ok();
        }
       //  println!("Socket {:?} on ns {} disconnected, reason: {:?}","", socket.ns(), reason);
    });

3.web中shell命令的异步输出.

flask用线程调用了shell命令,获得标准输出,
sh.py 文件:

  
import subprocess
import os 
import threading
callback=print
def run(command):
  global callback

  try:
       # 起线程执行ping命令
        
        task = threading.Thread(target=sh, args=(command, callback))
        task.start()
  except Exception as e:
          print(e)
  
def disconnect():
  global callback
  command = "./sndcpy.sh  %s i"  
  run(command) 
@app.route('/api/disconnect')
def disconnect():
    sh.disconnect()
    return "OK"

flask定义回调函数

@app.route("/api/shellout/<msg>")    
def shellout(msg):
    # socketio.emit('shellout',msg,namespace='/chat')
    import requests
    res=requests.post('http://127.0.0.1:3002/postmsg', json={'room':'shellout','msg':msg})
    return res.text
#绑定回调函数    
sh.callback=shellout  
  • 调用回调函数,核心命令request post .
requests.post('http://127.0.0.1:3002/postmsg', json={'room':'backmsg','msg':"asdfsdf"}).text

  • axum handle,调用state中的io,emit
 if let  Some(io)= &state.read().unwrap().socket{
   
        io.of("/chat").unwrap()
        .emit(&payload.room,&payload.msg).ok();
 
       }
  println!("/r.n /postmsg to room:{},msg:{}",&payload.room,&payload.msg); 

总结

更新到这里,主要注重 axum的socketio结合.而前期的是flask-socketio实现签到系统状态更新的前端后端的介绍.这里不再重复.
在实现过程中,修改了html中的js代码. 精简了流程。梳理过程发现了一个在线汇总列表的漏洞.就是上面说的从ip为标识,变更为socket id为标识.在离线过程搜索其他在线IP会话。
本次更新性能和灵活度有了提升,目前的二进制socketio不依赖环境, 随地可运行,扫清了未来全面迁移到rust在障碍。并且本单一功能可以作为扩展,起到保护源代码加密项目逻辑的目的。
![(https://i-blog.csdnimg.cn/direct/09e4d5f0d5c94650a71abd12e1c1a7ad.jpeg#pic_center)

下棋再见.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wjcroom

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值