利用Rust与Flutter开发一款小工具

2023-05-16

在这里插入图片描述

1.起因

起因是年前看到了一篇Rust + iOS & Android|未入门也能用来造轮子?的文章,作者使用Rust做了个实时查看埋点的工具。其中作者的一段话给了我启发:

无论是 LookinServer 、 Flipper 等 Debug 利器,还是 Flutter / Web Debug Tools,都是在电脑上调试 App。那我们也可以用类似的方式,把实时埋点数据显示在电脑上,不再局限于同一块屏幕。

我司目前的埋点走查是在测试盒子中有一个埋点查看页面,Debug包在数据上报的同时会将信息临时保存起来。当进入这个页面时会以列表的形式展示出来。并且iOS 和Android的页面展示和使用方式也略有不同。

后面我觉得这样进入退出页面查看不方便,就将页面改成了悬浮窗。虽然方便了一些,但是也发现了新的问题:

  • 手机上屏幕大小有限,悬浮窗只有屏幕的一半,可展示信息有限。
  • 悬浮窗会遮挡页面,有时不便于点击页面上的按钮。

刚好前阵子升级了手机系统到Android 13,发现log在控制台都打印不出来了(后面发现App适配到13就正常了。。)。所以有了一个想法,使用Rust通过WebSocket进行数据发送,使用Flutter实现服务端接收App发送的信息并显示出来。

当然了,如果我们的应用是flutter写的,可以直接使用Dart的ffi来直接调用Rust函数。这个我后面有时间会单独写一篇来分享。

2.实现

之所以选择RustFlutter是看中它们的跨平台能力。使用Rust进行WebSocket数据发送,就不用Android和iOS端去重复开发这个功能,只需要简单调用即可,并且Rust有许多开箱即用的库。

Flutter的跨平台能力就更不用说了。比如这个小工具我就可以一套代码输出Windows和macOS两个平台的安装包,保证接收端逻辑和UI的一致。

发送端

Rust部分

关于Rust库的打包以及双端的使用可以看我上一篇分享的Rust库交叉编译以及在Android与iOS使用。这里主要说一下具体的实现代码。

首先是添加WebSocket 库 ws-rs依赖到Cargo.toml文件:

[dependencies]
ws = "0.9.2"
# 全局的静态变量
lazy_static = "1.4.0"

实现代码如下:

use std::collections::HashMap;
use std::sync::Mutex;
use std::{ffi::CStr, os::raw::c_char};
use ws::{connect, Handler, Sender, Handshake, Result, Message, CloseCode, Error};
use ws::util::Token;
#[macro_use]
extern crate lazy_static;

lazy_static! {
    static ref DATA_MAP: Mutex<HashMap<String, Sender>> = {
        let map: HashMap<String, Sender> = HashMap::new();
        Mutex::new(map)
    };
}

struct Client {
    sender: Sender,
    host: String,
}

impl Handler for Client {
    fn on_open(&mut self, _: Handshake) -> Result<()> {
        DATA_MAP.lock().unwrap().insert(self.host.to_owned(), self.sender.to_owned());
        Ok(())
    }

    fn on_message(&mut self, msg: Message) -> Result<()> {
        println!("<receive> '{}'. ", msg);
        Ok(())
    }

    fn on_close(&mut self, _code: CloseCode, _reasonn: &str) {
        DATA_MAP.lock().unwrap().remove(&self.host);
    }

    fn on_timeout(&mut self, _event: Token) -> Result<()> {
        DATA_MAP.lock().unwrap().remove(&self.host);
        self.sender.shutdown().unwrap();
        Ok(())
    }

    fn on_error(&mut self, _err: Error) {
        DATA_MAP.lock().unwrap().remove(&self.host);
    }

    fn on_shutdown(&mut self) {
        DATA_MAP.lock().unwrap().remove(&self.host);
    }

}

