xmodem文件传输协议

XMODEM是一个简单的文件传输协议。该协议在1977年被设计出来,具有包校验码,连接取消和自动重传等功能。它被广泛使用在串行接口上用于文件传输。

1. XMODEM的性质

XMODEM是二进制协议: 字节以二进制形式(01序列)被发送和接受。

XMODEM是半双工通信: 同一时间,发送方和接收方最多一方在工作。

XMODEM是packet-based:数据被划分为128字节的包进行传输。

2. 文件传输

预定于如下常量

1
2
3
4
5
const SOH: u8 = 0x01; // start of header
const EOT: u8 = 0x04; // end of transmission
const ACK: u8 = 0x06; // acknowledge
const NAK: u8 = 0x15; // not acknowledge
const CAN: u8 = 0x18; // cancel

2.1. 包结构

Byte 1 Byte 2 Byte 3 Byte 4-131 Byte 132
start of header packet number complement of packet number packet data 16-bit CRC
  • 字节1仅可以为上述定义的常量中SOH,EOT其它情况为error
  • 字节2为packet number
  • 字节3为packet number的补码
  • 构成数据包的字节4-131可以为任意数据
  • 字节132为校验码(后文并没有使用crc作为校验码,crc算法的实现见末尾)

2.2. 传输建立

为启动文件传输,接收者发送NAK字节,发送者等待该字节。当发送者接收到NAK后,开始数据包(packet)的传输。

一旦文件传输开始,每一个数据包的传输和接收是相同的。数据包从1开始按序标号,并在255之后重新从0开始计数。

每个数据包128字节,如果不够128字节使用0padding。

取消传输(canncel the transfer):某一方若取消传输则要向对方发送CAN.

2.3. 发送方

  1. 发送SOH字节
  2. 发送数据包号
  3. 发送数据包号的补码
  4. 发送数据包
  5. 发送数据包校验和
    • 校验和为数据包所有字节之和模256.
  6. 从接收方读取一个字节
    • 若字节是NAK,则重传相同的数据包(重传上限为10次)
    • 若字节是ACK,则发送下一个数据包

2.4. 接受方

  1. 等待发送方的SOHEOT字节
    • 如果收到一个不同的字节,取消此次传输
    • 若收到EOT字节,接收者主动终止传输(end of transmission)
  2. 读取下一个字节并和当前数据包号比较
    • 如果收到错误的数据包号,接收者取消传输
  3. 读取下一个字节并和数据包号的补码比较
    • 如果收到的数据错误,取消传输
  4. 从发送者处读取数据包(128字节)
  5. 为数据包计算校验和
  6. 读取下一个字节同时比较和计算得到的校验和比较
    • 若不同,发送NAK同时重新尝试接收相同的数据包
    • 若相同,发送ACK同时接收下一个数据包

为了取消传输,CAN字节被接收方或发送方使用。当任意一方收到CAN,报错,并终止连接。

2.5. 终止传输

发送方

  1. 发送EOT字节
  2. 等待NAK字节,如果收到一个不同的字节,报错
  3. 发送第二个EOT字节
  4. 等待ACK字节,如果收到一个不同的字节,报错

接收方

  1. 发送NAK字节
  2. 等待第二个EOT字节,如果收到一个不同的字节,取消传输
  3. 发送ACK字节

XMODEM文件传输

3. rust实现

考虑如下设计模式:

为单次连接设置数据结构记录该次连接的状态。i) 通过静态方法向外导出数据发送抽象和数据接收抽象。ii) 对符合T: io::Read + io::Write的类型,实现底层的字节、数据包传输抽象。

静态方法仅使用底层提供的i) 构造器 ii) read/write_packet()抽象,并处理底层返回的error和重传机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 模拟单次连接,数据的发送/接收方状态
pub struct Xmodem<R> {
    packet: u8,
    started: bool,
    inner: R,
    progress: ProgressFn
}

impl Xmodem<()> {
    // 构造Xmodem实例,处理数据padding和重传
    pub fn transmit<R, W>(data: R, to: W) -> io::Result<usize> where W: io::Read + io::Write, R: io::Read;
    // 构造Xmodem实例,处理数据接收和重试
    pub fn receive<R, W>(from: R, into: W) -> io::Result<usize> where R: io::Read + io::Write, W: io::Write;
}

impl Xmodem<T: io::Read + io::Write> Xmodem<T> {
    // 构造器
    pub fn new(inner: T) -> Self;

    // 底层读写方法
    fn read_byte(&mut self, abort_on_can: bool) -> io::Result<u8>;
    fn write_byte(&mut self, byte: u8) -> io::Result<()>;
    fn expect_byte(&mut self, byte: u8, expected: &'static str) -> io::Result<u8>;

    // 根据当前状态(第一个数据包前的握手)读写一个数据包
    pub fn read_packet(&mut self, buf: &mut [u8]) -> io::Result<usize>;
    pub fn write_packet(&mut self, buf: &mut [u8]) -> io::Result<usize>;
}

4. 测试

4.1. 一些边界情况

  1. packet中的传输字符可以是任意的,因而可以存在CAN
  2. 根据1,若packet中只存在1个CAN计算得到checksum(求和对256取模)应当也为CAN。因此,若checksum若收到CAN不应当认为是接收方发的cancel。

4.2. 测试的实现

  • 一端的测试: 使用std::io::cursor套在内存缓冲区上
  • 两端通信的模拟:使用pipe模拟连接通路,两端分别使用线程调用transmitreceive方法。

pipe结构体的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct Pipe(Sender<u8>, Receiver<u8>, Vec<u8>);

fn pipe() -> (Pipe, Pipe) {
    let ((tx1, rx1), (tx2, rx2)) = (channel(), channel());
    (Pipe(tx1, rx2, vec![]), Pipe(tx2, rx1, vec![]))
}

impl io::Read for Pipe {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        for i in 0..buf.len() {
            match self.1.recv() {
                Ok(byte) => buf[i] = byte,
                Err(_) => return Ok(i)
            }
        }

        Ok(buf.len())
    }
}

impl io::Write for Pipe {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        buf.iter().for_each(|b| self.2.push(*b));
        for (i, byte) in buf.iter().cloned().enumerate() {
            if let Err(e) = self.0.send(byte) {
                eprintln!("Write error: {}", e);
                return Ok(i);
            }
        }

        Ok(buf.len())
    }

    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

5. 附录(crc算法的c实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int calcrc(char *ptr, int count)
{
    int crc;
    char i;
    crc = 0;
    while (--count >= 0)
    {
        crc = crc ^ (int) *ptr++ << 8;
        i = 8;
        do
        {
            if (crc & 0x8000)
                crc = crc << 1 ^ 0x1021;
            else
                crc = crc << 1;
        } while(--i);
    }
    return (crc);
}

6. 参考