本系列文章的创作来源主要是如下几点:
-
在过往的 Node.js 开发以及在社区经常看到一些需求场景:如何判断当前设备是否能够联通某个域名或者 IP ? 我们针对于这种需求过往最常见的实现一般是通过
process或者command等能力拉起子进程执行后处理结果。不过有时候这种方案也会面临一些问题,比如命令不可用,命令不满足我们自己的需求场景,无可执行权限等等,这时候我们就可能需要使用更加原始的方式来实现此类诉求。 -
希望借助一些更加真实的场景需求来讲解 Rust 在鸿蒙原生模块开发中的运用或者鸿蒙应用的一些常见开发场景。
在阅读之前,我们需要注意以下几点内容:
- 本系列文章重点并非在协议本身,我们不会花太多时间讲解协议的规范实现细则。不过笔者会在文章中加入一些自认为写的不错的讲解文章,可自行按需查阅。
- 并非所有协议都会涉及,主要以笔者在社区看到的一些诉求和过往经历中常用的一些为主,如果有诉求可以私聊。
以上就是本系列文章的简单讲解,现在我们开始正式的本文内容:实现一个简单的 Ping 能力。
协议简介
Ping 能力使用 ICMP 协议进行实现。
包简介
我们在 Rust 中网络协议编程有一些经过封装比较完善的库供我们使用,能够避免很多 unsafe 代码以及平台差异性,这部分我们将简单介绍下开发 ping 所涉及的一些网络开发库,在后续的文章中会逐渐讲解其他所用的库。
socket2
官方对于网络编程库的基础封装,一般基于 socket2 进行封装。提供了一系列文档且安全(safe)的接口用于我们编写网络程序。
当然 socket2 并非提供了全部的能力,部分比较底层且少见的能力在 socket2 中并没有提供。因此有时候我们可以选择使用 libc 或者 nix-rust 来直接通过 FFI 调用未提供的系统能力。
pnet
社区的封装,提供了一系列 4/7 层协议的包封装以及各种能力,包括校验和计算、数据转化等能力。我们可以使用该包来简化我们对数据包的使用成本。
实现
分析
在开始前,我们简单讲一下 ping 程序使用的协议以及实现过程。
Ping 一般是基于 ICMP 协议进行封装实现的,这里详细的协议包数据结构不做过多的讲解网上有相当多的解析。我们可以简单想象下那个经典问题: 把大象装进冰箱需要几步?
- 打开冰箱
- 把大象装进冰箱
- 关闭冰箱
那么类似的,我们实现 ping 程序的过程就是:
- 创建 Socket(
socket) - 发送 ICMP 协议包(
sendto) - 接受 ICMP 协议包(
recvfrom)
接下来我们基于这个过程来实现一个简单的 ping 程序。
API 封装
在开始之前,我们需要将 pnet 和socket2 添加到我们的依赖中来简化我们的一些步骤。
# 创建项目
ohrs init ping --package=@ohos-rs/ping
# 添加依赖
cargo add pnet
cargo add socket2
1. 创建 SOCKET
创建 socket 我们只需要从 socket2 中引入 Socket 即可。
use socket2::Socket;
let sock = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::ICMPV4));
这里需要额外注意,部分文档或者教程这里的Type使用的是Type::Raw,原始套接字是需要 root 权限的,在鸿蒙上面无法获取对应的权限这里必须使用Type::DGRAM替换实现。
2. 构建与发送ICMP包
创建 ICMP 包则可以通过 pnet 来简化结构体的定义和设置等过程,我们只需要引入 pnet 预先定义好的结构体和方法即可。
use pnet::packet::icmp::echo_request::MutableEchoRequestPacket;
use pnet::packet::icmp::{IcmpCode, IcmpPacket, IcmpTypes};
let mut buf = vec![0; BUFFER_SIZE];
let mut packet = MutableEchoRequestPacket::new(&mut buf)
.ok_or_else(|| Error::from_reason("Failed to create ICMPv4 packet"))?;
packet.set_icmp_type(IcmpTypes::EchoRequest);
packet.set_icmp_code(IcmpCode::new(0));
packet.set_sequence_number(sequence);
packet.set_identifier(std::process::id() as u16);
// 可有可无在当前实现下
packet.set_checksum(0);
let checksum = pnet::packet::icmp::checksum(&IcmpPacket::new(packet.packet()).unwrap());
packet.set_checksum(checksum);
这里有一个小细节注意,在包计算 crc 校验和之前部分场景需要先将校验和设置为0(非 pnet 的),否则会导致发送接受失败。
3. 解析和响应ICMP包
接收则是通过 recv_from 实现,实际对应底层的recvfrom函数实现。
match socket.recv_from() {
Ok() => {},
Err(e) => {}
}
注意:socket2 中 recv_from 接受的参数类型为 &mut [MaybeUninit<u8>] 而非 Vec 或者 bytes,这里为了简化操作,我们将这部分逻辑进行了一定的简单封装,后续我们都能用上。
use std::{mem::MaybeUninit, ptr::NonNull};
pub struct UninitBuffer {
ptr: NonNull<MaybeUninit<u8>>,
capacity: usize,
}
impl UninitBuffer {
pub fn new(size: usize) -> Self {
let mut vec = Vec::with_capacity(size);
let ptr = NonNull::new(vec.as_mut_ptr() as *mut MaybeUninit<u8>).unwrap();
let capacity = vec.capacity();
std::mem::forget(vec);
Self { ptr, capacity }
}
pub fn as_mut_slice(&mut self) -> &mut [MaybeUninit<u8>] {
unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.capacity) }
}
pub fn as_mut_slice_initialized(&mut self, size: usize) -> &mut [u8] {
if size > self.capacity {
panic!("Requested size is greater than the buffer's capacity");
}
unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr() as *mut u8, size) }
}
}
impl Drop for UninitBuffer {
fn drop(&mut self) {
unsafe {
Vec::from_raw_parts(self.ptr.as_ptr() as *mut u8, 0, self.capacity);
}
}
}
因此这里的最终实现为:
let mut recv_buf = UninitBuffer::new(2048);
match socket.recv_from(recv_buf.as_mut_slice()) {
Ok((_, _)) => {}
Err(e) => {},
}
这样就完成了使用 socket2 和 pnet 实现一个简单的 ping 程序,但是我们一般来说 ping 的次数是多次的,耗时也会相应的有一定增加。因此我们需要将整个过程异步化,避免阻塞主线程。
4. 异步化任务
异步化任务在鸿蒙中我们可以借助 ohos-rs 提供的 Task 来实现,其基本实现如下所示:
use use napi_ohos::Task;
struct PingTask {
// 结构体中的数据结构任意定义即可
host: String,
options: PingOptions,
}
impl Task for PingTask {
// 出参
type Output = Vec<PingResult>;
type JsValue = Vec<PingResult>;
// 真实执行逻辑 在子线程进行
fn compute(&mut self) -> napi_ohos::Result<Self::Output> {}
// compute 执行返回 Ok 时的执行函数
fn resolve(&mut self, _: Env, output: Self::Output) -> napi_ohos::Result<Self::JsValue> {}
// // compute 执行返回 Err 时的执行函数
fn reject(&mut self, _: Env, err: Error) -> Result<Self::JsValue> {}
}
那么从定义中我们可以清晰的看到想要将耗时任务放在子线程执行,只需要将耗时任务在 compute 函数中编写即可。
最终将 Task 暴露即可:
#[napi(ts_return_type = "Promise<PingResult[]>")]
fn ping_async(host: String, options: Option<PingOptions>) -> AsyncTask<PingTask> {
let opts = options.unwrap_or(PingOptions {
count: 4,
timeout: 1000,
interval: 1000,
ip_version: None,
});
AsyncTask::new(PingTask {
host,
options: opts,
})
}
注意:异步化任务需要将napi-ohos的 async feature 开启,否则编译无法通过。
napi-ohos = { workspace = true, features = ["async"] }
到这里整个开发流程就完成了,具体的代码可以参考 ping 这里不再重复赘述。
测试与发布
借助于 ohrs 命令行工具,我们可以快速将 native 模块构建成可用的 har 包用于测试和发布。
构建 har 包:
ohrs artifact
注意:执行该命令的前提是通过 ohrs 创建项目时,传入了 --package 参数,否则生成最终文件会失败。
执行完毕之后会在当前目录下生成一个 package.har 的文件,该文件是鸿蒙工程可用的文件,可以通过 file 协议引入:
{
"dependencies": {
"@ohos-rs/ping": "file:/path/tp/package.har"
}
}
最后在我们的代码中运行代码即可:
import { pingAsync } from "@ohos-rs/ping";
const ret = await pingAsync("www.baidu.com");
console.log(`${ret}`)
正常情况下,我们直接使用 ohpm 发布打包好的当前的 har 文件即可:
ohpm publish ./package.har
发布的时候,我们一般建议构建 release 包:ohrs build --release
尾
本文简单讲解了如何在鸿蒙中基于 Rust 以及相关生态实现一个简单的 ping 程序。其实网络上相关的教程非常多,因此我们省略了中间一些细节,比如: 协议包数据结构解析、网络编程的详细过程、rust 工程的解析等。
我们更多的将目光放在了整体流程上面,否则可能出现会写代码但是用不了的尴尬局面。这就是本文的所有内容,希望对你有所帮助~