Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

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

excel436
178 0 0
简介( O  h0 \0 d- @" r  Z+ L
https://github.com/Bytom/bytom
7 `/ b0 w0 {: n' j# v" n  P/ m本章介绍bytom代码P2P网络中addrbook地址簿
% {0 k* u! D3 v( u. b5 k) G$ p* H8 p
/ c" j" X# H# F7 e) X0 b作者使用MacOS操作系统,其他平台也大同小异  @) ?: E! m6 T3 N0 n
- ]+ P5 a6 m, F+ M' I

% w5 D2 `1 e8 e$ S' u6 XGolang Version: 1.8* t. ]% Y3 h4 j8 q

$ p" S2 [5 [: Aaddrbook介绍
7 F1 o2 i  L# l$ h% o1 n, Iaddrbook用于存储P2P网络中保留最近的对端节点地址
- ^' U* {0 P- q  c7 o在MacOS下,默认的地址簿路径存储在~/Library/Bytom/addrbook.json: }( n% [5 f0 G5 P3 k2 c
地址簿格式" P0 E" V. f: K1 B7 S
  1. ** ~/Library/Bytom/addrbook.json **
    7 l6 w2 ~9 @, N& O& A* ~* M" U8 g
  2. {' R  m+ S* X! Z4 A0 g
  3.     "Key": "359be6d08bc0c6e21c84bbb2",1 A, k  p* Y% D7 Y: q6 J
  4.     "Addrs": [
    - m3 e7 X; L0 Q) W7 Q6 Q
  5.         {
    # I. a4 b; s1 \) k
  6.             "Addr": {  R! T7 |: f9 A; P
  7.                 "IP": "122.224.11.144",9 E" {9 i2 M' o! O( U; g) ?
  8.                 "Port": 46657$ i" C( s% B, h7 D8 i9 x% F
  9.             },
    2 a$ u7 ]! J: ^, y
  10.             "Src": {
    4 y* H8 a* N& r2 b
  11.                 "IP": "198.74.61.131",
    $ ~( Q( n& W4 s2 m
  12.                 "Port": 466577 W' L" N* u/ A9 l5 s
  13.             },
    % }' _: l( P* W: D
  14.             "Attempts": 0,
    # [$ m# @* ]& P+ f  ~; c/ @4 L
  15.             "LastAttempt": "2018-05-04T12:58:23.894057702+08:00",! y" W3 }) @2 x( y7 X: X
  16.             "LastSuccess": "0001-01-01T00:00:00Z",
    / Y* J& [% p$ V) C8 R  ]% M3 \
  17.             "BucketType": 1,
    ) W1 f( q9 Y+ w1 F2 n
  18.             "Buckets": [! y; n1 w2 a; Q) o# x
  19.                 181,
    $ n* K% \6 |8 y7 P4 {- V. \5 J
  20.                 10
    6 ]! n" g9 g8 ]2 \2 ?' Y0 ?
  21.             ]
    7 H2 c: k8 ?' L/ Y+ d5 g
  22.         }9 C2 ]$ M) x6 L3 b
  23.     ]7 L% J( q' I, ]" |
  24. }
复制代码

& K3 |' z! r( S2 }6 M5 i2 Z! d地址类型0 ]; o& \9 @# T5 E8 F
在addrbook中存储的地址有两种:
6 p! m& K% K+ ?* I! j7 ]  M
  1. ** p2p/addrbook.go **; N/ B! f. p7 m0 J/ E' k3 F  f
  2. const (
    + W! H" F) h) D( S/ r, t
  3.         bucketTypeNew = 0x01  // 标识新地址,不可靠地址(未成功连接过)。只存储在一个bucket中
    1 O3 k( M0 _2 T8 A8 ]! Q+ ^9 x) k) V
  4.         bucketTypeOld = 0x02  // 标识旧地址,可靠地址(已成功连接过)。可以存储在多个bucket中,最多为maxNewBucketsPerAddress个- A' O8 ?" k8 q7 h) `, b
  5. )
