赞
踩
起因是年前看到了一篇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函数。这个我后面有时间会单独写一篇来分享。
之所以选择Rust
与Flutter
是看中它们的跨平台能力。使用Rust进行WebSocket
数据发送,就不用Android和iOS端去重复开发这个功能,只需要简单调用即可,并且Rust有许多开箱即用的库。
Flutter的跨平台能力就更不用说了。比如这个小工具我就可以一套代码输出Windows和macOS两个平台的安装包,保证接收端逻辑和UI的一致。
关于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端调用代码如下:
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和端口号后就可以发送数据了。或者扫描二维码连接。
目前实现功能如下:
因为小工具在公司内部使用,所以就不开源完整的代码了。有了文章中的核心代码,你可以根据自己的需求实现。也不必局限于这些功能,你完全可以通过Rust和Flutter的跨平台能力开发更多功能,本篇也只是抛砖引玉。
如果本篇对你有所启发帮助,不妨点赞支持一下。如果你有好的想法,也欢迎评论交流。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。