比特币源码分析-网络

比特币源码分析-网络

在梳理代码逻辑之前,首先介绍几个比较重要的结构:

CMessageHeader

消息头包含的内容:

class CMessageHeader {
public:
    enum {
        MESSAGE_START_SIZE = 4,
        //消息开始字符串,长度4字节,就是告诉你是属于哪种消息标识,在UTF-8中无效
        //主类型(MAIN):   0xd9b4bef9
        //测试网络(TESTNET):0x0709110b
        //回归测试(REGTEST):0xdab6bffa
        COMMAND_SIZE = 12,
        //定义了通信中的各种命令,由0x20~0x7F之间的字 符串构成,
        MESSAGE_SIZE_SIZE = 4,
        //最大值是32M (0x02000000)。 不包含消息头的大小
        CHECKSUM_SIZE = 4,
        //把消息数据经过2次SHA256算法运算得到校验和
    };

Message Headers

网络传输中,所有消息的消息头格式是一样的,下面解释一下每一个消息头具体包含哪些内容:

bytes name 数据类型 描述
4 start string char[4] 告诉发送端的节点,现在可以开始发送 Magic 的消息;用于在流状态未知时寻求下一条消息。
12 command name char[12] 标识有效载荷中包含的消息类型的ASCII字符串。随后是空值(0x00)来填充字节数; 例如:version\0\0\0\0\0
4 payload size uint32_t 有效 payload 中的字节数。Bitcoin Core 在有效的 payload 中允许的当前最大字节数(MAX_SIZE)为32个有效载荷大小超过此值的消息将被丢弃或拒绝。
4 checksum char[4] SHA256的前4个字节(SHA256(payload))以内部字节顺序排列。 如果有效payload为空,如verack和getaddr消息,则校验和始终为0x5df6e0e2(SHA256(SHA256())

GetDataMsg

getdata / inv消息类型。这些号码由协议定义。

enum GetDataMsg {
    UNDEFINED = 0,
    MSG_TX = 1,
    MSG_BLOCK = 2,
    // The following can only occur in getdata. Invs always use TX or BLOCK.
    //!< Defined in BIP37
    MSG_FILTERED_BLOCK = 3,
    //!< Defined in BIP152
    MSG_CMPCT_BLOCK = 4,

    //!< Extension block
    MSG_EXT_TX = MSG_TX | MSG_EXT_FLAG,
    MSG_EXT_BLOCK = MSG_BLOCK | MSG_EXT_FLAG,
};

消息类型大致分为:

  1. MSG_TX 交易信息
  2. MSG_BLOCK 区块
  3. MSG_FILTERED_BLOCK 过滤的区块
  4. MSG_CMPCT_BLOCK 紧凑区块 //bip152

NetMsgType

namespace NetMsgType {
	const char *VERSION = "version";//获取版本信息
	const char *VERACK = "verack";//版本信息回应
	const char *ADDR = "addr";//网络节点的地址
	const char *INV = "inv";//库存清单
	const char *GETDATA = "getdata";//获取数据
	const char *MERKLEBLOCK = "merkleblock";//merkle块
	const char *GETBLOCKS = "getblocks";//获取区块
	const char *GETHEADERS = "getheaders";//获取区块头
	const char *TX = "tx";//交易信息
	const char *HEADERS = "headers";//区块头
	const char *BLOCK = "block";//区块
	const char *GETADDR = "getaddr";//获取地址
	const char *MEMPOOL = "mempool";//内存池
	const char *PING = "ping";//判断网络是否连通
	const char *PONG = "pong";//ping消息回应
	const char *NOTFOUND = "notfound";//没有获取相匹配的数据
	const char *FILTERLOAD = "filterload";//加载过滤器
	const char *FILTERADD = "filteradd";//添加过滤交易信息
	const char *FILTERCLEAR = "filterclear";//清理过滤器
	const char *REJECT = "reject";//拒绝
	//======================网络协议版本号为70002之前======================//
	const char *SENDHEADERS = "sendheaders"; //bip130 发送块头信息
	const char *FEEFILTER = "feefilter";//BIP133 feefilter
	const char *SENDCMPCT = "sendcmpct";// BIP152 发送紧凑区块
	const char *CMPCTBLOCK = "cmpctblock";// BIP152 紧凑区块
	const char *GETBLOCKTXN = "getblocktxn";// BIP152 获取紧凑区块交易
	const char *BLOCKTXN = "blocktxn";// BIP152 紧凑区块交易
};

Inv

当需要获取inventory时,发送此命令,发送时,需要指定范围。接收到此命令后,按指定范围获取inventory数据(PushGetBlocks)。

bytes name 数据类型 描述
Varies count compactSize uint inventory 条目的数量
Varies inventory inventory 一个或多个库存条目,最多50,000个条目

Inv 消息的示例:

02 ................................. Count: 2

01000000 ........................... Type: MSG_TX
de55ffd709ac1f5dc509a0925d0b1fc4
42ca034f224732e429081da1b621f55a ... Hash (TXID)

01000000 ........................... Type: MSG_TX
91d36d997037e08018262978766f24b8
a055aaf1d872e94ae85e9817b2c68dc7 ... Hash (TXID)

getdata:

getdata消息请求来自另一个节点的一个或多个数据对象。这些对象由一个 inventory 请求,请求节点通常通过inv消息预先接收这些对象。

对 getdata 消息的响应可以是 tx 消息,阻塞消息,merkleblock 消息或未找到的消息。

getdata 不能用于请求任意数据,例如不再存在于内存池中的一些历史交易。如果全节点已经从其block数据库中打包了先前的交易,这时全节点可能无法提供这些block。 出于这个原因,getdata消息通常只能通过发送inv消息向先前通告它的节点请求数据。

getdata消息的格式和最大大小限制与inv消息相同,但是消息标题不同。

merkleblock:

如BIP37所述,在协议版本70001中添加。

merkleblock 消息是对使用 inventory 类型 MSG_MERKLEBLOCK 请求块的 getdata 消息的回复。这只是答复的一部分:如果找到任何匹配的交易,它们将作为tx消息单独发送。

如果之前已经使用过滤器加载消息设置了过滤器,则merkleblock消息将包含所请求块中与过滤器匹配的所有事务的TXID以及将这些事务连接到块头的必要块所需的块merkle树的任何部分 merkle根。 该消息还包含块头的完整副本,以允许客户端对其进行 hash 并确认其工作证明。

merkleblock消息示例:

01000000 ........................... Block version: 1
82bb869cf3a793432a66e826e05a6fc3
7469f8efb7421dc88067010000000000 ... Hash of previous block's header
7f16c5962e8bd963659c793ce370d95f
093bc7e367117b3c30c1f8fdd0d97287 ... Merkle root
76381b4d ........................... Time: 1293629558
4c86041b ........................... nBits: 0x04864c * 256**(0x1b-3)
554b8529 ........................... Nonce

07000000 ........................... Transaction count: 7
04 ................................. Hash count: 4

3612262624047ee87660be1a707519a4
43b1c1ce3d248cbfc6c15870f6c5daa2 ... Hash #1
019f5b01d4195ecbc9398fbf3c3b1fa9
bb3183301d7a1fb3bd174fcfa40a2b65 ... Hash #2
41ed70551dd7e841883ab8f0b16bf041
76b7d1480e4f0af9f3d4c3595768d068 ... Hash #3
20d2a7bc994987302e5b1ac80fc425fe
25f8b63169ea78e68fbaaefa59379bbf ... Hash #4

01 ................................. Flag bytes: 1
1d ................................. Flags: 1 0 1 1 1 0 0 0

getblocks

getblocks消息请求一个inv消息,该消息提供从块链中的特定点开始的块头hash。区块同步时,发送此命令,发送时需要指定区块范围(PushGetBlocks)。接收到此命令后,根据区块范围,获取相应的区块,反馈回去。接收的数据中包含区块范围的开始区块的定位信息(CBlockLocator)、结束区块的索引,从开始区块的下一个区块开始。每次最多获取500个区块信息。满500个时,记录获取的最后一个区块的hahs值,保存到源节点的hashContinue中。

bytes name 数据类型 描述
4 version uint32_t 协议版本号;与版本信息中发送的一样。
Varies hash count compactSize uint 提供的header数量不包括stop哈希。除了整个消息的字节大小必须低于MAX_SIZE限制外,没有限制;通常发送1到200次hash。
Varies block header hashes char[32] 一个或多个块头hash(每个32字节)以内部字节顺序排列。hash应该以块高度相反的顺序提供,因此最高高度 hash 指向第一个,最低高度哈希指向最后一个。
32 stop hash char[32] 请求最后一个header hash 的头部hash值; 设置为全零以请求包含所有后续头部hash的inv消息(最多500个将作为对此消息的回复发送;如果您需要超过500个,则需要发送另一个具有更高高度头部的getblocks消息 hash 作为块头 hash 字段中的第一个条目)。

示例如下:

71110100 ........................... Protocol version: 70001
02 ................................. Hash count: 2

d39f608a7775b537729884d4e6633bb2
105e55a16a14d31b0000000000000000 ... Hash #1

5c3e6403d40837110a2e8afb602b1c01
714bda7ce23bea0a0000000000000000 ... Hash #2

00000000000000000000000000000000
00000000000000000000000000000000 ... Stop hash

getheaders

getheaders消息请求 headers 消息,该消息提供从块链中的特定点开始的块 header。接收到此命令后,获取指定的范围的区块的头,将 headers消息发送给源节点。

getheaders消息几乎与getblocks消息相同,只有一点区别:对getblocks消息的inv回复将包含不超过500个块头hash; headers 回复 getheaders 消息将包含多达2000个块 headers。

tx

tx 消息以原始交易格式传输单个交易。它可以在各种情况下发送;

  • 交易响应:Bitcoin Core 和 BitcoinJ 将发送它以响应getdata消息,该消息请求 inventory 类型为MSG_TX的交易。
  • MerkleBlock响应:Bitcoin Core 将发送它以响应getdata消息,该消息请求inventory类型为MSG_MERKLEBLOCK的merkle块。 (这是发送merkleblock消息的补充。)在这种情况下,每个tx消息提供该块的匹配交易。
  • Unsolicited:BitcoinJ会发送一个tx消息来主动发起它的交易。

headers

headers 消息将 block message 发送到先前用getheaders消息请求特定 headers 的节点。headers 消息可以是空的。

bytes name 数据类型 描述
Varies count compactSize uint block headers 的数量最多可达200​​0个。注意:headers-first sync 假定发送节点将尽可能发送最大数量的headers
Varies headers block_header block headers:每个80字节block headers采用 block headers section中描述的格式,并附加一个0x00后缀。 这个0x00被称为交易计数器,但由于头部消息不包含任何事务,因此事务计数始终为零。

示例如下:

01 ................................. Header count: 1

02000000 ........................... Block version: 2
b6ff0b1b1680a2862a30ca44d346d9e8
910d334beb48ca0c0000000000000000 ... Hash of previous block's header
9d10aa52ee949386ca9385695f04ede2
70dda20810decd12bc9b048aaab31471 ... Merkle root
24d95a54 ........................... Unix time: 1415239972
30c31b18 ........................... Target (bits)
fe9f0864 ........................... Nonce

00 ................................. Transaction count (0x00)

block

block message以 serialized blocks section 描述的格式发送单个 serialized block。

  1. 获取数据响应:节点将始终发送它以响应一个getdata消息,该消息请求存储类型为MSG_BLOCK的块(假设该节点具有可用于发送的该块)。
  2. 主动提供:一些矿工会发送未经请求的block信息,将他们新挖掘的块块广播给他们的所有同行。 许多矿池做同样的事情,虽然有些可能被错误地配置为从多个节点发送块,可能不止一次地将同一块发送给别的节点。

notfound

notfound 的消息是对getdata消息的回复,该消息请求接收节点没有可用于发送的对象。 (预计节点不会传递不再存在于内存池或发送集中的历史事务,节点也可能从较旧的块中删除已用完的事务,使它们无法发送这些块。)

notfound消息的格式和最大大小限制与inv消息相同,只有消息的headers不同。

mempool

mempool消息请求接收节点已验证为有效但尚未出现在块中的交易的TXID。 也就是说,在接收节点的内存池中的交易。 对mempool消息的响应是一个或多个包含 inventory 格式的TXID的inv消息。

当程序首次连接到网络时,发送mempool消息非常有用。 全节点可以使用它来快速收集网络上可用的大部分或全部未确认的交易; 这对试图收取交易费用的矿工尤其有用。 SPV客户端可以在发送mempool之前设置过滤器,以仅接收与该过滤器匹配的交易; 这允许最近开始的客户获得与其钱包有关的大部分或全部未确认的交易。

对mempool消息的inv响应充其量只是一个节点的网络视图 – 而不是网络上未经确认的交易的完整列表。以下是列表可能不完整的一些其他原因:

  • 在Bitcoin Core 0.9.0之前,对mempool消息的响应只有一个inv消息。 inv消息被限制为50,000个库存,所以具有大于50,000个条目的内存池的节点不会发送所有内容。 Bitcoin Core 的更新版本根据需要发送尽可能多的inv消息以引用其完整的内存池。
  • mempool消息当前不与filterload消息的BLOOM_UPDATE_ALL和BLOOM_UPDATE_P2PUBKEY_ONLY标志完全兼容。 Mempool交易不像块内交易那样排序,因此一个消耗输出的交易(tx2)可以出现在包含该输出的交易(tx1)之前,这意味着自动过滤器更新机制将不会运行,直到第二次出现的交易 tx1) – 缺少首次出现的交易(tx2)。 在Bitcoin Core issue #2381中已经提出,交易在被过滤器处理之前应该被排序。

以上是对 Data Messages 的描述,其关系图如下:

001.png

version:

version 消息在连接开始时向接收节点提供关于发送节点的信息。在这两个节点交换 version 消息之前,不会接受其他消息。

当接收节点收到version消息之后,会回复给发送节点一个verack消息,同时把所有警告也反馈回去。但是在version消息的初始化未完成之前,是不会发送verack消息的。

发送端只能发送一次获取版本的命令,重复发送时,回应拒绝命令 (reject)。发送命令后,会把发送节点的地址信息添加到节点的地址管理器中。

示例如下:

72110100 ........................... Protocol version: 70002
0100000000000000 ................... Services: NODE_NETWORK
bc8f5e5400000000 ................... Epoch time: 1415483324

0100000000000000 ................... Receiving node's services
00000000000000000000ffffc61b6409 ... Receiving node's IPv6 address
208d ............................... Receiving node's port number

0100000000000000 ................... Transmitting node's services
00000000000000000000ffffcb0071c0 ... Transmitting node's IPv6 address
208d ............................... Transmitting node's port number

128035cbc97953f8 ................... Nonce

0f ................................. Bytes in user agent string: 15
2f5361746f7368693a302e392e332f ..... User agent: /Satoshi:0.9.3/

cf050500 ........................... Start height: 329167
01 ................................. Relay flag: true

verack:

verack消息确认先前收到的版本消息,通知连接节点它可以开始发送其他消息。 接收到版本回应命令后,设置节点的接收版本。与此同时,设置接收版本号,节点的接收版本(nRecvVersion)、接收消息的报头流的版本号、接收消息数据流的版本号(nVersion)都要设置。

adddr:

addr(IP地址)消息用来表示网络上节点的连接信息。 每个想要接受传入连接的节点创建一个addr消息,提供其连接信息,然后将该消息发送给未经请求的节点,当接收端收到此命令后把接收到的地址添加到节点的地址管理器中,发送、接收的地址数量最多1000个。

bytes name 数据类型 描述
4 time uint32 在协议版本31402中添加。采用Unix纪元格式的时间。通告自己IP地址的节点会将其设置为当前时间。通告他们连接的IP地址的节点将其设置为最后一次连接到该节点的时间。发送IP地址的节点不会改变时间。节点可以使用时间字段来避免传播旧的地址信息。恶意节点可能会改变时间,甚至可能会在未来进行设置。
8 services uint64_t 节点在其版本消息中广播的服务信息。
16 IP address char IPv6地址采用大端字节顺序。 IPv4地址可以作为IPv4映射的IPv6地址提供
2 port uint16_t 端口号以大端字节顺序排列。 请注意,为了寻找自己的伙伴,Bitcoin Core只会连接到具有非标准端口号的节点。 这是为了防止其他人尝试使用网络来破坏在其他端口上运行的非比特币服务。

Ping

ping消息有助于确认接收方仍处于连接状态(判断网络是否连通)。 如果在发送ping消息时遇到TCP / IP错误(例如连接超时),则发送节点可以假设接收节点已断开连接。 对ping消息的响应是pong消息。

在协议版本60000之前,ping消息没有 payload。从协议版本60001及所有更高版本开始,消息包含一个字段即nonce。

bytes name 数据类型 描述
8 nonce uint64_t 如BIP31所述,在协议版本60001中添加。将随机数分配给ping消息。响应的pong消息将包括这个随机数以识别它正在回复的ping消息。

ping消息的nonce字段,示例如下:

0094102111e2af4d ... Nonce

pong

pong消息回复ping消息,向pinging节点证明ponging节点仍然存在。默认情况下,Bitcoin Core将在20分钟内断开任何未响应ping消息的客户端。接收到pong命令后,更新节点的ping花费时间(nPingUsecTime),所花费的时间为当前时间与节点的ping开始时间(nPingUsecStart)的差。

为了允许节点跟踪等待时间,pong的回复消息中所包含的nonce字段与ping消息的nonce是相同的。

pong消息的格式与ping消息相同;只有消息头不同。

reject

发生特殊情况时,reject 消息通知接收节点其先前消息之一已被拒绝。

特殊情况如下:

  • 重复发送获取版本信息的命令(”version”)。
  • 发送端的版本号大于最大版本号(MIN_PEER_PROTO_VERSION = 209)。
  • 接收到DDoS攻击。
  • 处理消息时发生异常(ProcessMessage)。

发送拒绝命令时,带上参数,表示拒绝的原因。

Code In Reply To Description
0x01 REJECT_MALFORMED 处理消息时发生异常(ProcessMessage)
0x10 REJECT_INVALID 块、交易信息无效
0x11 REJECT_OBSOLETE 块版本、发送端版本过期
0x12 REJECT_DUPLICATE 重复发送获取版本命令、交易信息
0x40 REJECT_NONSTANDARD 交易信息不标准
0x41 REJECT_DUST
0x42 REJECT_INSUFFICIENTFEE 交易费不足
0x43 REJECT_CHECKPOINT 与校验点有关的错误

SendHeaders

sendheaders消息告诉接收方使用 headers 消息而不是inv消息发送新的块通告,具体可参照 bip130 。

GetAddr

getaddr消息请求来自接收节点的addr消息,最好是具有大量其他接收节点的IP地址的消息。 发送节点可以使用这些IP地址来快速更新其可用节点的数据库,而不是等待未经请求的addr消息随时间到达。

接收到getaddr命令后把节点的地址管理器中的地址返回给发送端。先清空源节点的发送地址数组(vAddrToSend)。再把节点的IP地址管理器 (addrman)中的地址(CAddress)发送给源节点。

FeeFilter

FeeFilter消息是对接收方的请求,不将任何交易inv消息转发给发送方,其中交易费率低于feefilter消息中指定的费率。

在Bitcoin Core 0.12.0引入mempool限制之后,feefilter在Bitcoin Core 0.13.0中引入。 Mempool限制功能可以防止费用较低的交易的攻击,并且不会将其纳入开采块中。 feefilter消息告诉其他节点,如果你的费率低于我预先设置的费率,那你的这个交易是不允许进入我的mempool的,同时,这些节点就没必要继续把低于该费率的交易的inv消息转达给该节点。

bytes name 数据类型 描述
8 feerate uint64_t 费用率(以每千字节为satoshis)低于该费率,交易不应传递给其它节点。

接收方可以选择不过滤这笔交易,直接忽略该消息。

FeeFilter与bloom过滤器相加。如果SPV客户端加载bloom过滤器并发送FeeFilter消息,则只有通过两个过滤器才能转发交易。

但请注意,feefilter对块传播或对getdata消息的响应没有影响。 例如,如果一个节点通过发送一个包含inv类型MSG_FILTERED_BLOCK的getdata消息来请求一个merkleblock,并且它先前已经向该节点发送了一个feefilter,那么即使他们低于feefilter的费率,该节点也应该响应一个包含所有匹配bloom过滤器的交易的merkleblock。

示例如下:

7cbd000000000000 ... satoshis per kilobyte: 48,508

FilterAdd

filteradd消息告诉接收方将单个元素添加到先前设置的布隆过滤器,例如新的公共hash。 该元素直接发送给接收方; 然后其它节点使用在过滤器加载消息中设置的参数来将该元素添加到布隆过滤器。

由于该元素直接发送到接收方,因此elem不会产生歧义,也不会出现布隆过滤器提供的似是而非的隐私。希望保持更高隐私性的客户端应自行重新计算布隆过滤器,并使用重新计算的布隆过滤器发送新的过滤器负载消息。

bytes name 数据类型 描述
Varies element bytes compactSize uint 下列元素字段中的字节数。
Varies element uint8_t[] 要添加到当前过滤器的元素。最大为520个字节,这是在pubkey或签名脚本中可以压入堆栈的元素的最大大小。元素必须以它们在原始交易中出现时使用的字节顺序发送; 例如,hash应以内部字节顺序发送。

注意:除非先前使用的filterload消息设置了过滤器,否则节点将不接受filteradd消息。

示例如下:

20 ................................. Element bytes: 32
fdacf9b3eb077412e7a968d2e4f11b9a
9dee312d666187ed77ee7d26af16cb0b ... Element (A TXID)

FilterClear

filterclear消息告诉接收方删除先前设置的bloom过滤器。这也消除了将版本消息中的转发字段设置为0的效果,允许未经过滤的访问广播新交易的inv消息。

Bitcoin Core在替换过滤器加载filterload之前不需要filterclear消息。它也不需要filterclear消息之前的filterload消息。

FilterLoad

filterload消息告诉接收方需要过滤所有转发的交易,并通过提供的过滤器请求merkle块。 这允许客户接收与其钱包相关的交易。

bytes name 数据类型 描述
Varies nFilterBytes compactSize uint 以下过滤器位字段中的字节数。
Varies filter uint8_t[] 任意字节对齐大小的位字段。最大大小是36,000字节。
4 nHashFuncs uint32_t 在此过滤器中使用的hash函数的数量。该字段中允许的最大值为50。
4 nTweak uint32_t 一个任意值,用于添加到布隆过滤器使用的哈希函数中的种子值。
1 nFlags uint8_t 一组控制与匹配的pubkey脚本相对应的outpoint的标志被添加到过滤器中。请参阅下面的更新布隆过滤器小节中的表格。

示例如下:

02 ......... Filter bytes: 2
b50f ....... Filter: 1010 1101 1111 0000
0b000000 ... nHashFuncs: 11
00000000 ... nTweak: 0/none
00 ......... nFlags: BLOOM_UPDATE_NONE

sendcmpct and cmpctblock、getblocktxn、blocktxn参照BIP152.

以上是对Control Messages的描述,其关系如下:

Control Messages

下表列出了一些值得注意的P2P网络协议版本,其中最新版本列在第一位:

version Initial Release Major Changes
70015 Bitcoin Core 0.13.2 (Jan 2017) v0.14.0中对无效压缩 blocks 的新禁用行为#9026, 在#9048中回退到v0.13.2 .
70014 Bitcoin Core 0.13.0 (Aug 2016) BIP152: 增加了 sendcmpctcmpctblockgetblocktxnblocktxn 消息类型;给 getdatamessage.增加了MSG_CMPCT_BLOCK inventory 类型
70013 Bitcoin Core 0.13.0 (Aug 2016) BIP133: 增加了 feefilter message.移除了 alert message 系统. 具体参照 Alert System Retirement
70012 Bitcoin Core 0.12.0 (Feb 2016) BIP130: 增加了 sendheaders message.
70011 Bitcoin Core 0.12.0 (Feb 2016) BIP111: 在包含此版本后,filter* 消息在没有NODE_BLOOM的情况下被禁用。
70002 Bitcoin Core 0.7.0 (Sep 2012) BIP35: 增加了 mempool message. 扩展了 getdata message 允许下载内存池中的交易
60001 Bitcoin Core 0.6.1 (May 2012) BIP31: 增加nonce字段为 ping message 、增加了 pong message
31800 Bitcoin Core 0.3.18 (Dec 2010) 增加了 getheaders message 和 headers message.
31402 Bitcoin Core 0.3.15 (Oct 2010) 给 addr message添加时间字段
311 Bitcoin Core 0.3.11 (Aug 2010) 增加了 alert message.
209 Bitcoin Core 0.2.9 (May 2010) 给 message headers增加了时间字段, 增加了 verack message, 并且增加了开始的 height 给 version message.
106 Bitcoin Core 0.1.6 (Oct 2009) 给发送端的version message添加了IP地址字段,随机数和用户代理(subVer)

本文由 copernicus 团队 冉小龙 分析编写,转载无需授权!

Leave a Reply

Your email address will not be published. Required fields are marked *