#[no_mangle]
pub extern "C" fn websocket_connect(host: *const c_char) {
    let c_host = unsafe { CStr::from_ptr(host) }.to_str().unwrap();
    if let Err(err) = connect(c_host, |out| {
        Client {
            sender: out,
            host: c_host.to_string(),
        }
    }) {
        println!("Failed to create WebSocket due to: {:?}", err);
    }
}

#[no_mangle]
pub extern "C" fn send_message(host: *const c_char, message: *const c_char) {
    let c_message = unsafe { CStr::from_ptr(message) }.to_str().unwrap();
    let c_host = unsafe { CStr::from_ptr(host) }.to_str().unwrap();
    let binding = DATA_MAP.lock().unwrap();
    let sender = binding.get(&c_host.to_string());
    
    match sender {
        Some(s) => {
            if s.send(c_message).is_err() {
                println!("Websocket couldn't queue an initial message.")
            };
        } ,
        None => println!("None")
    }
}

#[no_mangle]
pub extern "C" fn websocket_disconnect(host: *const c_char) {
    let c_host = unsafe { CStr::from_ptr(host) }.to_str().unwrap();
    DATA_MAP.lock().unwrap().remove(&c_host.to_string());
}

简单实现了连接,发送,断开连接三个方法。思路是连接成功后会将发送结构体(Sender)保存在Map中,每次发送时先检查是否连接再发送。这样也就实现了连接多台设备,一对多发送的功能。

Android还需要添加对应的JNI方法:

#[cfg(target_os = "android")]
#[allow(non_snake_case)]
pub mod android {
    extern crate jni;

    use self::jni::objects::{JClass, JString};
    use self::jni::JNIEnv;
    use super::*;

    #[no_mangle]
    pub unsafe extern "C" fn Java_com_weilu_utils_EventLogUtils_sendMessage(
        env: JNIEnv,
        _: JClass,
        host: JString,
        message: JString,
    ) {
        send_message(
            env.get_string(host)
                .expect("invalid pattern string")
                .as_ptr(),
            env.get_string(message)
                .expect("invalid pattern string")
                .as_ptr(),
        );
    }

    #[no_mangle]
    pub unsafe extern "C" fn Java_com_weilu_utils_EventLogUtils_connect(
        env: JNIEnv,
        _: JClass,
        host: JString,
    ) {
        websocket_connect(
            env.get_string(host)
                .expect("invalid pattern string")
                .as_ptr(),
        );
    }

    #[no_mangle]
    pub unsafe extern "C" fn Java_com_weilu_utils_EventLogUtils_disconnect(
        env: JNIEnv,
        _: JClass,
        host: JString,
    ) {
        websocket_disconnect( 
            env.get_string(host)
                .expect("invalid pattern string")
                .as_ptr(),
        );
    }
}

至此,发送端部分完成。打包集成进项目就可以使用了。

Android部分

Android端调用代码如下:

public class EventLogUtils {

    static {
        System.loadLibrary("event_log_kit");
    }

    private static native void sendMessage(final String host, final String message);
    private static native void connect(final String host);
    private static native void disconnect(final String host);

    private static List<String> addressList = null;

    public static List<String> getAddressList() {
        return addressList;
    }

    /**
     * 保存 IP 地址,传空时断开所有连接
     */
    public static void saveAddress(String address) {
        if (TextUtils.isEmpty(address)) {
            if (addressList != null) {
                for (String url : addressList) {
                    disconnect(url);
                }
            }
            addressList = null;
            return;
        }
        // 多个地址逗号隔开
        if (address.contains(",")) {
            addressList = new ArrayList<>(Arrays.asList(address.split(",")));
        } else {
            addressList = new ArrayList<>();
            addressList.add(address);
        }

        for (String url : addressList) {
            // 子线程调用,可替换为其他方案,这里使用了线程池
            Executor.getExecutor().getExecutorService().submit(new Runnable() {
                @Override
                public void run() {
                    // 循环,如果意外断开,自动重连
                    while (addressList != null) {
                        connect("ws://" + url);
                    }
                    // 工具连接彻底断开
                }
            });
        }
    }

