TFTP (RFC 1350/2347/2348/2349) client and server in pure MoonBit.
moon update # 首次必跑:拉取 registry 索引,否则依赖解析失败跳过它的典型报错是 Failed to calculate build plan + module was not found in the registry(例如 mizchi/x、moonbitlang/async、 moonbitlang/regexp),同时伴随提示 you may need to run 'moon update'。 CI(scripts/ci.sh)与 Copilot 环境(.github/workflows/copilot-setup-steps.yml) 都已把 moon update 作为第一步。
moon check # 类型检查
moon test # 运行测试
moon fmt # 格式化
moon info # 更新 .mbti 接口# 在 6969 端口把 ./tftpboot 目录(含子目录)发布出去,允许覆盖已有文件
moon run cmd/main -- serve --root ./tftpboot --port 6969 --allow-overwrite| 参数 | 说明 | 默认 |
|---|---|---|
| --root DIR | 对外服务的根目录,请求路径被限制在其中 | . |
| --port N | 监听的 UDP 端口 | 69 |
| --allow-overwrite | 允许 WRQ 覆盖已存在的文件 | 关闭(仅新建) |
| --max-sessions N | 并发会话上限,超出的请求回 ERROR 4 且不建立会话 | 32 |
# 把服务器上的 boot.img 下载到本地同名文件
moon run cmd/main -- get 192.168.1.10:69 boot.img
# 指定本地落盘路径,并协商 1428 字节块
moon run cmd/main -- get 192.168.1.10:69 boot.img ./downloads/boot.img --blksize 1428
# 文本文件用 netascii 模式
moon run cmd/main -- get 192.168.1.10:69 config.txt --netascii# 第三个位置参数省略时,远端沿用本地文件名
moon run cmd/main -- put 192.168.1.10:69 ./boot.img
# 显式指定远端文件名
moon run cmd/main -- put 192.168.1.10:69 ./boot.img remote/boot.img| 参数 | 说明 | 默认 |
|---|---|---|
| --blksize N | 协商的块大小(8~65464) | 512 |
| --timeout N | 单包超时(秒) | 5 |
| --retries N | 放弃前的重传次数 | 6 |
| --netascii | 使用 netascii 模式(默认 octet) | 关闭 |
boot.img: 12% (30720/262144 bytes)
boot.img: 24% (61440/262144 bytes)# 终端 A
moon run cmd/main -- serve --root /tmp/tftproot --port 6969 --allow-overwrite
# 终端 B
moon run cmd/main -- put 127.0.0.1:6969 ./big.bin # 上传
moon run cmd/main -- get 127.0.0.1:6969 big.bin ./got.bin # 下载回来
cmp ./big.bin ./got.bin && echo "byte-for-byte identical"///|
async fn download() -> Unit {
let transport = @tftp.SocketTransport::new("192.168.1.10:69")
defer transport.close()
match
@tftp.get(
transport,
"192.168.1.10:69",
"boot.img",
@tftp.ClientOptions::default(),
) {
Ok(outcome) =>
match outcome.received() {
Some(bytes) => println("got \{bytes.length()} bytes")
None => ()
}
Err(_) => println("transfer failed")
}
}| 目标 | check 结果 | 说明 |
|---|---|---|
| native | ✅ 通过 | CI 全流程(check / test / build)在 native 上跑 |
| js | ❌ 编译失败 | moonbitlang/async 的 socket / fs 只实现了 native 与 wasm |
| wasm | ❌ 编译失败 | 同上 |
| wasm-gc | ❌ 编译失败 | 同上 |
| 目标 | check 结果 | 说明 |
|---|---|---|
| native | ✅ 通过 | scripts/ci.sh 的 CLI 演示在真实 UDP 套接字上跑通 |
| js | ❌ 编译失败 | cmd/main/main.mbt:53 的 extern "C" fn exit(error 4156) |
| wasm | ❌ 编译失败 | 同上 |
| wasm-gc | ❌ 编译失败 | extern "C" fn exit,并叠加根包在 js / wasm-gc 上的 socket 报错 |
结论:moon check 的失败源在「CLI 的 extern "C"(js / wasm)」与 「moonbitlang/async 的 socket / fs 只覆盖 native 与 wasm(根包 + CLI)」 两处,与协议层本身无关。除 native 外都还没做端到端验证。
pub(open) trait FileSystem {
async fn read(Self, String) -> Result[Bytes, ServerError]
async fn write(Self, String, Bytes) -> Result[Unit, ServerError]
async fn exists(Self, String) -> Bool
}pub(open) trait ServerTransport {
async fn send(Self, Bytes, peer : String) -> Unit
async fn recv(Self, timeout_ms : Int) -> Recv
fn local_tid(Self) -> String
fn close(Self) -> Unit
}pub trait TidAllocator {
fn allocate(Self, peer : String) -> String
}pub(open) trait Transport {
async fn send(Self, Bytes, peer : String) -> Unit
async fn recv(Self, peer : String, timeout_ms : Int) -> Recv
fn local_peer(Self) -> String
}pub struct ClientOptions {
mode : Mode
block_size : Int
timeout_seconds : Int
max_retries : Int
options : Array[TftpOption]
} derive(Eq, Debug)impl TidAllocator for CounterTidpub(all) enum Dispatch {
Started(String)
Rejected(tid~ : String, error~ : ServerError)
Ignored
} derive(Eq, Debug)pub struct Dispatcher[A] {
allocator : A
started : Int
active : Int
max_sessions : Int
} derive(Debug)pub struct HostFileSystem {
root : String
}impl FileSystem for HostFileSystemasync fn HostFileSystem::write(self : HostFileSystem, path : String, payload : Bytes) -> Result[Unit, ServerError]impl FileSystem for MemoryFileSystemasync fn write(self : MemoryFileSystem, path : String, payload : Bytes) -> Result[Unit, ServerError]async fn MemoryFileSystem::read(self : MemoryFileSystem, path : String) -> Result[Bytes, ServerError]async fn MemoryFileSystem::write(self : MemoryFileSystem, path : String, payload : Bytes) -> Result[Unit, ServerError]impl Transport for MemoryTransportfn MemoryTransport::pair(left_address : String, right_address : String) -> (MemoryTransport, MemoryTransport)pub(all) enum Packet {
Rrq(String, Mode, Array[TftpOption])
Wrq(String, Mode, Array[TftpOption])
Data(UInt16, Bytes)
Ack(UInt16)
Error(ErrorCode, String)
Oack(Array[TftpOption])
} derive(Eq, Debug)pub struct ServerOptions {
root : String
write_policy : WritePolicy
max_sessions : Int
max_retries : Int
} derive(Eq, Debug)impl Transport for SocketSessionpub struct SocketTransport {
socket : UdpServer
request_peer : String
peer : String
address : String
}impl Transport for SocketTransportfn[U, T] TransferSession::new(transport : U, files : T, peer : String, options : ServerOptions) -> TransferSession[U, T]async fn[U : Transport, T : FileSystem] TransferSession::serve(self : TransferSession[U, T], request : Packet) -> SessionOutcomeasync fn[U : Transport, T] TransferSession::step_write(self : TransferSession[U, T], acknowledged : UInt16, collected : Bytes, max_retries : Int) -> (Bool, SessionOutcome)fn block_after(value : UInt16, reference : UInt16) -> Boolfn block_before(value : UInt16, reference : UInt16) -> Boolfn block_follows(value : UInt16, reference : UInt16) -> Boollet default_block_size : Intlet default_port : Intlet default_timeout : Intasync fn[T : Transport] get(transport : T, remote : String, filename : String, request : ClientOptions) -> Result[TransferOutcome, ClientError]async fn[T : Transport] get_with_progress(transport : T, remote : String, filename : String, request : ClientOptions, on_progress : (Progress) -> Unit) -> Result[TransferOutcome, ClientError]let max_block_size : Intlet max_filename_length : Intlet max_timeout : Intlet max_tsize : Intlet min_block_size : Intlet min_timeout : Intfn netascii_decode_all(bytes : Bytes) -> Bytesfn netascii_encode_all(bytes : Bytes) -> Bytesfn next_block(block : UInt16) -> UInt16async fn[T : FileSystem] plan_request(options : ServerOptions, files : T, request : Packet) -> Result[RequestAction, ServerError]async fn[T : Transport] put(transport : T, remote : String, filename : String, data : Bytes, request : ClientOptions) -> Result[TransferOutcome, ClientError]async fn[T : Transport] put_with_progress(transport : T, remote : String, filename : String, data : Bytes, request : ClientOptions, on_progress : (Progress) -> Unit) -> Result[TransferOutcome, ClientError]fn resolve_options(requested : RequestedOptions, server_value : (RequestedOption) -> Result[String?, NegotiationError]) -> Result[Options, NegotiationError]async fn[T : Transport] serve_read(transport : T, peer : String, payload : Bytes, accepted : AcceptedOptions, options : ServerOptions) -> Result[Int, ServerError]async fn[T : Transport] serve_write(transport : T, peer : String, accepted : AcceptedOptions, options : ServerOptions) -> Result[Bytes, ServerError]fn take_cstring(bytes : Bytes, offset : Int) -> (String, Int)?fn take_u16(bytes : Bytes, offset : Int) -> (UInt16, Int)?fn timeout_to_ms(seconds : Int) -> IntInstall
Download zipTFTP (RFC 1350/2347/2348/2349) client and server in pure MoonBit.