Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

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

excel436
188 0 0
简介& }8 o6 h3 e! l* d
https://github.com/Bytom/bytom
( Y* \  `9 [; P5 ]本章介绍bytom代码P2P网络中addrbook地址簿
/ d  P# \- i% D/ r1 |' ?4 m( r% }3 _" @( \4 `  B& W
作者使用MacOS操作系统,其他平台也大同小异) v) _. W6 `2 [/ r8 M& G
; n5 J/ ~0 j7 j" b; C
2 B, m  U( y' c* i( W) Z, }
Golang Version: 1.82 \4 K; g6 z9 I& y

; r5 L5 z) b3 S6 _  uaddrbook介绍# r! B  j5 c. B7 b9 J: m7 m  p
addrbook用于存储P2P网络中保留最近的对端节点地址
( p; C* \; e. X3 U在MacOS下,默认的地址簿路径存储在~/Library/Bytom/addrbook.json# \( ?' c2 l# k% R8 S  ]) T, r/ ?
地址簿格式* b' h3 J9 M% W
  1. ** ~/Library/Bytom/addrbook.json **- c* f- i# B3 n8 ?% l9 b! k5 z/ b% ~* G, W
  2. {, c( O6 @. A' i( D" i7 S0 R
  3.     "Key": "359be6d08bc0c6e21c84bbb2",: O- I: I& q! _' Z
  4.     "Addrs": [
    1 X8 H1 {1 |; e; I3 {; Y
  5.         {
    & n  e* |7 l2 @6 ~) o& D+ m% N/ O; ?
  6.             "Addr": {  \2 N! N' J. b& Z* I
  7.                 "IP": "122.224.11.144",3 K* _1 }6 t8 l( G6 W. M8 r
  8.                 "Port": 46657
    ; o" P1 k4 V, h2 M0 i
  9.             },2 c$ ~( N% C$ g8 H) c
  10.             "Src": {6 x5 Q. h$ `  o4 A" h5 k5 K
  11.                 "IP": "198.74.61.131",
    9 m6 O! ?3 U& I  Z! I. v$ Q; W; D
  12.                 "Port": 46657
      u; q) Z- v4 Z7 X
  13.             },1 i5 |; k9 |& _# ?0 T! D& `6 y
  14.             "Attempts": 0,1 \  K, W# V: W! N! j/ T6 w
  15.             "LastAttempt": "2018-05-04T12:58:23.894057702+08:00",
    ' ?& e( L( M* F! m2 e
  16.             "LastSuccess": "0001-01-01T00:00:00Z",
    # Y3 t. v/ q- O4 u7 s) K# `$ m
  17.             "BucketType": 1,
    ) m* p7 T% w) R7 C9 e+ R+ P5 |6 x2 R
  18.             "Buckets": [
      o% ^7 p& T9 q0 J9 i9 u
  19.                 181,
    9 u4 M/ U- Z5 R9 ~2 f1 S8 }' h2 \
  20.                 10
    6 T3 f4 K9 C1 R% z1 o8 V$ |4 ~1 D
  21.             ]
    9 b+ J, Q. ^  {) U" M. B1 M
  22.         }
    8 g! {- v: F6 s+ c* x
  23.     ]
    : L9 h" e8 K$ u/ H
  24. }
