前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Bittorrent 协议浅析(七)uTorrent 传输、穿透拓展和 UDP Tracker

Bittorrent 协议浅析(七)uTorrent 传输、穿透拓展和 UDP Tracker

原创
作者头像
青橙.
修改2023-10-12 18:40:54
7540
修改2023-10-12 18:40:54
举报
文章被收录于专栏:橙、橙、

0.?回顾

前序文章:

-?Bittorrent?协议浅析(一)元数据文件?/developer/article/2332701

-?Bittorrent?协议浅析(二)Tracker?和?对等节点/developer/article/2333043

-?Bittorrent?协议浅析(三)对等数据传输实例/developer/article/2333677

-?Bittorrent?协议浅析(四)分布式哈希?/developer/article/2334440

-?Bittorrent?协议浅析(五)拓展协议?及?元数据传输拓展?/developer/article/2334776

前文内容回顾:

BitTorrent?是一种用于分发文件的协议,元数据文件采用?bencode?编码,分片进行?SHA-1?哈希计算比对,并介绍元数据文件数据结构,通过?HTTP?请求由?Trakcer?交换节点信息,节点直接直接进行通讯。

拓展协议中的元数据传输拓展可在节点之间传输元数据,PEX 拓展允许节点交换节点信息,DHT 可通过 KRPC 根据信息哈希获取节点,本地服务发现基于组播,在私有种子中这些内容均须禁用。

截至目前,所阐述和涉及的内容几乎都是基于 TCP 的 BitTorrent 实现。在部分网络环境下,通过 TCP 建立连接具有一定的局限性,过多的 TCP 连接会不公平的消耗网络资源,基于 UDP 的 uTorrent 和 穿透拓展能很好的解决这方面的问题,同时为位于 NAT 或防火墙后的下载器提供连接可能。

uTorrent

结合Bittorrent 协议浅析(八)uTP 数据包分析、超级种子获得最佳阅读体验。

uTorrent 传输协议(uTorrent transport protocol,uTP) 是一种建立在 UDP 之上的传输协议。uTP 动态调整数据包大小,通常传输速越快,使用的数据包越大。通常最小到每个数据包 150 字节。小数据包不会堵塞缓慢的上行链路但标头的网络开销较大。

数据格式

uTorrent 的数据格式如下:

代码语言:txt
复制
0       4       8               16              24              32
+-------+-------+---------------+---------------+---------------+
| type  | ver   | extension     | connection_id                 |
+-------+-------+---------------+---------------+---------------+
| timestamp_microseconds                                        |
+---------------+---------------+---------------+---------------+
| timestamp_difference_microseconds                             |
+---------------+---------------+---------------+---------------+
| wnd_size                                                      |
+---------------+---------------+---------------+---------------+
| seq_nr                        | ack_nr                        |
+---------------+---------------+---------------+---------------+

其中,

类型(type)

  • ST_DATA(0):ST_DATA数据包带有数据负载;
  • ST_FIN(1):完成连接。这是最后一个数据包,用于关闭连接,类似于TCP的FIN标志。连接应将此序列号记录为 eof_pkt,以继续等待可能丢失并无序到达的数据包;
  • ST_STATE(2):状态数据包。用于传输不带任何数据的ACK。不会增加seq_nr;
  • ST_RESET(3):强制终止连接。类似于TCP的RST标志。
  • ST_SYN(4):连接SYN。类似于TCP的SYN标志,此数据包启动连接。序列号初始化为1。所有后续数据包(除了重发 ST_SYN)都使用应使用连接 ID 和连接 ID+ 1发送。

版本号(version)

协议的版本号。当前为1。

连接 ID(connection_id):

用于标识属于同一连接的随机内容。连接发起方生成 ID,而响应数据使用 ID + 1。

时间戳(timestamp_microseconds):

该数据包发送时的时间戳。时间戳的分辨率越高越好。

时间戳差异(timestamp_difference_microseconds):

在上次接收数据包时,本地时间与上次接收数据包的时间戳之间的差异。

窗口大小(wnd_size):

已就绪的接收窗口大小,以字节为单位。窗口大小表示当前正在传输但尚未被确认的字节数。

扩展字段(extension):

扩展链中第一个扩展的类型。0表示没有扩展。(目前只有一个扩展,即选择性确认。)

选择性确认(Selective ACK):

选择性确认是一种可以选择性地确认非顺序接收的数据包的扩展。其有效载荷是一个至少32位的位掩码,以32位的倍数表示。每个位代表发送窗口中的一个数据包。多余位忽略。置 1 的位表示已接收,清 0 的位表示尚未接收。