    /**
     * 发送信息
     */
    public static void sendMessage(String message) {
        if (addressList == null) {
            return;
        }
        for (String url : addressList) {
            sendMessage("ws://" + url, message);
        }
    }
}

代码也比较简单,连接方法在子线程调用,如果发现连接断开会自动重连。

iOS部分就不具体说明了,实现思路一样的。

接收端

首先是发送数据的定义,发送的是json格式字符串。定义的主要参数如下:

class EventLogEntity {
  /// event/log
  String type = '';
  /// 事件名称或log tag
  String? name;
  /// 手机型号
  String? deviceModel;
  /// 时间戳
  int time = 0;
  String data = '';

  ...
}
  • type:用于区分数据类型,目前分为埋点事件与log。
  • name:事件名称或log tag,用于数据的筛选。
  • deviceModel:设备名用于区分数据来源,如果有多个设备同时发送数据可以便于分类。
  • time:时间戳,用于数据排序。

其他参数可以根据自己的需求添加,比如log的等级,数据展示时展开或者收起。

UI组件我使用了fluent_ui,它提供了原生Windows应用风格的组件,比较适合桌面端程序。状态管理使用flutter_riverpod。

具体的代码实现就不多说了,主要说一下核心的数据接收部分。

// https://doc.xuwenliang.com/docs/dart-flutter/2499
class WebSocketManager{

  HttpServer? requestServer;

  Future startWebSocketListen() async {
    final String ip = '192.168.31.232';
    final String port = '51203';
    stopWebSocketListen();
    //HttpServer.bind(主机地址,端口号)
    requestServer = await HttpServer.bind(ip, int.parse(port)).catchError((error) {
      debugPrint('bind error: $error');
    });
    await for(HttpRequest request in requestServer!) {
      serveRequest(request).catchError((error){
        debugPrint('listen error: $error');
      });
    }
  }

  void stopWebSocketListen() {
    requestServer?.close();
    requestServer = null;
  }

  Future serveRequest(HttpRequest request) {
    //判断当前请求是否可以升级为WebSocket
    if (WebSocketTransformer.isUpgradeRequest(request)) {
      //升级为webSocket
      return WebSocketTransformer.upgrade(request).then((webSocket) {
        //webSocket消息监听
        webSocket.listen((msg) async {
          debugPrint('listen:$msg');
		  if (webSocket.closeCode == null) {
            // 这里可以回复客户端消息
            webSocket.add('收到');
          }
          // 可以在这里解析数据,刷新页面
		  ...
        });
      });
    } else {
      return Future((){});
    }
  }
}

然后为了便于使用,避免使用者自己查询填写ip,我们需要获取当前设备ip地址:

  Future<String> getDeviceIp() async {
    String ip = "";
    if (!kIsWeb) {
      for (var interface in await NetworkInterface.list()) {
        for (var address in interface.addresses) {
          ip = address.address;
        }
      }
    }
    return ip;
  }

端口可以给个默认值或者自己随便输入一个,然后可以用shared_preferences插件保存用户配置。下次启动时就自动连接了。
请添加图片描述
手机端可以实现一个输入连接地址的页面,输入电脑端的ip和端口号后就可以发送数据了。或者扫描二维码连接。

3.成果展示

目前实现功能如下:

  • 可同时接收多台设备发送数据,数据按机型名称分类展示。
  • 数据的筛选,搜索(关键字高亮)。
  • 搜索记录的保存。
  • json数据格式化展示。

请添加图片描述


因为小工具在公司内部使用,所以就不开源完整的代码了。有了文章中的核心代码,你可以根据自己的需求实现。也不必局限于这些功能,你完全可以通过Rust和Flutter的跨平台能力开发更多功能,本篇也只是抛砖引玉。

