大家新年好啊,这是 2023 年第一篇文章,对笔者来说也是一个新的技术尝试,看似标题党,但确实是在实话实说 ~
起因
虽然我司从长远打算上是准备使用 Rust 来做底层及逻辑层跨端架构,但这落地时间会拉的很长,且没有一个合适的切入点。
现状是年底的时候大家都?了(笔者很嘴硬的没阳,但带薪病假浪费了 (o_ _)ノ),需求上也基本是停滞状态,那就有空来折腾下 Rust,让自己锈一锈。
学习上从零开始拜读 《Rust 圣经》,但在基础入门篇看了一大半之后,实在是看不下去了…学而不思则惘,虽然搞明白了最关键的所有权和借用,但不到项目里用一下就等于啥也不会[手动狗头]。
刚好有一个由来已久的业务痛点,我司的埋点是否符合预期,这个验证在移动端上是较为复杂的,原因有以下几点:
- 埋点传输过程中是加密的,很难通过抓包的形式看到埋点是否正确。
- 埋点在移动端是有缓存的,并不会实时上报。
- 埋点在数据入库的时候也是有延迟的,在平台上查询埋点也需要一定时间成本。更重要的是,埋点需要验证各种时机是否正确,那需要实时性更高的方式。
而在跨端开发越来越频繁的今天,矛盾也就越来越凸显,对于 H5 开发的同学或者 Flutter 开发的同学来讲,埋点出现查不到或者埋入错误的问题后,需要耗费大量的时间和精力来排查原因,所以迫切需要一个能实时查看埋点是否正确的方式。
思考
最早的想法是让埋点实时显示在 App 上,但这存在以下的问题:
- App 屏幕实在不大,没有更多的空间能完整显示当前的埋点信息,如果做成需要打开页面来查看的方式,实时性也不高。
- 需要额外开发页面,虽然可以用 Flutter 来减少双端开发成本,但也是需要一定的开发成本。
那有没有一种方式可以解决呢?
思路换一下,无论是 LookinServer 、 Flipper 等 Debug 利器,还是 Flutter / Web Debug Tools,都是在电脑上调试 App。那我们也可以用类似的方式,把实时埋点数据显示在电脑上,不再局限于同一块屏幕。
通信方式
我们需要的是一个尽量简单的通信,所以目标上选定使用 WebSocket,且上述解决方案也大都是这样的选择。
技术选型
在大方向上,首先排除掉 iOS 、Android 原生开发的方式来实现。
而 Flutter 并不适合做底层及逻辑层,它更适合做页面。
Rust 更符合我们的需求,可以覆盖了所有终端,性能又高,而且不会加入到线上生产包中,很适合做 Rust 在移动端的第一步落地。
唯一的问题就是还不会,所以需要花时间摸索下。
当前效果
打开 Rust 服务端执行程序,等待连接:
在 App 上输入要连接的 IP 及 端口号:
服务端执行程序会实时显示收到的埋点信息:
实现过程
笔者在之前已经搭建好了 Rust 的开发环境,也成功的跑通了 “Hello World”,所以配置部分并不在本文的介绍中。
因为是完全陌生的技术领域,所以对笔者来说有2个需要解决的难点:
- Rust WebSocket 实现。
- Rust 与 iOS / Android 通信。
WebSocket
笔者落地方案的时候并没有了解 Rust WebSocket 原理,本着快速落地的方式,先去找了 《Rust 圣经》里面的日常开发三方库精选,里面有这三个推荐库:
库好是好,也都是生产级的库,但笔者 Rust 功力几近于无,要不是 Demo 跑不动,要不是理解不了,改不动 (o_ _)ノ。
后面查到一个实现尤为简单的库,刚好能拿来就用。
[dependencies]
ws = "0.9.2"
服务端
服务端的作用就是接收 App 来的消息并显示,用配套的 ws-rs 库 Rust 服务端代码,很容易实现:
if let Err(error) = listen(address, |out| {
...
}) {
// 通知用户故障
println!("创建Websocket失败,原因: {:?}", error);
}
address 是 ip + 端口号,顺便打印出来,让 App 输入连入。
let address = format!("{}:{}", get_ip().unwrap(), get_available_port());
println!("当前地址为:{}", address);
get_ip()
获取本机 ip 地址。
get_available_port()
获取本机可用的端口号。
方法网上搜的,可能不是是最佳的方法:
最后把收到的信息格式化输出即可,用 serde_json
库做 json 解析:
...
// 处理程序需要获取 out 的所有权,因此我们使用 move
move |msg: ws::Message| {
let text = msg.as_text();
match text {
Ok(_t) => {
// 格式化
let json: Value = serde_json::from_str(_t).unwrap();
println!(
"触发埋点\n event_id: {}\n event_name: {}\n attributes: {}\n\n",
json["event_id"],
json["event"],
serde_json::to_string(&json["attributes"]).unwrap()
);
// 使用输出通道发送消息
out.send(msg)
}
Err(_) => todo!(),
}
}
...
然后执行 cargo run --release
打包成执行程序。
客户端
客户端代码也很简单,连接服务端的 ip + 端口,并发送消息即可。
...
if let Err(error) = connect(c_host, |out| {
// 将WebSocket打开时要发送的消息排队
if out.send(c_message).is_err() {
println!("[gaoding-log-view-kit]: 无法初始消息排队")
}
// 关闭连接
out.close(CloseCode::Normal)
})
...
这里感叹下,Rust 中真的是轮子众多,集成度又高,真的是不懂所以然也可以拆箱即用。
Native 通信
上面的 websocket 过程很顺利的完成了,但如何在 App 中发送 ws 消息?网上包括掘金里有大量的资料,但基本都是同一份来源,看翻译的不如看原文更详细,能少踩坑。
这2篇文章以及 git 源码 会教你 App 如何让 Rust 打印个 “Hello World”,但里面也有一些遇到的弯弯绕绕需要注意。
Rust + iOS
先来看 iOS 是如何做的。
前面的配置过程文章讲的很详细,这里略过。
比较关键的几个环节:
库模式配置
[lib]
crate-type = ["staticlib", "cdylib"]
入口更改为 src/lib.rs
,没有就创建一个。(这里可以不用 lib.rs 做文件名吗?笔者还没具体了解,有了解的同学可以评论告知下)
暴露方法
use std::{ffi::CStr, os::raw::c_char};
...
#[no_mangle]
pub extern "C" fn send_wind_info(message: *const c_char, host: *const c_char) {
...
}
暴露 send_wind_info(...)
发送埋点信息方法,且提供2个参数,一个是当前消息、一个是发送的域名。
理论上,host 参数在 Rust 的内存持有即可,但这还写不出来 … 当然我们把这个持有放到 Native 来做即可以绕过去。
打包及制造头文件
终端执行 cargo lipo --release
命令就可拿到 .a 文件,Rust 还是很方便的。
然后我们需要提供文件来给 iOS 调用:
api.h
#include <stdint.h>
/// 发送埋点信息
void send_wind_info(const char *message, const char *host);
Cocoapods
按文章上说的硬链到项目里,太令人难受了,我们当然选择用 pod 本地库引用的形式。
podspec 关键部分:
...
s.source_files = 'GDLogViewKit/Classes/**/*.{m,h}'
s.vendored_libraries = 'GDLogViewKit/Library/**/*.a'
s.public_header_files = 'GDLogViewKit/Classes/*.h'
...
再封装
GDLogViewKit.h / GDLogViewKit.m 提供的就是 api.h 的封装,毕竟外部直接调用 api.h 的 send_wind_info(...)
方法实在不够优雅。
#import "api.h"
static NSString *connectAddress;
...
+ (void)sendWindMessage:(NSString *)message {
if (!connectAddress || !message) {
return;
}
send_wind_info([message cStringUsingEncoding:NSUTF8StringEncoding], [connectAddress cStringUsingEncoding:NSUTF8StringEncoding]);
}
connectAddress
就是用于保存 ip + 端口的静态变量。
iOS 上还是很简单的,也可能因为笔者毕竟是一个 iOSer [手动狗头]。整体上就是构造一个 ObjectiveC – C – Rust 通信过程。
Rust + Android
Android 就坎坷了很多,笔者对 Android 并不算熟,遇到了挺多问题,这里一一记录下。
NDK 配置
首先要找到自己 Android SDK 的安装目录,然后找到里面的 NDK 文件夹
然后执行文章中的语句来生成到一个目录下(这个目录根据文章所说生成到项目目录下)。
但是,
根据文章来执行这三句总是失败 – -!
我也尝试用 make-standalone-toolchain.sh
shell 脚本也不行
报 ERROR: Failed to create toolchain
这个错误网上搜了下也没有人解释原因,最后想到,我在 python 开发上用的是 python3, 会不会是这个原因?
python3 'xxx/make_standalone_toolchain.py' --api 26 --arch arm64 --install-dir NDK/arm64
成功了 …
再往下照文章执行即可。
JNI
Android 与 iOS 不同的是,多了一层 JNI,相当于 JAVA – JNI – C – Rust,这一部分以前并没有接触过,一开始靠硬写,后面发现可以通过 javac
来生成。
先在 Cargo.toml 上增加 jni 工具配置,能简便我们编写 jni
[target.'cfg(target_os="android")'.dependencies]
jni = { version = "0.20.0", default-features = false }
在 src/lib.rs 增加 Android 胶水代码:
打包 so 库
学文章的方式,增加一个 cargo-config.toml
再执行拷贝命令 cp cargo-config.toml ~/.cargo/config
(但这个很奇怪,是每个项目都要执行吗?有懂的同学评论告知下)。
然后调用生成 so 库命令
cargo build --target aarch64-linux-android --release
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release
Gradle
同样,我们也制作一个独立的 gradle 库
将 so 库放入到 jniLibs 目录下,这里也有一个坑
文章上是放到这个位置,但我们项目里需要放入到如下位置才能被读取到
据 Android 开发同学说是不同的项目上依赖包配置不同导致的。
再封装
跟 iOS 一样,封装的工具方法:
GDLogViewKit.java
package com.gaoding.log.view.kit;
/**
* 日志可视化工具包
*/
public class GDLogViewKit {
static {
// 库引用
System.loadLibrary("gaoding_log_view_kit");
}
private static String kAddress = null;
// native 方法,对应 JNI
private static native void sendWindInfo(final String message, final String host);
...
public static void sendWindMessage(String message) {
if (kAddress == null) {
return;
}
GDLogViewKit.sendWindInfo(message, kAddress);
}
}
总结
一个简单的埋点实时可视化的轮子就算是做好了,相信它能在项目中起到应有的作用,真的要再感叹 Rust 生态环境的强大,也坚定了后续学习 Rust 的决心。
后续扩展上,可以抛弃 Rust 的服务端,WS 服务上到内部平台上,来把轮子做大。也可以扩展到其他日志信息,甚至可以提供控制指令来操作 App。因为是 Rust,所以能很方便的集成到 Web、桌面等其他终端应用上。
不足的点,还是因为 Rust 没入门,继续啃圣经,先尝试把 ip 地址缓存的功能放到 Rust 中,待修炼飞升后,把上述过程加入到跨端工具链全家桶中 …
另外,以上代码已开源 git 传送门,有兴趣的可以了解。
感谢阅读,如果对你有用请点个赞 ❤️