序列号(seq_nr):

这是此数据包的序列号。与 TCP 不同,uTP 序列号不是指字节,而是指数据包。

确认号(ack_nr):

这是上一个连接收到的数据包的序列号。

连接流程

在 BEP 中给出了类似 C 语言的连接状态图来对连接流程进行描述,c.* 是连接的状态,pkt.*是数据包的字段,其连接过程与 TCP 的握手过程极为相似:

代码语言:txt
复制
连接发起方                                               连接接收方

          | c.state = CS_SYN_SENT                         |
          | c.seq_nr = 1                                  |
          | c.conn_id_recv = rand()                       |
          | c.conn_id_send = c.conn_id_recv + 1           |
          |                                               |
          |                                               |
          | ST_SYN                                        |
          |   seq_nr=c.seq_nr++                           |
          |   ack_nr=*                                    |
          |   conn_id=c.rcv_conn_id                       |
          | >-------------------------------------------> |
          |             c.receive_conn_id = pkt.conn_id+1 |
          |             c.send_conn_id = pkt.conn_id      |
          |             c.seq_nr = rand()                 |
          |             c.ack_nr = pkt.seq_nr             |
          |             c.state = CS_SYN_RECV             |
          |                                               |
          |                                               |
          |                                               |
          |                                               |
          |                     ST_STATE                  |
          |                       seq_nr=c.seq_nr++       |
          |                       ack_nr=c.ack_nr         |
          |                       conn_id=c.send_conn_id  |
          | <------------------------------------------<  |
          | c.state = CS_CONNECTED                        |
          | c.ack_nr = pkt.seq_nr                         |
          |                                               |
          |                                               |
          |                                               |
          | ST_DATA                                       |
          |   seq_nr=c.seq_nr++                           |
          |   ack_nr=c.ack_nr                             |
          |   conn_id=c.conn_id_send                      |
          | >-------------------------------------------> |
          |                        c.ack_nr = pkt.seq_nr  |
          |                        c.state = CS_CONNECTED |
          |                                               |已建立连接
     .. ..|.. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..|.. ..
          |                                               |
          |                     ST_DATA                   |
          |                       seq_nr=c.seq_nr++       |
          |                       ack_nr=c.ack_nr         |
          |                       conn_id=c.send_conn_id  |
          | <------------------------------------------<  |
          | c.ack_nr = pkt.seq_nr                         |
          |                                               |
          |                                               |
          V                                               V

其内容简单分析,指定连接发起方为 A,接收方为 B,将连接分为几个部分讨论:

初始化

A 设置为 CS_SYN_SENT 状态,发送连接请求。

A 的 seq_nr(序列号)初始化为1,conn_id_recv和conn_id_send分别初始化为随机数和conn_id_recv + 1。

握手

  • A 向 B 发送一个 SYN 数据包,建立连接请求。
  • B 从数据包中获取连接ID,并将其加1设置为 receive_conn_id,同时将 连接 ID 设置为send_conn_id,为接下来的数据包生成随机的seq_nr,将的状态切换为CS_SYN_RECV,接受连接请求。
  • B 向 A 发送一个状态数据包,其中包含自己的 seq_nr、ack_nr 和连接 ID,A 切换到CS_CONNECTED 状态,连接已建立。

数据传输

双方可以在连接上发送和接收数据。

超时和数据包丢失

超时(Timeouts)

初始超时时间为 1000 毫秒,之后会进行进行更新,对于每个超时的连续后续数据包的超时时间将加倍,更新逻辑:

当在只发送一次的数据包上进行通讯时,当数据包被确认,应更新连接时间(round trip time,RTT)和 RTT 方差(rtt_var),与此同时,与连接相关的数据包的默认超时时间也会在每次更新RTT和RTT方差时进行更新,超时时间设置为RTT和RTT方差的一定倍数,但不低于500毫秒,即:

代码语言:txt
复制
timeout = max(rtt + rtt_var * 4, 500)

数据包丢失(Packet Loss)

  • 数据包丢失判定:如果具有序列号(seq_nr - cur_window)的数据包尚未被确认,但在它之后已经有 3 个或更多数据包被确认,则认为该数据包已经丢失。
  • 快速重传:当收到 3 个重复的确认时,即假定序列号为(ack_nr + 1)的数据包也已经丢失(在已发送该数据包的前提下)。

当数据包丢失时,最大窗口大小(max_window)减半,类似 TCP 的拥塞控制方法。

拥塞控制

