使用 Golang 实现上一篇文章设计的云存储系统文件传输协议。将实现代码封装,提供外部访问接口。
设计 transmitter
socket 无法维持协议格式,因此需要提供一个 transmitter 类(尽管 Golang 没有提供 OOP 显式表达,但以下将使用 “类” 称呼 strcut,使用 “接口” 称呼 interface,使用 “对象” 称呼 struct 的一个具体实例),该类将 socket 连接包装起来,并向用户提供符合协议要求的接口。此外,下面将使用 “消息” 表示一组符合协议格式的数据流,即满足此前定义的协议格式的一个数据包。
类的私有变量
conn net.Conn: transmitter 的内部实现应当基于 socket 通信,因此内部需要一个net.Conn对象。buf []byte: socket 通信需要一个缓冲区,因为 socket 本身无法维持应用层的消息边界,所以每次从缓冲区读取的数据长度可能超过一个消息的长度,超出部分的数据实际为下个消息的首部。为了保证数据不被丢失,超过一组消息长度的数据应当被保存在缓冲区中,并和下次读取的数据组成一个新的消息。一个net.Conn对象应当使用同一个缓冲区,以保证数据不被遗漏。因此 transmitter 类中应当维护net.Conn使用的缓冲区。buflen int64: transmitter 中维护的缓冲区大小,单位为字节。block cipher.Block: transmitter 应当保证传输数据的安全性,block是 transmitter 使用的加密模块,将在后面加密部分的文章中介绍。下面假定已有函数AesEncode(plain []byte, block cipher.Block) []byte和AesDecode(encipher []byte, length int64, block cipher.Block)([]byte, err),这两个函数的功能分别是使用block对plain字节流加密,以及使用block对encipher密文解密。前者会返回加密后的字节流,后者会返回解密后的明文,如果解密失败则返回值携带 error。recvLen int64: 缓冲区中已存储的数据长度,即从 socket 读取的,但超过一个消息长度、尚未被使用的数据长度。每次将 socket 接收到的消息读取到缓冲区后,应当将超过一个消息长度的数据移动至缓冲区最开始的位置,这部分数据长度即为recvLen,下次 socket 的 read 操作将向缓冲区buf[recvLen:]写入。
接口需求分析
- 基本的内部数据获取和设置
GetConn() net.Conn:获取该 transmitter 内部的 socket 连接。GetBuf() []byte: 获取该 transmitter 内部的缓冲区。GetBuflen() int64: 获取该 transmitter 的缓冲区长度。GetBlock() cipher.Block: 获取该 transmitter 的加密模块。SetBuflen(int64) bool: 为该 transmitter 设置缓冲区大小,并自动拓展/缩小原有的缓冲区。
- 析构函数:当 transmitter 寿命终止时,应当调用析构函数
Destroy()销毁内部的数据并断开 socket 连接。 - 数据传输接口:transmitter 应当提供一些易使用的公有方法,这些方法可以让用户方便的发送/接收字节流,或者从特定的 Reader 发送字节流,并接收字节流至特定的 Writer。我们在《认证、传输协议设计》中假设过已存在一个
RecvBytes()函数,该函数能够从 socket 缓冲区读取一组消息。除了要实现该函数,还应当实现对称的SendBytes(message []byte)函数,用于向 socket 发送一组消息。同样,应当设计函数SendFromReader(reader)和RecvToWriter(writer),它们提供了数据源/目的地是 reader/writer 的操作。
具体定义接口
在工程目录下新建文件夹 transmit ,在该目录下新建代码文件 transmit.go,将私有类 transmitter 的公有接口定义为 Transmitable,其 Golang 代码表示如下。其中,SendFromReader 和 RecvToWriter 选择的 Reader/Writer 是 bufio 包中的读写器,因为在传输长数据流中将使用 bufio.Reader 和 bufio.Writer 去读/写文件。SendFromReader 的第二个参数是要发送的长度,如果这个参数大于 Reader 所能读取的长度,则读取到 Reader 末尾结束,否则读取到参数长度即结束。1
2
3
4
5
6
7
8
9
10
11
12
13// transmit.go
type Transmitable interface {
SendFromReader(*bufio.Reader, int64) bool
SendBytes([]byte) bool
RecvToWriter(*bufio.Writer) bool
RecvBytes() ([]byte, error)
Destroy()
SetBuflen(int64) bool
GetConn() net.Conn
GetBuf() []byte
GetBuflen() int64
GetBlock() cipher.Block
}
实现 transmitter
SendBytes() 和 RecvBytes()
SendBytes([]byte) bool的发送过程如下:首先发送一个 8 字节、大端序的明文表明即将发送的总长度,之后按照《认证、传输协议设计》设计的数据协议格式 格式1 发送消息,因为每个消息内包含的数据长度受缓冲区限制,因此可能需要传送多个消息才能发送所有数据。代码如下,其中第 6 行发送待发送数据明文的总长度,用到的auth.Int64Bytes(int64) []byte可以将一个int64数据转化为 8 字节、大端序的字节数组,与之对称的BytesToInt64([]byte) int64可以将参数的前 8 个字节转化为一个 int64 类型,这两个函数将在后面要实现的authenticate包中介绍,现在假设已经拥有这两个函数。
1 | // transmit.go |
- 上面的代码中,
chRate是一个只读的 channel,它来自内置的time库,可用于控制发送速度。如果发送方发送频率大于接收方接收频率,则可能出现阻塞和数据丢失,因此必须限制发送方的chRate速度慢于接收方的接收速率。alSend表示已经发送的数据长度。经验表明,使用 AES CFB 加密时,密文长度通常不会超过明文长度的两倍,为了保险,我们设置密文长度上限为缓冲区长度,因此每个消息可携带的明文长度至多为缓冲区长度的 1/3。代码的第 18 行用于判断接下来要发送的消息需要携带的明文长度,如果剩余要发送的数据长度大于 1/3 个缓冲区长度,则发送 1/3 个缓冲区长度的明文,否则只发送剩余的明文。23 ~ 27 行构造发送的消息,消息格式和此前设计的 格式1 相同。函数返回的 bool 值表明发送是否成功。 RecvBytes()([]byte, error)的接收过程如下:首先从 socket 读取 8 个字节,按大端序转为 int64 类型,得到要接收数据的总长度。之后严格按照 格式1 接收消息,每次先接收 16 字节,根据前 8 字节获得即将接收的消息携带的明文长度,根据后 8 字节获得整个消息的长度。当接收到的数据长度达到总长度时,将停止接收并以字节流返回本次调用接收到的所有数据。代码如下,其中第 7 行用到的RecvUntil(until int64, init int64, chRate <-chan time.Time)(int64, err)方法的作用是,当前缓冲区已有长度为init的数据,该方法将以chRate指定的速率一直从 socket 读取,直到缓冲区长度达到了until,并返回现在缓冲区持有的数据长度。如果在方法调用期间出现异常,返回值将携带错误消息。该方法的代码将在介绍完RecvBytes()后立刻给出,你可以先拖到下面查看该方法具体实现。
1 | // transmit.go |
- 上面的代码中,第 12 行获取要接收的数据总长度,并将
toRecvLength和totalLength均赋值为该长度。第 16 行将pRecv赋值为当前缓冲区已有数据长度,在方法执行期间,该变量将始终指代缓冲区存在的待处理数据长度,第 17 行指定当方法结束时将pRecv赋值给 transmitter 内私有变量recvLen。在每次接收消息前,先判断当前已接收到全部明文(toRecvLength == 0),之后接收每个消息的前 16 个字节,再接收整个消息。elength为当前要接收的消息的总长度(密文长度+16),plength为当前要接收消息携带的明文长度。在上面代码的倒数第 3 ~ 4 行,pRecv -= elength维护缓冲区待处理数据长度,copy(t.buf, t.buf[elength:pRecv])维护缓冲区待处理数据内容。 - 方法
RecvUntil(int64, int64, <-chan time.Time) (int64, error)的实现如下。这个方法应当被定义为私有的(方法首字母小写),这里因为编写时的失误没有注意,在这里保留这个漏洞,作为警醒。
1 | // transmit.go |
SendFromReader() 和 RecvToWriter()
- 这两个方法和上面的
SendBytes()、RecvBytes()类似,区别在于读取/接收的对象是 Reader/Writer。 SendFromReader(*bufio.Reader, int64) bool方法代码如下,和SendBytes()的主要不同在,每次发送的数据长度由交付 Reader 的缓冲区约束,同时要判断当前读取的 Reader 是否到达 EOF 边界,如果到达则表明发送结束,应当返回 true,否则才返回 false 表明传输出错。
1 | // transmit.go |
RecvToWriter(*bufio.Writer) bool方法实现代码如下,它和RecvBytes()的主要不同在于,在函数退出时需要使用 Writer 的Flush()方法将缓冲区数据全部写入,并且在 Writer 写入的过程中需要检查写入是否正确。此外,增加了valid变量,因为 Writer 写入的错误不应当影响网络连接的传输,因此当出现本地系统错误时,为了不破坏传输,将使valid为 FALSE,但传输将继续进行,最终返回给用户的值为 FALSE。这里的实现非常不人性化,另一种实现方式是,无论出现的错误是用户客户端本地错误还是网络连接错误,均直接返回 FALSE,通常应用RecvToWriter的场景是传输长数据流,在返回 FALSE 时,多数调用 transmitter 的父函数会立刻命令 transmitter 终止。因此这里直接返回 FALSE 可能是更好的实现方式,避免了不必要的带宽浪费。这里经过权衡,最终代码中选择直接返回 FALSE,但这里保留使用 valid 变量的代码留作对比。
1 | // transmit.go |
Transmitable 接口的调用
- 当试图发送一个文本类型的指令
command时,Transmitable 变量可以直接调用SendBytes(command),远端调用RecvBytes()即可获取command。 - 当试图发送一个文件时,发送端创建一个 Reader 对象
reader并获取文件大小length,调用SendFromReader(reader, length),接收端创建一个 Writer 对象writer,调用RecvToWriter(writer)。
专栏目录:顶点云(应用)设计与实现
此专栏的上一篇文章:顶点云(应用)认证、传输协议设计
此专栏的下一篇文章:顶点云(应用)认证基础模块实现
原创作品,允许转载,转载时无需告知,但请务必以超链接形式标明文章原始出处(http://blog.forec.cn/2016/11/14/zenith-cloud-2/) 、作者信息(Forec)和本声明。