如果本篇对你有所启发帮助,不妨点赞支持一下。如果你有好的想法,也欢迎评论交流。

4.参考

  • Rust + iOS & Android|未入门也能用来造轮子?
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

利用Rust与Flutter开发一款小工具 的相关文章

  • ubuntu时钟同步配置

    1 手动执行 ntpdate host 2 自动执行 配置文件 xff1a etc systemd timesyncd conf ntp使能 xff1a timedatectl set ntp true 重启时钟服务 xff1a syste
  • 百元买百鸡(C++)

    我国古代数学家张丘建在 算经 一书中曾提出过著名的 百钱买百鸡 问题 xff0c 该问题叙述如下 xff1a 鸡翁一 xff0c 值钱五 xff1b 鸡母一 xff0c 值钱三 xff1b 鸡雏三 xff0c 值钱一 xff1b 百钱买百鸡
  • git 放弃更改常用命令

    1 未add commit git checkout filename path 2 已add未commit git reset HEAD filename 3 放弃已经commit的修改 git reflog查看commit日志 git
  • linux解压缩常见命令

    tar详解 c 建立压缩档案 x xff1a 解压 t xff1a 查看内容 r xff1a 向压缩归档文件末尾追加文件 u xff1a 更新原压缩包中的文件 这五个是独立的命令 xff0c 压缩解压都要用到其中一个 xff0c 可以和别的
  • linux 安装 gcc 方法

    今天安装Linux虚拟机发现竟然没有gcc 感觉是崩溃的 xff0c 上网寻找一下安装方法 xff0c 借鉴总结了一下得出下面步骤 使用root权限下载 sudo yum y install gcc gcc c 43 43 autoconf
  • MFC编程基础(—)

    MFC类的基本层次结构如图所示 xff1a CObject类是MFC提供的绝大多数类的基类 该类完成动态空间的分配和回收 xff0c 支持一般的诊断 出错信息处理和文档序列化等 CCmdTarget类主要负责将系统事件 xff08 消息 x
  • 开启windows10系统组策略更新、开启Windows自动更新、关闭自动更新

    win 43 R键打开运行 xff0c 输入regedit 按路径 计算机 HKEY LOCAL MACHINE SOFTWARE Policies Microsoft Windows WindowsUpdate AU 找到NoAutoUp
  • linux 中 /etc/systemd/system和/usr/lib/systemd/system 的区别

    每一个 Unit xff08 服务等 xff09 都有一个配置文件 xff0c 告诉 Systemd 怎么启动这个 Unit Systemd 默认从目录 etc systemd system 读取配置文件 但是 xff0c 里面存放的大部分
  • Vue3使用axios请求数据后使用v-for显示不出数据

    getAccount let that 61 this axios url 39 http localhost 8080 api GetAllUniAccounts 39 method 39 post 39 then res 61 gt t
  • Axios请求中Content-Type的使用总结

    Axios请求头中的Content Type常见的有3种 xff1a 1 Content Type application json 2 Content Type application x www form urlencoded 3 Co
  • mycat全局序列号数据库方式

    1 server xml lt property name 61 34 sequnceHandlerType 34 gt 1 lt property gt 2 schema xml lt table name 61 34 z test 34
  • Warning: Packets out of order. Expected 8 received 0

    环境 xff1a linux 43 mycat 43 hyperf 43 es 原因 xff1a mycat进行了分片 解决 xff1a 修改文件路径 xff1a vendor hyperf database src Connectors
  • Ubuntu下不能切换中文,qt creator无法输入中文,sogo输入法(详细步骤)

    目录 xff1a 1 解决ubuntu 不支持切换中文 xff0c 并安装sogo输入法步骤 xff1b 2 解决Qt Creator不支持中文输入 xff1a 详细步骤 xff1a 一 解决ubuntu 不支持切换中文 xff0c 并安装
  • wordpress插件API入口

    lt php 64 package Moodo Zhong 64 version 1 0 0 Plugin Name Moodo Zhong Plugin URI http wordpress org plugins hello dolly
  • PHP源码目录说明

    1 build 和编译有关的目录 2 ext 扩展库代码 xff0c 例如 MySQL zlib iconv 等我们熟悉的扩展库 其中 ext standard 目录下是常用的标准函数集 3 main 主目录包含主要的 PHP 宏和定义 4
  • 终端程序定义示例

    interrupt 0 外部中断0 interrupt 1 T0中断 interrupt 2 外部中断1 interrupt 3 T1中断 interrupt 4 串口中断 我来告诉你实质 xff1a 单片机的中断处理是这样工作的 xff0
  • lisp学习资料

    中文LISP学习网站 CL HTTP franz com 217条消息 使用hunchentoot搭建Lisp web 服务器 keyboardOTA的博客 CSDN博客
  • GO调用C语言之字符串传递

    C xff1a hello h include lt stdlib h gt include lt stdio h gt include lt string h gt char abc int strnum char str C hello
  • C++-基础语法-cin.getline() 与 cin.get() 的区别,以及getline()函数使用方法

    参考声明 xff1a C 43 43 primer plus https blog csdn net best fiends zxh article details 53064771 https blog csdn net u0114216
  • Kotlin开发中的一些Tips(二)

    接着上一篇 xff0c 最近又整理了一些 1 作用域函数选择 目前有let run with apply 和 also五个作用域函数 官方文档有张表来说明它们之间的区别 xff1a 总结一下有几点区别 xff1a apply和also返回上