复制代码
& S* M0 J! ?% K" l2 {8 ?# y
% H& `) n* e$ B0 o  E
注意: 一个地址的类型变更不在此文章中做介绍,后期的文章会讨论该问题: ]5 o' x! ], o) Y. n1 U; t

, C" B0 E1 [6 X+ a; Z; L- C- z地址簿相关结构体3 R! p, b  `1 U6 M0 D% W
地址簿
9 m& i5 v* P# R" c) p! W
  1. type AddrBook struct {; e5 H, d7 g$ P- A
  2.         cmn.BaseService! C6 g7 m$ J; p, `7 m$ W; Q
  3.         mtx               sync.Mutex
    . ^8 x$ h: h/ f' X) Y
  4.         filePath          string  // 地址簿路径1 }' I. o5 {9 B* E3 Y6 P
  5.         routabilityStrict bool  // 是否可路由,默认为true' W) C& o0 Q  z8 f, I9 a) x
  6.         rand              *rand.Rand
    . Y; e& n* b& n4 Q- J2 v, ~% c; ?% J
  7.         key               string  // 地址簿标识,用于计算addrNew和addrOld的索引
    1 f2 E9 C& g2 n* _5 `% E1 j- l4 B$ G
  8.         ourAddrs          map[string]*NetAddress  // 存储本地网络地址,用于添加p2p地址时做排除使用
    : j/ i% k6 N$ m: G6 s: e8 A
  9.         addrLookup        map[string]*knownAddress // 存储新、旧地址集,用于查询
    0 Y& H7 X# h0 w  Y; T" s% s
  10.         addrNew           []map[string]*knownAddress // 存储新地址
    6 l! X- Z3 e! \. C6 U  |
  11.         addrOld           []map[string]*knownAddress // 存储旧地址! Q$ o" T& H! B# j- p
  12.         wg                sync.WaitGroup
    ; P: ^$ T4 i  V1 b: b4 u, d6 g
  13.         nOld              int // 旧地址数量
    4 o& R- u# F5 q3 j$ ~* ?4 q
  14.         nNew              int // 新地址数量2 t% X. ^3 d( C
  15. }
复制代码

