协议设计

顶点云的协议格式非常简单且对带宽的利用率不高,但相比长数据流的传输,其低效还是可以容忍的。

认证流程

此部分主要介绍顶点云客户端在和服务器授权认证时的流程:

  1. 客户端申请与服务器开放端口建立 TCP 连接
  2. 服务器响应客户端的请求,以明文方式向客户端发送一个约定长度的 token ,记为 T
  3. 若此次认证是客户端初次与服务器建立连接,则客户端将用户名的明文和用户输入的密钥 md5 加盐加密值以字符串形式连接,记为 B ,转 ,否则转 4
  4. 若此次认证是客户端试图与服务器建立 长数据流连接被动监听连接 ,则将用户名的明文和首次认证身份时接收到的 token 以字符串形式连接在一起,记为 C
  5. 客户端接收 token 后按照 认证消息格式 使用 token 作为 AES CFB 算法的密钥加密 BC ,得到结果 S 并将 S 发送回服务器
  6. 服务器接收客户端响应信息,根据 认证消息格式 从认证消息中解析出用户名的明文和一个字符串 A ,该字符串或者为用户身份认证使用的密钥 md5 值,或者为客户端初次认证接收的 token
  7. 服务器检查在线用户列表是否存在用户名与请求认证的用户名一致,如存在,则比对服务器存储的该用户模型的 token 值,若该 tokenA 相同,则转 8,否则转 9 。若在线用户列表不存在用户名和请求认证的用户名一致,则转 10
  8. 服务器认为该用户使用的客户端试图建立 长数据流连接被动监听连接 ,因此检查服务器存储的该用户模型中是否已存在被动监听连接,若存在,则认为客户端新申请的连接用于长数据流传输,将其加入该用户的活动连接池中,否则将其设为该用户的被动监听连接。
  9. 服务器认为该用户在另一地点重复登录,向当前在线的该用户发送异地登录的警告信息、登出原用户并登入新认证的客户端
  10. 服务器在线用户列表不存在该用户,因此此连接必定为初次认证。服务器从数据库检索用户信息,比对该用户的密钥 md5 值与 A ,若相同则认为客户端认证信息合法,转 11 ,否则转 13
  11. 服务器向在线用户列表添加该用户,并初始化该用户的信息,如为该用户设置 token 值为 T ,设置昵称、已使用容量、最大容量等
  12. 服务器向该用户以 普通字节流消息格式 方式发送 T ,以协助客户端确定连接建立。之后服务器将该用户请求转发给用户代理,认证流程结束
  13. 服务器认为客户端认证信息不合法,单方面中断 TCP 连接,认证流程结束

协议消息格式

此部分主要介绍顶点云 C/S 交互时使用的消息格式。消息格式主要分为 认证消息格式普通字节流消息格式

认证消息格式

认证消息格式指的是客户端和服务器建立连接之初,客户端响应服务器认证身份请求时使用的协议格式。此格式的报文在每个连接开始时发送且仅发送一次,如果对此报文验证失败,服务器会主动断开和客户端的连接,客户端需要重新申请接入。

认证消息格式分为四个部分,大致如下表所示,所有长度的单位均为字节。如果你不了解表头各项含义,请阅读 认证流程

B 或 C 的明文长度 S 的长度 用户名明文的长度 S
8 字节 8 字节 8 字节 长度不定

上表中,前三个字段均为由一个 Int64 类型数据按大端序转化得到 8 个字节。

普通字节流消息格式

顶点云在认证过程以外的任何传输过程中均使用此格式,每个包可分为三个部分,大致如下表所示,表中所有长度的单位均为字节。应当注意,如果一个字节流过长, SendBytesSendFromReader 会将其自动拆分为多段,组成多个包发送。

该包携带的明文长度 该包总长度 携带的加密数据
8 字节 8 字节 长度不定

加密

顶点云的应用程序服务器采用 AES CFB 对称密钥加密算法,理论上会受到中间人攻击威胁。我计划在下一版本中修改加密方式,用 RSA 取代当前的对称加密。

代码中使用了 Go 语言内置的 cipher 加密模块,具体加密、解密函数请查阅 数字处理模块

MD5 计算

顶点云的应用程序服务器采用如下方式计算文件/字节流的 MD5 值:

  • 对于一串字节流,直接计算其 MD5 值,转化为 16 进制大写字符串(共 32 个字符)作为结果。
  • 对于一个文件,将其每 4MB 划分为一块,最后一块不足 4MB 也算作一块。分别计算每块的 MD5 值并转化为 16 进制大写字符串,将其按块顺序拼接成一个新的字符串。计算这个新字符串的 MD5 值并转化为 16 进制大写字符串作为结果。

