#typescript #binary #binary-data #codec #decoding #websocket #generate-typescript

bufferfish

适用于Rust和TypeScript互操作的二进制消息实用库

1 个不稳定版本

0.1.0 2024年7月5日

#9#generate-typescript

每月26次下载

MIT/Apache

63KB
1.5K SLoC

bufferfish

bufferfish 是一个在Rust和TypeScript之间处理二进制网络消息(如通过WebSockets)的实用库。它提供了一个简单的API来将数据编码和解码成二进制数组,以及从你的Rust代码生成TypeScript定义和解码函数。

此库具有不稳定的API,可能缺少一些基本功能。我虽然在自己的生产项目中使用它,但并不推荐在生产环境中使用。

目录

入门

使用 cargo add bufferfish 将Rust库添加到项目中。

使用 npm install bufferfish 将TypeScript库添加到项目中。

示例

use bufferfish::{Encode};
use futures_util::SinkExt;
use tokio::net::{TcpListener, TcpStream};
use tokio_tungstenite::{accept_async, tungstenite::Message};

#[derive(Encode)]
#[repr(u16)]
enum PacketId {
    Join,
}

// We need to make sure we can convert our enum to a u16, as that is the type
// `bufferfish` uses to identify packets. You can use the `num_enum` crate and
// derive `IntoPrimitive` and `FromPrimitive` to remove this step completely.
impl From<PacketId> for u16 {
    fn from(id: PacketId) -> u16 {
        match id {
            PacketId::Join => 0,
        }
    }
}

// We annotate our packet with the #[Encode] macro to enable automatic
// encoding and decoding to or from a `Bufferfish`.
//
// Additionally, we use the #[bufferfish] attribute to specify the packet ID.
#[derive(Encode)]
#[bufferfish(PacketId::Join)]
struct JoinPacket {
    id: u32
    username: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;

    while let Ok((stream, _)) = listener.accept().await {
        tokio::spawn(async move {
            if let Err(e) = process(stream).await {
                eprintln!("Error processing connection: {}", e);
            }
        });
    }

    Ok(())
}

async fn process(steam: TcpStream) -> Result<(), Box<dyn std::error::Error>> {
    let mut ws = accept_async(steam).await?;

    let packet = JoinPacket {
        id: 1,
        username: "Rob".to_string(),
    };
    let bf = packet.to_bufferfish()?;

    ws.send(Message::Binary(bf.into())).await?;

    Ok(())
}

使用生成的解码函数(JavaScript)

const ws = new WebSocket("ws://127.0.0.1:3000")
ws.binaryType = "arraybuffer"

ws.onmessage = (event) => {
  const bf = new Bufferfish(event.data)
  const packetId = bf.readUint16()

    if (packetId === PacketId.Join) {
        const packet = decodeJoinPacket(bf)

        console.log(packet) // { id: 1, username: "Rob" }
    }
}

手动解码Bufferfish(JavaScript)

const ws = new WebSocket("ws://127.0.0.1:3000")
ws.binaryType = "arraybuffer"

ws.onmessage = (event) => {
    const bf = new Bufferfish(event.data)
    const packetId = bf.readUint16()

    if (packetId === PacketId.Join) {
        const id = bf.readUint32()
        const username = bf.readString()

        console.log({
            id,
            username,
        }) // { id: 1, username: "Rob" }
    }
}

TypeScript代码生成

bufferfish 提供了一个 generate 函数,可以在 build.rs (或用于CLI脚本,由服务器在启动时调用等)中使用,以从你的Rust代码生成TypeScript定义和函数,这意味着你的Rust服务器成为所有网络消息的真理来源,并减少了客户端手动与 bufferfish 交互。

// build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("cargo:rerun-if-changed=build.rs");

    bufferfish::generate("src", "../client/src/generated/Packet.ts")?;

    Ok(())
}

代码生成示例

use bufferfish::Encode;

pub enum PacketId {
    Join = 0,
    Leave,
    Unknown = 255,
}

#[derive(Encode)]
#[bufferfish(PacketId::Join)]
pub struct JoinPacket {
    pub id: u8,
    pub username: String,
}

#[derive(Encode)]
#[bufferfish(PacketId::Leave)]
pub struct LeavePacket;
/* AUTOGENERATED BUFFERFISH FILE, DO NOT EDIT */
import { Bufferfish } from 'bufferfish';

export enum PacketId {
    Join = 0,
    Leave = 1,
    Unknown = 255,
}

export interface JoinPacket {
    id: number
    username: string
}

export const decodeJoinPacket = (bf: Bufferfish): JoinPacket => {
    return {
        id: bf.readUint8() as number
        username: bf.readString() as string
    }
}

可编码/可解码类型

支持类型 解码为
u8 数字
u16 数字
u32 数字
i8 数字
i16 数字
i32 数字
bool 布尔值
字符串 字符串
Vec<T> 其中T:可编码 数组<T>
T其中T:可编码 object 或原始类型

解码时相反。

说明

  • 我建议使用 num_enum crate 对你希望进行 Encode 的枚举进行 IntoPrimitiveFromPrimitve 的推导。这可以消除大量的样板代码。
  • 在TypeScript中,枚举(Enums)经常被提及为“不好”的特性,这在考虑典型的Web开发用例时通常是正确的。然而,在将“操作码”列表映射到对开发人员友好的名称的情况下,它们实际上非常有用。现代打包器——例如 esbuild —— 实际上可以将其内联,这意味着我们最终输出中只有整数字面量。

安全

bufferfish 函数确保输入在“尽最大努力”的情况下是有效的。内部缓冲区使用最大容量 (默认为1024字节) 进行构建,如果输入会导致内部缓冲区超过该阈值,则会失败构建。

在读取数据时,您总是会得到正确的返回类型——然而,如果输入不正确但技术上有效,您仍然会受到损坏数据的影响。例如,如果您在一个包含在光标位置处的 u16 的缓冲区上调用 read_u8,您将得到一个 u8,因为缓冲区无法知道它最初是作为 u16 编码的。这是有效数据,但很可能是一个意外的值。

在操作缓冲区之前应该处理这类问题。

通过 Decode 特性解码过大的 bufferfish 将仅忽略/丢弃额外的数据,因为它只会读取由 Encodable 实现生成的特定字节长度。

解码过小的 bufferfish 将返回一个 BufferfishError::FailedWrite

贡献

bufferfish 欢迎贡献,但应注意的是,该库是为我的游戏项目创建的,我没有兴趣使其成为广泛通用的库。如果您认为某个功能请求或错误修复对其他人有用,请随时打开一个问题或PR。

许可证

bufferfish 源代码根据您的选择 dual-licensed under either

at your option。

依赖关系

~135–410KB