Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

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

excel436
171 0 0
简介
: d0 g4 U. o$ ]7 S" Bhttps://github.com/Bytom/bytom  a  ^, `: @' W/ J
本章介绍bytom代码P2P网络中addrbook地址簿" H  J3 o: R4 ~6 `  \' P

2 @: q3 y, V9 C1 T! k作者使用MacOS操作系统,其他平台也大同小异
6 |  J( g! V" s7 j8 L4 |5 V

2 E* R1 [1 P+ x# |8 k) S; A7 m8 m" a
Golang Version: 1.8- N9 O$ g- a9 \; V+ o0 h* e7 `

9 D! F4 J2 R; N% {( baddrbook介绍
& X8 Z& X0 M6 @" Naddrbook用于存储P2P网络中保留最近的对端节点地址
) e; I: R  A0 d在MacOS下,默认的地址簿路径存储在~/Library/Bytom/addrbook.json! H; n) n; G, A
地址簿格式4 D/ u! ^: O% _+ z1 ~! K
  1. ** ~/Library/Bytom/addrbook.json **
    3 r5 s2 z! r1 N: P: i" U
  2. {- \1 ?- G, k* }
  3.     "Key": "359be6d08bc0c6e21c84bbb2",
    9 F4 q4 Y, T% }* i$ \* J& W
  4.     "Addrs": [
    0 m$ d9 Z  A4 o7 `
  5.         {7 I# E- X5 ^2 [8 n4 O' g- M
  6.             "Addr": {
    : j1 V0 M* W4 {$ @# I& X/ f# v/ N
  7.                 "IP": "122.224.11.144"," l9 L* d! V+ |8 h5 @# z$ e7 c2 |2 O0 N
  8.                 "Port": 466573 ~  C1 F+ G" {" T1 Q
  9.             },, r) n' R1 \% \# ^6 ?. T1 @
  10.             "Src": {
    0 c. e6 M) P% O+ B% d4 {. h! _
  11.                 "IP": "198.74.61.131",
    6 {) W) s# g0 V
  12.                 "Port": 46657
    . Q$ _$ F7 e- b+ _# ~' x1 L1 a
  13.             },% @  _) K9 |! [2 L
  14.             "Attempts": 0,
    ; M/ @: }3 [3 |% x' x
  15.             "LastAttempt": "2018-05-04T12:58:23.894057702+08:00",3 |8 h" w3 v0 A" C" S
  16.             "LastSuccess": "0001-01-01T00:00:00Z",! V( Z; P5 ]$ {2 l2 ?
  17.             "BucketType": 1,4 _1 Y: ]; z: N# A/ Z; D9 w
  18.             "Buckets": [2 O. ]# V5 U: Q
  19.                 181,: d/ M. M+ {& E8 j
  20.                 10
    4 _0 e* O( z; Z  U4 v
  21.             ]5 `9 p; L3 Y8 ?4 G& `
  22.         }
    % V8 u" R1 q7 y% R# q
  23.     ]- Y: ~# ~0 O1 E* R1 g8 x
  24. }
复制代码