TCP 有基于窗口的 拥塞控制,但 UDP 并没有类似的内容,所以需要自行实现拥塞控制,在 BEP 中给出了使用延时作为控制标准的拥塞控制方案,uTP 设置目标延迟为100毫秒,即拥塞控制的目标是每个连接不会在缓冲区内存在超过 100 毫秒的延迟。如果超过,则减缓发送速度来有效让步 TCP 流量。

通过uTP发送的数据包中包含高分辨率时间戳,接收端计算该时间戳与接收到数据时本地时间的差距,将这个差距反馈给发送方,通过最近 2 分钟的最低值作为基线(近似于最小延时)作为基准延时进行计算。

其计算由下决定

代码语言:txt
复制
delay_factor = off_target / CCONTROL_TARGET; 
window_factor = outstanding_packet / max_window;
scaled_gain = MAX_CWND_INCREASE_PACKETS_PER_RTT * delay_factor * window_factor;

delay_factor 是一个表示延迟偏离目标延迟的因子,window_factor 是一个表示窗口大小与未确认数据包数量之间关系的因子,scaled_gain 是一个表示要应用于窗口大小的增益值,scaled_gain 的值用于调整窗口大小,窗口大小为零意味着套接字不能发送任何数据包,此时将触发超时,并重新设置窗口大小,以避免过度拥塞。

穿透(Holepunch)拓展

穿透拓展协议基于基础的拓展协议,它提供了一种利用中继节点帮助建立 uTorrent 连接的方式,拓展协议相关内容参考Bittorrent?协议浅析(五)拓展协议?及?元数据传输拓展进行了解,该拓展标识为ut_holepunch,下面是一个仅包括穿透拓展的握手实例,这里选择了 4 作为信息 ID,在实际过程中不同下载器会有不同选择:

代码语言:json
复制
{
  m: {
    ut_holepunch: 4,
  }
}

穿透拓展数据除了数据长度、类型、消息 ID 外包括:

  • 1 字节类型
  • 1 字节地址类型,IPv4 为 0,IPv6 为 0x01
  • 4 或 16 字节 IP 地址(IPv4 或 IPv6)
  • 端口(2 字节);
  • 4 字节 错误代码,若无错误为 0

支持的消息类型包括:

类型编码

类型

描述

0x00

rendezvous

会面,向发起对等点和目标对等点发送连接消息

0x01

connect

连接,打开 uTP 连接

0x02

error

错误,无法完成

一个节点可以将连接的目标节点信息写入数据包发送给另一个节点(后称中继节点)一个会面信息,如果中继节点已连接到目标节点,且目标节点支持,中继节点会向这个节点和目标节点都发送一个连接消息,其中包含彼此的端口信息。在接收到连接消息后,每个节点都会启动与另一个节点的uTP连接,需要注意,有可能两次 uTP 连接尝试会同时成功,此时系统需处理多个连接。如果无法处理,中继节点应该向发起节点回复一个错误消息。

在实现过程当中,如果目标节点不希望连接,它应该在忽略连接消息或连接请求,不向中继节点或请求节点发送错误消息,如果请求节点没有在拓展握手阶段表明支持 ut_holepunch ,那么中继节点须忽略 ut_holepunch 消息。如果节点之间已经建立了连接,也应忽略连接信息。

常见的错误信息有:

编码

错误信息

描述

0x01

NoSuchPeer

目标节点无效

0x02

NotConnected

中继节点未连接到目标节点

0x03

NoSupport

目标节点不支持 holepunch 扩展

0x04

NoSelf

目标节点的端点信息被错误地设置为中继节点的信息

在 NoSuchPeer 的情况下,也可以选择发送 NotConnected 错误代码。

穿透拓展为位于阻止传入连接后的下载器提供了更多连接到外部节点的可能,但仍然存在非常多的局限性,需结合网络环境,下载器实际进行分析。

基于 UDP 的 Tracker

在 BitTorrent 标准协议中,节点使用 HTTP 的方式与 Tracker 服务器进行通讯获取节点列表,请求内容和响应内容相对较短,在此情况下,需要建立 TCP 并频繁打开关闭连接的 HTTP 存在较大网络开销,使用 UDP 进行 Tracker 请求可以减少数据流量,同时可以简化 Tracker 的实现,虽然对于节点而言几乎没有区别,但对于需要处理大量请求的 Tracker 显得较为重要。

