Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

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

excel436
138 0 0
简介
' u. S2 D9 v$ X) fhttps://github.com/Bytom/bytom  s& {8 O/ v0 O8 K8 b
本章介绍bytom代码P2P网络中addrbook地址簿" [! x, r7 j6 T0 T. g; @

, F# N* g' C7 Q% s" Z7 _作者使用MacOS操作系统,其他平台也大同小异! v. C# J/ p# q' R% C

* o) H6 U1 P0 f% @2 [- J& Y# `
# H( z# f& z1 t- r7 ]Golang Version: 1.86 @: h  L! l+ K; ]

. d) v- L- H' B/ {. D& daddrbook介绍4 i! u! l: e" D- w' L
addrbook用于存储P2P网络中保留最近的对端节点地址( Z" g2 G$ B' |# C5 [
在MacOS下,默认的地址簿路径存储在~/Library/Bytom/addrbook.json( X- [5 J) F7 P' r2 |' p* \& @. t
地址簿格式
1 F3 M9 S4 u* S* W8 |
  1. ** ~/Library/Bytom/addrbook.json **
    - B9 z' t! F$ |1 b) ~& B
  2. {
    : T1 H' x, O+ t
  3.     "Key": "359be6d08bc0c6e21c84bbb2",
    0 [6 i& s) G9 e/ d! u; \/ i) f
  4.     "Addrs": [- t/ V: x: J/ N3 K5 l
  5.         {
    ( ~: }  G# J( T. S
  6.             "Addr": {% p# p/ c# \2 H& _6 S$ f5 _6 |
  7.                 "IP": "122.224.11.144",/ N' }0 ]% H7 K: @" c  M/ i$ i3 h
  8.                 "Port": 466574 A  Q* ?  o& M) a" J3 F" `" d
  9.             },
    + `7 v, @* C5 B; _0 k
  10.             "Src": {5 a( ]2 @# T) W- Y& Y, I
  11.                 "IP": "198.74.61.131",0 Y/ q/ z7 O4 O' p5 Q- O( X
  12.                 "Port": 46657
    3 S: B1 c; I* s" s
  13.             },0 |* ^$ S  T" v) v
  14.             "Attempts": 0,3 o4 }! g* H! s1 m  _, q
  15.             "LastAttempt": "2018-05-04T12:58:23.894057702+08:00",8 y* X* ]* L0 N: c
  16.             "LastSuccess": "0001-01-01T00:00:00Z",
    - J& a- d! z1 E- S& r) B
  17.             "BucketType": 1,2 M8 V' Y" k8 E2 s
  18.             "Buckets": [
    8 e$ _* C! Y0 p, p# j
  19.                 181,
    5 r4 W# O  \8 y% ^8 F: x
  20.                 10
    5 C  {# |0 ?, O0 R& {6 i
  21.             ]3 ~! b' u, Z, B" Q
  22.         }
    " i  K& I7 {1 ^* i9 {. L
  23.     ]0 q% y# I6 E7 [% T5 ]
  24. }
复制代码