* m* ?6 a( o( q( S! x6 v& B地址类型
, D7 g( I$ G+ S  H. ?. Z在addrbook中存储的地址有两种:
; p! Y- [5 d$ o' N, U  Z
  1. ** p2p/addrbook.go **
      f- U- f5 d+ X- F2 g. J. k
  2. const (* g2 a# O: P# H/ k0 \
  3.         bucketTypeNew = 0x01  // 标识新地址,不可靠地址(未成功连接过)。只存储在一个bucket中/ L* K7 O6 M+ |% Q2 y5 c
  4.         bucketTypeOld = 0x02  // 标识旧地址,可靠地址(已成功连接过)。可以存储在多个bucket中,最多为maxNewBucketsPerAddress个
    * X0 c; E8 r4 Y7 @4 j5 [. k
  5. )
复制代码
. W. ]1 f0 G+ Z/ d9 V

1 T+ b7 _% }; s9 H注意: 一个地址的类型变更不在此文章中做介绍,后期的文章会讨论该问题5 s0 s6 r1 o8 K; |+ f
; P4 L. D4 w) j( k0 f
地址簿相关结构体; L& e' k7 `" V8 j) S3 E
地址簿
8 k# t# Z' i+ Z+ d4 j2 l
  1. type AddrBook struct {5 X7 F; m' \, k% V. l
  2.         cmn.BaseService
    , K' D7 Q1 y+ a! C
  3.         mtx               sync.Mutex
    ' e; Z9 z( b1 X8 t% r2 X: }; A1 r
  4.         filePath          string  // 地址簿路径
    , c. q1 x) y. c. r1 A8 m- \8 [8 M
  5.         routabilityStrict bool  // 是否可路由,默认为true) |# s' V+ ~) ~4 |% ^0 j  \( c4 v! J$ d
  6.         rand              *rand.Rand
    5 g7 a  G; X6 g8 U0 g
  7.         key               string  // 地址簿标识,用于计算addrNew和addrOld的索引- Y4 M8 D5 R9 Q# Z0 s
  8.         ourAddrs          map[string]*NetAddress  // 存储本地网络地址,用于添加p2p地址时做排除使用: g- |6 _* w2 c6 J' I# B
  9.         addrLookup        map[string]*knownAddress // 存储新、旧地址集,用于查询
    # I/ c! v/ N( |4 F. f9 o5 X
  10.         addrNew           []map[string]*knownAddress // 存储新地址
    9 K) N' s' F- g( Q) K* T$ T, q
  11.         addrOld           []map[string]*knownAddress // 存储旧地址7 {/ A; `4 q" X. Y0 i
  12.         wg                sync.WaitGroup
    ! C1 l1 K2 p: T
  13.         nOld              int // 旧地址数量2 }- r. \5 O: w) V; n  Y9 J. L
  14.         nNew              int // 新地址数量
      S* z) y! W& }) T% \
  15. }