UDP 是一种 “不可靠” 的协议,下载器需要在 15 * 2 ^ n 秒未收到响应后重新发送请求,n 为失败的请求次数,最高到 8 ,特别的,连接 ID(connection ID)过期也需要重新发送请求。连接 ID 是缓解 UDP 源地址伪造的手段,Tracker 在收到请求后生成一个连接 ID 发送给下载器,下载器需要将该 ID 再次发送至 Trakcer 以进行源地址校验。这个 ID 可以复用于多个请求中,通常 1 分钟有效期。

连接

Tracker 请求内容均按大端序进行发送,请求开始后,需要首先请求连接 ID,首先需要选择一个随机的消息传输 ID,构造并发送下述数据包:

代码语言:txt
复制
0       4       8               16
+-------+-------+---------------+
| 0x41727101980 |   0   | tra_id|
+---------------+---------------+

偏移(字节)

大小、类型

描述

0

64位 整数

协议 ID

0x41727101980

8

32位 整数

操作

0

12

32位 整数

消息ID

将收到如下至少 16 字节的响应:

代码语言:txt
复制
0       4       8               16
+-------+-------+---------------+
|   0   |tra_id |     con_id    |
+---------------+---------------+

偏移(字节)

大小、类型

描述

0

32位 整数

操作

0

4

32位 整数

消息ID

8

64位 整数

连接ID

收到响应后,应将连接 ID 进行存储,在超时(1分钟)之前均使用这一连接 ID 进行数据请求。

请求

可以同时获取约 74 个种子的信息,发送的请求如下:

代码语言:txt
复制
0       4       8               16
+-------+-------+---------------+
|     con_id    |   2   |tra_id |
+---------------+---------------+
|     info_hash ...      
+---------------+---------------+

偏移(字节)

大小、类型

描述

0

64位 整数

连接ID

8

32位 整数

操作

2

12

32位 整数

消息ID

16+20*n

20字节 字符串

信息哈希

下载器公告

在 IPv4 环境下,请求:

代码语言:txt
复制
0       4       8               16              24              32
+-------+-------+---------------+---------------+---------------+
|   con_id      |   1   |tra_id |     info_hash...              |
+-------+-------+---------------+---------------+---------------+
| ...   |                   peer_id             |    download   |
+---------------+---------------+---------------+---------------+
|      left     |     upload    | event |   ip address  |  key  |
+---------------+---------------+---------------+---------------+
|  want | port  |
+---------------+---------------+---------------+---------------+

偏移(字节)

大小、类型

描述

0

64位 整数

连接ID

8

32位 整数

操作

1

12

32位 整数

消息ID

16

20字节 字符串

信息哈希

36

20字节 字符串

节点ID

56

64位 整数

下载量

64

64位 整数

剩余量

72

64位 整数

上传量

80

32位 整数

事件

0:无;1:完成;2:开始;3:停止

84

32位 整数

IP地址

默认 0

88

32位 整数

key

92

32位 整数

期望返回数量

默认 -1

96

16位 整数

端口

虽然包括了 IP 和端口,但大部分 Tracker 很少会识别并根据内容进行响应。

响应:

代码语言:txt
复制
0       4       8               16              24              32
+-------+-------+---------------+---------------+---------------+
|   1   |tra_id |inter_ |leech_ |seeders|   IP  |pot|IP...
+-------+-------+---------------+---------------+---------------+

偏移(字节)

大小、类型

描述

0

32位 整数

操作

1

4

32位 整数

消息ID

8

32位 整数

请求间隔

12

32位 整数

下载数

16

32位 整数

做种数

20 + 6 * n

32位 整数

IP 地址

24 + 6 * n

16位 整数

端口

对于 IPv6,返回的地址和端口从 6 字节变为 18 字节,其他均一致,此时请求中的 IP地址 无效,应保持为 0。

错误

一个错误信息如下:

偏移(字节)

大小、类型

描述

0

32位 整数

操作

3

4

32位 整数

消息ID

8

字符串

错误信息

该部分完

自此,BitTorrent 的最终提案和已接受的提案除了快速交换和 WebSeed 已经都进行了分析和阐述,后续还会继续进行上述内容的分析,同时会增加更多的 BitTorrent 草案的分享,敬请关注,如果更新,会在这里有链接:

最后,本文参加的征文活动广告:

我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0.?回顾
  • uTorrent
    • 数据格式
      • 连接流程
        • 超时和数据包丢失
          • 超时(Timeouts)
          • 数据包丢失(Packet Loss)
        • 拥塞控制
        • 穿透(Holepunch)拓展
        • 基于 UDP 的 Tracker
          • 连接
            • 请求
              • 下载器公告
                • 错误
                • 该部分完
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
                http://www.vxiaotou.com