Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

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

excel436
137 0 0
简介) }6 q2 Q. f7 G4 I
https://github.com/Bytom/bytom8 N2 |9 y% k1 D! N( [/ v6 r" q: h
本章介绍bytom代码P2P网络中addrbook地址簿  K' _, R( s' A7 J# l, I% t4 G" f

1 X1 l# X( G- p& }& P3 |& w$ f+ i作者使用MacOS操作系统,其他平台也大同小异
$ K0 N  W; M( Z8 j! K
; B5 e) V0 D- A

# G! P7 v( Q6 F0 E- F5 S" HGolang Version: 1.8' _8 Z' K0 ^" w* Q

7 w& n) w/ T( n; V7 P5 y' S# Naddrbook介绍! K, y+ o# o7 U# T% i" n* Q
addrbook用于存储P2P网络中保留最近的对端节点地址- {; X1 c& t  g9 [, H
在MacOS下,默认的地址簿路径存储在~/Library/Bytom/addrbook.json- ^* y; C$ Y: b( J8 n
地址簿格式
6 i: q$ v- G( H1 R* o" B
  1. ** ~/Library/Bytom/addrbook.json **
    & ~. \) _: q3 c$ P2 m0 F1 r* _4 B
  2. {
    3 v# t- M8 M* O+ ?
  3.     "Key": "359be6d08bc0c6e21c84bbb2",
    " P$ \1 o+ b- T0 ~
  4.     "Addrs": [
    2 b& s: G+ O, m" P, R4 E6 A
  5.         {
    4 m5 i  ^& V! b, j# t+ Z6 I* V
  6.             "Addr": {% J1 b+ V1 ~& V$ R  G
  7.                 "IP": "122.224.11.144",
    ' D) B& G3 X! O
  8.                 "Port": 46657
    9 V9 I$ B5 h4 B- d9 D
  9.             },8 U* W8 p5 d9 ~' j8 N. b
  10.             "Src": {
    1 t2 T* Y$ E# i
  11.                 "IP": "198.74.61.131",: N' J1 q8 t% x7 g0 h# B
  12.                 "Port": 46657
      M+ B, A+ x+ |3 q4 F
  13.             },
    1 c# u1 B/ D* A; B" l4 M& W& B
  14.             "Attempts": 0,
    , j" W( T$ M9 R* }9 B+ {5 E
  15.             "LastAttempt": "2018-05-04T12:58:23.894057702+08:00",5 i+ I" O6 Z8 L2 C& H
  16.             "LastSuccess": "0001-01-01T00:00:00Z",
    8 K, b+ _) w3 [
  17.             "BucketType": 1,
    % ^* J/ u( P8 Y% u+ \6 K) T* J
  18.             "Buckets": [
    3 N: m3 ~& f* N5 }; T; h( ^
  19.                 181,
    0 `) x' }( y' j8 V
  20.                 10
    : u9 z: T( T3 b) ?: E
  21.             ]! n1 W8 ]6 Y3 g0 P
  22.         }  z8 t5 N$ V  w& A, n
  23.     ]7 O( [. b  T" A
  24. }
复制代码
+ k5 B" L4 ^- g' @
地址类型
* Y# p7 d4 b1 u在addrbook中存储的地址有两种:5 g6 m1 i2 t! Y$ E
  1. ** p2p/addrbook.go **6 [7 e. I* \' ]. ?5 ~' [
  2. const (" t* \- |. Q) T' c1 r* z+ v
  3.         bucketTypeNew = 0x01  // 标识新地址,不可靠地址(未成功连接过)。只存储在一个bucket中
    % {* O1 c! Z6 J+ A2 |* v+ O
  4.         bucketTypeOld = 0x02  // 标识旧地址,可靠地址(已成功连接过)。可以存储在多个bucket中,最多为maxNewBucketsPerAddress个6 z2 f& O7 x" B9 t4 D/ K9 Y1 t
  5. )
复制代码

; A# f' c. |* C* n4 r) |$ ^# N$ K3 l8 U: t8 d! v2 Z5 I
注意: 一个地址的类型变更不在此文章中做介绍,后期的文章会讨论该问题$ X& T( W6 a% `  S/ t& H

+ Y- n" O3 O+ W. S  M( a地址簿相关结构体! ?! A7 {; |& P8 ^; d
地址簿7 c. ]2 h  w; F2 }5 w, R
  1. type AddrBook struct {
    0 _" p2 n" }; w) b
  2.         cmn.BaseService4 x' W, u$ ^) V, ?" j% ~, V
  3.         mtx               sync.Mutex
    ' u6 T4 V7 M# P$ j" r
  4.         filePath          string  // 地址簿路径; X, M1 s: |; M4 |
  5.         routabilityStrict bool  // 是否可路由,默认为true/ |8 z& P0 z% |4 A
  6.         rand              *rand.Rand
    8 K% V/ Y/ T: }3 i
  7.         key               string  // 地址簿标识,用于计算addrNew和addrOld的索引
    - Z! y% C( x. O& F
  8.         ourAddrs          map[string]*NetAddress  // 存储本地网络地址,用于添加p2p地址时做排除使用
    " B0 {+ ?( r" K& p% _
  9.         addrLookup        map[string]*knownAddress // 存储新、旧地址集,用于查询) {& C7 A; t) e/ T& }; a
  10.         addrNew           []map[string]*knownAddress // 存储新地址
    - ]$ V) C+ R. A' G1 ~- ^/ i
  11.         addrOld           []map[string]*knownAddress // 存储旧地址
    ' {9 r! t( _5 i& T% e
  12.         wg                sync.WaitGroup9 @3 r) }1 V* C# J! i6 o; K3 i$ M
  13.         nOld              int // 旧地址数量
    ' [# C1 h7 _5 |% ^1 }
  14.         nNew              int // 新地址数量7 f2 p$ R) T4 U1 z5 W
  15. }
