Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

bytom源码分析-P2P网络-地址簿

excel436
162 0 0
简介) A! F. {8 T$ I& S0 ]$ v. l' m$ I
https://github.com/Bytom/bytom+ X6 }0 J+ X6 X% ~' r
本章介绍bytom代码P2P网络中addrbook地址簿% W7 b/ @3 V7 w; K: J

/ a) s& O" Y& s% R# m0 E作者使用MacOS操作系统,其他平台也大同小异: h* l0 K0 q7 K* W( j' a

6 b0 B" h( m) O7 c3 i; I9 R! s# c7 A& i0 I' G$ ?2 v- W
Golang Version: 1.8- g* I5 R- C. a2 k3 g, ?
8 V$ n2 k9 P8 {8 C
addrbook介绍
4 p' l( C, G. H! B% d8 F# F- Taddrbook用于存储P2P网络中保留最近的对端节点地址. [4 \  {1 Q. I' V; h: D
在MacOS下,默认的地址簿路径存储在~/Library/Bytom/addrbook.json
8 S' H: q) r) n; o+ n  g  B7 X* B8 R# K  I地址簿格式8 `8 d( Z+ |  I1 P2 S0 N( h
  1. ** ~/Library/Bytom/addrbook.json **) I8 e$ c4 ~  V' L6 B5 c7 q
  2. {
    4 s  w7 o* q  }! P$ }7 O9 o# J- U  B
  3.     "Key": "359be6d08bc0c6e21c84bbb2",
    : C* r% d3 k' h4 f5 e7 D* j. @
  4.     "Addrs": [4 ]1 d7 t! F1 Q: t4 |$ F/ l
  5.         {0 @/ d; N2 t3 w- w, Q
  6.             "Addr": {8 ^9 ^+ z6 l! B, ~; x7 F
  7.                 "IP": "122.224.11.144",% d$ }" v  @7 B
  8.                 "Port": 46657
    7 v" Q0 Z+ D1 Q8 u5 r4 `8 X
  9.             },3 M; a; t! h) g
  10.             "Src": {* C% O. E+ d6 C8 j  X+ W4 {
  11.                 "IP": "198.74.61.131",
    & n1 n% l( K2 B3 R+ ?
  12.                 "Port": 46657
    9 q7 O7 q# J$ E* B
  13.             },3 P7 W6 B/ j7 m# f. z9 `
  14.             "Attempts": 0,  {. E4 d$ Q7 ?- r% _
  15.             "LastAttempt": "2018-05-04T12:58:23.894057702+08:00",9 q8 }  _, A) O! {9 T. _
  16.             "LastSuccess": "0001-01-01T00:00:00Z",0 c, h7 [6 I8 r% B
  17.             "BucketType": 1,4 _4 E2 n9 ?' q% T7 h( `
  18.             "Buckets": [8 |$ ^; d. I! L3 w6 n, i
  19.                 181,# Q& ~2 x- B" v' t8 }6 M- g
  20.                 10" O+ a9 a3 r5 g' T
  21.             ]
    ! ^/ l' b% p6 d; w# H0 ~8 f7 N
  22.         }5 I2 F; ~5 n4 M8 o: l+ s/ H* G5 v
  23.     ], y7 `  x  P( X6 q
  24. }
复制代码

0 G- v; o- w6 H; G# F地址类型# @/ {6 \  E; t8 X+ g/ l% \
在addrbook中存储的地址有两种:# u8 K. j. C1 |
  1. ** p2p/addrbook.go **
    * U! g  V; B1 s; g# q3 U
  2. const (
    . q+ K3 w4 ^- i7 e# U8 P: c. i1 k
  3.         bucketTypeNew = 0x01  // 标识新地址,不可靠地址(未成功连接过)。只存储在一个bucket中
    4 J% k. ]( _, Y0 a
  4.         bucketTypeOld = 0x02  // 标识旧地址,可靠地址(已成功连接过)。可以存储在多个bucket中,最多为maxNewBucketsPerAddress个6 E" c, Y  y& s9 g+ o1 N
  5. )
复制代码
4 @& t9 x4 S1 @2 W$ j7 `
, _4 K" t' ~# A* w1 k: F/ E
注意: 一个地址的类型变更不在此文章中做介绍,后期的文章会讨论该问题% J3 K' t" N- l+ m& ?* ?) \
4 C, Q" s) R0 |0 t1 I( q
地址簿相关结构体& G( h) U) k1 ^2 d
地址簿
6 }* X: G' L- J
  1. type AddrBook struct {
    9 {; A8 Z3 `3 P% C9 ~
  2.         cmn.BaseService% Y/ U0 N9 l/ c. e
  3.         mtx               sync.Mutex: k% w6 p; u# f9 ]8 m1 N1 z& ~
  4.         filePath          string  // 地址簿路径1 P, [; M! S& y$ O' E
  5.         routabilityStrict bool  // 是否可路由,默认为true
    2 i( Z, H8 x3 r$ d
  6.         rand              *rand.Rand
    # h5 R6 E' r! D' S
  7.         key               string  // 地址簿标识,用于计算addrNew和addrOld的索引& d9 Q9 k2 V/ u, e; R
  8.         ourAddrs          map[string]*NetAddress  // 存储本地网络地址,用于添加p2p地址时做排除使用
    ) h$ n% v8 o' V; O' Q
  9.         addrLookup        map[string]*knownAddress // 存储新、旧地址集,用于查询7 F3 l4 o' w5 Y' G4 ~. X1 G+ `
  10.         addrNew           []map[string]*knownAddress // 存储新地址, D- k( S, G% V4 ^
  11.         addrOld           []map[string]*knownAddress // 存储旧地址. o" W' Y& C, Z" {6 V' B; @: f
  12.         wg                sync.WaitGroup7 l: |  ?) u# j7 T( U$ A" ]
  13.         nOld              int // 旧地址数量
    + k) `5 V/ m5 H) m
  14.         nNew              int // 新地址数量' X+ q3 `) g. D, i8 j
  15. }