8 |; t* C5 T9 Z8 W6 \; V' ?+ Q: L0 E地址类型+ _  \3 I  t2 a) ]5 y
在addrbook中存储的地址有两种:
  O( N: j  ^2 r7 K% z! {
  1. ** p2p/addrbook.go **
    - @+ G" m- Y' L) R! B4 e
  2. const (, I% u2 `! r) F( ~
  3.         bucketTypeNew = 0x01  // 标识新地址,不可靠地址(未成功连接过)。只存储在一个bucket中' Q  W, x, B' A0 ]
  4.         bucketTypeOld = 0x02  // 标识旧地址,可靠地址(已成功连接过)。可以存储在多个bucket中,最多为maxNewBucketsPerAddress个' N( C, d+ b) q* k6 D  p: ^6 B+ D( G
  5. )
复制代码

+ D1 a* k/ J5 Q1 R) P5 b8 q
- X* p; S5 z, R1 y注意: 一个地址的类型变更不在此文章中做介绍,后期的文章会讨论该问题
' b' z  t# [$ h- l' z- ]) H9 ?" E7 {( u; W% `/ l; ]
地址簿相关结构体
2 Y3 J  A3 F. @( Y5 z' D8 ?1 e地址簿& I8 [  S: Z" U" `* I0 `& d. T
  1. type AddrBook struct {; s' L4 L3 r2 U0 Z; x
  2.         cmn.BaseService
    , @! d0 g& `3 i* b+ I
  3.         mtx               sync.Mutex8 m7 H' Z" R, N" M& y3 i2 H
  4.         filePath          string  // 地址簿路径
    & O* w3 T" @* ?' S
  5.         routabilityStrict bool  // 是否可路由,默认为true8 A$ O- W! L/ ~0 ^3 z& T/ ^
  6.         rand              *rand.Rand
    3 V1 z3 p* a. o9 [' |5 O* f4 U
  7.         key               string  // 地址簿标识,用于计算addrNew和addrOld的索引! v1 B8 K" `- z$ z% L' m
  8.         ourAddrs          map[string]*NetAddress  // 存储本地网络地址,用于添加p2p地址时做排除使用
    $ n( p% ?: l2 e5 |* ^  j
  9.         addrLookup        map[string]*knownAddress // 存储新、旧地址集,用于查询) i- M: l$ n+ W( X. i
  10.         addrNew           []map[string]*knownAddress // 存储新地址" I! U9 O/ S! T& F
  11.         addrOld           []map[string]*knownAddress // 存储旧地址  V3 L' u& H. y
  12.         wg                sync.WaitGroup
    # M) ~  ^) o# p; Z( ~
  13.         nOld              int // 旧地址数量
    " x4 P# `  j9 L$ d2 w5 i; r
  14.         nNew              int // 新地址数量
    : B" b2 f% Z1 Y, Z
  15. }
复制代码

7 d! @2 n, U, j) ?已知地址
/ r) Y" [  ~# H! ]8 y  i: X
  1. type knownAddress struct {
    ; e( J- _  L* m9 G
  2.         Addr        *NetAddress // 已知peer的addr
    * Z4 z+ z1 Y7 [- q: h
  3.         Src         *NetAddress // 已知peer的addr的来源addr
    8 [- P% W9 g0 n$ i0 m5 @8 m1 y8 v7 q
  4.         Attempts    int32 // 连接peer的重试次数
    7 f/ i+ x7 G& {- B+ Z) x
  5.         LastAttempt time.Time // 最近一次尝试连接的时间0 \( D; W0 N+ k9 V) j
  6.         LastSuccess time.Time // 最近一次尝试成功连接的时间, |% j1 e/ A% |  x+ Z3 U
  7.         BucketType  byte // 地址的类型(表示可靠地址或不可靠地址)
    % N3 E2 |$ L% c) [4 Q$ I" p
  8.         Buckets     []int // 当前addr所属的buckets
    / m8 b7 m+ e3 t7 \
  9. }
复制代码

/ i. k2 w. m: k7 troutabilityStrict参数表示地址簿是否存储的ip是否可路由。可路由是根据RFC划分,具体参考资料:RFC标准5 S- m; _% B2 s  v% J6 t
初始化地址簿
7 v3 n0 z$ J- o- @9 K# u1 p7 ~
  1. // NewAddrBook creates a new address book.* W& p* p* D+ w. P. C" \
  2. // Use Start to begin processing asynchronous address updates.) o( e! {& ~0 }7 Q5 B0 I; C/ h
  3. func NewAddrBook(filePath string, routabilityStrict bool) *AddrBook {
    $ Y( s; P& ~1 o$ W2 ^3 x! t
  4.         am := &AddrBook{7 o$ C% N4 {6 @5 w& V2 H/ O" j
  5.                 rand:              rand.New(rand.NewSource(time.Now().UnixNano())),
    9 |7 x" @' c- c+ I4 Z
  6.                 ourAddrs:          make(map[string]*NetAddress),
    # w/ ^5 E! C6 s, z
  7.                 addrLookup:        make(map[string]*knownAddress),6 k& z3 ]% C% k: n+ D: t
  8.                 filePath:          filePath,
    0 e" u0 A5 K) g+ p  `/ I# b
  9.                 routabilityStrict: routabilityStrict,
    ' m) I6 L+ Y2 g0 M7 S
  10.         }1 j3 n3 H3 s! z8 z2 x
  11.         am.init(); V* J: A% A( s* V( ]/ ~0 w" J" T
  12.         am.BaseService = *cmn.NewBaseService(nil, "AddrBook", am)) ^) q7 N" u, H! W3 I, D8 P9 c; w
  13.         return am5 J3 q. t* h8 G4 F
  14. }
    ; E# a" r+ I5 E+ s. K5 H" B! k
  15. // When modifying this, don't forget to update loadFromFile()
    7 H8 _) j+ H3 G" w7 W# R: M
  16. func (a *AddrBook) init() {) I  L# O& H4 @1 u  Q
  17.   // 地址簿唯一标识
    . Y7 P* b" K% ]& L
  18.         a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits6 q2 S; u1 K# }* w: N% w# T
  19.         // New addr buckets, 默认为256个大小
    " o+ Q( A# T* |- T2 C: r
  20.         a.addrNew = make([]map[string]*knownAddress, newBucketCount)5 @, f7 g5 V/ q9 s  t
  21.         for i := range a.addrNew {7 H5 O/ @9 \3 D* |: Y' S
  22.                 a.addrNew<i> = make(map[string]*knownAddress)
    9 J2 o- F. e: O5 [% d
  23.         }
    ! D3 K, \, j+ K7 I
  24.         // Old addr buckets,默认为64个大小
    8 g1 m+ I* w$ _. ~/ h
  25.         a.addrOld = make([]map[string]*knownAddress, oldBucketCount)$ g$ U+ D1 z* t. k2 _+ y/ n
  26.         for i := range a.addrOld {6 i8 Q7 j1 \7 U1 y# u# v5 p
  27.                 a.addrOld<i> = make(map[string]*knownAddress)# I* S) c  y$ g& k
  28.         }5 ~9 }6 P0 h) |( [* W+ T
  29. }</i></i>
复制代码

5 r5 C$ S/ ]7 R' T+ m6 F- t9 Jbytomd启动时加载本地地址簿1 M' d' A( n, R8 a4 o, W, X
loadFromFile在bytomd启动时,首先会加载本地的地址簿0 B8 c; ~1 W0 U: W
  1. // OnStart implements Service.( ~/ o" Y, y8 e. F) q8 ^, D- W
  2. func (a *AddrBook) OnStart() error {$ p% g$ |- Q2 w+ h
  3.         a.BaseService.OnStart()( A* R( o) h: [/ R* c
  4.         a.loadFromFile(a.filePath)+ r. O% A; i8 E- a
  5.         a.wg.Add(1)
    2 A( H+ p5 q( I7 M
  6.         go a.saveRoutine()1 y' ]: i( m5 J6 u& a2 s
  7.         return nil$ B, {- Q" r0 F) F" g8 l# s5 G! V
  8. }
    * w; L# I" Z* ~' q. G# p$ c
  9. // Returns false if file does not exist.* V3 {: [8 a9 y: ~) W1 F
  10. // cmn.Panics if file is corrupt.: a9 D! C7 m: P3 ]  Z
  11. func (a *AddrBook) loadFromFile(filePath string) bool {
    # p: }. o( Q6 g  K+ N  t) \; t
  12.         // If doesn't exist, do nothing.4 r, c% \; k9 w
  13.         // 如果本地地址簿不存在则直接返回! T3 k. i! q* E9 W: ^; C, d3 J
  14.         _, err := os.Stat(filePath)
    , `3 g& o) @' w  c( j
  15.         if os.IsNotExist(err) {# z# H* Q& T' I0 R) M  w7 u1 Y" F0 {
  16.                 return false+ U! k# J/ i9 O2 o( I
  17.         }
    1 o0 m" ~, y0 M- \1 \( `" m
  18.   // 加载地址簿json内容% |9 _6 W& D, H) I& X& h
  19.         // Load addrBookJSON{}
    " f' h. V5 _2 U( h& X; e, I
  20.         r, err := os.Open(filePath)! h- z3 u: O) u9 x5 f8 q* ]0 f
  21.         if err != nil {+ f9 B* {" o" C8 O* T5 r
  22.                 cmn.PanicCrisis(cmn.Fmt("Error opening file %s: %v", filePath, err))
    7 @1 O2 k' `/ p0 k# Y$ _
  23.         }& d: ^3 D' }5 |  [! V
  24.         defer r.Close()
    - q' \5 ^( t3 P
  25.         aJSON := &addrBookJSON{}) `# Z9 i4 T1 K1 [$ _5 ^
  26.         dec := json.NewDecoder(r)0 Q! d; u! t$ b9 W: y0 Z- {
  27.         err = dec.Decode(aJSON)1 F& Y1 K: c5 K' Q. p) A; y; r- n) o
  28.         if err != nil {
    : A0 }+ f$ W8 }/ j; \7 T; N
  29.                 cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", filePath, err))6 G# b) ~% e& X( x1 J4 p
  30.         }
    ; C' `  O4 F4 ]+ {! `' \
  31.   // 填充addrNew、addrOld等4 K7 [5 c. v/ i: ?/ \2 R7 ?7 \$ F
  32.         // Restore all the fields...
    9 p- \" p! |# r; t; e
  33.         // Restore the key; s4 G: m9 ?" B( d8 s# X8 `* j) z
  34.         a.key = aJSON.Key
    2 z/ Z3 f5 h: n+ M6 J0 r" z) r4 t. F! @, @
  35.         // Restore .addrNew & .addrOld
    # F( q8 t; u$ w9 Z  h+ \
  36.         for _, ka := range aJSON.Addrs {: u4 a7 v; y& Y: B7 I
  37.                 for _, bucketIndex := range ka.Buckets {
    4 R9 |! _. q9 p. Q
  38.                         bucket := a.getBucket(ka.BucketType, bucketIndex)
    ; G+ ?/ ]; Q' w$ S
  39.                         bucket[ka.Addr.String()] = ka
    " t1 [, A3 E* S
  40.                 }
    - w  v/ a$ X$ p' W
  41.                 a.addrLookup[ka.Addr.String()] = ka+ u: @1 g, g6 N+ j& f  n8 I
  42.                 if ka.BucketType == bucketTypeNew {
    7 @2 F- }! X0 s
  43.                         a.nNew++; t/ q4 t6 s" a: `/ Q
  44.                 } else {" r# J4 b* f: q- q
  45.                         a.nOld++: q- k# N' G- N, u, Y
  46.                 }  ?7 d* e2 ]; p  x
  47.         }
    1 M, e) p; k, ~
  48.         return true9 A; Q) }& G' }2 z6 v/ i1 W
  49. }
