Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

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

excel436
142 0 0
简介
; _8 q' M4 [: D7 z3 L) a) Qhttps://github.com/Bytom/bytom' g9 m' n4 A8 ^9 d) N4 L
本章介绍bytom代码P2P网络中addrbook地址簿4 B  @  ?+ R, A) h' \' k: I# V" F3 e
+ {; b) ?& G- C" X: f/ x; g
作者使用MacOS操作系统,其他平台也大同小异
; M2 e' K+ t$ h8 v8 G8 F5 V
0 f: a6 I5 G! A+ K; t# i

  a- V& w6 [" D; h6 J' P3 mGolang Version: 1.8: `% E4 c2 J: C& x
0 @8 M+ z( P0 P4 `% F& L- f% j/ ~
addrbook介绍% I* S1 c/ u/ [- }  ], g
addrbook用于存储P2P网络中保留最近的对端节点地址
8 V2 w% v. u4 G/ s1 O在MacOS下,默认的地址簿路径存储在~/Library/Bytom/addrbook.json
' ^" {5 M  a1 W8 E4 X" S9 ]5 k地址簿格式
$ ]) u) `, ]9 }# M7 X8 H! X9 h
  1. ** ~/Library/Bytom/addrbook.json **" X/ T; X; N. Z- {  L5 ]! [' h
  2. {1 Z9 _$ ^' z( ~. g# i
  3.     "Key": "359be6d08bc0c6e21c84bbb2",: P$ e. t! P! n+ N; q6 d# E  n
  4.     "Addrs": [' [7 g4 m7 m; C6 E! ~% t  D( M
  5.         {
    ' U1 |, L( W" E
  6.             "Addr": {# D' e& d) p3 I! G/ x
  7.                 "IP": "122.224.11.144",
    ) k! |  z3 e9 x! C6 M- @, s
  8.                 "Port": 466573 H2 B9 B8 [# ~3 A/ p
  9.             },
    , d) c  `' ?! k0 m0 G
  10.             "Src": {8 w+ o1 |/ C0 a1 Z; y
  11.                 "IP": "198.74.61.131",
    ; Q9 _, [5 X' C0 q% ?4 U
  12.                 "Port": 466576 C- j6 M9 v0 h" O; N: c* O; w: x
  13.             },
    3 T* J2 u) k. H# i) c" [
  14.             "Attempts": 0,
    0 P* j$ t, g  d. `
  15.             "LastAttempt": "2018-05-04T12:58:23.894057702+08:00",
    4 o/ v9 W1 C4 f* @" K
  16.             "LastSuccess": "0001-01-01T00:00:00Z",% Y! V5 Z8 l% D$ f5 T2 L
  17.             "BucketType": 1,
    , R" ?7 \* R' P8 j$ ^* G; c# ^" c
  18.             "Buckets": [
    $ O1 V7 J9 _+ C3 M2 z
  19.                 181,: K5 y& t) z# J9 m, z9 U
  20.                 106 N' F7 L: n  {' d3 b
  21.             ]
    6 k8 t7 r) n5 {  p1 G4 \' r! L
  22.         }
    % @, v9 X) d$ B
  23.     ]
    3 V, S9 }# O- ]( V( _# a7 z' I6 v
  24. }
