协议流程图


主要关键 UDP 输入 输出 逻辑时钟

要点

  1. 服务端需要保存一段时间发送历史
  2. 客户端需要保存已接收完整数据的进度,以及接收队列
  3. 如果接收完整数据进度与接收队列中间断了一截,需要主端像服务端请求重传
  4. 难点在进度更新、重传逻辑、封包协议
  5. 数据发送接收抽象为了数据层与UDP本身无关系,要使用UDP外层还得套一个发送队列和接收队列

定义数据结构

  • 消息队列

recvQueue 接收队列

sendQueue 发送队列

sendHistory 发送历史-用作重传

  • 消息队列元素

{byteBuffer, id, tick} 分别是消息体、消息序号、逻辑时钟

协议

[tag][len][id]

0 心跳包 tag = 0

1 连接异常 tag = 1

2 连接关闭 tag = 2 (比云风设计那个多了一种tag)

3 请求包 tag = 3

4 异常包 tag = 4

其中tag区分这几种情况

普通情况tag(1-2byte) + len + id + data,其他情况后面说明

tag < 127 为 1字节

tag >= 127 为 2字节 高位为1

其实可以都用两个字节表示tag, 之所以这样做是因为尽量把协议消耗的字节变小。(参考哈夫曼编码)

接收消息

  • rudp.Input

每次放入一段字节流

  1. 更新逻辑时钟last_recv_tick = current_tick
  2. 判断tag类型(和127做比较) (UDP不会后TCP的粘包问题)

  • 2.1 心跳包 PING

有一个recvIDMin记录了已接受有序的ID中最大的,recvSkip记录了这个recvIDMin的时间点

如果当前接受队列recvQueue里最先到那个元素大于recvIDMin,说明revIDMin到recvQueue.head.ID之间的消息丢了(也可能还没发过来), 然后判断如果时间已经超过了missingTime,然后把这段丢失的包id插入重新请求队列reqSendAgain

  • 2.2 连接断开包 EOF CORUPT

没其他操作

  • 2.3 请求包 REQUEST

表示对方缺少了包,向这边请求,除了tag占2个byte, 还有4个byte表示id, 分别表示缺失的beginID,endID

然后放入待重新发送数据包队列 addSendAgain(每次封包的时候会带上这段数据)

  • 2.4 请求包异常 MISSION

表示对方缺少了包,向服务发请求,结果服务这边没有这些包,就向请求方返回这个序号,请求方更新进度,(可以打日志表示丢包了)

  • 2.5 普通数据包 DEFAULT

真正的发包内容,有序插入recvQueue,recvIDMin表示真正收到的被消费的最近一个,recvIDMax等于收到的最大的

发送消息

  • rudp.Send

从sendChan(业务层装进去的)取出来再先发送到sendQueue

  • rudp.Update

只保留5分钟的sendHistoryQueue (可调节)

发送三种包 Missing replyRequest Normal

因为判断是否丢失之类的是按接收端来判断,所以发送端不用管理进度之类的复杂逻辑,所以这里只需要保留5分钟的HistoryQueue即可。

相信参照这个文章再去阅读云风或者另一版go语言的源码会有更清晰的认识,在此感谢以下引用链的作者。

引用

https://github.com/cloudwu/rudp/blob/master/rudp.c

https://blog.codingnow.com/2016/03/reliable_udp.html

https://github.com/u35s/rudp

Comments
Write a Comment