顶点云默认提供了公有函数计算这两类数据的 MD5 值,你可以查看 MD5CalcMD5ForReader 以了解更多。

命令格式

顶点云采用简单文本格式的字节流传输用户指令,客户端 GUI 将用户输入格式化后按命令格式发送给服务器以获取支持。顶点云的内置命令可分为 交互式命令文件传输命令 。不同命令的参数数目可能不同,参数间用 SEPERATER (如果您尚不了解,请查阅 应用程序服务器全局设置 )分隔,因此用户参数中不应包含任何 SEPERATER 字符。

交互式命令

交互式命令指用户向服务器请求查询、文件软操作等服务时使用的指令,此类命令的发送和响应均通过客户端和服务器建立的交互式 传输器 传输,响应格式也为纯文本字节流。默认的顶点云包含如下交互式指令:

Command Param1 Param2 Param3 Others
ls Recurssive Quering Path Quering Keywords
touch Filename Path Type Identifier Nothing
cp File Id New Path Nothing
mv File Id New Filename New Path Nothing
rm File Id Nothing
fork File Id Password New Path Nothing
chmod File Id Is Private Password Nothing
send Nickname Message Nothing

命令参数解释

下面详细说明各指令参数含义,在阅读前请确保您已经了解 模型介绍 中的基本内容:

  • ls
  • Recurrsive :可为 0 或 1。为 1 代表递归查询,服务器会返回当前用户云盘空间中 Quering Path 下任意深度的文件和目录;为 0 时仅返回一级目录的资源。
  • Quering Path :用户要查询的云盘空间路径,路径不存在时等同于查询空目录。
  • Quering Keywords :用户查询使用的关键词,数量不限。例如查询名称中包含 zenithcloud 的资源,可附加这两个单词作为参数。
  • 样例:查询 /home/ 任意深度目录下的名称包含 test 的资源,使用命令 ls<SEP>1<SEP>/home/<SEP>test<SEP> 为用户配置文件中指定的 SEPERATER 字符。
  • touch
  • Filename :要创建的新文件的名称。
  • Path :创建的新文件所在的路径,若该路径不存在,服务器会自动创建该路径以及该路径中所有层次的目录。
  • Type Identifier :可以为 1 或 0,为 1 代表创建文件夹,否则为文件。
  • cp
  • File Id :要拷贝的资源编号。
  • New Path :要拷贝到的目标路径。
  • mv
  • File Id :要移动的资源编号。
  • New Filename :移动后为资源重新命名的名称。
  • New Path :移动到的目标路径。
  • rm
  • File Id :要删除的资源编号。
  • fork
  • File Id :要 Fork 的资源编号。
  • Password :要 Fork 的资源的提取码。
  • New Path :要 Fork 到的目标路径。
  • chmod
  • File Id :要修改权限的资源编号。
  • Is Private :可为 1 或 0。为 1 则将资源设置为私有,若资源为目录则该目录下所有子目录/文件均被设置为私有;为 0 则将资源设置为共享,并使用 Password 设置资源的提取码,若资源为目录则该目录下所有子目录/文件均被设置为共享且提取码均为 Password
  • Password :将资源设置为共享时指定的提取码。若 Is Private 为 1,则此项可为任意非空字符。
  • send
  • Nickname :消息接收方的用户昵称。
  • Message :消息实体,若消息中包含 SEPERATER 字符,则服务器会将该字符修正为空格。

交互式命令缓冲区

交互式命令缓冲区指服务器和客户端之间传输、响应交互式命令时,传输器 使用的缓冲区大小。此值由 config/config.go 中的 AUTHEN_BUFSIZE 项指定。

文件传输命令

文件传输命令指用户向服务器申请文件上传/下载/更新时使用的指令,此类命令的发送和响应均通过客户端和服务器建立的临时长数据流 传输器 传输。默认的顶点云包含如下文件传输指令:

Command Param1 Param2 Param3
put File Id File Size File MD5
get File Id Password

命令参数解释