7 ~( D) c! V/ @  r4 V已知地址( O  v, D7 X  X8 w$ h
  1. type knownAddress struct {7 t: p3 o- x# {1 t4 S* K
  2.         Addr        *NetAddress // 已知peer的addr$ I3 ^4 [% @  r% T: K7 ~- l# `' n6 ^" Q
  3.         Src         *NetAddress // 已知peer的addr的来源addr
    ) Z& f. D2 [6 I/ Q
  4.         Attempts    int32 // 连接peer的重试次数6 y' Y* K. |7 D. i
  5.         LastAttempt time.Time // 最近一次尝试连接的时间% P/ [  J; P- ]1 M
  6.         LastSuccess time.Time // 最近一次尝试成功连接的时间
    ; A9 M# @5 g( n8 W7 V  Z. A
  7.         BucketType  byte // 地址的类型(表示可靠地址或不可靠地址)
    ( {3 i8 f! c! @  v  X( k
  8.         Buckets     []int // 当前addr所属的buckets
    5 y' a6 s# c9 W+ R6 f; @# x
  9. }
复制代码
- \; D: B; v: S' G" d) c
routabilityStrict参数表示地址簿是否存储的ip是否可路由。可路由是根据RFC划分,具体参考资料:RFC标准  l% a2 Y1 F0 C3 V4 ^2 N8 j
初始化地址簿
3 N' F+ X4 `& h* p8 q
  1. // NewAddrBook creates a new address book.# o; Q) q, n/ P0 M" f' G, z
  2. // Use Start to begin processing asynchronous address updates.0 n6 _% A' z% x/ }6 q$ S! K
  3. func NewAddrBook(filePath string, routabilityStrict bool) *AddrBook {
    : w! R* r7 c- G. L- k( M1 t9 e' h
  4.         am := &AddrBook{4 `& u- t* q- ^2 S% [
  5.                 rand:              rand.New(rand.NewSource(time.Now().UnixNano())),
    : m1 @7 i8 v4 j* G& ^8 p7 [
  6.                 ourAddrs:          make(map[string]*NetAddress),  S8 G. B5 F- c; k2 q
  7.                 addrLookup:        make(map[string]*knownAddress),
    8 f. l! e  V# C# h
  8.                 filePath:          filePath,4 v  [8 y3 r- e1 [' X; g  c
  9.                 routabilityStrict: routabilityStrict,, i; ?5 Q8 |/ K- J8 R9 E
  10.         }* `' N1 [8 T0 p
  11.         am.init()+ @$ {1 E# U$ X
  12.         am.BaseService = *cmn.NewBaseService(nil, "AddrBook", am)+ l( x4 B2 H. r$ o* e  }5 L0 a" E
  13.         return am) H0 ]" e$ C9 c2 i: g
  14. }
    5 p0 u7 X5 ?: d/ k
  15. // When modifying this, don't forget to update loadFromFile()
    * X! \+ n4 d: Z- Y: f3 X( _
  16. func (a *AddrBook) init() {/ S5 Z' q# D7 ^- U
  17.   // 地址簿唯一标识
    1 Q8 W8 F/ r7 ?- n
  18.         a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits
    5 }" j+ a. E8 t  q9 o6 W3 k2 K
  19.         // New addr buckets, 默认为256个大小
    ( E# S/ ]. p0 \/ |+ c
  20.         a.addrNew = make([]map[string]*knownAddress, newBucketCount)
    7 m6 \' p$ T5 ^/ h5 [2 n- I
  21.         for i := range a.addrNew {6 z2 O& G+ Z  e' @$ _
  22.                 a.addrNew<i> = make(map[string]*knownAddress)
    6 s. Q) G4 e# f6 W% n+ P
  23.         }
    , A0 C# ]/ k: w+ |  E# W* T- V
  24.         // Old addr buckets,默认为64个大小+ p4 D) x  Z4 ^" U7 ?! @
  25.         a.addrOld = make([]map[string]*knownAddress, oldBucketCount)
    ; E' Y6 @1 i; Y4 W% ~
  26.         for i := range a.addrOld {. j8 r9 |- B, f2 b7 B4 s) M
  27.                 a.addrOld<i> = make(map[string]*knownAddress)# S3 Y9 R# ?* ]8 _' D
  28.         }( d8 Q. ~) g* B) f5 L* m5 j
  29. }</i></i>
复制代码

, Q7 C/ z, ^0 U0 C: \3 o9 gbytomd启动时加载本地地址簿& q) F; U  f7 S, p1 i
loadFromFile在bytomd启动时,首先会加载本地的地址簿
8 M. [" y2 R, }1 T9 F& n7 W
  1. // OnStart implements Service.
    1 {# W( h7 @) R3 s) V
  2. func (a *AddrBook) OnStart() error {3 e& n. U3 o% p" }
  3.         a.BaseService.OnStart()- `  z# {2 M6 h+ u2 C8 R0 C
  4.         a.loadFromFile(a.filePath)" ]& G  I: U, H9 v" V, l& H
  5.         a.wg.Add(1); f- x, E8 ?4 |, _/ ?
  6.         go a.saveRoutine()# E- H: @$ V$ _, [& i( ]
  7.         return nil6 u, J1 ], p1 p0 J- C; e3 Q
  8. }
    5 c1 k+ n* O6 ?) Z4 v: n0 ^
  9. // Returns false if file does not exist." u& ^" n( Y! ~
  10. // cmn.Panics if file is corrupt.5 d# T$ P: y7 K) R# U/ `* l
  11. func (a *AddrBook) loadFromFile(filePath string) bool {
    3 f7 P; v( e, r
  12.         // If doesn't exist, do nothing.8 Q* t, w; A6 S8 E! ?. n" {  w, B1 }$ r
  13.         // 如果本地地址簿不存在则直接返回
    ) t0 }& K( [, }4 J4 q
  14.         _, err := os.Stat(filePath)( X1 |7 d/ l% Z0 L% @
  15.         if os.IsNotExist(err) {, q( D  k! j0 h5 h8 C& c, ]
  16.                 return false# x4 F. S) |3 B, s( j5 p0 g
  17.         }' ]  O5 m. n8 B- H
  18.   // 加载地址簿json内容
    6 a1 ?4 a* ?8 `2 v9 T. M0 P
  19.         // Load addrBookJSON{}
    # W: i# u9 @5 @4 a& i
  20.         r, err := os.Open(filePath)# V% k& u) r/ g# u& F$ a& ]) e5 y
  21.         if err != nil {
    7 Q4 j: Q- b0 M8 r# ]
  22.                 cmn.PanicCrisis(cmn.Fmt("Error opening file %s: %v", filePath, err))
    0 F+ O, D' k/ K: m
  23.         }# B4 ?% F: ~: U! n& \) V# r6 N3 ^
  24.         defer r.Close()3 [: s$ X% F& p; x) d& U6 P5 D
  25.         aJSON := &addrBookJSON{}
    & b" L' ]1 ^9 g* R: C
  26.         dec := json.NewDecoder(r)4 q( \; S6 ~& A
  27.         err = dec.Decode(aJSON)5 ?0 y" Y- Y# A/ M- t# E4 q
  28.         if err != nil {
    . j, o* ]% _8 z: D
  29.                 cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", filePath, err))
    , E" N8 q) s" O# C7 K) R1 S# S
  30.         }
    $ O/ R$ G" r8 d
  31.   // 填充addrNew、addrOld等/ I3 m$ M( W: n3 t# @! b" f3 x' p
  32.         // Restore all the fields...4 A+ i& h$ {0 c' K. t
  33.         // Restore the key
    & X+ Z5 o1 s' ~
  34.         a.key = aJSON.Key
    ( R( o0 n( T9 p2 y  I
  35.         // Restore .addrNew & .addrOld% d/ W0 r* l% s* z1 S3 q
  36.         for _, ka := range aJSON.Addrs {
    3 G: A1 d0 l# `6 U4 Z7 g
  37.                 for _, bucketIndex := range ka.Buckets {- Y) T' u4 U1 p% _
  38.                         bucket := a.getBucket(ka.BucketType, bucketIndex)9 {- `( b; N# K. k4 b
  39.                         bucket[ka.Addr.String()] = ka
    4 _. X- H- p( z( j/ {$ r: y
  40.                 }
    # k; I1 T/ X$ j
  41.                 a.addrLookup[ka.Addr.String()] = ka
    # Z7 h. {# x$ h
  42.                 if ka.BucketType == bucketTypeNew {  N- z# Z7 x7 r6 [! I+ }. a. l9 e. r/ N
  43.                         a.nNew++3 [: R  U# _( L' f; j% i
  44.                 } else {
    ' R7 ]( T4 `. a) V' W* V
  45.                         a.nOld++  h' A9 ~/ [5 \! C* U& U
  46.                 }
    & v% _6 F* Y: j% _4 {3 f
  47.         }* V# e& ]; Q4 X% s
  48.         return true% }3 X8 q" r' G: A) l# W. ]% _
  49. }
复制代码

( k# C/ H! D4 B; r9 R* Z; O定时更新地址簿- v9 X: V6 s  |. g
bytomd会定时更新本地地址簿,默认2分钟一次
) I+ A7 T8 ]  w$ o
  1. func (a *AddrBook) saveRoutine() {' X  P2 G% P9 V2 @) F5 F
  2.         dumpAddressTicker := time.NewTicker(dumpAddressInterval)1 U5 c3 X  S$ e! s/ p# B1 j
  3. out:( m; X5 h  [8 C- a, ~% A
  4.         for {! u% K& `7 ^# S+ k
  5.                 select {$ C: y% \  i' E, V5 S/ g( ?! K+ j
  6.                 case
复制代码

. G4 U+ T9 C5 \& w! w2 r添加新地址
& h8 [7 n1 t. ~" z. u, M2 j( w当peer之间交换addr时,节点会收到对端节点已知的地址信息,这些信息会被当前节点添加到地址簿中
" z! N3 W2 ~7 D: M  I( W
  1. func (a *AddrBook) AddAddress(addr *NetAddress, src *NetAddress) {: ~- e4 R- f: l: }! m% W) X- O
  2.         a.mtx.Lock()
      c+ @6 J- r( y. B- ]
  3.         defer a.mtx.Unlock()
    : z* w" L% ?4 x2 A4 R! q  V* M. Q/ S$ H
  4.         log.WithFields(log.Fields{
    ( B. d* N% A+ S7 p
  5.                 "addr": addr,2 y  @. j7 W2 j6 V' [/ X
  6.                 "src":  src,
    3 Y$ Z. D" p3 _2 L
  7.         }).Debug("Add address to book")1 p' I+ b- H. @% i
  8.         a.addAddress(addr, src)
    2 m. h* p: \3 @" E
  9. }" K1 z0 F8 b8 C( N6 @" C
  10. func (a *AddrBook) addAddress(addr, src *NetAddress) {6 q: O% X' p! d6 |
  11.         // 验证地址是否为可路由地址5 C4 |" Z4 u' J' o9 e9 l
  12.         if a.routabilityStrict && !addr.Routable() {$ e& r' o$ l6 P' `, F# }
  13.                 log.Error(cmn.Fmt("Cannot add non-routable address %v", addr)): L, @* Y% ]1 n* v1 Y" u/ f# N
  14.                 return; K: A& m3 Q* g: d4 D% I7 ^$ E
  15.         }/ U% h/ d+ q! [3 e* G
  16.         // 验证地址是否为本地节点地址
    & m3 y; ?2 I$ t: I9 h
  17.         if _, ok := a.ourAddrs[addr.String()]; ok {
    ! D3 w' B' W+ i- e/ z% i
  18.                 // Ignore our own listener address.
    & Z( [( w5 p: ~
  19.                 return: n7 P$ k$ n9 r" v; `- p
  20.         }
    ! h2 w- j4 G- N6 u9 H
  21.         // 验证地址是否存在地址集中
    $ z. p0 h( J$ X3 x8 h
  22.         // 如果存在:则判断该地址是否为old可靠地址、是否超过了最大buckets中。否则根据该地址已经被ka.Buckets引用的个数来随机决定是否添加到地址集中
    4 z8 G, x0 h. W) T
  23.         // 如果不存在:则添加到地址集中。并标识为bucketTypeNew地址类型
    ! ?, u% `$ [0 {1 ]
  24.         ka := a.addrLookup[addr.String()]/ v/ m. t! [: k
  25.         if ka != nil {
    3 s5 Q8 s: Y& U0 l& Z; f: e) N8 M
  26.                 // Already old.4 @# t0 W$ i; G) t& X
  27.                 if ka.isOld() {& g6 H! s6 n7 W
  28.                         return3 p/ C1 M7 ?% c. e) l8 _
  29.                 }
    & i8 x+ ]: T9 T6 x( _
  30.                 // Already in max new buckets.. z# {; r: N, @' }3 z' }+ D
  31.                 if len(ka.Buckets) == maxNewBucketsPerAddress {( l1 F: F. K0 J
  32.                         return
    6 [1 @! `) z4 u8 u: i
  33.                 }  r9 L) y1 c: G; ]- V& C
  34.                 // The more entries we have, the less likely we are to add more.
    ( }, \2 A4 B% i+ j: Q3 ~
  35.                 factor := int32(2 * len(ka.Buckets))
    $ j* f9 U) u* e+ d8 m/ b- o7 ?
  36.                 if a.rand.Int31n(factor) != 0 {
    & H0 v7 D4 j0 `2 v$ ]" a2 v
  37.                         return
    3 f" x5 _8 j8 Q1 }) _
  38.                 }
    ; S5 I) d, r, h9 Q( `1 T
  39.         } else {' \1 e- p( X. C2 A+ T
  40.                 ka = newKnownAddress(addr, src)
    6 |4 ~& r; I1 r, k1 d) ^/ e6 i
  41.         }" z" ^1 A: [; o4 k* `
  42.         // 找到该地址在地址集的索引位置并添加! }0 s3 D* ]4 X* Q$ A
  43.         bucket := a.calcNewBucket(addr, src)
    % K1 y! V& d3 x. [0 v% |6 J
  44.         a.addToNewBucket(ka, bucket)- J1 {( e& V( K. Q8 S& W5 S
  45.         log.Info("Added new address ", "address:", addr, " total:", a.size())
      e" e% E2 Q( u7 k/ i
  46. }
复制代码
3 S6 I3 i+ h3 R, L1 n* \* \
选择最优节点
: `& F# D$ |* s' k, Y; N+ c地址簿中存储众多地址,在p2p网络中需选择最优的地址去连接
, m, J. d. w( _0 K) M/ DPickAddress(newBias int)函数中newBias是由pex_reactor产生的地址评分。如何计算地址分数在其他章节中再讲, F% q/ {) ~7 Y# Z  F" }
根据地址评分随机选择地址可增加区块链安全性
" ]6 a* n$ r$ M$ y
  1. // Pick an address to connect to with new/old bias.
    3 y% m" N9 m/ {1 q# K
  2. func (a *AddrBook) PickAddress(newBias int) *NetAddress {
    ' F6 x3 v& r/ N, l7 i9 N. j& i
  3.         a.mtx.Lock()
    ) _7 G% L$ a- S: q* S& g
  4.         defer a.mtx.Unlock()* w, Z7 f$ R4 R0 l
  5.         if a.size() == 0 {
      N  n/ F5 n5 G1 Q; D
  6.                 return nil. R6 O& T) M/ i" f
  7.         }
    : A( p. W4 K- N! k. a
  8.         // newBias地址分数限制在0-100分数之间
    2 g- ^& t; n; @+ U8 C3 J+ |8 G0 a
  9.         if newBias > 100 {' |0 {$ k5 |& _; p; E6 C: L
  10.                 newBias = 100  r3 V# g4 O! x, V  e
  11.         }# X3 P2 d1 i* f: w  P2 Q8 N& D
  12.         if newBias
复制代码
1 C4 H- d/ m& j8 p; J
移除一个地址" N" M# W6 b  a3 F/ t( m8 n
当一个地址被标记为Bad时则从地址集中移除。目前bytomd的代码版本并未调用过
+ N+ H- L5 N  D7 ?
  1. func (a *AddrBook) MarkBad(addr *NetAddress) {
    9 Q& V- h( Z! b+ Z& _- }# ~
  2.         a.RemoveAddress(addr)+ {2 i2 v+ o" l; r  l6 }% b
  3. }
    : Y- {& g( D# f6 \+ T. k& L3 ?
  4. // RemoveAddress removes the address from the book.
    " o. G- m9 Z- V3 U6 ~  {
  5. func (a *AddrBook) RemoveAddress(addr *NetAddress) {
    , d; n# w1 U6 W' |) `% O# O- z5 y
  6.         a.mtx.Lock()
    2 {+ K6 j/ r. f) I9 @3 M2 Z
  7.         defer a.mtx.Unlock()
    - Z$ ~$ Z. h% l3 D5 l, k$ L/ r( B
  8.         ka := a.addrLookup[addr.String()]( J9 R0 K4 Z% F! W" N  P3 c* \
  9.         if ka == nil {) F, k: ?7 I% v/ F
  10.                 return9 a# n9 s" B$ D: D% _' `
  11.         }
    2 [* g" n0 m/ V# K
  12.         log.WithField("addr", addr).Info("Remove address from book")) V6 q9 J' E8 `
  13.         a.removeFromAllBuckets(ka)( \9 o6 N" N7 |7 V  c% L: q# z
  14. }/ y) Z% ^$ V+ a. L
  15. func (a *AddrBook) removeFromAllBuckets(ka *knownAddress) {" V5 ^$ J& n( \* o
  16.         for _, bucketIdx := range ka.Buckets {! q0 c" Y" C9 w3 \
  17.                 bucket := a.getBucket(ka.BucketType, bucketIdx)
      T0 d% s7 z# b+ e! a( l, w
  18.                 delete(bucket, ka.Addr.String())
    ) C/ h/ _1 |- `) B! Z
  19.         }
    5 c# u: `$ E/ s% @
  20.         ka.Buckets = nil
    ! D1 L9 G( T1 d# u3 M
  21.         if ka.BucketType == bucketTypeNew {" u2 A5 i2 V( X) ^, b" @
  22.                 a.nNew--
    + {$ H- f2 p- h2 ^4 z" _
  23.         } else {
    . H1 j' m+ d/ K/ a( n6 ]
  24.                 a.nOld--
    " A- Q9 {6 K$ E" Z# }
  25.         }# ^7 C! x# [* X
  26.         delete(a.addrLookup, ka.Addr.String())! ~, W9 T6 m% w$ q8 B
  27. }
复制代码

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

本版积分规则

成为第一个吐槽的人

excel436 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    7