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

news2024/9/21 18:34:27

在这里插入图片描述

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|未入门也能用来造轮子?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/357822.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于springboot+vue的疾病匿名检测查询系统

基于springbootvue的疾病匿名检测查询系统 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背…

【拥抱开源】发布自己的项目到maven中央仓库

文章目录&#x1f388;第一步&#xff0c;注册账号&#x1f4bf;第二步&#xff0c;登录&#x1f4c0;第三步&#xff0c;设置信息&#x1f4be;第四步&#xff0c;创建问题&#x1f4f9;第五步&#xff0c;验证信息&#x1f3a5;第六步&#xff0c;上传jar包到中央仓库&#x…

网页中字体混淆的处理(简易方法)——爬虫学习笔记

网页中字体混淆的处理——爬虫学习笔记之评论爬取一、网页分析1、打开某点评网址。2、在网页源代码中寻找评论信息&#xff08;1&#xff09;进入这个“火锅店”的详情页&#xff0c;然后右击“检查”&#xff08;2&#xff09;点上面“刷新”详情页&#xff0c;逐步寻找。&…

RK3288 GPIO记录

1、引脚对应的GPIO 编号第一种 使用/sys/kernel/debug/gpio查询所有gpio引脚的基数第二种 cat /sys/class/gpio/gpiochip248/label对应的label就是GPIO引脚&#xff0c;例如下图GPIO8对应的基数就是2482、计算编号编号 基数 PIN脚如GPIO8的基数是248&#xff0c; GPIO8_A6的编…

Java开发 - 数风流人物,还看“微服务”

目录 前言 服务器端的发展历程 早期的服务器 动态的页面 用户内容网站 微服务 企业级应用 互联网应用 微服务介绍 什么是微服务&#xff1f; 为什么使用微服务 怎么使用微服务 Spring Cloud 什么是Spring Cloud Nacos注册中心 什么是Nacos 创建微服务项目 创建…

【Servlet篇】一文带你吃透Request对象

文章目录1. 前言2. Request 对象2.1 Request 继承体系2.2 Request 获取请求参数1. 获取请求行数据2. 获取请求头数据3. 获取请求体数据4. 获取请求参数的通用方式3. IDEA中快速创建 Servlet你问我青春还剩几年&#xff1f;我的回答是&#xff0c;趁现在&#xff0c;正当时。身边…

CS144-Lab2

实验架构 除了写入传入流之外&#xff0c;TCPReceiver 还负责通知 sender 两件事&#xff1a; “First unassembled” 字节的索引&#xff0c;称为“acknowledgment”或 “ackno”。这是接收方需要来自发送方的第一个字节。“first unassembled ” 索引和“first unacceptable…

【项目精选】基于SSH的任务调度系统的设计与实现(视频+源码+论文)

点击下载源码 虽然科技进步在改革开发这几十年来速度飞快&#xff0c;计算机行业也发展迅速&#xff0c;但仍然有大量商家或企业&#xff0c;甚至项目组&#xff0c;采用落后的人工管理方式或者低效的任务调度策略&#xff0c;这无疑是对计算机的一种无视。 计算机处理信息的准…

Python每日一练(20230220)

目录 1. 存在重复元素 II 2. 按要求实现程序功能 3. 分割链表 附录 链表 1. 存在重复元素 II 给定一个整数数组和一个整数 k&#xff0c;判断数组中是否存在两个不同的索引 i 和 j&#xff0c;使得 nums [i] nums [j]&#xff0c;并且 i 和 j 的差的 绝对值 至多为 k。 …

高级数据类型

为了解决单一的业务而存在bitmapsBitmaps类型的基础操作 获取指定key对应偏移量上的bit值getbit key offset 设置指定key对应偏移量上的bit值&#xff0c;value只能是1或0setbit key offset valueBitmaps类型的扩展操作状态位的统计业务需求&#xff1a;1. 统计每天某一部电…

云计算ACP云服务器ECS实例题库(三)