复制代码

: `1 I3 }) C: ^) y# p+ q定时更新地址簿
' ~' q4 }) ]' _2 J; ebytomd会定时更新本地地址簿,默认2分钟一次) y9 X0 j! {6 ]* W( P- h
  1. func (a *AddrBook) saveRoutine() {" a/ q# J7 |: Z/ I
  2.         dumpAddressTicker := time.NewTicker(dumpAddressInterval)
    % w  P9 e9 x# `$ x9 Y$ N5 W& j6 Y
  3. out:
    5 F8 z+ X5 h; W, D' H  C/ O( [+ _) x
  4.         for {
    9 X: h+ \6 N# Q& b$ ?
  5.                 select {
      W  i" e6 N6 M( G3 g9 a! {
  6.                 case
复制代码

% E: n! W& C2 f9 R添加新地址: E' K- i* R1 ?8 v  N4 F
当peer之间交换addr时,节点会收到对端节点已知的地址信息,这些信息会被当前节点添加到地址簿中
9 `: E  c+ D/ I5 |% {
  1. func (a *AddrBook) AddAddress(addr *NetAddress, src *NetAddress) {
    . {) k# s9 i+ Y8 y1 H# p( E) @4 k
  2.         a.mtx.Lock()% W7 A, p9 A3 b  |9 [5 S
  3.         defer a.mtx.Unlock()
    ' w# S+ G* D6 s2 c2 d7 z) j
  4.         log.WithFields(log.Fields{
    : d  b/ h9 f: ~- p! A
  5.                 "addr": addr,
    / `5 f! z# O: y8 n" a& |
  6.                 "src":  src,6 [9 P4 `# W3 M& t/ X
  7.         }).Debug("Add address to book")9 w" g& r$ y* R/ f. Q
  8.         a.addAddress(addr, src)# e3 m' P/ ]: H
  9. }9 t( ~* z% ^4 \" |3 {; X1 h2 X2 m
  10. func (a *AddrBook) addAddress(addr, src *NetAddress) {1 S0 c( C8 m$ F) O3 [, s+ b2 @
  11.         // 验证地址是否为可路由地址
    - ~) d  h) p9 O, Z# d. X
  12.         if a.routabilityStrict && !addr.Routable() {. D  q, C- m3 x6 ^6 A* Z
  13.                 log.Error(cmn.Fmt("Cannot add non-routable address %v", addr))6 Y# {1 g3 i. L: ], X
  14.                 return
    / A+ U6 m! L0 |5 k4 l/ D6 E
  15.         }
    : h6 e7 f6 W9 D/ @/ f* f
  16.         // 验证地址是否为本地节点地址" C: e1 A, H: _8 w* u
  17.         if _, ok := a.ourAddrs[addr.String()]; ok {
    ) `: e# _1 F: p5 F. D! x
  18.                 // Ignore our own listener address.
    9 r" S3 b% m9 [: B( d( t
  19.                 return: g+ _) ]; I" ]9 D! @; ^
  20.         }
    & I# a' u8 n. g( E. N" p
  21.         // 验证地址是否存在地址集中! s% @% n: C" H0 p9 ~4 T) R" l
  22.         // 如果存在:则判断该地址是否为old可靠地址、是否超过了最大buckets中。否则根据该地址已经被ka.Buckets引用的个数来随机决定是否添加到地址集中) K( l/ v8 N% g$ }6 k. r# c
  23.         // 如果不存在:则添加到地址集中。并标识为bucketTypeNew地址类型$ X6 u$ v2 i0 @. H6 C
  24.         ka := a.addrLookup[addr.String()]; t1 f3 Z- M3 `: w0 Q# S2 e6 ^' u
  25.         if ka != nil {+ _* z# x( z+ S; j& g7 T+ s) c4 I
  26.                 // Already old.) Z* L7 l" i2 a7 U* W6 T1 r
  27.                 if ka.isOld() {9 D  q4 x* N3 ?9 L8 P; y+ y
  28.                         return  [! V1 b: d& P( u* }# b
  29.                 }
    5 O1 p9 o: `) Q) {: R' I* o9 C) S
  30.                 // Already in max new buckets.
    2 }& N/ a- i/ O9 L$ b2 g
  31.                 if len(ka.Buckets) == maxNewBucketsPerAddress {4 q9 y$ W* Y- G! ]- Z
  32.                         return+ x' ]: a- R( S1 ~1 Y
  33.                 }' L+ s! M( F8 f, I( c2 G
  34.                 // The more entries we have, the less likely we are to add more.
    ! d% [+ B1 Q  H3 ?
  35.                 factor := int32(2 * len(ka.Buckets))
    8 \) N% s) J, u; [' c
  36.                 if a.rand.Int31n(factor) != 0 {9 _+ F3 G! y- u% y  }% Z
  37.                         return
    - F: T9 x% r$ U: Y: C
  38.                 }! ]0 p% a! I& L) i8 B
  39.         } else {
    " N& d$ U8 Z/ _, ]4 r2 g2 ^" _
  40.                 ka = newKnownAddress(addr, src), _7 c. ^6 L$ ^2 U, [
  41.         }# D' r% J  x1 c8 g. H4 `% y; J4 C
  42.         // 找到该地址在地址集的索引位置并添加. }& _- Z+ u- s5 ]( P* a$ N& z
  43.         bucket := a.calcNewBucket(addr, src)
    . Z- T' u- h& y- o
  44.         a.addToNewBucket(ka, bucket)' j) E  `; T+ ?; q* c
  45.         log.Info("Added new address ", "address:", addr, " total:", a.size())
    & x1 P0 A( |! K5 u) S3 U4 k
  46. }