随机推荐

  • Jetpack Compose 从入门到入门(一)

    Jetpack Compose 是用于构建原生 Android 界面的新工具包 它使用更少的代码 强大的工具和直观的 Kotlin API xff0c 可以帮助您简化并加快 Android 界面开发 xff0c 打造生动而精彩的应用 它可让
  • Jetpack Compose 从入门到入门(二)

    开始布局部分 这部分我个人感觉没有必要每个组件 属性都详细说到 xff0c 否则篇幅会很长 建立起Compose中的组件与 Android Views的一个对应关系就够了 具体还是需要在实际的使用中去熟悉 1 Column 子元素按竖直顺序
  • Jetpack Compose 从入门到入门(三)

    本篇开始介绍Jetpack Compose 中的修饰符Modifier 修饰符可以用来执行以下操作 xff1a 更改可组合项的大小 布局 行为和外观 添加信息 xff0c 如无障碍标签 处理用户输入 添加高级互动 xff0c 如使元素可点击
  • 【操作系统】Linux系统中直接优化提升CPU性能(已解决)

    文章目录 问题 xff1a 服务器CPU没有调用最高性能 xff0c 导致跑算法的时候处理速度慢一 BIOS方法二 终端直接设置CPU调节器方法1 查看当前CPU调节器2 安装各种依赖库3 最后安装cpufrequtis工具包并设置CPU调
  • Jetpack Compose 从入门到入门(四)

    本篇开始介绍Jetpack Compose 中常用的组件 有一部分之前的文章中也出现过 xff0c 今天详细说明一下 1 Text 日常最常用的应该就是显示文字 xff0c 所以有必要说一下Text控件 首先源码如下 xff1a span
  • Jetpack Compose 从入门到入门(五)

    应用中的状态是指可以随时间变化的任何值 这是一个非常宽泛的定义 xff0c 从 Room 数据库到类的变量 xff0c 全部涵盖在内 由于Compose是声明式UI xff0c 会根据状态变化来更新UI xff0c 因此状态的处理至关重要
  • Jetpack Compose 从入门到入门(六)

    本篇说说Compose中的Canvas 1 Canvas span class token annotation builtin 64 Composable span span class token keyword fun span sp
  • Jetpack Compose 从入门到入门(七)

    本篇进入Compose 动画部分 1 动画预览 在本系列第一篇中我们提到过 xff0c 64 Preview可以帮我们实现UI的预览功能 xff0c 简单的交互和播放动画 在Android Studio Bumblebee xff08 大黄
  • Android 12 变更及适配攻略

    这几个月有点忙 xff0c 一年一篇的适配文章来的有点晚了 但其实也还好 xff0c 因为我们项目也是下半年才适配 我这边也是提前调研踩坑 xff0c 评估一下工作量 这个时间点也完全跟得上Google Play的审核要求 xff08 11
  • Jetpack Compose 从入门到入门(八)

    接着上一篇的动画部分 xff0c 本篇主要是自定义动画与Animatable AnimationSpec 上一篇中 xff0c 出现了多次animationSpec属性 xff0c 它是用来自定义动画规范的 例如 xff1a span cl
  • Jetpack Compose 从入门到入门(九)

    本篇是Compose的手势部分 点击 添加clickable修饰符就可以轻松实现元素的点击 此外它还提供无障碍功能 xff0c 并在点按时显示水波纹效果 span class token annotation builtin 64 Comp
  • 记参加 2022 Google开发者大会

    前几天有幸参加了2022年Google 开发者大会 Google Developer Summit xff0c 上一次参加Google开发者大会还是2019年 这期间因为众所周知的原因 xff0c 开发者大会都改为了线上举办 和上次相比可以
  • Jetpack Compose 从入门到入门(十)

    本篇介绍如何将Jetpack Compose 添加到已有应用中 xff0c 毕竟大多数情况都是在现有项目中使用 Jetpack Compose 旨在配合既有的基于 View 的界面构造方式一起使用 如果您要构建新应用 xff0c 最好的选择
  • Flutter状态管理之Riverpod 2.0

    两年前分享过一篇Flutter状态管理之Riverpod xff0c 当时riverpod的版本还是0 8 0 xff08 后来文章更新到0 14版本 xff09 当时提到过有一些不足之处 xff1a 毕竟诞生不久 xff0c 它还不能保证
  • Python:元组和字典简述

    目录 1 列表的方法2 for循环遍历列表2 1 语法2 2 range 函数 3 元组3 1 元组的基本概念3 2 元组的创建3 3 元组的解包3 3 1 号在解包中的用法 4 字典4 1 字典的基本概念4 2 字典的使用4 2 1 字典
  • 七种常见软件开发模型

    目录 瀑布模型 xff08 面向文档的软件开发模型 xff09 演化模型 螺旋模型 增量模型 构件组装模型 统一过程 xff08 up xff09 xff08 迭代的软件过程 xff0c 以架构为中心 xff09 敏捷开发模型 瀑布模型 x
  • IP安全策略:只允许指定IP连接远程桌面,限制IP登录

    一 xff0c 新建IP安全策略 WIN 43 R打开运行对话框 xff0c 输入gpedit msc进入组策略编辑器 依次打开 本地计算机 策略 计算机配置 Windows设置 安全设置 IP安全策略 在 本地计算机上 在右面的空白处右击
  • 2022年终总结

    不知不觉就到了年末 xff0c 感叹时间过的真快 我自己坚持写了七年多的博客 xff0c 但这其实是我第一次去写年终总结 也不知道怎么写 xff0c 就简单聊聊 写博客的初衷就是个人收获 xff0c 学习的记录 xff0c 分享出来如果能帮
  • Rust库交叉编译以及在Android与iOS中使用

    本篇是关于交叉编译Rust库 xff0c 生成Android和iOS的二进制文件 xff08 so与a文件 xff09 xff0c 以及简单的集成使用 1 环境 系统 xff1a macOS 13 0 M1 Pro xff0c Window
  • 利用Rust与Flutter开发一款小工具

    1 起因 起因是年前看到了一篇Rust 43 iOS amp Android xff5c 未入门也能用来造轮子 xff1f 的文章 xff0c 作者使用Rust做了个实时查看埋点的工具 其中作者的一段话给了我启发 xff1a 无论是 Loo