Skip to main content

Gatech CS3210 学习笔记 - lab2 part1

· 5 min read

这里继续上一部分的内容,实现剩下的两个库。

1C xmodem

这个模块实现的是一个十分古老的文件传输协议 xmodem,它只有五种控制字符,没有身份验证功能,抗干扰能力和纠错能力也很弱。优点是协议简单,用 rust 实现的发送端和接收端的代码量都在一百行左右。 实现这个模块需要我自己打的代码大约只有一百行。由于协议文档并不长,也预先给好了一部分定义和函数,所以在前期花了一些时间梳理协议的逻辑和提供的函数定义,课程也建议可以参考测试数据 debug,后面发现测试函数里面的一些代码不容易理解,自己对于协议的纠错能力过于自信,所以花了不少时间 debug。实际上这个协议一旦出现非预期的结果就抛出异常,不会尝试纠错。 在这个过程中我更加理解了 rust 的 match 语句、解构、问号操作符的优势,我最初没有利用好这些特性的代码有最终版本的两倍长,而且有很多嵌套分支结构,在修改之后就清晰了许多。以接收端的核心逻辑为例,仅两个 match 语句就实现了分支选择,还有函数调用后面的问号操作符简化了返回值的判断。

match self.read_byte(true) {
Ok(byte) if byte == SOH => {
self.expect_byte_or_cancel(self.packet, "packet number")?;
self.expect_byte_or_cancel(255 - self.packet, "packet number 1s complete")?;

for i in 0..128 {
buf[i] = self.read_byte(false)?;
}
match self.expect_byte(get_checksum(buf), "checksum") {
Ok(_) => {
self.write_byte(ACK)?;
self.packet += 1;
return Ok(128);
},
Err(_) => {
self.write_byte(NAK)?;
return ioerr!(Interrupted, "checksum failed");
}
}
},
Ok(byte) if byte == EOT => {
self.write_byte(NAK)?;
self.expect_byte_or_cancel(EOT, "second EOT");
self.write_byte(ACK)?;
return Ok(0);
},
Ok(_) => {
self.write_byte(CAN);
return ioerr!(InvalidData, "")
},
Err(e) => {
self.write_byte(CAN)?;
return Err(e)
}
}

1D ttywrite

这个模块是一个串口发送端 cli 程序,使用的依赖包相比前面的模块要多一些,参数解析使用了structopt,串口配置与通信使用了serial,通信协议用到了上一个模块 xmodem,串口相关参数的检查代码也预先提供了,实际需要编写的内容非常少。串口参数没有什么可说的,这个模块支持两种数据源和两种发送方式,如何用更少的代码写出判断逻辑花了我一些时间,后来参考别人的代码看到装箱茅塞顿开。调试好代码后又去学习了一遍错误处理和智能指针的内容。

let mut _file:Box<dyn io::Read> = if let Some(path) = opt.input {
Box::new(File::open(path).unwrap())
} else {
Box::new(io::stdin())
};

if opt.raw {
io::copy(&mut _file, &mut port).unwrap();
} else {
Xmodem::transmit_with_progress(_file, port, progress_fn).unwrap();
}

在处理输入的部分,两种输入方式分别为std::fs::Filestd::io::Stdin,这两种结构体都实现了后面需要用到的std::io::Read,满足多态的条件。Box<T>会将变量移动到堆上并维护一个指针,通过装箱实现了多态。 在处理输出的部分,由于io::copy接受两个相同类型的参数,所以只能将两个参数都设为可变类型。在上面装箱的输入流在这里并没有显式拆箱,我理解是由于 Rust 的解引用强制多态(deref coercions)特性实现了隐式的拆箱和类型匹配。 代码中的unwrap()函数是异常处理代码,一旦前面的函数抛出错误就恐慌(panic)。