Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

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

excel436
134 0 0
简介
2 y* |8 x5 A3 v* shttps://github.com/Bytom/bytom
+ Q; L+ M% W" ~- l% l本章介绍bytom代码P2P网络中addrbook地址簿) I! \4 P% Z: c! [6 G

) h8 A* F  ^3 @- z, i# R. K作者使用MacOS操作系统,其他平台也大同小异9 b; h9 n8 K( ?( F; z: Q3 Q
6 a( v4 w: X% t- e7 T4 a
0 ?/ g! ^: L% R% U5 @  G# t
Golang Version: 1.8
: b3 c; ]5 L' Y! h0 |$ o
4 n$ J) L- h) a( q) ]
addrbook介绍. j. j7 a( i: H$ Q4 M' Q
addrbook用于存储P2P网络中保留最近的对端节点地址0 q2 Y+ s4 L% Z* d
在MacOS下,默认的地址簿路径存储在~/Library/Bytom/addrbook.json/ i4 T2 j" I5 Q' q) k
地址簿格式, W3 |- f3 l& @, `' |
  1. ** ~/Library/Bytom/addrbook.json **
    . x/ T* L) T, y: u: n
  2. {
    6 k+ F: @5 |) H* ^) Y/ r
  3.     "Key": "359be6d08bc0c6e21c84bbb2",9 |) Y* ^) n+ b& Z( c7 T
  4.     "Addrs": [3 X+ l" g, \& {: F9 E+ M
  5.         {
    7 t. }" \1 }2 U" T2 G: }$ ~$ S/ Y
  6.             "Addr": {0 `+ J8 W& Z  [9 D# i
  7.                 "IP": "122.224.11.144",) z: ~6 ^' A3 v9 r! Y* t  K( z6 x
  8.                 "Port": 46657
    6 X7 l3 [$ h, i
  9.             },9 |$ s  u6 T; G5 Z* @: L
  10.             "Src": {
    + i9 `; p# V& p7 w: z' W" G. b
  11.                 "IP": "198.74.61.131",
    + _3 {4 g& @% b9 D  l8 o8 g* ~
  12.                 "Port": 46657
    ; Z! G1 W. A: |3 v1 \3 T" R, d: M
  13.             },9 q) t5 x. h' x; S
  14.             "Attempts": 0,
    ! A' U0 c( ~) L7 m' @' N
  15.             "LastAttempt": "2018-05-04T12:58:23.894057702+08:00",; J. v1 j$ L8 W% {6 H9 |
  16.             "LastSuccess": "0001-01-01T00:00:00Z"," i4 s9 e, S% Y" U
  17.             "BucketType": 1,9 i; Q1 \7 S+ A8 N8 I/ N
  18.             "Buckets": [$ @. z/ r) m9 I5 r" l$ }0 ~9 V
  19.                 181,
    1 v) l2 P) Z* F6 M
  20.                 10
    5 l. h! o6 ^: W3 O8 F- G3 S6 w
  21.             ]  p9 E. E. z/ u
  22.         }+ T3 p' u& s  Y0 s1 }
  23.     ]8 b  N. s9 ~% e
  24. }
复制代码

; ], R# B8 ~. _( z9 B. I- C  i1 A地址类型$ R' @! G$ d0 T& x3 I4 i- E
在addrbook中存储的地址有两种:
& L# W( c& }1 N; ?( C* S1 t+ H
  1. ** p2p/addrbook.go **/ d/ t! n/ Y, N/ Y0 t  X0 t
  2. const (
    ! R) O/ L/ {" K+ F; i
  3.         bucketTypeNew = 0x01  // 标识新地址,不可靠地址(未成功连接过)。只存储在一个bucket中
    8 L4 A9 M0 W1 f8 |
  4.         bucketTypeOld = 0x02  // 标识旧地址,可靠地址(已成功连接过)。可以存储在多个bucket中,最多为maxNewBucketsPerAddress个
    6 j" T$ ?( Z1 u- S2 y  |( E
  5. )
复制代码
  O  A& X  `8 g

$ H' [5 K( G. A% [注意: 一个地址的类型变更不在此文章中做介绍,后期的文章会讨论该问题% T9 R( j0 ]8 a, {, B5 `; G

7 E) O' f# I' n0 p6 X( F地址簿相关结构体7 J0 ?/ R. [  D3 K
地址簿
( g$ [+ }# w% K8 b9 O) H
  1. type AddrBook struct {* o; h5 ], H7 Z% ?  c5 f
  2.         cmn.BaseService# p& P' i$ ~- x: V5 m
  3.         mtx               sync.Mutex
    * b- m7 N. \$ V% F1 R% _/ `
  4.         filePath          string  // 地址簿路径$ p/ Z, u9 l  b5 w. O: ^
  5.         routabilityStrict bool  // 是否可路由,默认为true
    ' ^8 w6 X# ~: d8 p% H
  6.         rand              *rand.Rand
    : c  g" ^7 ^2 k8 s0 [8 }9 R
  7.         key               string  // 地址簿标识,用于计算addrNew和addrOld的索引4 N+ s9 o4 ]: m5 t, F3 t* V
  8.         ourAddrs          map[string]*NetAddress  // 存储本地网络地址,用于添加p2p地址时做排除使用
    3 ^$ r9 ^/ L2 u# R. b" a7 \7 w& {
  9.         addrLookup        map[string]*knownAddress // 存储新、旧地址集,用于查询
    & l2 Y9 _5 }8 E1 j9 H/ ?$ Q
  10.         addrNew           []map[string]*knownAddress // 存储新地址
    ' f( P1 f6 g, |' ?) S( j
  11.         addrOld           []map[string]*knownAddress // 存储旧地址
    $ C$ p! l& K; w/ T0 x) a9 l
  12.         wg                sync.WaitGroup
    5 {7 E8 ^6 H2 l2 U) X2 {
  13.         nOld              int // 旧地址数量
    5 [, c9 C% ]; M, p$ m" C' V
  14.         nNew              int // 新地址数量' R3 C7 z( f) T( K" T. k4 W
  15. }
复制代码
9 \1 H0 B; D& T* l4 f/ ^' n" f
已知地址
( J; u& O# p7 m7 a9 |: _
  1. type knownAddress struct {
    ' I* E- L4 H  u$ W- p
  2.         Addr        *NetAddress // 已知peer的addr* \+ R. W8 `7 }4 h1 D" c
  3.         Src         *NetAddress // 已知peer的addr的来源addr( C+ e  f* V  a9 Y$ v: C
  4.         Attempts    int32 // 连接peer的重试次数
    ' T- I6 K0 e( B7 m# s$ Z! k
  5.         LastAttempt time.Time // 最近一次尝试连接的时间
    : N* M9 l1 D9 M/ j) @
  6.         LastSuccess time.Time // 最近一次尝试成功连接的时间2 I7 a7 {; v: z" j% r9 _( ]% l, y
  7.         BucketType  byte // 地址的类型(表示可靠地址或不可靠地址)
    0 w% w- w; r# _' [7 e+ {9 [
  8.         Buckets     []int // 当前addr所属的buckets
    5 k% e8 k: \) J0 x
  9. }