复制代码
, v+ [! s% [6 H6 x2 g2 d
已知地址
' W* f5 Y6 U  D, o# _( b$ b
  1. type knownAddress struct {
    % C$ c2 a8 ^. W$ X% K3 N
  2.         Addr        *NetAddress // 已知peer的addr$ `+ W4 l6 A1 V& i0 L
  3.         Src         *NetAddress // 已知peer的addr的来源addr+ j+ l* u/ j6 P5 s0 D
  4.         Attempts    int32 // 连接peer的重试次数
    ; t! K; I& p( {9 ?: p6 `, N/ t
  5.         LastAttempt time.Time // 最近一次尝试连接的时间# m0 ?# Q" d- p' h  C6 [
  6.         LastSuccess time.Time // 最近一次尝试成功连接的时间! [6 L7 g. X- a# i  |/ F( A
  7.         BucketType  byte // 地址的类型(表示可靠地址或不可靠地址)  U5 e5 W$ v) R  }" J: }, ^
  8.         Buckets     []int // 当前addr所属的buckets
    ( s/ E5 |  p1 m0 A$ z# e
  9. }
复制代码
  V; P5 E5 ~$ Z: L/ d) m5 d' g
routabilityStrict参数表示地址簿是否存储的ip是否可路由。可路由是根据RFC划分,具体参考资料:RFC标准
7 J# A" P# `7 h初始化地址簿0 d! J1 r' F5 ^3 b! |2 j( [5 i6 P
  1. // NewAddrBook creates a new address book.
    6 Y7 }0 S+ S9 R) l
  2. // Use Start to begin processing asynchronous address updates.& @) v8 L; v/ M
  3. func NewAddrBook(filePath string, routabilityStrict bool) *AddrBook {' k3 N& F% G; R& E# |+ y3 d
  4.         am := &AddrBook{4 v0 k5 `+ \7 `1 h; T9 b
  5.                 rand:              rand.New(rand.NewSource(time.Now().UnixNano())),0 `1 u% h. W7 t& I6 h% G9 B5 ?
  6.                 ourAddrs:          make(map[string]*NetAddress),, _' p7 a6 H7 P
  7.                 addrLookup:        make(map[string]*knownAddress),
    : Q( W9 m  I' r; B( n  ^
  8.                 filePath:          filePath,5 b% G- z9 G- G
  9.                 routabilityStrict: routabilityStrict,9 ?' o& c3 P! h& c$ I4 p! X
  10.         }9 J0 u( Y/ }; F1 w! j' R$ e
  11.         am.init()
    ; z: b, g% c6 R3 P- R
  12.         am.BaseService = *cmn.NewBaseService(nil, "AddrBook", am)+ W. k2 S; f! R  T# ]
  13.         return am- ^3 [, s& W8 `/ K$ F5 v
  14. }
    6 E2 D! N( x3 L! U* C$ q
  15. // When modifying this, don't forget to update loadFromFile()
    8 e# I5 q, Z! N( F) w7 F' r5 x
  16. func (a *AddrBook) init() {
    / X6 [6 [6 {4 I# c
  17.   // 地址簿唯一标识7 F# r* a8 b, [5 e! b5 w. ]
  18.         a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits, f+ j0 @0 L: c: \( t: A2 I
  19.         // New addr buckets, 默认为256个大小+ Y# Z: Q7 F) t6 z5 r
  20.         a.addrNew = make([]map[string]*knownAddress, newBucketCount)
    % m' \- @9 e8 s1 p* t% e' a
  21.         for i := range a.addrNew {
    , z0 J, a- n; S  a
  22.                 a.addrNew<i> = make(map[string]*knownAddress)" Q: H8 q, l: v# R( @$ G2 A' z9 g
  23.         }2 W& ]' D, @9 @/ Q, b/ j
  24.         // Old addr buckets,默认为64个大小
    1 Y+ L0 y* x5 J; l; y4 v+ {
  25.         a.addrOld = make([]map[string]*knownAddress, oldBucketCount)8 W( h- `1 i$ h# I6 {
  26.         for i := range a.addrOld {
    9 D* x! X* Y2 c( h6 K) R! D
  27.                 a.addrOld<i> = make(map[string]*knownAddress)
    . B, V( g0 H: u8 I7 I, q
  28.         }
    3 B) E8 t3 b. \6 T2 R; h
  29. }</i></i>
复制代码

7 N6 P& C, ^1 c$ ?, ^bytomd启动时加载本地地址簿
# h# |& d1 z4 {loadFromFile在bytomd启动时,首先会加载本地的地址簿! F0 G7 ~& F" S
  1. // OnStart implements Service.9 D2 Y0 K/ C4 i( S+ [
  2. func (a *AddrBook) OnStart() error {9 z. K, i! U. f9 Y: [
  3.         a.BaseService.OnStart()7 Z& z4 k4 k1 h! J# Y
  4.         a.loadFromFile(a.filePath)- L9 x3 c: f" I! L: j
  5.         a.wg.Add(1)! x2 Q* }4 G3 P3 B/ U
  6.         go a.saveRoutine()' Z# y$ x1 M4 L7 U
  7.         return nil
    ' c6 v3 g6 ?" m( d
  8. }8 }, v% F+ ^) V8 ~$ Y0 ?
  9. // Returns false if file does not exist.* @, F/ ~4 I- r, E: n8 C
  10. // cmn.Panics if file is corrupt.8 b# F' O$ [0 S% w* w$ L
  11. func (a *AddrBook) loadFromFile(filePath string) bool {" A3 u/ s6 i$ u, e
  12.         // If doesn't exist, do nothing.
    ' `. K6 K. w2 x1 `4 D
  13.         // 如果本地地址簿不存在则直接返回4 r; d0 p8 d5 [) l( _
  14.         _, err := os.Stat(filePath)% i, T3 a" K& e% V2 v
  15.         if os.IsNotExist(err) {
    4 |% c  l! S' v! o, D% k, {
  16.                 return false
    $ ~, I3 i3 f# o. B: M* I, P
  17.         }
      Z2 D$ B# \2 O
  18.   // 加载地址簿json内容
    / O/ `+ c0 S3 i6 ]; n5 G5 L9 U; C7 c
  19.         // Load addrBookJSON{}5 y) U8 s1 k9 A; `" ~5 p3 R
  20.         r, err := os.Open(filePath)5 _; t5 O2 s2 |0 w6 H- o" ?
  21.         if err != nil {( o+ o) L5 @2 o1 b$ q* L  D3 C
  22.                 cmn.PanicCrisis(cmn.Fmt("Error opening file %s: %v", filePath, err))
    & ]7 N& M% a3 I; n; {8 Y- j1 Y
  23.         }1 w- @/ i4 I! F; ?
  24.         defer r.Close()5 c% u) _( z* k3 y
  25.         aJSON := &addrBookJSON{}/ _& L; H- u4 y; K4 G& i& {% I6 Y
  26.         dec := json.NewDecoder(r)$ x4 U% L1 j% o/ C  f9 h
  27.         err = dec.Decode(aJSON)
    3 \3 W) F0 }* e1 I: E( C+ c5 T% p
  28.         if err != nil {9 h; E0 S& ?* A" s" L
  29.                 cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", filePath, err))1 M- c8 N( Z( P" E
  30.         }
    / d9 L$ B/ a8 k
  31.   // 填充addrNew、addrOld等. O' r6 ~3 m; {0 K( T" Y2 f1 w
  32.         // Restore all the fields...6 V/ `, A( Q- R( H2 `1 S4 j
  33.         // Restore the key# D* L9 ^' c( J
  34.         a.key = aJSON.Key9 l3 p/ l: E7 G  h, v+ k
  35.         // Restore .addrNew & .addrOld8 [* w' V7 o" k/ }; Q- {! m7 p
  36.         for _, ka := range aJSON.Addrs {
    3 J$ y  z" p4 N$ z  }9 k
  37.                 for _, bucketIndex := range ka.Buckets {8 s/ P; ]) T; ^- B8 r: G( T
  38.                         bucket := a.getBucket(ka.BucketType, bucketIndex)
    ! G" [. m* O) K9 h) a; O1 G
  39.                         bucket[ka.Addr.String()] = ka$ Q# V8 }; L2 M3 P# N
  40.                 }
    ( S- G6 a) }* Q5 `# J- M
  41.                 a.addrLookup[ka.Addr.String()] = ka
    : V8 N& i  \& w/ F' M. ~; W
  42.                 if ka.BucketType == bucketTypeNew {+ ~8 ^- v3 j/ d8 ^* r2 K# W
  43.                         a.nNew++
    4 V2 B, ]; H7 |0 u' D* A5 `! U
  44.                 } else {
    ( ]& u5 A+ r0 b+ o* j9 Q) P
  45.                         a.nOld++2 E0 f; F; f; K: h# ~
  46.                 }
    " ]5 o5 Q# t# w
  47.         }+ u  R+ C* V# d  A+ G
  48.         return true1 m% A0 s8 W5 g8 j
  49. }
复制代码
4 d+ {0 a' T1 a  a  [
定时更新地址簿
- O+ `$ k$ Q' \  d! p+ p* O3 C" R2 ^bytomd会定时更新本地地址簿,默认2分钟一次
0 \. c: @- Z0 e. Y
  1. func (a *AddrBook) saveRoutine() {
    / o* e8 F; r1 S. ?5 K
  2.         dumpAddressTicker := time.NewTicker(dumpAddressInterval)
    3 V; K. g$ E+ j& A: o' X2 I0 I8 S7 m9 J
  3. out:
    % k- u3 P1 }. d# i3 l; o' @9 m
  4.         for {% E" P4 S$ Q1 F% c5 \( [
  5.                 select {/ u6 }9 Q$ P; k3 V8 B
  6.                 case
复制代码

, ^5 C0 r/ k4 F5 s0 I添加新地址
2 r4 E4 |. K! a; o. O. H; d. J当peer之间交换addr时,节点会收到对端节点已知的地址信息,这些信息会被当前节点添加到地址簿中
2 K' `1 U  U7 \- O" \( B( K
  1. func (a *AddrBook) AddAddress(addr *NetAddress, src *NetAddress) {
    7 L% Z3 `( O) p" P* p. k% r
  2.         a.mtx.Lock()3 w' E' `/ W2 m! c9 z: {, j5 _
  3.         defer a.mtx.Unlock()
    & S0 G7 o% h" r- g: ~, X1 e
  4.         log.WithFields(log.Fields{
    8 Z, K0 o) W6 [. a$ F
  5.                 "addr": addr,
    1 k# L# \" r! d% n4 D  l1 n  a
  6.                 "src":  src,3 L6 W( O8 o) J
  7.         }).Debug("Add address to book")
    : B( {# Y! c$ G' Y) ~( a. Z8 k
  8.         a.addAddress(addr, src), K# ?) J- B8 z8 t
  9. }0 Y/ B, Q* F0 h7 B1 Q) g; ^; R
  10. func (a *AddrBook) addAddress(addr, src *NetAddress) {/ D: r% [1 Z8 M1 h/ ^; L  v
  11.         // 验证地址是否为可路由地址. Y8 l. h) V+ R. C( n& N; n: |
  12.         if a.routabilityStrict && !addr.Routable() {3 {) l; w8 ~/ V# ]" V4 s4 w3 M  w
  13.                 log.Error(cmn.Fmt("Cannot add non-routable address %v", addr))
    ( m- R( {4 {# L! j2 L) O3 n
  14.                 return
    7 p; M+ C2 X0 j
  15.         }
    # K$ e- [( Z7 h5 {9 D3 V
  16.         // 验证地址是否为本地节点地址
    3 K$ J, L8 Z0 F3 C0 v0 C" W
  17.         if _, ok := a.ourAddrs[addr.String()]; ok {" |! k2 M) q2 `# G  ^
  18.                 // Ignore our own listener address.- f. Y& F, W% c) B& M- ]
  19.                 return7 F0 A' U$ Q3 O9 `- s
  20.         }
    ; W7 F; P$ J  h3 ?- N* v
  21.         // 验证地址是否存在地址集中
    , S4 \$ ]: T8 A- T% I1 s- @5 E
  22.         // 如果存在:则判断该地址是否为old可靠地址、是否超过了最大buckets中。否则根据该地址已经被ka.Buckets引用的个数来随机决定是否添加到地址集中4 X* J& o# w( h7 M$ }% `
  23.         // 如果不存在:则添加到地址集中。并标识为bucketTypeNew地址类型
    7 z& Q- u! x5 ?4 K' B6 n4 ^
  24.         ka := a.addrLookup[addr.String()]
    % R  q; k: i4 c& T1 z1 V
  25.         if ka != nil {1 t! U% P" t. D7 @; M/ N+ o3 E
  26.                 // Already old.
    : E8 ?3 Z4 }4 f- r5 h( I
  27.                 if ka.isOld() {
    * T* Y- N: r  M/ b+ L& K+ n
  28.                         return! x) Y: C6 R8 r
  29.                 }
    6 g0 s, B8 J" I% a
  30.                 // Already in max new buckets., h/ D* q6 O, z* `/ P
  31.                 if len(ka.Buckets) == maxNewBucketsPerAddress {( B8 T: Z8 \+ }" F
  32.                         return# ]9 p! c/ A8 s3 P
  33.                 }
    9 n5 C  Q; G. e6 d8 B1 ?* i
  34.                 // The more entries we have, the less likely we are to add more., l9 v4 I1 i% @! w- k) ~
  35.                 factor := int32(2 * len(ka.Buckets))
    . J2 P4 i1 d0 @4 K/ `$ E6 @# l+ b
  36.                 if a.rand.Int31n(factor) != 0 {
    - c3 _7 \" f& r3 ?
  37.                         return/ i) {6 g" Q& w7 F4 u. |+ p
  38.                 }
    1 d  b$ z* q/ U7 s1 E$ |4 Z1 v5 @
  39.         } else {
    + e) t9 w! _3 Y' d3 G
  40.                 ka = newKnownAddress(addr, src)
    3 Y1 L0 k7 o' g9 p) x; ~
  41.         }
    ) q1 j1 [" k3 l/ f) H
  42.         // 找到该地址在地址集的索引位置并添加
    " a9 A* |9 b6 i) C. R1 @& T; j7 m
  43.         bucket := a.calcNewBucket(addr, src)
    " \  F+ q0 l# k! G% w) o0 N/ O4 ^, x
  44.         a.addToNewBucket(ka, bucket)% a0 E1 v- i4 h( @7 b) i8 B" t
  45.         log.Info("Added new address ", "address:", addr, " total:", a.size())
    5 H% @) ~) _1 H) \, h
  46. }
复制代码
# N9 i0 @- u) Y
选择最优节点0 e  _% i" p2 r8 g) D3 L. e
地址簿中存储众多地址,在p2p网络中需选择最优的地址去连接+ L' e, s& x4 r! J6 e( o
PickAddress(newBias int)函数中newBias是由pex_reactor产生的地址评分。如何计算地址分数在其他章节中再讲8 T, @0 T: Q. f( [9 {) U
根据地址评分随机选择地址可增加区块链安全性
$ u* D; o: r  ^1 j# ~6 m8 r7 `( ~
  1. // Pick an address to connect to with new/old bias.
    1 f6 a9 H7 x( C/ V
  2. func (a *AddrBook) PickAddress(newBias int) *NetAddress {
    : S+ I9 H. Z% C; a: k+ D' A& f# j- Q
  3.         a.mtx.Lock()
    0 m0 H$ F7 z, N! B
  4.         defer a.mtx.Unlock()
    $ }0 G* Y! M  H0 M$ t& r, D6 ^
  5.         if a.size() == 0 {0 ]5 |8 U5 r' C7 O& f1 Y
  6.                 return nil
    7 G2 P) z0 t7 D5 E3 J. U  t
  7.         }
    & A6 ^! j" W% j$ h0 _
  8.         // newBias地址分数限制在0-100分数之间3 B3 [: d: l5 N2 Q4 R8 o  [
  9.         if newBias > 100 {" W* ^; f  P* z6 Y3 m: ~
  10.                 newBias = 100
    , W3 c0 s1 v9 s. y; p
  11.         }
    3 z5 S. I. H: @2 r
  12.         if newBias
复制代码

- o3 y1 r# g8 ~, q9 V* }移除一个地址) t9 C% g" w9 G& z$ W( z" f
当一个地址被标记为Bad时则从地址集中移除。目前bytomd的代码版本并未调用过
! a. N) d; O/ U! j
  1. func (a *AddrBook) MarkBad(addr *NetAddress) {
    ) j; O3 J" w1 ~
  2.         a.RemoveAddress(addr)' m& F( x. c- |9 s: h: x: G
  3. }' D8 o2 P/ q7 @, V6 x1 ^8 [
  4. // RemoveAddress removes the address from the book.
    $ D  U8 x8 ]1 s' j8 b) Y
  5. func (a *AddrBook) RemoveAddress(addr *NetAddress) {+ A9 g1 T- s4 l9 Z+ l! [$ |
  6.         a.mtx.Lock()' q* P2 E. Q( e/ d8 T9 A/ M/ X
  7.         defer a.mtx.Unlock()
    ! |" }2 r4 X( G3 f: A
  8.         ka := a.addrLookup[addr.String()]
    6 K$ k9 v! B5 k/ U9 u
  9.         if ka == nil {8 i& e! ?0 d! w; c4 n- ~: Y+ W# ^: V
  10.                 return
    ; K, x/ o' r+ L
  11.         }
    , p: @- p# o; n4 y5 ^+ i' x
  12.         log.WithField("addr", addr).Info("Remove address from book")2 _" i! |5 {3 X0 @) s# M: j
  13.         a.removeFromAllBuckets(ka)
    2 `& H5 n3 Y) I
  14. }6 X# c: g# j2 T9 s
  15. func (a *AddrBook) removeFromAllBuckets(ka *knownAddress) {! x3 y% a1 k+ R5 `# T
  16.         for _, bucketIdx := range ka.Buckets {
    " G0 T0 p2 k% P% [% y+ ]
  17.                 bucket := a.getBucket(ka.BucketType, bucketIdx)" z, s! {8 u0 h- [6 b* T
  18.                 delete(bucket, ka.Addr.String())
    ! q  V7 x) T( y6 W. p" A  N
  19.         }
    6 o3 f) D% S6 ]
  20.         ka.Buckets = nil
    2 s4 Q* C% @! J, O, y" b- d' o
  21.         if ka.BucketType == bucketTypeNew {
    " i( W- c2 F/ H& Q) i8 q; `
  22.                 a.nNew--* `. F% }* n# @8 r
  23.         } else {
    6 u9 L& z. `* R6 H% `8 }
  24.                 a.nOld--
    , t  S. Y/ K* b' ~0 h. g/ x
  25.         }3 u1 h% x: v' ~5 \3 ]) t
  26.         delete(a.addrLookup, ka.Addr.String())
    & w+ D1 T( G, n6 ]
  27. }
复制代码

: n9 A3 |: Q* Y3 \
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

excel436 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    7