复制代码
7 ?! }3 c1 A, \% L) v
已知地址/ f( x. M- \1 B# T
  1. type knownAddress struct {' j6 R* I) [0 y* Y; i1 x5 t
  2.         Addr        *NetAddress // 已知peer的addr. |* a# n- V+ ?; Y: `, x
  3.         Src         *NetAddress // 已知peer的addr的来源addr) Y( e+ }% r. A  o7 v
  4.         Attempts    int32 // 连接peer的重试次数
    , D- l; P+ {% f* x$ F) M: o
  5.         LastAttempt time.Time // 最近一次尝试连接的时间- t; v& s# Z* n9 R
  6.         LastSuccess time.Time // 最近一次尝试成功连接的时间
    ! [/ X2 [! W2 z+ h
  7.         BucketType  byte // 地址的类型(表示可靠地址或不可靠地址)
    9 F$ {. |- U7 T% o3 n
  8.         Buckets     []int // 当前addr所属的buckets( i  Y9 E! `& W" b5 n  t# f; _
  9. }
复制代码
. e& _. C. L+ `% S
routabilityStrict参数表示地址簿是否存储的ip是否可路由。可路由是根据RFC划分,具体参考资料:RFC标准
: E2 s3 g; a/ ~9 c7 C2 j初始化地址簿
- u  l) j7 C0 {
  1. // NewAddrBook creates a new address book.
    # ]; r, _$ ?6 ?7 ?" ^5 W
  2. // Use Start to begin processing asynchronous address updates.
    5 b. W3 C9 g& D% u! a  w$ p0 @7 @
  3. func NewAddrBook(filePath string, routabilityStrict bool) *AddrBook {
    + A1 R- ~' V- j0 ?) H. O9 }
  4.         am := &AddrBook{% [7 E  \3 I$ M! K9 r& _
  5.                 rand:              rand.New(rand.NewSource(time.Now().UnixNano())),/ w6 t7 O6 a( @) U" s6 R  @
  6.                 ourAddrs:          make(map[string]*NetAddress),
    + x, V( k/ |7 b" w, o- E/ z( g% B- G' R
  7.                 addrLookup:        make(map[string]*knownAddress),- f5 p: y% c4 a0 Z+ l4 O
  8.                 filePath:          filePath,) @" l; {/ S7 a3 Z0 X
  9.                 routabilityStrict: routabilityStrict,
    # C2 V0 v5 L, h% U0 G9 K7 }6 _, t
  10.         }
    9 d: T9 `2 V6 n
  11.         am.init()) i9 e7 R- y5 n8 a
  12.         am.BaseService = *cmn.NewBaseService(nil, "AddrBook", am)
    2 }+ Y/ k2 L+ E' i/ T; `8 |
  13.         return am; S+ C! b  f8 W/ v
  14. }
    , m* m( [* t- _- a* K3 v5 Q
  15. // When modifying this, don't forget to update loadFromFile()
    2 b8 n) E9 X( [6 p4 N8 A' e
  16. func (a *AddrBook) init() {8 A7 k. u' k+ C9 s8 e( c* ~! g
  17.   // 地址簿唯一标识0 n8 X! }1 b) m
  18.         a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits' k7 m/ a! s/ j7 {, ^: q8 @
  19.         // New addr buckets, 默认为256个大小
    4 ?- a) X) E5 Q) f! S8 G$ P7 E
  20.         a.addrNew = make([]map[string]*knownAddress, newBucketCount)+ f- S# ~' c9 T. A7 f5 O  `
  21.         for i := range a.addrNew {
    6 x; z3 |6 p+ H  d2 B* a
  22.                 a.addrNew<i> = make(map[string]*knownAddress)& Y, {- `. I* i& J6 V7 z
  23.         }& J/ X, s& d% {
  24.         // Old addr buckets,默认为64个大小
    * G( v( J/ R7 E7 U( o; _& N4 K( e
  25.         a.addrOld = make([]map[string]*knownAddress, oldBucketCount)
    1 X  L/ [# X5 t( w: P0 l
  26.         for i := range a.addrOld {: u& X. `6 {9 o5 x
  27.                 a.addrOld<i> = make(map[string]*knownAddress)6 }- w1 d, W; d. |- X! x7 M) S0 V
  28.         }2 X& x$ h( Y0 V- |) E' C7 y8 \
  29. }</i></i>
复制代码

' D0 e. b( y! @8 Q+ tbytomd启动时加载本地地址簿
' d+ E9 q$ C& AloadFromFile在bytomd启动时,首先会加载本地的地址簿
% p- d( C6 r/ }' G
  1. // OnStart implements Service.2 B6 u5 a! e/ L* c% k1 E% E  j" |
  2. func (a *AddrBook) OnStart() error {
    % `& K6 V6 X  I  G; `: s
  3.         a.BaseService.OnStart()/ r* D, v: Q( k* G
  4.         a.loadFromFile(a.filePath)
    1 ~* ?% P9 C: d
  5.         a.wg.Add(1)5 l8 i6 D- }. X3 ~5 ?
  6.         go a.saveRoutine()# b/ A8 A# `8 p+ h9 i9 s
  7.         return nil
    ' k  L* ~5 L8 y6 f4 _; u
  8. }# ~/ d2 c! U; M) Z* Z
  9. // Returns false if file does not exist.
      `5 V; c; L4 }% W
  10. // cmn.Panics if file is corrupt.
    $ w, ^9 y5 [0 W3 i* |9 @/ i- `$ |7 ]
  11. func (a *AddrBook) loadFromFile(filePath string) bool {! f6 `( q# t$ v$ @- `
  12.         // If doesn't exist, do nothing.
    0 z$ [- Z4 h& O/ o
  13.         // 如果本地地址簿不存在则直接返回
    ) s. ~' `0 ^$ q5 L! |( U3 j
  14.         _, err := os.Stat(filePath)
    2 I* V/ Z: A$ }; `, x6 d1 k$ h( s
  15.         if os.IsNotExist(err) {
    & d' Y. S2 T3 E( [1 v% d
  16.                 return false4 p# b! ?8 g! B- ]. J
  17.         }' b! D7 K! r' ^( Q, E  t$ Q+ E
  18.   // 加载地址簿json内容+ b; r$ W! W  k1 I1 e8 [, d
  19.         // Load addrBookJSON{}
    ! V' s0 t$ h3 E, S7 e$ Y
  20.         r, err := os.Open(filePath)
    # R0 @8 L; \" c2 @
  21.         if err != nil {4 `7 c$ S% x1 O, B4 M/ }/ T# \5 D8 V
  22.                 cmn.PanicCrisis(cmn.Fmt("Error opening file %s: %v", filePath, err))9 Q& P" {( L+ y/ q
  23.         }
    2 n0 O* `4 B: i1 y! r$ e
  24.         defer r.Close(). e' u) x) }$ Y
  25.         aJSON := &addrBookJSON{}
    6 v0 j3 x2 M$ T$ Z1 L0 O0 x! x! |) O
  26.         dec := json.NewDecoder(r)  W0 \1 T; b$ k. C6 k
  27.         err = dec.Decode(aJSON)
    % ?# V( x6 J* O6 @; `
  28.         if err != nil {
    6 u, O! f' ~, \# T" |
  29.                 cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", filePath, err))  e5 V0 D! o. {8 ~3 [4 O' `2 @1 J' F$ O
  30.         }1 f* m6 ]. q7 h1 o! Q3 |( g
  31.   // 填充addrNew、addrOld等
    & E. `( Z! T* }) L  c
  32.         // Restore all the fields...6 G" @) ]  S5 Z( @5 ~1 o6 e" G5 `' E
  33.         // Restore the key
    * @- U0 a: e$ {: S1 w% I  a
  34.         a.key = aJSON.Key
    7 h& h0 a. L" W0 D4 W, X# d% f
  35.         // Restore .addrNew & .addrOld5 [% C# s. D: w* _1 t# f! p
  36.         for _, ka := range aJSON.Addrs {
    . v8 w3 z3 Q) g/ V2 t1 G
  37.                 for _, bucketIndex := range ka.Buckets {
    . D5 ?; a3 e) D* O
  38.                         bucket := a.getBucket(ka.BucketType, bucketIndex)# d" j+ i  v" S+ X0 W: Z
  39.                         bucket[ka.Addr.String()] = ka
    / `3 m& W1 v. \& C  ^
  40.                 }
    - y7 X9 f2 J% D/ W& j5 l
  41.                 a.addrLookup[ka.Addr.String()] = ka
    7 z) Y) M: j4 s) x9 J( \3 A
  42.                 if ka.BucketType == bucketTypeNew {0 l9 @8 c1 d2 K( ?% L' \
  43.                         a.nNew++
    ! m0 \* d0 i' Y1 n, d; l$ d
  44.                 } else {- }4 J- u9 \- X1 M# D
  45.                         a.nOld++7 z+ W8 y4 z* F1 b
  46.                 }, u; N) ?1 l# w2 s! s1 \
  47.         }5 q, }3 e+ ^8 I
  48.         return true% n; B" h6 o) q$ ^1 M$ ~3 x1 }7 i6 c
  49. }
复制代码
& T, K% y8 p7 c% H9 }
定时更新地址簿8 P7 I! y. v! S3 P. F8 `
bytomd会定时更新本地地址簿,默认2分钟一次0 g" A5 H, |2 Y1 C
  1. func (a *AddrBook) saveRoutine() {
    - M4 a1 n; k0 q/ ~
  2.         dumpAddressTicker := time.NewTicker(dumpAddressInterval)
    + c" J: R1 i7 R# E
  3. out:
    * q* {+ n' w+ o6 g9 W/ }' n" q* s
  4.         for {
    & z: H' l, w; R! X; v3 e: G
  5.                 select {
    : g/ W& x3 d7 s3 z3 P  m. L) ?
  6.                 case
复制代码

" C5 f- q. Z- W+ E$ ?# ?添加新地址1 g0 F+ B7 i& j( K
当peer之间交换addr时,节点会收到对端节点已知的地址信息,这些信息会被当前节点添加到地址簿中
6 S8 F3 `; |' U! y, R0 L
  1. func (a *AddrBook) AddAddress(addr *NetAddress, src *NetAddress) {
    ( J9 @( h# K+ T+ ~3 q9 x3 |. Y
  2.         a.mtx.Lock()
    ) W" c2 P. o+ s$ j' F7 p
  3.         defer a.mtx.Unlock()+ x6 @+ t1 n& |3 I9 O2 b- A+ w
  4.         log.WithFields(log.Fields{
    ) |, ^# y  z, ?# K' _
  5.                 "addr": addr,& ]2 J/ u4 H2 ^$ M- r$ Z% f7 J
  6.                 "src":  src,1 V" z% U6 }% p+ z2 D& n1 B0 `- O6 |
  7.         }).Debug("Add address to book")! t8 w. k. F! U' Y
  8.         a.addAddress(addr, src)
    $ j2 H3 I* T& ~2 K) x& u
  9. }
    ' }6 c6 F) |2 ]1 \& ]5 v
  10. func (a *AddrBook) addAddress(addr, src *NetAddress) {
    9 ?: a. T9 ~5 F3 V4 G: y: S
  11.         // 验证地址是否为可路由地址
    ; G9 u" R" ?1 z$ b3 Q7 H2 t/ ^
  12.         if a.routabilityStrict && !addr.Routable() {) I% n* d; e- q% h' Y
  13.                 log.Error(cmn.Fmt("Cannot add non-routable address %v", addr))
    ! {* W( X& e8 ~
  14.                 return+ u9 \& E3 [  x( l6 d
  15.         }
    ' q+ n2 g% H; w/ D0 y3 v
  16.         // 验证地址是否为本地节点地址. S1 F! W" Z' s1 D; [$ s. q3 q% r, ^6 X
  17.         if _, ok := a.ourAddrs[addr.String()]; ok {
    . M5 y& f  a1 I* p% l% }
  18.                 // Ignore our own listener address.
    9 \0 e1 K' ~$ w& [# c
  19.                 return0 p0 ]3 X9 N+ n, M
  20.         }
    + j% q% q2 w: p7 r0 |. e
  21.         // 验证地址是否存在地址集中
    % l. O: g' b9 g0 U" _+ f& j# n& m
  22.         // 如果存在:则判断该地址是否为old可靠地址、是否超过了最大buckets中。否则根据该地址已经被ka.Buckets引用的个数来随机决定是否添加到地址集中
    3 E' l* z$ g2 ]5 D7 K0 C2 o7 f
  23.         // 如果不存在:则添加到地址集中。并标识为bucketTypeNew地址类型
    & E9 G" U4 j( N1 ~, S4 P
  24.         ka := a.addrLookup[addr.String()]1 T8 z  P% c, ~
  25.         if ka != nil {4 r  d! V/ p; I0 l
  26.                 // Already old.' z0 d1 o$ E% b! e& k4 j
  27.                 if ka.isOld() {
    : R! y6 D; B5 T( H
  28.                         return9 L2 [( w6 }7 v  ?* ^( ?% S9 R
  29.                 }
    7 j$ H/ U  r! z2 E" g- l. J( Q
  30.                 // Already in max new buckets.
    # J& D9 d% p  l7 a! ]& e
  31.                 if len(ka.Buckets) == maxNewBucketsPerAddress {% w# `* q+ k5 Q$ Z
  32.                         return8 ?6 w! P+ I3 Z" |# o# F+ A' G
  33.                 }/ J, z" S  y, w$ ]$ U# h2 a" U: u
  34.                 // The more entries we have, the less likely we are to add more.: a1 k  \/ [% t- n
  35.                 factor := int32(2 * len(ka.Buckets))4 r) H  ~( f& |6 i3 k2 i. R) I& o/ f+ R0 x
  36.                 if a.rand.Int31n(factor) != 0 {
    ; G* B7 Z: ]. W4 `/ j' U- K
  37.                         return' O% f, M0 @$ l  b# _4 S
  38.                 }
      l! m; {% {5 ^' z3 [/ _7 d
  39.         } else {
    : A' G6 }" z1 ]9 `7 u) H
  40.                 ka = newKnownAddress(addr, src)
    9 Q) W0 ?2 W- k6 c2 p3 ]3 _9 |
  41.         }
    8 ~1 |3 Z8 s, `+ s2 p4 n
  42.         // 找到该地址在地址集的索引位置并添加
    ; n& @' h# g: o% W0 e9 L& h0 U
  43.         bucket := a.calcNewBucket(addr, src)
    * Z8 u2 g$ }, g2 {
  44.         a.addToNewBucket(ka, bucket)# j# s* o1 M6 D5 g2 U
  45.         log.Info("Added new address ", "address:", addr, " total:", a.size())
    # t+ n% f8 ~6 [% G8 E- ?( }
  46. }
复制代码

1 j- H  x! c' W. V: P选择最优节点
; G2 i# l% _; c) a" h1 q3 Y地址簿中存储众多地址,在p2p网络中需选择最优的地址去连接! i7 j" f& l% C. j
PickAddress(newBias int)函数中newBias是由pex_reactor产生的地址评分。如何计算地址分数在其他章节中再讲. e: r7 Y: `1 _) e9 m7 w
根据地址评分随机选择地址可增加区块链安全性
# f7 o: ~) O% U4 ?- P1 n( F& d
  1. // Pick an address to connect to with new/old bias.; n# R  n5 A) K6 |7 w0 k
  2. func (a *AddrBook) PickAddress(newBias int) *NetAddress {
    ) f* |3 W& a4 _* B
  3.         a.mtx.Lock()
    5 v+ S, k4 E3 w- ]% I/ e
  4.         defer a.mtx.Unlock()$ d* X9 J- v! s$ `' A! V; F
  5.         if a.size() == 0 {; F. e: [+ w# @7 I
  6.                 return nil
    6 o! r* z# x: C. u0 {. }9 G" P
  7.         }* \3 Y# }3 f1 M; q" @* s
  8.         // newBias地址分数限制在0-100分数之间
    + t# Y- i: O  M( x: v/ q1 M% i6 K+ D
  9.         if newBias > 100 {4 `( u/ Q2 B2 k5 E- J
  10.                 newBias = 100+ g1 o+ @: I* S4 S0 W
  11.         }
    7 v# x5 S* R7 x: O* C8 T4 F
  12.         if newBias