下面详细说明各指令参数含义,在阅读前请确保您已经了解 模型介绍 中的基本内容:

  • put
  • File Id :要写入数据的资源的编号,该资源编号对应的资源类型必需为文件,且必需属于当前用户,否则服务器将请求视作不合法。 put 操作会将数据写入资源编号对应的文件,该文件原本指向的实体文件引用会被替换为新的实体文件编号。换句话说,此操作等于向一个已存在的文件重新写入数据,该文件的内容会被替换为写入的数据,大小会被设定为新数据的长度。
  • File Size :待写入数据的长度,单位为字节。
  • File MD5 :根据待写入数据计算出的 MD5 值,计算方法请参考 MD5 计算
  • get
  • File Id :要下载的资源编号,该资源编号对应的资源类型可以为文件或目录,如果为目录,客户端会自动在本地构建云盘中的虚拟目录并将整个目录下载到本地磁盘中。
  • Password :要下载的资源的提取码。
  • 该资源属于当前用户:提取码可填写任意非空字符,服务器会自动忽视该字段;
  • 该资源属于其他用户且为共享资源:若该资源的提取码为空,则服务器不会检查用户填写的提取码,否则将比对提取码是否一致,一致则允许下载,否则认为请求不合法
  • 该资源属于其他用户且为私有资源:请求不合法

长数据流连接

长数据流连接指的是用户发出文件传输请求时,服务器和客户端建立的临时连接。这类连接的缓冲区更大,传输速度更快,在传输结束后连接会断开,因此此类连接的寿命较短。

长数据流传输缓冲区

长数据流传输缓冲区指服务器和客户端之间传输文件时,传输器 使用的缓冲区大小。此值由 config/config.go 中的 BUFSIZE 项指定。

上传命令协议

用户上传文件指令 put 的执行流程如下:

  1. 假定服务器已经验证了客户端合法身份(如指令是否合法、资源是否存在、是否属于当前用户),若验证不通过则返回错误码 300 (指令格式不合法)、 301 (资源不存在)等。
  2. 服务器检查是否已存在文件实体的 MD5 值与用户要上传的 MD5 值相同,若存在则认为文件相同,返回状态码 200 并结束流程,否则发送状态码 201 表示需要传输并转 3。
  3. 服务器使用用户提供的 MD5 值作为文件名新建临时文件,启动传输并将数据写入此临时文件,若传输过程出现错误则返回错误码 203
  4. 数据传输完成后计算临时文件的 MD5 值,若与用户提供的相同则向 cfile 表中添加新记录,并将此文件重命名为新记录的编号,否则删除此文件并返回错误码 403
  5. 服务器更新用户指定资源的实体文件引用编号,并返回状态码 200 ,结束传输。
  6. 以上任何一步出现因服务器原因导致的错误,将返回错误码 500

下载命令协议

用户下载资源指令 get 的执行流程如下:

  1. 假定服务器已经验证了客户端的合法身份(如指令是否合法、资源是否存在、用户提供的提取码是否正确),若验证不通过则返回字符串 NOTPERMITTED 并中断连接,否则返回字符串 VALID 启动传输。
  2. 服务器计算用户要下载的文件/目录总数。若用户下载的资源为单个文件,则总数为 1,否则为用户试图下载的目录下的全部文件和目录数量之和加 1。例如用户试图下载如下目录结构中的目录 a ,则需要发送的总数为 5,包括:目录 a ,目录 a/b ,目录 a/c 以及文件 a/b/d.db 和文件 a/b/e/db
- parent-folder
  - a
        - b
          - d.db
          - e.db
        - c
  1. 服务器按顺序调用 SendBytes ,向客户端发送用户要下载的根目录名,以及类型(即数字 1,使用 Int64 与字节转化 转化为 8 个字节)。
  2. 服务器将剩余的待发送 目录 按路径长度排序,之后新建一个列表 A,对于待发送的每个目录,将其在云盘中相对待下载根目录的路径和目录名连接起来,生成新的字符串加入到列表中。
  3. 服务器按排序后的顺序调用 SendBytes ,向客户端发送列表 A 中的字符串、类型(即数字 1,使用 Int64 与字节转化 转化为 8 个字节)。
  4. 服务器将剩余所有文件按任意顺序发送,对于每个文件,按如下顺序调用 SendBytes ,依次发送文件相对待下载根目录的路径+文件名、文件类型(即数字 0,转化为 8 个字节),并启动连接发送此文件。此文件传输结束后进入下一个文件的循环。对于上面的目录结构,下载流程如下:
  • 服务器发送 8 个字节(使用 Int64 与字节转化 转化)表示的数字 5;
  • 服务器使用 SendBytes 发送用户试图下载的根目录名称 a
  • 服务器使用 SendBytes 发送 8 个字节表示的数字 1;
  • 服务器将剩余目录 a/ba/c 按路径长度排序,顺序维持不变;之后对其目录名作处理,将其相对目录 a 的父目录的路径保存到一个新列表中,即: ["a/b", "a/c"]
  • 服务器按列表的顺序,使用 SendBytes 分别发送:字符串 a/b 、8 字节数字 1、字符串 a/c 以及 8 字节数字 1;
  • 服务器按任意顺序发送文件。假设先发送 a/b/e.db 。使用 SendBytes 发送 e.db 的路径+文件名,即 a/b/e.db ,之后发送 8 字节表示的数字 0,最后启动 SendFromReader 传输该资源引用的实体文件内容。
  • 重复上一步的流程发送 d/db 。发送完成后断开连接,完成整个流程。