复制代码
8 t/ t& ^- j0 Q# j: G
选择最优节点0 l" m) m& j! _& ?
地址簿中存储众多地址,在p2p网络中需选择最优的地址去连接8 J# T: C1 L) y% L! g7 s
PickAddress(newBias int)函数中newBias是由pex_reactor产生的地址评分。如何计算地址分数在其他章节中再讲/ h8 D0 j+ X" G4 c, f( `
根据地址评分随机选择地址可增加区块链安全性- O- G+ `/ @  R7 @6 C6 g
  1. // Pick an address to connect to with new/old bias.6 R; F8 v! ]+ o# |$ p5 ^
  2. func (a *AddrBook) PickAddress(newBias int) *NetAddress {$ K3 J( [' V) T; O
  3.         a.mtx.Lock()) r7 c! Y/ \. s  p
  4.         defer a.mtx.Unlock()+ I" o9 N6 u# X) p) u% `  {
  5.         if a.size() == 0 {! N# y/ H# y* D: e! S
  6.                 return nil
    ' B9 i$ B  ~4 Y. y( W3 I+ x, B' \* }
  7.         }4 K& ~+ V( d; p+ P4 j4 v& F
  8.         // newBias地址分数限制在0-100分数之间+ V3 ]1 d1 \/ {+ S; w
  9.         if newBias > 100 {$ B6 `. \8 ?7 K8 s7 B$ \- M( t
  10.                 newBias = 100
    : W& a6 a8 T# Q2 O. t: f5 ^
  11.         }
    + V5 U" \) c4 g# v
  12.         if newBias
复制代码

; u) ^1 Q0 F% g  X9 t移除一个地址
" T2 e7 ]9 e& k2 k9 Z, i当一个地址被标记为Bad时则从地址集中移除。目前bytomd的代码版本并未调用过' ~8 P; K+ Q& R# K8 r
  1. func (a *AddrBook) MarkBad(addr *NetAddress) {1 s5 |. x( v* ?  [6 W
  2.         a.RemoveAddress(addr), u5 k- d/ F9 Z: H( y; }5 ^( k- w
  3. }
    ' Q" _; Q; |4 c: q6 _
  4. // RemoveAddress removes the address from the book.) I8 r- r" s. D6 l$ ~8 R
  5. func (a *AddrBook) RemoveAddress(addr *NetAddress) {- a. Q7 D& h; g) M% P& B" i
  6.         a.mtx.Lock()/ S/ |6 b5 [  P; _/ C
  7.         defer a.mtx.Unlock()
      y5 {$ A# t( ?/ M5 T
  8.         ka := a.addrLookup[addr.String()]
    8 |$ N6 T% t& D4 m; o# d
  9.         if ka == nil {' X& P- u8 i1 Q  `' Q9 T$ g' e& L
  10.                 return
    9 d* c+ t5 [% O, s, V
  11.         }$ F% A/ v! |2 u4 P5 a/ C# [7 D
  12.         log.WithField("addr", addr).Info("Remove address from book"); B$ j  C6 Y- J
  13.         a.removeFromAllBuckets(ka)7 x' i, w9 z, n% A
  14. }
    , n, y1 b+ ^2 D/ V# P$ E/ u8 A
  15. func (a *AddrBook) removeFromAllBuckets(ka *knownAddress) {8 }7 ]* i: ~& ^0 J2 h
  16.         for _, bucketIdx := range ka.Buckets {
    7 l1 U! b0 t3 Y) d1 _3 U1 z6 D
  17.                 bucket := a.getBucket(ka.BucketType, bucketIdx)
    0 s: C! ]& i+ z7 v
  18.                 delete(bucket, ka.Addr.String())
    ; {3 m/ j4 \1 |& S. C# B
  19.         }
    1 }3 |- J' P) |) [6 e% S! `. C
  20.         ka.Buckets = nil
    + m5 p- u) c; L7 @2 n- H
  21.         if ka.BucketType == bucketTypeNew {
      [7 U$ y; k2 Y. a4 _
  22.                 a.nNew--. ~( X5 ^. s! T+ i7 x5 N
  23.         } else {- [" k- |- G6 k. p
  24.                 a.nOld--
    3 i+ S4 u) c$ E* a
  25.         }
    7 H' i% Y. T2 g* N
  26.         delete(a.addrLookup, ka.Addr.String())6 x2 x! L2 O" c* @- B2 b
  27. }
复制代码

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

本版积分规则

成为第一个吐槽的人

excel436 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    7