复制代码

! w4 t$ o7 y4 z- o移除一个地址
5 R. W  Q$ e3 v当一个地址被标记为Bad时则从地址集中移除。目前bytomd的代码版本并未调用过$ y. Z- s2 P; c! a. B
  1. func (a *AddrBook) MarkBad(addr *NetAddress) {
    " ~* X; {1 K# b* y2 Q: b
  2.         a.RemoveAddress(addr)
    2 I  z& q9 ^: v( U! `
  3. }
      T4 m4 ?4 N; B4 {* F0 Y6 q$ H
  4. // RemoveAddress removes the address from the book.
    5 H: f3 b1 J3 e& J) `/ t
  5. func (a *AddrBook) RemoveAddress(addr *NetAddress) {! R+ C8 n2 z0 c- J- a! n; `( u6 i% @
  6.         a.mtx.Lock(): ~4 N7 e5 O4 i% L& x$ U
  7.         defer a.mtx.Unlock()
    , e" z; Y; x- g- ]$ ~* T1 @' [
  8.         ka := a.addrLookup[addr.String()]! u, d' h" d! T- C  D( o# B, i
  9.         if ka == nil {
    * O4 t! p- ~1 i* b
  10.                 return* i: A; @0 U; r% [, d8 O/ y# e
  11.         }
    - f& [  i# s! U! U
  12.         log.WithField("addr", addr).Info("Remove address from book")$ U; H% {0 Z+ J* W" h7 o9 y6 k. R9 m9 x
  13.         a.removeFromAllBuckets(ka)+ }7 V3 a( p, p" `
  14. }# E2 }# ], A: U$ E, M
  15. func (a *AddrBook) removeFromAllBuckets(ka *knownAddress) {
    9 p! w" _3 y( V  Z' [3 B
  16.         for _, bucketIdx := range ka.Buckets {
    . d  A* j2 J$ C1 y* D, p+ s
  17.                 bucket := a.getBucket(ka.BucketType, bucketIdx)5 H/ Z' m: [3 [. A
  18.                 delete(bucket, ka.Addr.String())
    2 T# X! \- ]' ~- H! J3 Y8 h8 _+ _1 b
  19.         }) \, M% L/ i# l- y7 g7 F
  20.         ka.Buckets = nil( @$ q3 p) I' g4 w
  21.         if ka.BucketType == bucketTypeNew {  ^& f( _5 Z3 i1 }
  22.                 a.nNew--0 @! u  w9 v, n' B% `. w
  23.         } else {' o" w' B( S: G- F. M
  24.                 a.nOld--
    1 O+ B" _9 p7 i' I: _6 J
  25.         }6 A( \6 @, O% l# F/ Y- ^6 j) e3 U
  26.         delete(a.addrLookup, ka.Addr.String())
    0 t" h' h! w% U* `# r
  27. }
复制代码
+ D8 v: g8 {1 i. G( t
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

excel436 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    7