天空中的大海--Kubernetes网络模型--铺垫篇①

想要深入分析k8s网络,需掌握iptables、ipvs、veth-pair、虚拟网桥等概念以及Wireshark、tcpdump、brctl等工具的使用。

本章研究一下P2P网络,并使用Wireshark抓包分析。

使用Go构建一个BitTorrent客户端

目标:下载debian.iso。

BT文件简介

使用的torrent文件是https://cdimage.debian.org/debian-cd/current/amd64/bt-cd/debian-10.9.0-amd64-netinst.iso.torrent

打开BT文件

内容又多又杂,原因是种子文件是Bencoding编码,使用Bencoding规则解码之后得到

天空中的大海--Kubernetes网络模型--铺垫篇①_第1张图片

  • length代表了文件大小(353370112Byte/1024/1024=337M
  • piece length代表了piece的大小(262144Byte/1024=256K
  • piece代表了每一块的哈希值(26960/20=1348块或者353370112/1024/256=1348块)。SHA-1产生20字节的哈希码。所以总共是1348*20=26960字节。可以看出这个BT文件将debian.iso分成1348块。

天空中的大海--Kubernetes网络模型--铺垫篇①_第2张图片

This mechanism allows us to verify the integrity of each piece as we go. It makes BitTorrent resistant to accidental corruption or intentional torrent poisoning.

读入BT文件

首先将种子文件(debian-10.9.0-amd64-netinst.iso.torrent)Unmarshal之后存入bencodeTorrent结构体,如下图所示

在这里插入图片描述

然后调用bencodeTorrent结构体的toTorrentFile方法,这个方法主要做两件事。

  1. 对info字典做SHA-1,得到infoHash
  2. 对Pieces进行20字节一组的切片,得到[] [20] byte类型的PieceHashes,PieceHashes的长度是1348

天空中的大海--Kubernetes网络模型--铺垫篇①_第3张图片

最后构造出torrentFile,如下

天空中的大海--Kubernetes网络模型--铺垫篇①_第4张图片

之后我们就基于这个torrentFile进行联系Tracker、握手校验、下载校验等操作。

获取一起聊天的peer萌

我们首先要生成一个随机的20字节的peerID,然后依此构造一个出url:

http://bttracker.debian.org:6969/announce?compact=1&downloaded=0&info_hash=%9F%29%2C%93%EB%0D%BD%D7%FFzJ%A5Q%AA%A1%EA%7C%AF%E0%04&left=353370112&peer_id=%8Fh%7D%DBS%1BO%9F%CD%F1%11%24%00%F3%EC%0D%C8%92%9E%B4&port=6881&uploaded=0

  • compact:此参数设为1,表示期望得到紧凑模式的节点列表,也就是host:port形式
  • downloaded、uploaded、left:主要用于Tracker统计数据
  • info_hash:种子文件的哈希,可以理解为下载主题,因为联系Tracker就是为了加入以info_hash为下载主题的swarm中。我们联系到peer之后也要验证一下info_hash看看我们是否说的是同一个事情。
  • peer_id、port:peer_id就是我们之前生成的20字节的随机数,port是我们自己开放的端口。

这些参数以RawQuery的格式append到base之后(base.RawQuery = params.Encode()),base就是Tracker服务器的域名http://bttracker.debian.org:6969/announce

然后对Tracker服务器发起Get请求。

请求之后得到response如下

在这里插入图片描述

Interval tells us how often we’re supposed to connect to the tracker again to refresh our list of peers. A value of 900 means we should reconnect every 15 minutes (900 seconds).

Peers is another long binary blob containing the IP addresses of each peer. It’s made out of groups of six bytes. The first four bytes in each group represent the peer’s IP address—each byte represents a number in the IP. The last two bytes represent the port, as a big-endian uint16.

peers是以字节的形式发送过来的,转换关系如下图所示

天空中的大海--Kubernetes网络模型--铺垫篇①_第5张图片

将返回的Peers信息Unmarshal之后(peers.Unmarshal([]byte(trackerResp.Peers)))得到IP加端口的形式,如下图所示

天空中的大海--Kubernetes网络模型--铺垫篇①_第6张图片

可见有相当多的Peers正在同时下载。(截图所示的四个IP地址分别是加拿大、俄罗斯、美国和意大利)

将所有下载所需信息装入Torrent结构体中然后开始下载任务。

天空中的大海--Kubernetes网络模型--铺垫篇①_第7张图片

构建管道和纤程

首先构建两个管道,workQueue管道和result管道。其中workQueue管道相当于装着任务卡的盒子,result管道相当于提交任务成果的盒子。

workQueue管道的大小是1348,也就是debian.iso分成的块的个数,每个goroutine都从这个管道里领取任务让后去疯狂下载。

result管道是no buffer类型的,各个goroutine下载完成后将成果扔到这里。

workQueue管道的buffer如下图所示

天空中的大海--Kubernetes网络模型--铺垫篇①_第8张图片

若要领取任务则必须先设置任务,接下来遍历PieceHashes将任务存储至管道

天空中的大海--Kubernetes网络模型--铺垫篇①_第9张图片

之后开始构建同peer等量的纤程,意思是从tracker处发现了多少peer,就构建多少纤程来进行下载。

// Start workers
for _, peer := range t.Peers {
     
    go t.startDownloadWorker(peer, workQueue, results)
}

我debug的时候向tracker请求到50个peer,所以开启50个纤程。

天空中的大海--Kubernetes网络模型--铺垫篇①_第10张图片

你好,peer

现在站在纤程的角度来看。

先对peer建立TCP连接,然后发送握手消息,peer之间采用的是Peer Wire协议,握手消息的格式是:

  1. The length of the protocol identifier, which is always 19 (0x13 in hex)
  2. The protocol identifier, called the pstr which is always BitTorrent protocol
  3. Eight reserved bytes, all set to 0. We’d flip some of them to 1 to indicate that we support certain extensions. But we don’t, so we’ll keep them at 0.
  4. The infohash that we calculated earlier to identify which file we want
  5. The Peer ID that we made up to identify ourselves

TCP连接,建立后序列化握手请求

天空中的大海--Kubernetes网络模型--铺垫篇①_第11张图片

然后发送给peer,peer收到消息后会返回一个握手协议,格式和上面相同,然后client校验info_hash是否相同,来保证peer之间讨论的是同一个东西。

天空中的大海--Kubernetes网络模型--铺垫篇①_第12张图片

每个纤程的握手示意图如下:

天空中的大海--Kubernetes网络模型--铺垫篇①_第13张图片

在peer回复handshake之后,紧接着会发送回一个messageID为bitfield的消息。如下所示
天空中的大海--Kubernetes网络模型--铺垫篇①_第14张图片

该消息的payload是一个169字节的数组。

天空中的大海--Kubernetes网络模型--铺垫篇①_第15张图片

168*8+4=1348正好是debian.iso分成的虚拟块的个数,这个bitfield可以理解为掩码,示意图如下

天空中的大海--Kubernetes网络模型--铺垫篇①_第16张图片

相比于使用byte的代表有无,bit代表有无的方式使传输的数据变小了。

One of the most interesting types of message is the bitfield, which is a data structure that peers use to efficiently encode which pieces they are able to send us. A bitfield looks like a byte array, and to check which pieces they have, we just need to look at the positions of the bits set to 1. You can think of it like the digital equivalent of a coffee shop loyalty card. We start with a blank card of all 0, and flip bits to 1 to mark their positions as “stamped.”

天空中的大海--Kubernetes网络模型--铺垫篇①_第17张图片

By working with bits instead of bytes, this data structure is super compact. We can stuff information about eight pieces in the space of a single byte—the size of a bool. The tradeoff is that accessing values becomes a little more tricky. The smallest unit of memory that computers can address are bytes, so to get to our bits, we have to do some bitwise manipulation:

// A Bitfield represents the pieces that a peer has
type Bitfield []byte

// HasPiece tells if a bitfield has a particular index set
func (bf Bitfield) HasPiece(index int) bool {
      
 byteIndex := index / 8
 offset := index % 8
 return bf[byteIndex]>>(7-offset)&1 != 0
}

// SetPiece sets a bit in the bitfield
func (bf Bitfield) SetPiece(index int) {
      
 byteIndex := index / 8
 offset := index % 8
 bf[byteIndex] |= 1 << (7 - offset)
}

到现在为止,本地client对peer的连接已经建立,可以开始下载数据了。

疯狂下载

从workQueue中取出任务,对比bitfield,以此确定对方是否拥有此piece。若这个peer没有,则将任务放回workQueue。

若存在此piece,则进入attemptDownloadPiece函数尝试下载此piece。

此时client的状态是被peer给choke住了。

Once we’ve completed the initial handshake, we can send and receive messages. Well, not quite—if the other peer isn’t ready to accept messages, we can’t send any until they tell us they’re ready. In this state, we’re considered choked by the other peer. They’ll send us an unchoke message to let us know that we can begin asking them for data. By default, we assume that we’re choked until proven otherwise.

Once we’ve been unchoked, we can then begin sending requests for pieces, and they can send us messages back containing pieces.

天空中的大海--Kubernetes网络模型--铺垫篇①_第18张图片

当我们收到peer发来的unchoke消息后,就可以对数据进行请求。BT文件中规定的piece length是256K,但是我们下载的时候将piece再次划分成block,每个block的大小是16K,然后进行流水线传输。

Files, pieces, and piece hashes aren’t the full story—we can go further by breaking down pieces into blocks. A block is a part of a piece, and we can fully define a block by the index of the piece it’s part of, its byte offset within the piece, and its length. When we make requests for data from peers, we are actually requesting blocks. A block is usually 16KB large, meaning that a single 256 KB piece might actually require 16 requests.

Network round-trips are expensive, and requesting each block one by one will absolutely tank the performance of our download. Therefore, it’s important to pipeline our requests such that we keep up a constant pressure of some number of unfulfilled requests. This can increase the throughput of our connection by an order of magnitude.

天空中的大海--Kubernetes网络模型--铺垫篇①_第19张图片

接下来进行请求,请求的格式是固定的。

request:

The request message is fixed length, and is used to request a block. The payload contains the following information:

  • index: integer specifying the zero-based piece index
  • begin: integer specifying the zero-based byte offset within the piece
  • length: integer specifying the requested length.

项目作者设置了pipeline的MaxBacklog为5,即连续发送5次request,然后去readMessage。示意图如下

天空中的大海--Kubernetes网络模型--铺垫篇①_第20张图片

piece:

The piece message is variable length, where X is the length of the block. The payload contains the following information:

  • index: integer specifying the zero-based piece index
  • begin: integer specifying the zero-based byte offset within the piece
  • block: block of data, which is a subset of the piece specified by index.

client收到messageID为piece的消息后,解析出payload中的msg.Payload[8:]的字节存入buffer中。

看一眼下载到的内容:

天空中的大海--Kubernetes网络模型--铺垫篇①_第21张图片

是一个16K的block,这就是我们真正需要的内容。

其中peer如果下载校验完成新的piece后,还会发来have消息。让我们来更新bitfield。

have消息的格式为:

have:

The have message is fixed length. The payload is the zero-based index of a piece that has just been successfully downloaded and verified via the hash.

将一块piece存储到buffer中,此时attemptDownloadPiece函数就结束了,如果其中有err发生,就将此piece再次放到管道中,等待其他纤程再次分配到这个piece继续下载。

然后进入checkIntegrity函数,对buffer进行SHA-1,之后再对比piece的SHA-1,以校验下载的完整性。

最后将校验结果正确的buffer放入result管道。

拼凑

将result从管道中取出,拼成完整的buffer。

//创建最终存储的buf数组
buf := make([]byte, t.Length)

res := <-results
begin, end := t.calculateBoundsForPiece(res.index)
copy(buf[begin:end], res.buf)

最后将buf写入文件

outFile, err := os.Create(path)
_, err = outFile.Write(buf)

到此为止文件的P2P下载就结束了。

此项目的不足:

  1. 只支持.torrent文件,并不支持磁力链接
  2. 只支持HTTP类型的Tracker,可以查看https://ngosang.github.io/trackerslist/trackers_best.txt,许多Tracker服务器都是UDP的
  3. 不支持包含多文件的BT种子
  4. 吸血鬼模式,并不支持上传

虽然有许多不足,但是对于了解P2P网络已经足够。


为保持简短,本章到此为止。下一章再使用Wireshark进行抓包分析。

由于BT种子还是存在Tracker服务器,并不是完全的去中心化,容易遭到封锁,所以使用DHT的磁力链接已经成为P2P下载的主流,之后单独开篇介绍。

参考:

项目作者

项目博文

wiki BitTorrentSpecification

Go 超时设置

你可能感兴趣的:(云计算,golang)