传输器设计

传输器模块是顶点云设计中最重要的一部分。顶点云使用传输器将协议与 Socket 连接封装在一起,简化了代码编写的复杂度。

因为 Socket 可能将多个包合并发送,接收方必需按协议设定的包格式接收、并将剩余数据缓存以待下次使用。因此每个传输器必须维护一定缓存空间,并且不能遗漏尚未使用但已接收的数据。传输器的设计目标即提供以下几个方法,使服务器的传输器和客户端对应的传输器之间每次通讯只需调用对应 API 即可。

  • SendBytes :任意一方调用此方法即可将一串字节流发送到远端传输器,若对方未调用 RecvBytes 响应则阻塞,超时后断开;
  • RecvBytes :任意一方调用此方法即可接收到远端传输器使用 SendBytes 发来的一份报文,若对方未调用 SendBytes 发送数据则阻塞,超时后断开。即使远端多次调用 SendBytes ,传输器也可将接收到的、暂不需使用的剩余部分缓存以供后续使用。即 SendBytesRecvBytes 是相对应的方法,二者通过传输器内部的缓存虚拟了两个缓冲池实现全双工通信,双方均可以将自己要发送的内容投放到虚拟缓冲池(对方传输器的缓冲区)中,也可以从虚拟缓冲池(我方传输器缓冲区)中获取对方发送的内容;
  • SendFromReader :任意一方调用此方法即可将一个可读结构中的指定长度数据发送到远端服务器,若对方未调用任何接收函数等待则阻塞,超时后断开。类似 SendBytes ,但若指定长度超过可读结构的内容长度,则认为执行失败;
  • RecvToWriter :任意一方调用此方法即可响应对方调用的 SendFromReader 并将接收到的数据还原后写入作为参数的可写结构。即 SendFromReaderRecvToWriter 是相对应的方法,这两者的调用应当对称。

以上四个函数中, SendBytesSendFromReader 二者发送流程相同, RecvBytesRecvToWriter 二者接收流程相同。

SendBytes 发送流程如下:

  1. 发送方以明文方式发送 8 个字节(大端序,使用 Int64 与字节转化 转化)表示的明文长度
  2. 发送方从待发送字节数组中读取不超过缓冲区长度 1/3 的数据,统计待发送的明文长度,记为 A,若 A 为 0 则发送结束
  3. 将待发送明文使用传输器内置的加密模块编码,得到待发送数据,统计其长度,记为 B,则 B+16 为此次要发送的包的长度
  4. 拼接一个待发送数据包,该包的格式遵守 普通字节流消息格式 。发送完成后转 2

RecvBytes 接收流程如下:

  1. 接收方调用 RecvUntil 接收满 8 字节,提取待接收明文长度
  2. 接收方调用 RecvUntil 接收满 16 字节,提取当前包头
  3. 接收方根据包头提取本包长度、明文长度,并调用 RecvUntil 接收整个包
  4. 接收方解包,提取数据并附加到返回值中
  5. 接收方将待接收明文长度减去本包包含的明文长度,若待接收长度为 0 则接收流程结束
  6. 转 2,接收下一个包

用户通讯

为了降低模块之间的耦合度,顶点云的用户代理模块不具有回调服务器方法的权限。用户使用 send 命令向特定用户发送消息时,用户代理仅仅会将此消息写入数据库,而发送则交由服务器的守护线程处理。

被动监听连接

为了向客户端推送系统或其他用户发送的消息,每个客户端与服务器除了基本的交互式 传输器 外,还有一个被动监听连接用于被动接受服务器发送的消息并将消息展示给用户。这个连接在用户登录认证成功后立刻启动,以长数据流的方式通过认证。因为它是第一个长数据流连接,因此服务器将此连接视作被动监听线程并分配给已登录的对应用户。

接下来请您阅读 框架分析