复制代码
1 ^& O: T+ ]% ?8 ?9 L8 d/ h
已知地址
: ]4 ]( [  V( T: b* F+ U/ Q( Q
  1. type knownAddress struct {
    $ V7 {; M0 @# n7 Z( h  W
  2.         Addr        *NetAddress // 已知peer的addr
    , C  d4 a" i4 T
  3.         Src         *NetAddress // 已知peer的addr的来源addr
    3 F' n7 l! v$ p. z, X
  4.         Attempts    int32 // 连接peer的重试次数
    * Q5 N& ]5 A+ l0 J3 H& _+ r
  5.         LastAttempt time.Time // 最近一次尝试连接的时间8 _' ~3 [: n$ P' ~
  6.         LastSuccess time.Time // 最近一次尝试成功连接的时间2 O, y2 `9 O* y; M, ^7 I% s- ]
  7.         BucketType  byte // 地址的类型(表示可靠地址或不可靠地址); Z) H- g" X/ k0 U! E
  8.         Buckets     []int // 当前addr所属的buckets+ e1 t! O1 M, W/ d
  9. }
复制代码
+ T) U" _8 ~/ D, {! p
routabilityStrict参数表示地址簿是否存储的ip是否可路由。可路由是根据RFC划分,具体参考资料:RFC标准
+ E0 X1 \. A. g2 p5 c初始化地址簿5 d6 B, ?& G9 P1 V& B0 P8 Y
  1. // NewAddrBook creates a new address book.
    0 R# C: Z% i; [' F" N9 \3 v
  2. // Use Start to begin processing asynchronous address updates.
    9 ~" W4 W; ?! I
  3. func NewAddrBook(filePath string, routabilityStrict bool) *AddrBook {
    ; o7 G7 _& n$ w
  4.         am := &AddrBook{
    2 b: A8 C- j# Q3 h  Q
  5.                 rand:              rand.New(rand.NewSource(time.Now().UnixNano())),  m! J& Q3 e4 U) r% ^+ ~0 B0 x
  6.                 ourAddrs:          make(map[string]*NetAddress),2 b& \7 c- i4 Y  L$ T
  7.                 addrLookup:        make(map[string]*knownAddress),- r9 q5 l- D% e5 R% t, ?
  8.                 filePath:          filePath,# W9 H! G# m; t
  9.                 routabilityStrict: routabilityStrict,1 @9 q  P5 d# l+ P
  10.         }+ A5 O8 j: ^  H2 I9 b  w7 B1 X
  11.         am.init()4 ?8 O, x7 l- O, L. d3 `" R4 z
  12.         am.BaseService = *cmn.NewBaseService(nil, "AddrBook", am)8 ^/ ?5 k. a6 h+ @0 }7 U" ]
  13.         return am3 t7 V; J# l: C% m0 \0 e% k  E' w" A
  14. }6 p/ `1 P; W3 x7 M
  15. // When modifying this, don't forget to update loadFromFile()+ ?7 {: ?  k& B; c: h) l* V! K) b6 d
  16. func (a *AddrBook) init() {9 U2 }* j' X. d, G
  17.   // 地址簿唯一标识
    # f& d# G7 P( J( n' u
  18.         a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits# A+ m  q7 s, i  m  n* c9 p6 y
  19.         // New addr buckets, 默认为256个大小0 P% _) Y7 ~) d" j0 |
  20.         a.addrNew = make([]map[string]*knownAddress, newBucketCount)- C5 Y/ ?) h6 t( F
  21.         for i := range a.addrNew {
    5 W" v% n; h! b4 J0 S/ e5 Y
  22.                 a.addrNew<i> = make(map[string]*knownAddress)6 z% a! S2 g! z1 G% ?0 H' U
  23.         }: {  v* [6 ^) c8 e0 a0 t
  24.         // Old addr buckets,默认为64个大小
    ! \5 ~# @9 r# F
  25.         a.addrOld = make([]map[string]*knownAddress, oldBucketCount)9 f" H) `, |2 y5 w" ?' K. N
  26.         for i := range a.addrOld {6 p. T0 m  f5 W+ G
  27.                 a.addrOld<i> = make(map[string]*knownAddress): e( a. G, T  s& N6 Z
  28.         }
    : f; G: F- @! T+ R% u( b! C
  29. }</i></i>
复制代码
8 U" L, g# N# a
bytomd启动时加载本地地址簿
5 x# @6 ]6 [3 m3 E7 c4 `5 L4 qloadFromFile在bytomd启动时,首先会加载本地的地址簿
# n  n- P5 z& ]- ^* Z7 g
  1. // OnStart implements Service.
    ! m$ \# P2 Y. X8 Y8 f
  2. func (a *AddrBook) OnStart() error {
    ) h. c' e) ~0 L7 d8 G6 Y
  3.         a.BaseService.OnStart()9 Q: {* U; S) b+ Z
  4.         a.loadFromFile(a.filePath)0 {4 t6 n+ Q' i* }8 y% U
  5.         a.wg.Add(1)4 y3 T* U. n/ n7 j- J3 g
  6.         go a.saveRoutine()9 M  e6 v' s+ x3 [+ t6 d
  7.         return nil5 @3 p3 U/ j- k2 G& r0 f
  8. }
    3 ^+ c( A. ?  N; H5 q) Q
  9. // Returns false if file does not exist.
    8 _+ o' x* y" {1 a: w
  10. // cmn.Panics if file is corrupt.3 q# M7 s0 s# s" ~: U/ o  b
  11. func (a *AddrBook) loadFromFile(filePath string) bool {( X3 ?8 `1 D) M$ ?
  12.         // If doesn't exist, do nothing.: N* o2 P3 T5 q5 j! h% g
  13.         // 如果本地地址簿不存在则直接返回  {9 m) r( b( }1 W) W: T
  14.         _, err := os.Stat(filePath)  K  P, b2 j2 Q! V1 i( d: o6 I
  15.         if os.IsNotExist(err) {
    4 b+ b$ R- @3 f! _) o
  16.                 return false
    % N, l" m0 _* Y: V/ q( p  j
  17.         }8 I5 R: s! V9 ~& J' w, B; w
  18.   // 加载地址簿json内容. p( q' ^6 O' f
  19.         // Load addrBookJSON{}1 Z- g! w6 H2 y1 k& N/ F# Y, g
  20.         r, err := os.Open(filePath)
    5 {' F0 f; \! u
  21.         if err != nil {( a: `+ q8 `, P! ~- [- d1 ?- U
  22.                 cmn.PanicCrisis(cmn.Fmt("Error opening file %s: %v", filePath, err)), \; y) j3 K8 l7 b2 q+ z& g  L
  23.         }
    $ n3 T; `( O. c0 K* S% O
  24.         defer r.Close()% _8 c( O. m& m% L. \$ W
  25.         aJSON := &addrBookJSON{}
    : u; j4 c5 M$ I
  26.         dec := json.NewDecoder(r)$ N5 d4 w( w! b" G6 t# [3 k0 u
  27.         err = dec.Decode(aJSON)
    & V; h7 |6 C8 m, Q9 T0 M4 [9 t; x
  28.         if err != nil {
    " @" m  Y; w: ?6 h+ G. o# A: g* y
  29.                 cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", filePath, err))
    ) u( U5 j- H& b8 p+ o  a$ i/ k
  30.         }/ T! X+ L! n, t( F; d6 y. k: r
  31.   // 填充addrNew、addrOld等! x1 E" T  v$ Y) s% p/ M
  32.         // Restore all the fields...
    ( @, `& J: h! X+ E, Q
  33.         // Restore the key5 p/ t: B1 k+ h+ G0 T. H1 X  U
  34.         a.key = aJSON.Key  Z  |5 o# ^# b2 |
  35.         // Restore .addrNew & .addrOld. u- b+ v" k, K
  36.         for _, ka := range aJSON.Addrs {
    8 s# {/ w. z6 |) ~1 p
  37.                 for _, bucketIndex := range ka.Buckets {
    1 Z7 x# S4 B9 B6 H1 ]3 d/ {
  38.                         bucket := a.getBucket(ka.BucketType, bucketIndex)2 l+ P/ f* m& B0 m9 B. Z
  39.                         bucket[ka.Addr.String()] = ka
    ' k4 V. S1 X" l) I: h5 c$ a
  40.                 }
    % _, F4 A1 Y0 A8 }: a( Y
  41.                 a.addrLookup[ka.Addr.String()] = ka* s# O% ?0 n2 K8 M* d8 Z
  42.                 if ka.BucketType == bucketTypeNew {3 {$ W, N' t0 ]+ f+ _- }* N& u% @
  43.                         a.nNew++1 O! R( P4 n: f# k; I
  44.                 } else {
    ; |# W+ k  w0 V( c, h
  45.                         a.nOld++1 K. |$ X4 ?# b- ^2 }
  46.                 }" t6 P& t- z, a  K1 j
  47.         }
    ' N: l: J4 Z5 f3 j. ?% s
  48.         return true
    + ^9 C1 N) A, e  n  A" N
  49. }
复制代码

2 O3 t& Z; Z" ]; E/ u' d9 m% t定时更新地址簿2 v5 F8 A# J; \( a& U5 `# I
bytomd会定时更新本地地址簿,默认2分钟一次
3 L% \: n& H0 ^& c; c& x/ ?5 T) x
  1. func (a *AddrBook) saveRoutine() {: x0 c# h6 `; q1 h1 h
  2.         dumpAddressTicker := time.NewTicker(dumpAddressInterval); y; P! A& j9 {
  3. out:
    ! i: m, ]7 W" E1 l. T6 H
  4.         for {
    6 U. v( h( v: W0 ^' a
  5.                 select {# M# y% A7 l5 l7 ~" F) L9 V. x
  6.                 case
复制代码

( G- h2 C7 \0 u添加新地址% O' n5 s/ }2 D8 x2 R" p- R$ E" K: z8 z
当peer之间交换addr时,节点会收到对端节点已知的地址信息,这些信息会被当前节点添加到地址簿中- \4 j! H: E) b; W$ i
  1. func (a *AddrBook) AddAddress(addr *NetAddress, src *NetAddress) {/ q$ _$ e$ m% Y2 ], \1 e9 l+ ?
  2.         a.mtx.Lock()
    0 Z, g) l) S+ v0 Q1 b7 k
  3.         defer a.mtx.Unlock(), ^+ Y! Q7 }  a! `: z: p8 U1 H
  4.         log.WithFields(log.Fields{
    % I& c0 o# L# R; I2 G7 I
  5.                 "addr": addr,
    0 v2 U4 d% h0 f, I0 [2 z5 }
  6.                 "src":  src,1 ?) N8 ^2 m1 R0 ]
  7.         }).Debug("Add address to book")5 u4 B; t; s  S7 X+ O3 V, }% W
  8.         a.addAddress(addr, src)! f- z$ m- _- m, I* n$ `* ~
  9. }
    & W8 F# \" O# g1 E! \
  10. func (a *AddrBook) addAddress(addr, src *NetAddress) {
    5 D2 n7 H) @, Q# i' I
  11.         // 验证地址是否为可路由地址' Z. L* D+ M/ O7 ~
  12.         if a.routabilityStrict && !addr.Routable() {
    1 a$ N8 g/ `- p
  13.                 log.Error(cmn.Fmt("Cannot add non-routable address %v", addr))
    8 o0 \) D# A8 ]; V1 F) b& l
  14.                 return
    . c. ~* D& p* ]' ]
  15.         }
    ( u: Q2 k( C+ U8 F# Q- W- ]
  16.         // 验证地址是否为本地节点地址
    ' p' D2 V8 B  h$ d
  17.         if _, ok := a.ourAddrs[addr.String()]; ok {
    6 I  ?+ H' Q; p. x6 [! E: e9 W
  18.                 // Ignore our own listener address.
    3 f; d& [: m/ z5 N+ o# f  e
  19.                 return
    8 _( T8 v0 `; F# N5 k
  20.         }% E3 L- g# W1 N# T
  21.         // 验证地址是否存在地址集中' W' ]6 o& G. B* @' q
  22.         // 如果存在:则判断该地址是否为old可靠地址、是否超过了最大buckets中。否则根据该地址已经被ka.Buckets引用的个数来随机决定是否添加到地址集中* O7 `9 [/ u3 y' |
  23.         // 如果不存在:则添加到地址集中。并标识为bucketTypeNew地址类型
    & p2 g1 |& B; d
  24.         ka := a.addrLookup[addr.String()]+ U* L/ d/ e! A5 U! Z+ V4 a1 G* f9 ?
  25.         if ka != nil {% I9 K/ J) N, p; Y6 v
  26.                 // Already old." d3 T' }" e# B! a* ?! C# o! L
  27.                 if ka.isOld() {
    7 j' s3 G7 c- D3 h4 r& m9 |; }+ g
  28.                         return
    $ n- T, s; a( W6 Z/ a+ x
  29.                 }# b8 F0 C0 W- U& |1 ?* W
  30.                 // Already in max new buckets.
    - t* ?# u( |3 Q( k4 h# T$ N: r
  31.                 if len(ka.Buckets) == maxNewBucketsPerAddress {
    ! M& ?! h& s' ~8 z, Y8 Q+ y
  32.                         return( W1 b. d- Z0 i7 v; s2 G; F4 v* H; r* y+ i
  33.                 }( z1 F4 k- p3 Y" q
  34.                 // The more entries we have, the less likely we are to add more.
    3 H! S: m! p1 `/ e) ~( {5 ~
  35.                 factor := int32(2 * len(ka.Buckets))$ T& m8 l1 z/ P  L/ m
  36.                 if a.rand.Int31n(factor) != 0 {
    7 P) D: B7 P' b6 i8 A
  37.                         return
    - g4 `6 y. J6 ]6 g
  38.                 }+ x, }8 |/ e+ u3 [0 x
  39.         } else {
    % T& Y2 a+ t) s
  40.                 ka = newKnownAddress(addr, src). u8 p3 @5 k* m- n8 ~
  41.         }
    4 ~& |- ^* ~. P5 G8 d$ n& r5 F
  42.         // 找到该地址在地址集的索引位置并添加1 t# H/ h+ G9 [& Y. o' {6 k% Q
  43.         bucket := a.calcNewBucket(addr, src)
    # \* X- N3 r$ f9 _( d3 o
  44.         a.addToNewBucket(ka, bucket)/ N, @$ s/ ?3 q
  45.         log.Info("Added new address ", "address:", addr, " total:", a.size()). ~8 L3 H3 O" K. h
  46. }
复制代码

# h! Q+ t! j+ s' m选择最优节点6 |+ e8 @& S$ ~: d1 C
地址簿中存储众多地址,在p2p网络中需选择最优的地址去连接
, f3 d4 ^, w3 L! a' IPickAddress(newBias int)函数中newBias是由pex_reactor产生的地址评分。如何计算地址分数在其他章节中再讲/ O. c; Y% Y4 `& w. N
根据地址评分随机选择地址可增加区块链安全性
# y  e+ c# q5 N4 ]& N
  1. // Pick an address to connect to with new/old bias.( p1 X. P- {4 h; T, R% B  ?4 a7 Y: U
  2. func (a *AddrBook) PickAddress(newBias int) *NetAddress {
    ( Y" k# v# B3 Z, c5 x
  3.         a.mtx.Lock()4 ]( R' O3 ~5 o6 M
  4.         defer a.mtx.Unlock()
    " C$ m7 C* z+ M: @: l. `
  5.         if a.size() == 0 {
    % u) H6 Z4 h3 V% u
  6.                 return nil! e5 [9 B* I+ \
  7.         }
    5 L5 }; i, U" E& ]5 V+ J7 ?" F8 P
  8.         // newBias地址分数限制在0-100分数之间
    4 g4 \* y- z1 M( G% [5 E
  9.         if newBias > 100 {
    * u& y9 n3 r" l- C" w  d. o4 g: S0 u
  10.                 newBias = 100- u" Q/ d( f! T# o3 k! R* @! V
  11.         }$ Z* N3 k& h3 @! h
  12.         if newBias
复制代码
# `* G- u* F4 f$ o7 A
移除一个地址- R3 z; T7 J! C0 d
当一个地址被标记为Bad时则从地址集中移除。目前bytomd的代码版本并未调用过
* V' R7 l5 q4 x, j. J6 f/ p
  1. func (a *AddrBook) MarkBad(addr *NetAddress) {3 a- W$ {1 N$ D, C1 @7 _# o
  2.         a.RemoveAddress(addr)
    , o& D4 Y  I2 S7 l; w& J
  3. }
    ( I3 B5 e, |" T3 U- k8 J
  4. // RemoveAddress removes the address from the book.
      ?6 B8 |; E1 }0 J' M& I- ^. a% v
  5. func (a *AddrBook) RemoveAddress(addr *NetAddress) {
    : j' V+ A2 p. B2 X7 I: \$ ]1 H! {
  6.         a.mtx.Lock()
    % t8 d) @( I% N* h# I
  7.         defer a.mtx.Unlock()2 P  ]2 `2 t8 B* L4 l) i
  8.         ka := a.addrLookup[addr.String()]/ v2 x! X2 `) Q
  9.         if ka == nil {+ x3 `  ?% N6 q  C1 W0 T9 m
  10.                 return% F; |9 x$ Z/ x1 {
  11.         }9 X6 ]6 W, d+ i0 ~( ?
  12.         log.WithField("addr", addr).Info("Remove address from book")
    : Q2 j6 D. `& Q% Y
  13.         a.removeFromAllBuckets(ka)
    2 W4 `/ C; n0 t0 J5 A, ^/ j% L9 S
  14. }
    / |8 e' G( q& p1 h& L
  15. func (a *AddrBook) removeFromAllBuckets(ka *knownAddress) {4 ?) E$ L# }) _$ N5 M# |+ Y
  16.         for _, bucketIdx := range ka.Buckets {
    4 Y: S, K) A- {7 W" n9 Z4 E
  17.                 bucket := a.getBucket(ka.BucketType, bucketIdx)" `. F1 r2 C+ E, r. l3 n3 e# a7 b
  18.                 delete(bucket, ka.Addr.String())" L$ F& a9 b  P7 c) }0 ~
  19.         }1 y1 P1 H( U$ L7 l, d. t
  20.         ka.Buckets = nil
    5 f- H# p" x$ `
  21.         if ka.BucketType == bucketTypeNew {
    3 ^8 z1 R7 }' |9 T
  22.                 a.nNew--# J2 y# E4 P4 r( Y& ]: ?! [! P
  23.         } else {& e/ u/ h3 O- O/ I3 C# q
  24.                 a.nOld--. j6 l9 B+ L6 X! z6 Q, s0 r. X
  25.         }8 `3 j. {; n* T
  26.         delete(a.addrLookup, ka.Addr.String())
    . V% w' o2 I4 ~6 C$ P! N* J7 P
  27. }
复制代码
. J. u& G, b+ Z6 N
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

excel436 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    7