&#x1f618;作者简介&#xff1a;一名99年软件运维应届毕业生&#xff0c;正在自学云计算课程。&#x1f44a;宣言&#xff1a;人生就是B&#xff08;birth&#xff09;和D&#xff08;death&#xff09;之间的C&#xff08;choise&#xff09;&#xff0c;做好每一个选择。&…

关于监控服务器指标、CPU、内存、警报的一些解决方案

文章目录关于监控服务器指标、CPU、内存、警报的一些解决方案Prometheus Grafana 配置 IRIS / Cach 监控服务器Prometheus简介特点架构图Grafana简介特点配置流程自定义Prometheus接口定义配置 Exporter 监控服务器系统资源简介配置流程使用 Alertmanager报警简介配置流程基于…

软考高级-信息系统管理师之知识管理(最新版)

知识管理 知识与知识管理知识管理常用的方法和工具显性知识的管理隐形知识的管理知识管理的工具学习型组织知识产权保护计算机软件保护条例商标法专利法补充建议学的考点:知识与知识管理 1、知识的分类 知识可分为两类,分别是显性知识与隐性知识。 凡是能以文字与数字来表达…

【C++】关联式容器——map和set的使用

文章目录一、关联式容器二、键值对三、树形结构的关联式容器1.set2.multiset3.map4.multimap四、题目练习一、关联式容器 序列式容器&#x1f4d5;:已经接触过STL中的部分容器&#xff0c;比如&#xff1a;vector、list、deque、forward_list(C11)等&#xff0c;这些容器统称为…

新能源汽车,有毒

作者| Mr.K 编辑| Emma来源| 技术领导力(ID&#xff1a;jishulingdaoli)新能源汽车到底有多火&#xff0c;生生逼得奥迪某4S店挂出横幅&#xff1a;我们也有纯电新能源&#xff01;老牌名车的辛酸憋屈溢出屏幕。网友神评补刀“这标语给人‘诺基亚也有智能大屏机’的感觉。”一…

【Java基础】变量

Java基础 变量 variable 变量类型 实例变量(非静态字段) Instance Variables (Non-Static Fields) 类的非静态属性 类变量(静态字段) Class Variables (Static Fields) 类的静态属性 局部变量 Local Variables 参数 Parameters 变量命名 大小写敏感 开头&#xff1a;字…

爬虫基本知识的认知(爬虫流程 HTTP构建)| 爬虫理论课,附赠三体案例

爬虫是指通过程序自动化地从互联网上获取数据的过程。 基本的爬虫流程可以概括为以下几个步骤&#xff1a; 发送 HTTP 请求&#xff1a;通过 HTTP 协议向指定的 URL 发送请求&#xff0c;获取对应的 HTML 页面。解析 HTML 页面&#xff1a;使用 HTML 解析器对获取的 HTML 页面…

linux shell 入门学习笔记4 shell运维和编程语言

shell 运维和编程语言 脚本注释 shell脚本中&#xff0c;#后面的内容表示注释内容&#xff0c;一般是给开发者或使用者观看&#xff0c;解释器会忽略此部分内容注释可以单独写一行&#xff0c;也可以跟在文件末尾保持注释的习惯&#xff0c;尽量使用英文 例子&#xff1a; #…

C++类和对象(2)构造、析构函数

类的6个默认成员函数 如果一个类中什么成员都没有&#xff0c;简称为空类。 class Date{}; 空类中真的什么都没有吗&#xff1f;并不是&#xff0c;任何类在什么都不写时&#xff0c;编译器会自动生成以下6个默认成员 函数。 默认成员函数&#xff1a;用户没有显式实现&…

什么是健康建筑?

WIKIPEDIA健康建筑是指支援建筑和建筑环境中人们&#xff0c;身体、心理和社会健康与福祉的新兴兴趣领域。建筑物可以成为健康和福祉的关键促进者&#xff0c;因为大多数人大部分时间都花在室内。根据全美国人类活动模式调查&#xff0c;美国人「平均 87% 的时间花在封闭的建筑…