复制代码
$ s' r* Y! @2 n5 u' b% k. y/ U
地址类型9 `) Y3 s7 \& t8 Q
在addrbook中存储的地址有两种:
8 r" g; b: I% k% q$ V
  1. ** p2p/addrbook.go **7 w) T' q- P! G7 D9 Y
  2. const ($ ?  L( e7 X, H* o# w0 G
  3.         bucketTypeNew = 0x01  // 标识新地址,不可靠地址(未成功连接过)。只存储在一个bucket中+ y  G) C. a3 }; \- S4 r
  4.         bucketTypeOld = 0x02  // 标识旧地址,可靠地址(已成功连接过)。可以存储在多个bucket中,最多为maxNewBucketsPerAddress个5 z4 w/ @) D  d$ }* u
  5. )
复制代码
! r. M+ P/ |9 o( ]6 @8 i! r

& O  o% a! Q. H3 Q/ c/ H$ [4 ^注意: 一个地址的类型变更不在此文章中做介绍,后期的文章会讨论该问题
  k8 J, t: n/ u" C$ p
" o2 F* g3 m  g8 V地址簿相关结构体, Z* i6 Q5 M5 {. f) V4 m+ P3 C
地址簿
7 ?! w& r0 v& t2 [
  1. type AddrBook struct {/ ?4 N: ^0 F* \' K
  2.         cmn.BaseService
    # Q: ^4 \2 N: p  C& I0 {; J5 p; X6 r
  3.         mtx               sync.Mutex
    $ v( d) c  h3 C
  4.         filePath          string  // 地址簿路径
    * v" ^3 k2 C+ J) X" X. M
  5.         routabilityStrict bool  // 是否可路由,默认为true7 r# C  N0 ~6 k0 G- k& m, q
  6.         rand              *rand.Rand
    $ f; R8 _1 c! w. [( C% @8 M2 D
  7.         key               string  // 地址簿标识,用于计算addrNew和addrOld的索引
      f& i/ A; R+ _3 M
  8.         ourAddrs          map[string]*NetAddress  // 存储本地网络地址,用于添加p2p地址时做排除使用
    2 a, Y' p" b6 y: F) y, ?) c) ~
  9.         addrLookup        map[string]*knownAddress // 存储新、旧地址集,用于查询
    . |4 G* z- B; x  \+ o
  10.         addrNew           []map[string]*knownAddress // 存储新地址
    : X9 |0 S( ?& i0 ?3 |
  11.         addrOld           []map[string]*knownAddress // 存储旧地址
    - Z+ E$ \: @5 I& D3 o. T: d
  12.         wg                sync.WaitGroup; T# B% y' W& t, f
  13.         nOld              int // 旧地址数量
    ! u. C; t! a1 |; K: F8 W( K
  14.         nNew              int // 新地址数量  m) p* X' k# y) @9 r& A* T
  15. }
复制代码

: q0 G6 l5 H: P3 L( ?. t3 X已知地址( I# O# p, _8 [/ I$ w
  1. type knownAddress struct {- ]* d! D) V6 Q
  2.         Addr        *NetAddress // 已知peer的addr  M" ~9 ?! H# A" b
  3.         Src         *NetAddress // 已知peer的addr的来源addr
    " X( s; v. s9 L3 x* r; o7 d
  4.         Attempts    int32 // 连接peer的重试次数
    % X' y% i' t# ?' ~' C1 C# {' K$ m
  5.         LastAttempt time.Time // 最近一次尝试连接的时间
    ' C7 Q, j5 B, a9 \
  6.         LastSuccess time.Time // 最近一次尝试成功连接的时间
    ! R4 I# x5 B6 b6 A7 B5 @+ ?
  7.         BucketType  byte // 地址的类型(表示可靠地址或不可靠地址)
    . E' a  T" ^9 h8 Q  ^3 P$ v
  8.         Buckets     []int // 当前addr所属的buckets
    2 [1 Y7 u4 T4 h8 Q+ M6 G8 Y
  9. }
复制代码

0 ]8 I* K" @: ?+ x+ [- @; x! kroutabilityStrict参数表示地址簿是否存储的ip是否可路由。可路由是根据RFC划分,具体参考资料:RFC标准2 W& x7 d0 G# Z( P
初始化地址簿: z# D! A! H# X7 X% I4 Y) p" u, ]4 V
  1. // NewAddrBook creates a new address book.
    & Q0 t* ?2 X( q# B3 o
  2. // Use Start to begin processing asynchronous address updates.
    . X- m0 Z0 \7 G7 v8 {, k% @# l
  3. func NewAddrBook(filePath string, routabilityStrict bool) *AddrBook {
    $ f0 X% J) X' }9 D# G$ D1 K
  4.         am := &AddrBook{
    # _. ]2 ~5 Z5 ~: l& b
  5.                 rand:              rand.New(rand.NewSource(time.Now().UnixNano())),
    " o/ G: D3 b5 G, U7 A
  6.                 ourAddrs:          make(map[string]*NetAddress),) |: C4 x/ }/ @) F! n
  7.                 addrLookup:        make(map[string]*knownAddress),' D, {8 [% r) _5 Q
  8.                 filePath:          filePath,- |% `# L9 S# d/ Q: t& D
  9.                 routabilityStrict: routabilityStrict,6 d" F# l' a+ r
  10.         }
    , ?5 {# g: P+ d+ B
  11.         am.init()
    ; T4 O7 @; L1 m
  12.         am.BaseService = *cmn.NewBaseService(nil, "AddrBook", am)
    / u: U2 U* t$ q( [' R+ ?4 M
  13.         return am
    ; X( y) P8 ^- v' B/ N7 x! Z
  14. }& h+ n$ N6 w; S7 ?' {
  15. // When modifying this, don't forget to update loadFromFile()# ?  f/ u2 K  r/ ]) M; `# e
  16. func (a *AddrBook) init() {
      x( B. k- d2 |! G0 J
  17.   // 地址簿唯一标识
    " u; l$ D" ?/ v! ]; [" }. p
  18.         a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits
    * o8 Y! l( B+ m, w3 T! Y
  19.         // New addr buckets, 默认为256个大小) ^* N; R& t6 {; F" y& q
  20.         a.addrNew = make([]map[string]*knownAddress, newBucketCount)2 o! L# S1 U) l; d* y
  21.         for i := range a.addrNew {' r( D; X. J% q
  22.                 a.addrNew<i> = make(map[string]*knownAddress)* M/ I7 m, O; c- F4 R' R( x
  23.         }; ~% g  j6 Q' s5 l
  24.         // Old addr buckets,默认为64个大小
    0 I4 P& E( y. |
  25.         a.addrOld = make([]map[string]*knownAddress, oldBucketCount)
    & ]# R4 n" w) ]1 z
  26.         for i := range a.addrOld {, [1 [7 m& j8 N) t3 {
  27.                 a.addrOld<i> = make(map[string]*knownAddress)
    6 t$ |0 }5 B; F9 d1 Q, a; T% j
  28.         }
    5 O& f! o* b" z& b
  29. }</i></i>
复制代码

7 i% v+ j6 k+ T$ Hbytomd启动时加载本地地址簿1 m$ v4 k5 S$ s6 ~2 r5 q1 r
loadFromFile在bytomd启动时,首先会加载本地的地址簿+ k4 q& z& K) q
  1. // OnStart implements Service.7 N! I) Z7 T) X2 ~( [" j. Q7 m# u# Q
  2. func (a *AddrBook) OnStart() error {/ {/ ^5 q$ I' o4 A
  3.         a.BaseService.OnStart()
    . ~6 E8 T: n) M6 `3 c4 T7 L7 a
  4.         a.loadFromFile(a.filePath), g% @0 A( e* L
  5.         a.wg.Add(1)7 ^: ~7 `# ]  a* g* G0 d
  6.         go a.saveRoutine()
    3 |, e3 x: E) W8 X
  7.         return nil5 y: Y0 y! D! k! p% o
  8. }3 h! h7 I) H6 n
  9. // Returns false if file does not exist.8 P" h+ K3 J+ }. J! j1 g, q
  10. // cmn.Panics if file is corrupt.( L% p* Q' r( c! D
  11. func (a *AddrBook) loadFromFile(filePath string) bool {
    . u+ U% o: }) A; G
  12.         // If doesn't exist, do nothing.) u+ ^# t5 W- D9 F# B; I7 @8 E
  13.         // 如果本地地址簿不存在则直接返回$ L6 V- d- o! m
  14.         _, err := os.Stat(filePath)
    # N6 ^, G+ a+ h; h4 F. |6 E
  15.         if os.IsNotExist(err) {
    " n- b2 w( i& I5 I" ^
  16.                 return false
    ( r; i( s/ {$ a" O6 ?
  17.         }
    ' @5 v$ _) V6 K8 a0 o
  18.   // 加载地址簿json内容
    7 Q7 O' R# F/ F; @
  19.         // Load addrBookJSON{}! t; f$ u" }1 D
  20.         r, err := os.Open(filePath)! C- G) z5 Z7 `3 A; M) d
  21.         if err != nil {# x# h% f# S* O, H: C# j9 J
  22.                 cmn.PanicCrisis(cmn.Fmt("Error opening file %s: %v", filePath, err))
    5 A6 _9 `8 Z) [. P( \- [
  23.         }) ^4 K9 K8 U! u+ G
  24.         defer r.Close()
    * ~4 B- G" m% X0 U0 m
  25.         aJSON := &addrBookJSON{}. }( w& }7 f6 D' A/ o
  26.         dec := json.NewDecoder(r)- S! \% u0 E/ M" Y8 F- f2 F2 }; C
  27.         err = dec.Decode(aJSON)
    3 z! O! o1 t, c+ a: X
  28.         if err != nil {6 A8 t% \1 X7 l
  29.                 cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", filePath, err))  I% z6 l0 [7 C$ |) ]
  30.         }
    . E- e) L  D+ W: \5 F( H( z
  31.   // 填充addrNew、addrOld等
    8 S! v4 V+ s* w; v
  32.         // Restore all the fields...1 X, p* X: ~* B/ c# G
  33.         // Restore the key
    $ B9 d' a: }6 ]6 I+ l; h# V2 h, q: x
  34.         a.key = aJSON.Key" V+ c0 E7 B' T
  35.         // Restore .addrNew & .addrOld
    5 q# W( I( v/ S
  36.         for _, ka := range aJSON.Addrs {
    , ~: r5 L- [3 ]+ i
  37.                 for _, bucketIndex := range ka.Buckets {
    ! I* `0 Q1 C  }# @
  38.                         bucket := a.getBucket(ka.BucketType, bucketIndex)$ W( I" j& f) \' N3 y0 X  J" }+ }
  39.                         bucket[ka.Addr.String()] = ka. d6 u; ]5 E7 K" T6 g; b& p( x
  40.                 }$ X9 z% ~/ }5 W0 ^+ s$ L+ I
  41.                 a.addrLookup[ka.Addr.String()] = ka4 D+ i* V) n6 m* o( }! R
  42.                 if ka.BucketType == bucketTypeNew {
    / {6 d# `0 `; K' X3 Q. k3 g1 b
  43.                         a.nNew++
    1 W2 y' c) d( A( b
  44.                 } else {
      r: w" A$ o  O* z, V" Z+ W2 ]
  45.                         a.nOld++
    7 m- o; u1 v& K7 N. W
  46.                 }7 O& N* I6 m: B
  47.         }( B. x: V% ]. Z* a% Z
  48.         return true
    7 _2 F, z# Z9 S, d
  49. }
复制代码

- c; h# R) @8 G" E& V4 p定时更新地址簿
* \* J" ~+ X0 J% W; h$ ebytomd会定时更新本地地址簿,默认2分钟一次
2 O& P. {" m/ |$ M3 u6 F" o& S9 S
  1. func (a *AddrBook) saveRoutine() {+ S. ~! }) V, W6 ^2 d/ Q
  2.         dumpAddressTicker := time.NewTicker(dumpAddressInterval)8 `$ o( }+ u7 ^; r$ M/ K& a% N
  3. out:# ]1 n1 ~( `9 o* r% W" p
  4.         for {  X5 y* \" G0 Z/ p0 X
  5.                 select {" _* [# @# s' g. j7 Q8 k
  6.                 case
复制代码

* v8 H! W+ t" A/ n* N添加新地址
) d7 |$ [/ W( b当peer之间交换addr时,节点会收到对端节点已知的地址信息,这些信息会被当前节点添加到地址簿中. e5 O$ ^: n" T0 ]8 \
  1. func (a *AddrBook) AddAddress(addr *NetAddress, src *NetAddress) {
    % q& M+ ^: K- s
  2.         a.mtx.Lock()- F9 B/ v- d/ k; K2 N* C
  3.         defer a.mtx.Unlock()
    7 D1 t- J% x9 r9 [% ^* o( {
  4.         log.WithFields(log.Fields{7 M0 j3 k# b0 o+ D8 P. H
  5.                 "addr": addr,- k! }& E4 w3 ~% ~, e: E/ l
  6.                 "src":  src,8 M% S: T; C; Q$ ~; ^
  7.         }).Debug("Add address to book")
    * |; O0 u" B; X: ^4 P6 u/ @' I0 i/ p
  8.         a.addAddress(addr, src)0 O1 E* H4 D8 E; i' o
  9. }
    9 e0 w, F+ w9 h  [: D6 x" e
  10. func (a *AddrBook) addAddress(addr, src *NetAddress) {- s" K5 ?8 I$ N. V3 I1 v( I
  11.         // 验证地址是否为可路由地址! ^7 K8 a4 U$ H$ {# L! R& u
  12.         if a.routabilityStrict && !addr.Routable() {2 y# Z" c. A" _, O  {& {7 V
  13.                 log.Error(cmn.Fmt("Cannot add non-routable address %v", addr))4 T* \/ Q2 ]$ C* ~" W$ h
  14.                 return; o% l4 u# u+ e0 q( W. f& S7 f
  15.         }$ r; x9 Z% j! ]8 n: ^. x+ E
  16.         // 验证地址是否为本地节点地址) W3 P$ Y- x0 c
  17.         if _, ok := a.ourAddrs[addr.String()]; ok {3 a9 h- k) K+ E' d
  18.                 // Ignore our own listener address.
    $ l7 D  a) n. G9 N* g
  19.                 return
    8 E1 L( J2 m4 i' M. T6 h
  20.         }
    : Q0 L- G8 G3 l# L! K% E$ \( p, \
  21.         // 验证地址是否存在地址集中
    2 u$ Z% E1 W8 B( H0 o2 U
  22.         // 如果存在:则判断该地址是否为old可靠地址、是否超过了最大buckets中。否则根据该地址已经被ka.Buckets引用的个数来随机决定是否添加到地址集中; \1 ?9 \0 X/ ]3 D! s; p
  23.         // 如果不存在:则添加到地址集中。并标识为bucketTypeNew地址类型: ~! L( A; }2 g1 y4 a
  24.         ka := a.addrLookup[addr.String()]/ h8 E: w/ c( b# c* Q6 T
  25.         if ka != nil {" c; t4 K( r) @  r
  26.                 // Already old., n- \7 O2 F' ~! I5 s9 a* I% v
  27.                 if ka.isOld() {  i1 e$ L2 u- J" ^) s
  28.                         return
    / ?. {8 Z% B/ }. K4 ^# q; v: m
  29.                 }
    0 b. k3 D2 z5 I7 o8 h- p* G8 Q2 {
  30.                 // Already in max new buckets.
    5 f0 ^& b1 g2 ~/ H5 M5 Q
  31.                 if len(ka.Buckets) == maxNewBucketsPerAddress {& _  e$ w4 [; l" B8 i" y
  32.                         return
    1 }' y8 o/ O9 g  Z- f
  33.                 }
    6 K; V, C2 E8 y) W1 _' `
  34.                 // The more entries we have, the less likely we are to add more.9 \; ?" ^, U4 {" u- U
  35.                 factor := int32(2 * len(ka.Buckets))
    * ~/ T( X% p1 ?6 q5 Q
  36.                 if a.rand.Int31n(factor) != 0 {5 r' r/ v8 o5 ~" S6 h
  37.                         return8 R% F; m: f  V
  38.                 }
    $ k; _' p8 A, z8 X
  39.         } else {
    + D4 @% K1 r" Y( q, a( }7 J
  40.                 ka = newKnownAddress(addr, src)
    - I# D5 X# p7 R+ {3 F
  41.         }
      x# m% c/ B- o) q2 w
  42.         // 找到该地址在地址集的索引位置并添加. Z# Z% Z, t) P' Z! G4 y
  43.         bucket := a.calcNewBucket(addr, src)% o# m- v% n1 c: q  k; u9 b
  44.         a.addToNewBucket(ka, bucket)9 c% r  q# ?6 B# l$ `! Z. c" U
  45.         log.Info("Added new address ", "address:", addr, " total:", a.size())
    ( E2 J% }! J. Y7 V4 R
  46. }
复制代码
- O4 T0 E- m. H# `1 j  q2 E8 I
选择最优节点' J' ]: \% J0 L5 @( h$ a
地址簿中存储众多地址,在p2p网络中需选择最优的地址去连接" n) x; S7 r1 h, S# ?
PickAddress(newBias int)函数中newBias是由pex_reactor产生的地址评分。如何计算地址分数在其他章节中再讲
$ R2 z" ^- n- k1 J& f根据地址评分随机选择地址可增加区块链安全性! M' v! Y) ?* O# {4 h0 M8 V3 v
  1. // Pick an address to connect to with new/old bias.0 p; u' h  {& g: G- e% _
  2. func (a *AddrBook) PickAddress(newBias int) *NetAddress {
    % w# J8 e8 z% J* D& \
  3.         a.mtx.Lock()2 ?6 A) q5 y+ S6 n  b$ }9 O$ C
  4.         defer a.mtx.Unlock()
    ! h/ [. }) J2 C0 q# q
  5.         if a.size() == 0 {8 z) d; F$ d4 i8 U, g  I' I
  6.                 return nil7 s1 o% V( p# h6 |# X+ \) y
  7.         }
    * _. y- S( W8 _" d! ~% R5 o
  8.         // newBias地址分数限制在0-100分数之间
    - s/ z4 O5 v+ J' J
  9.         if newBias > 100 {! F  x# m8 A5 j7 W/ O3 p9 V6 z
  10.                 newBias = 100
    - o1 V( ?- J3 m# [- G
  11.         }$ A7 u  a7 B9 T4 M* g+ T! E
  12.         if newBias
复制代码
# F8 Y$ \3 n2 |4 f7 f& H; M
移除一个地址5 W9 T% D- A* _  T
当一个地址被标记为Bad时则从地址集中移除。目前bytomd的代码版本并未调用过6 K- ?$ k, }$ P' K$ A( o
  1. func (a *AddrBook) MarkBad(addr *NetAddress) {
      q1 W6 ~2 s! Y; z4 b4 ^+ v
  2.         a.RemoveAddress(addr)% Q' E8 X2 w5 ]- @8 L' }7 D3 J
  3. }
    $ b. S: M7 [& I9 V9 j& G" m3 `; ?0 h
  4. // RemoveAddress removes the address from the book.
    1 p8 T" \- k2 C; s
  5. func (a *AddrBook) RemoveAddress(addr *NetAddress) {) a5 ?: w- C* W9 {- r
  6.         a.mtx.Lock()9 P. K! m4 ^$ b
  7.         defer a.mtx.Unlock()
    7 d8 y( A7 ~& ]7 c4 x
  8.         ka := a.addrLookup[addr.String()]
    . G: Z' \" L9 R9 x
  9.         if ka == nil {
    7 T: A' P" K3 p6 j
  10.                 return' @" M3 i9 n6 c3 K7 ?
  11.         }" r3 r6 p. E  N% H1 ^
  12.         log.WithField("addr", addr).Info("Remove address from book")& r+ ~- i; l; j% f3 m) U
  13.         a.removeFromAllBuckets(ka)
    6 v& E" k, D. Y; X; w7 Q
  14. }
    ( s, R* E! j" Y# F; R) o( o  e
  15. func (a *AddrBook) removeFromAllBuckets(ka *knownAddress) {+ ]  q# H3 a- `% y3 ^5 `# d
  16.         for _, bucketIdx := range ka.Buckets {
    2 p! ^% c- e7 c+ F
  17.                 bucket := a.getBucket(ka.BucketType, bucketIdx)& W$ A- \3 I8 I4 c: F
  18.                 delete(bucket, ka.Addr.String())0 T5 I/ Z) l* ]' y: P; e5 J. Y
  19.         }
    / D/ J& F8 f8 Z1 Z
  20.         ka.Buckets = nil- {* x+ K! `; m, O% `$ r
  21.         if ka.BucketType == bucketTypeNew {
    * b# ]2 @0 C' G/ D3 i- W' z. B
  22.                 a.nNew--
    - ]: b* {8 \% g! S! l
  23.         } else {
    ( i8 t! ~. v/ I( ?1 U
  24.                 a.nOld--
    6 E% D3 u* [7 ?% b) i* S
  25.         }
    # b) O! e2 t8 I/ `
  26.         delete(a.addrLookup, ka.Addr.String())
    2 m, S0 P/ G& `2 z. H
  27. }
复制代码

% w- g# u) S4 ^9 M/ s8 X" ~7 o% c" v
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

excel436 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    7