复制代码
0 b3 T1 D7 z7 O6 B
routabilityStrict参数表示地址簿是否存储的ip是否可路由。可路由是根据RFC划分,具体参考资料:RFC标准
# e6 ]# h; q1 F5 D初始化地址簿1 j% }2 ~+ P$ Q+ Q; F
  1. // NewAddrBook creates a new address book.6 u. q0 @  P5 s; x) o% s+ K
  2. // Use Start to begin processing asynchronous address updates.  y* D- ]2 l4 p: N  P0 ~* D6 U% O. B
  3. func NewAddrBook(filePath string, routabilityStrict bool) *AddrBook {
    " v* g; o. v+ f9 O( |4 a$ t
  4.         am := &AddrBook{+ i7 y7 t" `5 U
  5.                 rand:              rand.New(rand.NewSource(time.Now().UnixNano())),7 ~( ~! R) H9 N7 q
  6.                 ourAddrs:          make(map[string]*NetAddress),/ n* c5 v6 h) t
  7.                 addrLookup:        make(map[string]*knownAddress),+ {7 w4 u; E# A% H0 d9 w( ^
  8.                 filePath:          filePath,
    4 u" e$ o$ y9 ^8 [! c2 a
  9.                 routabilityStrict: routabilityStrict,
    # M! E+ @8 {7 R$ S9 a, }% I" t
  10.         }
    ! S0 M$ r5 M# n: ~
  11.         am.init()
    3 o# p2 F! K) y- b( t: \; V
  12.         am.BaseService = *cmn.NewBaseService(nil, "AddrBook", am)
    1 f) |4 v; X( D# E  w
  13.         return am
    / u0 m  n' L' m, r5 @' j/ V3 {
  14. }
    # a. M! y) R" [6 _
  15. // When modifying this, don't forget to update loadFromFile()
    . v$ L/ D. v( `! M( e
  16. func (a *AddrBook) init() {
    : S. K! B3 U, E4 F+ f
  17.   // 地址簿唯一标识- L# R9 u" d0 b  x) Q
  18.         a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits5 W& `0 o$ o. @: X9 N% X0 V$ y
  19.         // New addr buckets, 默认为256个大小7 O) {" z) e& p& C5 p9 R
  20.         a.addrNew = make([]map[string]*knownAddress, newBucketCount)
    ) t+ s2 a" E" T
  21.         for i := range a.addrNew {
    8 H" q: V8 Q, B! d
  22.                 a.addrNew<i> = make(map[string]*knownAddress)0 i  x+ B- k7 G) R6 A" b
  23.         }8 s3 Z9 Q8 j& F% P: `' P: a0 x6 k
  24.         // Old addr buckets,默认为64个大小2 E- u4 [3 v% |- j3 c+ A+ I  U
  25.         a.addrOld = make([]map[string]*knownAddress, oldBucketCount)
    : w- {! B. a, Q* i/ J, a  {
  26.         for i := range a.addrOld {0 d: \$ y1 h- c0 E
  27.                 a.addrOld<i> = make(map[string]*knownAddress). H( ]3 o9 ^3 T; C- t
  28.         }9 {! S) z0 U0 q9 b/ @- Y9 Q0 M
  29. }</i></i>
复制代码
( ^/ T' d3 }  u# n* b+ v/ I8 \5 w8 S
bytomd启动时加载本地地址簿
. L5 F1 j8 K+ j' MloadFromFile在bytomd启动时,首先会加载本地的地址簿$ ?: c5 @% B4 e" U  ^" x; `$ {, U
  1. // OnStart implements Service.
      T" F8 a) u5 M* r
  2. func (a *AddrBook) OnStart() error {
    ; m' U2 r$ n" n8 D6 y
  3.         a.BaseService.OnStart()
    5 \( x3 |2 I$ q  I% N
  4.         a.loadFromFile(a.filePath)
    7 _. h  G" Q& a7 m4 X6 U
  5.         a.wg.Add(1)
    0 R4 y3 |  d5 p+ w* E' W) p
  6.         go a.saveRoutine()/ a* [& _. w6 [: i, \$ m3 H
  7.         return nil" \/ {$ v% [, L, }
  8. }
    % @5 r2 T4 }, ^/ P
  9. // Returns false if file does not exist.
    ; H0 l2 }5 l4 l) ]8 ~- g* ?
  10. // cmn.Panics if file is corrupt.# H( i) y# [6 N0 y  Z& p( N( i
  11. func (a *AddrBook) loadFromFile(filePath string) bool {1 a- c6 ~8 x7 R1 a& h
  12.         // If doesn't exist, do nothing.$ x, C: ^0 z6 S' [
  13.         // 如果本地地址簿不存在则直接返回' G& {9 V1 H- S
  14.         _, err := os.Stat(filePath)
    ) ?9 m& Z  a# t: ~* O$ g3 K5 g, x
  15.         if os.IsNotExist(err) {- B6 C% M+ d3 H6 \
  16.                 return false7 J1 N8 D3 S% q4 r( S2 A  A* p' t( ?# h
  17.         }
    + F1 `7 C, j& H' X* h1 |
  18.   // 加载地址簿json内容
    , v3 }6 F2 B* z; V. S1 b" @& ?
  19.         // Load addrBookJSON{}: L  k' H5 M4 R
  20.         r, err := os.Open(filePath)* ~- s  f) q$ o8 \
  21.         if err != nil {
    : y4 S% {3 |- P6 U
  22.                 cmn.PanicCrisis(cmn.Fmt("Error opening file %s: %v", filePath, err))$ J7 Y: z6 [: V+ m! I8 U! I1 l
  23.         }
    " b" i: m( K& D2 t
  24.         defer r.Close()9 N0 y' |' A+ x0 P& Q- h
  25.         aJSON := &addrBookJSON{}
    ! P) ?2 v; A8 l
  26.         dec := json.NewDecoder(r)
    5 N" w; e/ p2 P7 D
  27.         err = dec.Decode(aJSON)
    6 ^% z, K7 D8 g" x3 f/ G
  28.         if err != nil {
    , w' |" e7 n  g1 s# r# I, v" L
  29.                 cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", filePath, err)), g/ @# ]: s- m2 k3 N% l% f
  30.         }9 u; v0 a) W% ]- `+ Z
  31.   // 填充addrNew、addrOld等3 @# L$ C$ c) T* N+ s  G
  32.         // Restore all the fields...
    6 t0 i  L1 R/ ]5 Y
  33.         // Restore the key8 ^7 J5 K# v# \9 W2 T
  34.         a.key = aJSON.Key
    3 O8 |9 D/ r) L- ]  [
  35.         // Restore .addrNew & .addrOld  ]* j/ f6 \! y8 P3 p6 d
  36.         for _, ka := range aJSON.Addrs {
    4 X5 ~/ F4 P' }& X6 F0 W
  37.                 for _, bucketIndex := range ka.Buckets {
    - h  \7 _7 H% _* p: K" \: M2 E
  38.                         bucket := a.getBucket(ka.BucketType, bucketIndex)
    ; j5 O' N, @" {9 {6 e2 ^
  39.                         bucket[ka.Addr.String()] = ka
    5 c- \1 j% a% B( d+ M9 a* z
  40.                 }
    4 q0 y9 R" P, Z$ T6 l1 y5 P
  41.                 a.addrLookup[ka.Addr.String()] = ka" _# p6 Z3 |( L: ]- l$ c4 Y
  42.                 if ka.BucketType == bucketTypeNew {& _. @% I, P  @4 @" Z
  43.                         a.nNew++% k& l: K8 w  p( I1 _; F
  44.                 } else {
    - B$ e! ^7 E  V3 e
  45.                         a.nOld++3 o' B5 Y! f% g8 Q& r
  46.                 }
      ?/ N/ g4 |; w1 e; o
  47.         }/ _0 n& w* r0 \6 V) W
  48.         return true7 n4 d6 G+ S$ R6 F5 v& Q1 A
  49. }
复制代码
7 i6 B( d! Z7 q% b: h  L
定时更新地址簿
' Z( h* b: Q+ jbytomd会定时更新本地地址簿,默认2分钟一次
* K; q. r9 K0 y% b  s, q( Y9 N6 f
  1. func (a *AddrBook) saveRoutine() {
    $ H1 U* w& }* n: i3 ?! w4 i" ^
  2.         dumpAddressTicker := time.NewTicker(dumpAddressInterval)" ?2 U" W2 U; O, u* `2 [' `
  3. out:6 b) S" I( m/ L' H
  4.         for {
    1 w& q2 a: Z0 l5 z! ~
  5.                 select {, {; a% ~% D7 e& @
  6.                 case
复制代码

2 g3 ^5 R; E/ v: B8 _- S$ w添加新地址
. W# W7 ^: U/ y- x+ v4 G" U+ g$ `当peer之间交换addr时,节点会收到对端节点已知的地址信息,这些信息会被当前节点添加到地址簿中: i9 {6 T( k0 y- l' ]
  1. func (a *AddrBook) AddAddress(addr *NetAddress, src *NetAddress) {
    8 e' D+ x2 }7 X1 `
  2.         a.mtx.Lock()2 d; Z/ g8 c4 Q
  3.         defer a.mtx.Unlock()
    ' f3 E1 T  c4 U- l- h( ~# P
  4.         log.WithFields(log.Fields{
    : z; Z5 o9 y* s  c
  5.                 "addr": addr,* j5 n* H8 x6 P1 c* Z
  6.                 "src":  src," [+ J; v6 B: X, }
  7.         }).Debug("Add address to book")4 p& y( B& d, w- F
  8.         a.addAddress(addr, src)9 ~! ^4 D) }9 d# M
  9. }
    ! ^$ g+ }; \! y
  10. func (a *AddrBook) addAddress(addr, src *NetAddress) {+ v. T3 m3 s& c" }/ h
  11.         // 验证地址是否为可路由地址
    2 n; r, ~- r7 g( X3 [" i% \
  12.         if a.routabilityStrict && !addr.Routable() {0 N$ D: t1 V% Q: L. ^" C9 D. z3 k
  13.                 log.Error(cmn.Fmt("Cannot add non-routable address %v", addr))9 h0 w& C) u& Y3 Q( q
  14.                 return
    & s" e- L5 I- _7 z
  15.         }9 `" V7 F& S1 r) {
  16.         // 验证地址是否为本地节点地址
    # c' o: E8 Q9 o$ b+ y0 N
  17.         if _, ok := a.ourAddrs[addr.String()]; ok {( `7 D0 t9 N4 h3 s, R
  18.                 // Ignore our own listener address.& {7 k- j2 c3 Z& S6 c% |& @2 _' h
  19.                 return0 D' m' Z6 ?# V! I. w3 h
  20.         }
    % `) h; p1 p: J$ x& J$ b" t
  21.         // 验证地址是否存在地址集中4 f) Z( G+ \/ q! V  F& G/ w
  22.         // 如果存在:则判断该地址是否为old可靠地址、是否超过了最大buckets中。否则根据该地址已经被ka.Buckets引用的个数来随机决定是否添加到地址集中
    9 R' u9 [0 z6 U1 {9 A
  23.         // 如果不存在:则添加到地址集中。并标识为bucketTypeNew地址类型6 N- {6 V9 l& C' X% Y+ c
  24.         ka := a.addrLookup[addr.String()]& X; d. d0 W) J0 Z* @( D+ g7 L
  25.         if ka != nil {8 i. H: A, b: L# b! ?( }0 x
  26.                 // Already old.
    6 I7 m' A* g6 a+ p$ @& K
  27.                 if ka.isOld() {7 o% X$ j) P  L  T
  28.                         return7 V# f1 F- g6 _* K8 T' F
  29.                 }% ?. c# X0 I0 ^$ I" c* a  T
  30.                 // Already in max new buckets.: ^9 C; F0 }' [& J
  31.                 if len(ka.Buckets) == maxNewBucketsPerAddress {
    & U9 P2 H1 W9 I# a
  32.                         return
    * {( Z. g0 k& M, d) b. ^+ A0 o
  33.                 }
    ; M: {% `; f; _: [: M
  34.                 // The more entries we have, the less likely we are to add more.. C5 V' N$ q: s! k! [. L
  35.                 factor := int32(2 * len(ka.Buckets))
    ( n& ^+ O5 |# P0 m  \
  36.                 if a.rand.Int31n(factor) != 0 {
    + d0 N$ Q6 Z1 Q! [, V  {
  37.                         return
    - y2 P$ }' ?* Z: e) |9 i
  38.                 }
    4 K! ]9 w; C- I& `9 |7 w
  39.         } else {* {5 }/ b0 I6 Z$ {  E7 z8 x/ B' g2 F
  40.                 ka = newKnownAddress(addr, src); B3 n- j: w% s) F$ U, ?  t' L. u1 C
  41.         }
    9 V" K5 I: V5 F: v  Z7 X) Q
  42.         // 找到该地址在地址集的索引位置并添加6 ~( R8 R$ i/ o$ b
  43.         bucket := a.calcNewBucket(addr, src)/ L& ?! i7 i5 U0 p% V9 @: y
  44.         a.addToNewBucket(ka, bucket)
    2 E+ a8 G/ P+ R3 `
  45.         log.Info("Added new address ", "address:", addr, " total:", a.size())
    5 J% F, \4 U9 U) X  f
  46. }
复制代码

% C1 G0 n& ]) B8 h( v( P选择最优节点
# k, @( k% d/ T# A地址簿中存储众多地址,在p2p网络中需选择最优的地址去连接/ |/ |. J& n& i+ j. ^7 q1 y- F
PickAddress(newBias int)函数中newBias是由pex_reactor产生的地址评分。如何计算地址分数在其他章节中再讲
2 G; D3 m/ Z8 Q* I. p根据地址评分随机选择地址可增加区块链安全性
. S- z- R) T3 @" i) D
  1. // Pick an address to connect to with new/old bias.% w! i; o) D6 G9 T: I6 o0 h! {
  2. func (a *AddrBook) PickAddress(newBias int) *NetAddress {
    - a$ g. B- q) B- f
  3.         a.mtx.Lock()
    . B" F9 j: Z* `
  4.         defer a.mtx.Unlock()
    ! c, K* ~2 g" K/ N* ~) @8 p
  5.         if a.size() == 0 {0 _' @2 B5 t( B  H. t
  6.                 return nil
    $ `+ z. [* c0 k9 {+ |- T
  7.         }
    6 u6 f* y; c1 J+ S/ a! ?, T4 R
  8.         // newBias地址分数限制在0-100分数之间
    ' E& Z$ P& Q( ]0 u2 m( G# E
  9.         if newBias > 100 {$ D, L% C: F- [
  10.                 newBias = 100
    . e/ k* |) {$ k2 `. l
  11.         }
    1 I" C; h, q1 L
  12.         if newBias
复制代码
$ ~' O6 f6 D9 r" I1 R; Z
移除一个地址# U& d7 O: w. u
当一个地址被标记为Bad时则从地址集中移除。目前bytomd的代码版本并未调用过4 Q4 J- d# O& |% O# X( ?5 e
  1. func (a *AddrBook) MarkBad(addr *NetAddress) {
    6 h- l5 [% v8 |% H
  2.         a.RemoveAddress(addr)7 U1 {, a. j- M3 X) b& P6 V
  3. }
    ( H9 f! h0 I- B' }" T, a
  4. // RemoveAddress removes the address from the book.: i' [& @! Q. f
  5. func (a *AddrBook) RemoveAddress(addr *NetAddress) {
      b) D, X- [  u2 R) ]
  6.         a.mtx.Lock()% Y  W. Q& T9 W- y
  7.         defer a.mtx.Unlock()
    + `6 J6 `7 h7 B" X
  8.         ka := a.addrLookup[addr.String()]
    , r3 J  [  u9 e' V4 F7 u( N! E# |; _
  9.         if ka == nil {6 H6 A3 x( c& ^6 P! U
  10.                 return
    5 C& p: e6 X1 Q) g% N) W1 E: i
  11.         }
    ! S8 w, p3 _9 P+ f$ l( z
  12.         log.WithField("addr", addr).Info("Remove address from book")' }! H: C' I$ f9 N
  13.         a.removeFromAllBuckets(ka), ?, U- {4 ]1 k  u4 F
  14. }
    / }5 W9 d/ m1 _$ p+ D5 I
  15. func (a *AddrBook) removeFromAllBuckets(ka *knownAddress) {) X& }$ }8 M' |; T' ]$ I5 O4 b
  16.         for _, bucketIdx := range ka.Buckets {+ _. |) j0 o; Q" S+ q8 B6 _
  17.                 bucket := a.getBucket(ka.BucketType, bucketIdx)
    5 K) n6 b& l' c( U' c
  18.                 delete(bucket, ka.Addr.String())
    9 H- z' w- ?% _/ |; G. q
  19.         }
    $ u, R" s4 u& L7 l' T# h1 v$ _
  20.         ka.Buckets = nil) f  H( b3 b' }) @
  21.         if ka.BucketType == bucketTypeNew {6 f+ R( M: ]: e& D
  22.                 a.nNew--
    * y$ o% e' |" W4 ~/ v" C! i8 H2 Q
  23.         } else {( f5 P; D/ R4 ^- e
  24.                 a.nOld--
    & l; \( y0 t1 I3 \+ U
  25.         }6 z# A  u6 d5 x* T- ^: o% S3 E" L
  26.         delete(a.addrLookup, ka.Addr.String())! @% Z( K: R$ z; G$ a' v( h. M
  27. }
复制代码
4 P' l0 x: t( [
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

excel436 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    7