复制代码
0 E- X( B  ?9 [8 a! `& ^' U
地址类型  d& S" k3 G. S) \$ O" j: `
在addrbook中存储的地址有两种:
; c4 u6 P3 D6 J- t" }
  1. ** p2p/addrbook.go **
    0 S# ~6 Z- q2 ]$ z5 {* X) l
  2. const (6 d3 Z% N, ^) h: V% \, B; p; i
  3.         bucketTypeNew = 0x01  // 标识新地址,不可靠地址(未成功连接过)。只存储在一个bucket中
    $ X1 v( ^! j" a, X( p
  4.         bucketTypeOld = 0x02  // 标识旧地址,可靠地址(已成功连接过)。可以存储在多个bucket中,最多为maxNewBucketsPerAddress个
    % R1 B( x& `4 L4 ^: l
  5. )
复制代码
4 N& t3 \: H* D) k
1 ~% `8 \1 K1 Q9 Y; d  o5 j: P
注意: 一个地址的类型变更不在此文章中做介绍,后期的文章会讨论该问题
; o2 M0 C+ C! o. ?( D! n8 j/ c
# P1 m) ]) }5 B. L2 N地址簿相关结构体
" O4 q# E" Q: T: W地址簿$ I$ i4 n. K) c" h1 S- L
  1. type AddrBook struct {+ [# ?% ^, S% L% e
  2.         cmn.BaseService
    / C  f1 U) O# b; [; a+ [& R4 ^) Y
  3.         mtx               sync.Mutex
    6 D; r! k+ y  m+ E: W
  4.         filePath          string  // 地址簿路径
    " u, l5 [& U- E- N4 }, Z% n
  5.         routabilityStrict bool  // 是否可路由,默认为true
    1 C3 W. D5 |4 _
  6.         rand              *rand.Rand % G& _" p7 z' m# Q1 V/ @/ j
  7.         key               string  // 地址簿标识,用于计算addrNew和addrOld的索引
    : ?  H$ b# B. X' b. Y
  8.         ourAddrs          map[string]*NetAddress  // 存储本地网络地址,用于添加p2p地址时做排除使用  {+ l1 H3 j4 _* C. e" f2 o& h
  9.         addrLookup        map[string]*knownAddress // 存储新、旧地址集,用于查询
    3 H' g- L( m3 |) A7 I
  10.         addrNew           []map[string]*knownAddress // 存储新地址# t: H% ]: a0 ~, V+ m
  11.         addrOld           []map[string]*knownAddress // 存储旧地址; {- I3 v' n' X
  12.         wg                sync.WaitGroup
    ! h* t9 M; W9 M( N; P9 A8 d" U
  13.         nOld              int // 旧地址数量
    8 j) b+ U7 B2 m( f( U% j
  14.         nNew              int // 新地址数量$ s  ]( S% L( f% z+ y, q8 s, z# p
  15. }
复制代码
" j; q$ j. W4 C- O
已知地址
6 A- K0 ~& v! l; I6 u3 a( {
  1. type knownAddress struct {
    7 C( ?4 ]+ x! }' K4 @5 _9 ^) p
  2.         Addr        *NetAddress // 已知peer的addr3 g. q3 @6 k1 O1 V1 a
  3.         Src         *NetAddress // 已知peer的addr的来源addr
    - G3 D+ b3 m6 c& v  s
  4.         Attempts    int32 // 连接peer的重试次数
    $ T. k/ o0 F( O$ ^$ y% u& M
  5.         LastAttempt time.Time // 最近一次尝试连接的时间
    $ Y3 w/ ^4 n' E' ~1 L" `6 P
  6.         LastSuccess time.Time // 最近一次尝试成功连接的时间9 d' k1 f+ F* F; w, @, J
  7.         BucketType  byte // 地址的类型(表示可靠地址或不可靠地址)) b$ p: }% P6 X& X5 P
  8.         Buckets     []int // 当前addr所属的buckets- r/ Y; E/ w. g
  9. }
复制代码

5 k: J; G. s% ^( y5 lroutabilityStrict参数表示地址簿是否存储的ip是否可路由。可路由是根据RFC划分,具体参考资料:RFC标准
& `9 Q) y. Y7 f: c初始化地址簿
( R/ N9 e2 n8 }  i$ B8 A
  1. // NewAddrBook creates a new address book.
    5 _' c6 c5 p2 f6 P
  2. // Use Start to begin processing asynchronous address updates.
    6 P  A0 j9 R( B" g: }
  3. func NewAddrBook(filePath string, routabilityStrict bool) *AddrBook {5 s6 R- D) ?9 G( ?3 |/ P
  4.         am := &AddrBook{
    $ L: f; {+ |2 ]. V/ ^
  5.                 rand:              rand.New(rand.NewSource(time.Now().UnixNano())),' T5 v% f$ d" W' Q7 u, H; |4 E. H
  6.                 ourAddrs:          make(map[string]*NetAddress),6 C0 l! K# r! l3 r) h4 b3 C% t
  7.                 addrLookup:        make(map[string]*knownAddress),
    7 \7 C# X5 J& z# u" q# J
  8.                 filePath:          filePath,' q0 ~7 s* x! e) B# ^
  9.                 routabilityStrict: routabilityStrict," x- S5 Y, g0 @4 {7 x! b& T0 A
  10.         }
    . c8 d5 D, }" i& B% S# D
  11.         am.init()
    1 m7 @0 G# Y( k. R
  12.         am.BaseService = *cmn.NewBaseService(nil, "AddrBook", am)
    . u# r( g1 A) n. J
  13.         return am
    # ?$ }) j: ?0 U, ^* l
  14. }
    % _8 D9 D" y) r$ f: `1 A
  15. // When modifying this, don't forget to update loadFromFile()& t$ H3 ~/ O5 D! \  T  b7 Y
  16. func (a *AddrBook) init() {
    , d2 l: w- f6 f. w
  17.   // 地址簿唯一标识
    ) [2 T+ u& f' J# t4 O' M  H
  18.         a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits4 B8 f' B0 A3 B* G' T/ X& ]
  19.         // New addr buckets, 默认为256个大小
    & r, i/ i1 R% B: \
  20.         a.addrNew = make([]map[string]*knownAddress, newBucketCount)9 P! y, n$ u* p2 P
  21.         for i := range a.addrNew {
    9 z3 g, s6 m7 {* v- l* a7 p+ K
  22.                 a.addrNew<i> = make(map[string]*knownAddress)! p# w4 Y0 L( I3 ^: g
  23.         }* ~+ s2 Y9 u: l: f  z- y) J- e
  24.         // Old addr buckets,默认为64个大小
    & u7 S( U9 X1 I. `& G; n4 d5 m6 S, j
  25.         a.addrOld = make([]map[string]*knownAddress, oldBucketCount)  u: r6 g1 C8 U" ]2 [! }
  26.         for i := range a.addrOld {
    ' c) i3 E* U' K9 y7 @  o
  27.                 a.addrOld<i> = make(map[string]*knownAddress)
    3 s1 a& B) e& v0 k7 _
  28.         }; l( k! _  V! E( C; T5 F. r8 s7 g' R
  29. }</i></i>
复制代码

  z8 o6 w2 q9 y9 d1 n. ~6 ?bytomd启动时加载本地地址簿
7 v) J% n5 }/ D( m' i! L$ z' @loadFromFile在bytomd启动时,首先会加载本地的地址簿
) L; E9 u" E% a$ O( W
  1. // OnStart implements Service.- Q2 p- B7 ^8 V2 G; u+ e
  2. func (a *AddrBook) OnStart() error {
    . R8 X+ [2 {3 w# u* I6 W
  3.         a.BaseService.OnStart()
    ( l+ e4 p; A* h4 @/ s" X' A
  4.         a.loadFromFile(a.filePath)
    6 \  \* E3 m2 N
  5.         a.wg.Add(1)
    / D- r) p! x* Z- V( q0 S
  6.         go a.saveRoutine()! O. z9 h% H1 E* D
  7.         return nil
    5 ^4 M) l3 {4 {" `! M0 X
  8. }7 r* z' D  S& s! t  j! p
  9. // Returns false if file does not exist.
    ) Z6 F& E6 Q' C( s7 E; S9 Q' z0 D( V+ E
  10. // cmn.Panics if file is corrupt.: o" O- E! r+ a& x1 n0 t
  11. func (a *AddrBook) loadFromFile(filePath string) bool {
    2 u- {7 h4 W" S8 y. g; x# E/ q
  12.         // If doesn't exist, do nothing.
    ! t4 d& q' E' h, A" Y
  13.         // 如果本地地址簿不存在则直接返回
    7 O1 {8 T: Q& m
  14.         _, err := os.Stat(filePath)
    5 U+ o; h+ s+ F9 }, S6 K0 s
  15.         if os.IsNotExist(err) {7 h) c( k. K7 s: k! Z. A
  16.                 return false
    5 C; X. J  o' p' \
  17.         }
    9 K# Z' S3 M+ E/ E# ]2 K
  18.   // 加载地址簿json内容7 e! ~" n& I7 w; ^1 S. r
  19.         // Load addrBookJSON{}* W: W* {/ S! G$ o$ ]
  20.         r, err := os.Open(filePath)
    4 D" c( O, y0 m9 a
  21.         if err != nil {' o7 s' [( h) I! r6 ~- e( W1 Y/ w+ N
  22.                 cmn.PanicCrisis(cmn.Fmt("Error opening file %s: %v", filePath, err))' s+ y. f& h, j  d0 H( ^" E( _
  23.         }
      f2 N# O8 l- [0 t5 R& S
  24.         defer r.Close()4 q$ G! E$ l. Z5 }* a+ t+ a
  25.         aJSON := &addrBookJSON{}3 \5 v( I# v# p) J7 K. ^# h+ `
  26.         dec := json.NewDecoder(r)4 k+ K0 G) D/ F1 j4 l
  27.         err = dec.Decode(aJSON)
    % h/ ]8 s; u# M6 a& a* R
  28.         if err != nil {9 n( A( E* `- r9 @
  29.                 cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", filePath, err))
    2 o* k, f1 a( h8 n' i( M
  30.         }% F# |$ Y" L7 H
  31.   // 填充addrNew、addrOld等) ^2 ?( K- T3 T0 U0 [
  32.         // Restore all the fields...
    # n/ P3 B  \9 T
  33.         // Restore the key7 e) X5 i8 X3 v: V) ]
  34.         a.key = aJSON.Key2 x' d- m( ]! S2 t. a" Q6 `; P
  35.         // Restore .addrNew & .addrOld
    - l2 U4 _; K, W$ S3 N
  36.         for _, ka := range aJSON.Addrs {
    4 Q+ t6 Y" y% t1 C6 {
  37.                 for _, bucketIndex := range ka.Buckets {$ F1 j- w5 Q- a: @
  38.                         bucket := a.getBucket(ka.BucketType, bucketIndex); f3 k# z! R# b. g( I) `4 J
  39.                         bucket[ka.Addr.String()] = ka
    ; b/ Z+ [3 D6 c
  40.                 }
    + G' a; p- R5 K, \- Z6 j
  41.                 a.addrLookup[ka.Addr.String()] = ka5 A7 i, E2 }# n- w7 u
  42.                 if ka.BucketType == bucketTypeNew {
    $ A' ^* U' x* n
  43.                         a.nNew++9 ^( X! {( s% v9 W2 \; |9 v3 J
  44.                 } else {. E9 z2 I# m& F0 P$ P5 z
  45.                         a.nOld++8 V* X! ?! Q. G
  46.                 }* p4 `! K: v+ q6 q$ _- ?
  47.         }
    7 n6 b" \; p& I/ O7 K
  48.         return true# T* X  b. ?% n& b
  49. }
复制代码

% V1 `- R( q# h; y定时更新地址簿+ _6 H5 L/ D9 B' |4 S( }0 h
bytomd会定时更新本地地址簿,默认2分钟一次
# N, N" q: ^0 K7 r- g/ ^
  1. func (a *AddrBook) saveRoutine() {
    + \; v; H" z. V9 S
  2.         dumpAddressTicker := time.NewTicker(dumpAddressInterval)
    5 \, q) \  J% [5 F
  3. out:, v* G8 F. w/ Y8 ?) u$ E
  4.         for {
    9 I7 f2 y& x9 N2 r
  5.                 select {$ f# V6 C/ g  m1 y1 z" o
  6.                 case
复制代码
+ V( {, x0 z) E  o# R0 h
添加新地址3 c* K4 O& ^/ Z$ {# E  U' l6 \
当peer之间交换addr时,节点会收到对端节点已知的地址信息,这些信息会被当前节点添加到地址簿中0 ]: }. O. m1 }. R% H' O
  1. func (a *AddrBook) AddAddress(addr *NetAddress, src *NetAddress) {: |4 x/ A4 I! W# c3 T6 C
  2.         a.mtx.Lock()5 L5 h: J  K5 G6 \% m9 `
  3.         defer a.mtx.Unlock()# [) x0 ?) U  l  X. e
  4.         log.WithFields(log.Fields{
    2 X2 A" _* @0 a; E0 Z
  5.                 "addr": addr,
    2 X2 X' S! Q5 e* ]8 U
  6.                 "src":  src,
    # L+ |" [$ {# `8 C; t% T5 n
  7.         }).Debug("Add address to book")
    * G! Z, N9 I/ w% G  B
  8.         a.addAddress(addr, src)
    ; W1 u! l& b  N% z- C
  9. }& w% l$ Z- V- f1 T4 h
  10. func (a *AddrBook) addAddress(addr, src *NetAddress) {# H% V) N' l7 w. U6 Y' \
  11.         // 验证地址是否为可路由地址
    . z- U0 X- p9 P, q. k
  12.         if a.routabilityStrict && !addr.Routable() {
    / u& N- Z+ Y% D8 R& N" f
  13.                 log.Error(cmn.Fmt("Cannot add non-routable address %v", addr))
    ) g) @1 D. v) }  I( k) ?9 z
  14.                 return2 S. ]' `  y) ^$ z, @: t
  15.         }
      y2 j0 z8 i1 ^3 _6 i
  16.         // 验证地址是否为本地节点地址
    " D" o5 K+ s; V
  17.         if _, ok := a.ourAddrs[addr.String()]; ok {
    4 n% @1 ]8 E+ J! D! |
  18.                 // Ignore our own listener address.
    - O. Z) o, q* f0 D6 Y# g
  19.                 return( N+ m/ P8 P. r. U. [1 {1 |; x
  20.         }- Q/ S& e* h: t/ @3 |
  21.         // 验证地址是否存在地址集中
      y; d/ C/ W( N- U! {
  22.         // 如果存在:则判断该地址是否为old可靠地址、是否超过了最大buckets中。否则根据该地址已经被ka.Buckets引用的个数来随机决定是否添加到地址集中% S7 W. a7 z1 m/ c2 `
  23.         // 如果不存在:则添加到地址集中。并标识为bucketTypeNew地址类型: j0 f6 V  E$ z0 N# f: n% U! ^( O
  24.         ka := a.addrLookup[addr.String()]2 R8 y$ @5 T, d$ @, ?) k
  25.         if ka != nil {- {; ~+ U9 R0 @
  26.                 // Already old.
      D! Y5 Y5 ^1 V5 X( f3 V3 ?
  27.                 if ka.isOld() {
    ; a/ n5 j6 B) }; s
  28.                         return' r# t7 w: V8 Z' k
  29.                 }" f3 ?: W6 x, u3 z1 ?: n
  30.                 // Already in max new buckets.
    3 c( h3 ?( |7 r: J
  31.                 if len(ka.Buckets) == maxNewBucketsPerAddress {
    0 h* v8 b* e" ?* h3 `
  32.                         return
    2 h% h+ F! s" d, @
  33.                 }% T+ {6 ?& n% _; d2 B( _
  34.                 // The more entries we have, the less likely we are to add more.4 ?' |  ]+ L% a1 S( E2 O
  35.                 factor := int32(2 * len(ka.Buckets))
    % S& Z; v+ p" K
  36.                 if a.rand.Int31n(factor) != 0 {- c3 W( {- @, ~$ R
  37.                         return2 M( T* i7 a4 T4 c' b, a. P
  38.                 }# S( H+ t1 E7 G7 r/ }; K. U
  39.         } else {
    7 e7 e1 n  l' R) Z, q5 N
  40.                 ka = newKnownAddress(addr, src). \3 `  ~2 G9 F2 ^. H
  41.         }' [: O  W; [% X, `$ F6 ?: M5 c! F
  42.         // 找到该地址在地址集的索引位置并添加
    * i! A, m6 h9 w" H
  43.         bucket := a.calcNewBucket(addr, src)
    2 D- C9 J$ B8 ^& s
  44.         a.addToNewBucket(ka, bucket)
    : Z' V7 ^1 r+ O) O$ s- }- a% P" J
  45.         log.Info("Added new address ", "address:", addr, " total:", a.size())
    . `- x3 r7 T7 I
  46. }
复制代码
' z/ y& e( s" z
选择最优节点8 }7 j' x- r, ^  k! V/ I) \
地址簿中存储众多地址,在p2p网络中需选择最优的地址去连接
. ]# _4 \; h6 y) [" t- T  Q/ _PickAddress(newBias int)函数中newBias是由pex_reactor产生的地址评分。如何计算地址分数在其他章节中再讲
, s& ?+ O/ u3 R" E7 X根据地址评分随机选择地址可增加区块链安全性
7 }8 |) Z. A$ }8 f
  1. // Pick an address to connect to with new/old bias.
    3 r/ R. S  K- V/ }5 a
  2. func (a *AddrBook) PickAddress(newBias int) *NetAddress {6 }3 K9 b6 R: A# p, F8 B$ c- \0 M
  3.         a.mtx.Lock()
      A; z, h- h, v# [# n: C4 [
  4.         defer a.mtx.Unlock()( W: o, p+ v6 J0 X
  5.         if a.size() == 0 {
      u3 S5 e2 p2 i( b! `
  6.                 return nil
    ; E( Y  q, U2 M: P2 Q2 q3 n5 p8 h
  7.         }
    : p) u1 N. \$ [) R& T( E- N/ F0 @
  8.         // newBias地址分数限制在0-100分数之间7 R( J! Q# j% h2 @  M* ]  i+ K3 ^6 L1 r
  9.         if newBias > 100 {) i* c  y0 t5 H' M7 i
  10.                 newBias = 100
    " ^1 M0 X% ?6 z# S2 z" R. C
  11.         }. W0 p! N0 J# f1 ^/ @7 }
  12.         if newBias
复制代码
2 p8 B2 `. g2 _+ `! S* v0 e
移除一个地址
9 {1 j3 Y0 P3 E# `当一个地址被标记为Bad时则从地址集中移除。目前bytomd的代码版本并未调用过3 t" o% I' k4 v, v1 A. e
  1. func (a *AddrBook) MarkBad(addr *NetAddress) {1 p4 _- U1 t# K5 a+ M' C$ G
  2.         a.RemoveAddress(addr)
    2 G, y4 ~5 {( X: G
  3. }
    - h$ f9 O2 _, {! o
  4. // RemoveAddress removes the address from the book.
    7 F' F! k# ~8 ?- z8 M9 T+ x
  5. func (a *AddrBook) RemoveAddress(addr *NetAddress) {' z( L5 X, u4 f5 q  e' t' P- r4 @
  6.         a.mtx.Lock()
    - f) q8 a, _1 y. D& @$ E5 |, u" J* o
  7.         defer a.mtx.Unlock()
    5 D* ^4 x& F+ j1 j& X0 v
  8.         ka := a.addrLookup[addr.String()]
    ) F- ^+ R/ p& G3 g5 X5 `/ H# X
  9.         if ka == nil {
    3 J7 w  t% `; I! k" M% {) {8 k
  10.                 return
    , X4 _5 P- w) z: |/ F" }, |7 v
  11.         }- E9 G7 v5 Z1 v0 @+ J, E
  12.         log.WithField("addr", addr).Info("Remove address from book")( |4 @$ y+ e( r1 Q, M% E0 k4 t
  13.         a.removeFromAllBuckets(ka)& [3 o- W* a/ j  X9 k
  14. }  P8 U# K* N5 j9 P) N5 }! s
  15. func (a *AddrBook) removeFromAllBuckets(ka *knownAddress) {
    + k) `+ ?) b5 v0 A% v4 }" n; G
  16.         for _, bucketIdx := range ka.Buckets {4 l9 C6 l* V( E, K- Y# I: M
  17.                 bucket := a.getBucket(ka.BucketType, bucketIdx)1 w- g1 k) k; J9 T
  18.                 delete(bucket, ka.Addr.String())6 e. E- k1 N8 X1 |
  19.         }
    % e, H7 r4 D4 E# H0 `( Z' t" A+ v, a
  20.         ka.Buckets = nil; ]& S& ?& [3 [# C* N
  21.         if ka.BucketType == bucketTypeNew {' ~$ Z% l& n; J: z* u4 f1 K
  22.                 a.nNew--" D: q- D( e) |2 T" ^8 @% n
  23.         } else {
    ' n; n" [3 P; p9 Q# b0 Q! ^1 Y- t
  24.                 a.nOld--" ~1 x# ~' M+ H1 M6 k
  25.         }" T3 i# H8 _% b; Z, _+ t6 f; o
  26.         delete(a.addrLookup, ka.Addr.String())6 M; A5 {" f; R0 Z
  27. }
复制代码
8 U" v) S7 A2 t' {) ~
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

excel436 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    7