Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

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

excel436
179 0 0
简介
& J8 D/ I: o6 ^- |  q% ]https://github.com/Bytom/bytom' I0 `5 N0 K4 e/ B
本章介绍bytom代码P2P网络中addrbook地址簿
1 E; G" ~* ]2 u7 o. Q2 s
% i$ e4 Q" _" ^- k4 _( S4 o6 A作者使用MacOS操作系统,其他平台也大同小异
9 u# @4 x, m. n. g) U$ f1 u

3 J  x1 d- p, Q2 [, {0 ^- D+ j7 z
/ h1 B7 ~4 W  t9 PGolang Version: 1.8% }+ j9 C7 w. @9 X, v7 F
  e4 l1 X" _/ M! S: c2 i9 |, e
addrbook介绍
- L' E/ D3 P: J4 k% t. l. P8 Oaddrbook用于存储P2P网络中保留最近的对端节点地址
4 V7 S  Y. @: ?; L5 r( C- G5 s; V在MacOS下,默认的地址簿路径存储在~/Library/Bytom/addrbook.json
# c7 L- P# T8 T/ [( X; o地址簿格式
3 G& r# k* D( c
  1. ** ~/Library/Bytom/addrbook.json **
    / I# X4 i- R' x
  2. {% M0 e0 V, s3 H* c# H8 r
  3.     "Key": "359be6d08bc0c6e21c84bbb2",+ _7 t3 f3 D& e- X; X5 ?- r/ c
  4.     "Addrs": [& {0 a% C) A2 p1 U& o1 i4 w
  5.         {
    ! t7 R% y' M" H8 P4 L& [
  6.             "Addr": {1 ], v3 z* s4 q5 B' m/ B; D
  7.                 "IP": "122.224.11.144",! B/ d. {' {) R! f
  8.                 "Port": 46657$ |! x' n4 G5 q1 W  _, ]# n
  9.             },9 a- n- W! ?( @/ Q1 V/ K, D- p; {
  10.             "Src": {
    , i2 A$ i% \8 N
  11.                 "IP": "198.74.61.131",
    2 Y! q( B; f: q8 X$ T/ X
  12.                 "Port": 46657
    2 t. |# @( q+ h' j" c$ z+ O( H
  13.             },' v  T- h1 u# D, _2 G- s
  14.             "Attempts": 0,% ]2 l+ n4 a7 x5 |
  15.             "LastAttempt": "2018-05-04T12:58:23.894057702+08:00",
    0 J( o" F6 \( N% l
  16.             "LastSuccess": "0001-01-01T00:00:00Z",& N0 c5 x0 N9 G8 I
  17.             "BucketType": 1,. @; @% g$ e, {) ~
  18.             "Buckets": [
    + @# b* ^4 D. |$ C
  19.                 181,8 S' g- `: |  a' u6 Z
  20.                 10* I% \# C" X6 @
  21.             ]: U2 d* }0 J; ]7 i3 ~! f, ^3 D
  22.         }
    " n, r" C- [" n* P2 C% p' t% @
  23.     ]3 C) N9 d% N( y, z  D2 f
  24. }
复制代码
) q: Y/ E8 |/ Y: ?# c
地址类型
2 i" V! V8 t1 p" g  ?7 Y# p. H在addrbook中存储的地址有两种:
: O- ^/ I4 Y4 k, N7 s! v! E
  1. ** p2p/addrbook.go **& ~- [2 O  R$ c4 D( ?
  2. const (
    3 e6 c* F9 I4 F
  3.         bucketTypeNew = 0x01  // 标识新地址,不可靠地址(未成功连接过)。只存储在一个bucket中2 C: A* w# [- B& m! v
  4.         bucketTypeOld = 0x02  // 标识旧地址,可靠地址(已成功连接过)。可以存储在多个bucket中,最多为maxNewBucketsPerAddress个  X( D, o7 ]. U3 w
  5. )
复制代码

. V1 o2 _7 x& X: q6 R, H6 J. {( t0 `7 v! T
注意: 一个地址的类型变更不在此文章中做介绍,后期的文章会讨论该问题. B6 s# Z* U' j/ ~: p' a

" N' u" [7 U' C7 W6 S4 }2 G9 E3 q地址簿相关结构体
4 Y) p! S1 G! M0 B3 h地址簿
( G& ^, W" E# I7 K, j
  1. type AddrBook struct {
    % ~  y+ J' }0 Y/ N% B. [9 b
  2.         cmn.BaseService4 J; ]5 k$ N* b% p# N* t; |; O
  3.         mtx               sync.Mutex
    * N  c$ n8 N  _8 c% ?% z1 S
  4.         filePath          string  // 地址簿路径5 E! u) _7 f' e  W" O& |
  5.         routabilityStrict bool  // 是否可路由,默认为true
    , `6 ], U! w" j& L$ r5 U
  6.         rand              *rand.Rand
    1 Q+ N6 n4 o% i+ j) M3 c  r! ~
  7.         key               string  // 地址簿标识,用于计算addrNew和addrOld的索引
    3 v0 m. H$ f; b7 f4 j- G
  8.         ourAddrs          map[string]*NetAddress  // 存储本地网络地址,用于添加p2p地址时做排除使用
    5 i2 J  `: D1 D2 t
  9.         addrLookup        map[string]*knownAddress // 存储新、旧地址集,用于查询" d' T3 ^/ ~+ W, _9 @! U1 M' P# q
  10.         addrNew           []map[string]*knownAddress // 存储新地址8 F& z8 y0 L; f, a; E$ a3 }
  11.         addrOld           []map[string]*knownAddress // 存储旧地址
    + b) M% I& O) p, P5 S
  12.         wg                sync.WaitGroup) {" Q# [, l" @  ^
  13.         nOld              int // 旧地址数量
    0 x$ s$ q* n% @
  14.         nNew              int // 新地址数量1 i1 k+ f6 F) ]8 ?5 d+ U# I% I
  15. }
复制代码
1 ?) _! {: D* }; @2 g
已知地址
# J1 |9 Y( F# C: s" Z
  1. type knownAddress struct {
    6 N5 ]1 G. a( j* `
  2.         Addr        *NetAddress // 已知peer的addr/ q; X, k- ]: s, w
  3.         Src         *NetAddress // 已知peer的addr的来源addr4 `9 T  e; ^( f! M
  4.         Attempts    int32 // 连接peer的重试次数3 a' M( }+ Q5 P0 ^, N* ?  _5 a. Z
  5.         LastAttempt time.Time // 最近一次尝试连接的时间
    ! l4 p! K! o) d4 E; q% Y
  6.         LastSuccess time.Time // 最近一次尝试成功连接的时间, G0 [$ t" x/ Q
  7.         BucketType  byte // 地址的类型(表示可靠地址或不可靠地址)' Q" H  F; B/ r5 s; |
  8.         Buckets     []int // 当前addr所属的buckets; d* K3 d2 f7 J6 {  F
  9. }
复制代码
) |# F% s) Y5 n0 i
routabilityStrict参数表示地址簿是否存储的ip是否可路由。可路由是根据RFC划分,具体参考资料:RFC标准
( }- j4 }* b+ u9 y1 d, q初始化地址簿
) o$ q# U% s: u' F3 K: F
  1. // NewAddrBook creates a new address book.$ X  {' _' |' }1 |' I3 p' x
  2. // Use Start to begin processing asynchronous address updates." F& c, I/ t' D) y6 C0 J
  3. func NewAddrBook(filePath string, routabilityStrict bool) *AddrBook {
    2 {$ }* z$ x: {0 s8 i0 A0 }
  4.         am := &AddrBook{
    ; I9 i9 j1 p* T- C  D- ^
  5.                 rand:              rand.New(rand.NewSource(time.Now().UnixNano())),
    9 C; e8 t- O  x8 h
  6.                 ourAddrs:          make(map[string]*NetAddress),' v. @8 p7 {- Y; _9 o$ a+ {+ p5 M9 ?
  7.                 addrLookup:        make(map[string]*knownAddress)," g2 R: O/ b3 d+ y4 |: b
  8.                 filePath:          filePath,2 S+ f+ A8 u2 Q- L6 {. X
  9.                 routabilityStrict: routabilityStrict,# H" x1 Z' F3 x
  10.         }) a5 C! j2 T; d) `
  11.         am.init()
      `4 }- n$ ^# t2 ^1 U
  12.         am.BaseService = *cmn.NewBaseService(nil, "AddrBook", am)/ E' S1 {% A$ z) J
  13.         return am
    6 B- a7 Y4 r2 q8 b( v8 w% E1 j
  14. }& M! P9 G: `( h3 x9 V
  15. // When modifying this, don't forget to update loadFromFile()
    ( \5 m3 r! H, a: @$ |8 X
  16. func (a *AddrBook) init() {
    2 J! x6 p9 D" a
  17.   // 地址簿唯一标识
    5 d& p$ K8 ^' F4 R
  18.         a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits
    & t& Y, \4 `: ?) h; w! q1 v
  19.         // New addr buckets, 默认为256个大小
    * g: I8 K+ c, ~* t
  20.         a.addrNew = make([]map[string]*knownAddress, newBucketCount)& p' L; U5 r% V- i: z' m
  21.         for i := range a.addrNew {
    + m0 u2 x4 \8 T6 T5 [
  22.                 a.addrNew<i> = make(map[string]*knownAddress)$ J! z! P* }8 K7 v! B
  23.         }
    9 G3 `" [1 x! y' h4 _
  24.         // Old addr buckets,默认为64个大小- ~3 ?# u( t. o& ?( O( g0 c$ d" R0 q
  25.         a.addrOld = make([]map[string]*knownAddress, oldBucketCount)4 V; D  l: i, S' Q  p6 f
  26.         for i := range a.addrOld {! `% q* I9 j: M
  27.                 a.addrOld<i> = make(map[string]*knownAddress)# w9 D4 S- c) q  A+ a
  28.         }% X' {1 w3 V$ g& O4 {% A/ _7 N! d
  29. }</i></i>
复制代码
/ x3 _2 v* k; S2 [9 |) m7 t" y& t
bytomd启动时加载本地地址簿% F- ], @, G( O, w* B
loadFromFile在bytomd启动时,首先会加载本地的地址簿
0 n* p; n6 f, A  C5 N, i. M" D
  1. // OnStart implements Service./ c$ B2 }( K1 J! @- B/ W  Y
  2. func (a *AddrBook) OnStart() error {8 d5 Y% K+ L3 q. Q& X
  3.         a.BaseService.OnStart()5 x# a. h" l( b# q* K5 Z& C
  4.         a.loadFromFile(a.filePath)' l# A0 S: l* F$ K+ {$ ~
  5.         a.wg.Add(1)
    / g+ d  ^0 s* ^% g. i: r
  6.         go a.saveRoutine(). z0 d& `7 E7 K7 A
  7.         return nil
    0 }/ _3 R$ g2 |, W" E  l. O9 X
  8. }
    # q4 J: K! ], S* {# W
  9. // Returns false if file does not exist." Q9 C; J; ]1 _7 D& u
  10. // cmn.Panics if file is corrupt.
    & N. C/ g! ]$ T7 }$ W# }
  11. func (a *AddrBook) loadFromFile(filePath string) bool {
    1 Z5 |# J6 t- A; P% r7 w1 ^5 K5 w
  12.         // If doesn't exist, do nothing.8 J+ g! [7 g- Z* ~8 ?+ h; o' y8 p
  13.         // 如果本地地址簿不存在则直接返回1 Z# w" s0 L2 q$ T2 r3 e
  14.         _, err := os.Stat(filePath)5 O$ z/ E5 l. j! y3 @5 S
  15.         if os.IsNotExist(err) {
    . s! X+ {5 s. g5 s7 W4 P$ x
  16.                 return false
    ! Y0 A  o) e3 {) M. m: R7 ~
  17.         }
    8 E* H! d3 j" A8 T8 i7 v9 `. Y
  18.   // 加载地址簿json内容2 I" {* q: _$ L1 E
  19.         // Load addrBookJSON{}
    7 A* w/ A) K1 `6 x! R
  20.         r, err := os.Open(filePath)
    ! Q/ N+ d5 z  I* ~9 q- t- o
  21.         if err != nil {5 K/ w4 j& D; K4 e: K& I
  22.                 cmn.PanicCrisis(cmn.Fmt("Error opening file %s: %v", filePath, err))
    * h/ B# d) r& x* ]( n
  23.         }: Y+ V1 Z- J$ W( J6 F) V4 ]/ n. P
  24.         defer r.Close()5 ^& f  O3 r# }
  25.         aJSON := &addrBookJSON{}/ Y! B" d6 D2 A# F& `: ]7 `
  26.         dec := json.NewDecoder(r)
    % Y7 O# e  G9 q, a
  27.         err = dec.Decode(aJSON)* r6 z5 T* E; ^$ y- B
  28.         if err != nil {
    ( v9 a: [% c" a( |
  29.                 cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", filePath, err)): K( `9 E6 P7 F6 c, [- l+ Q2 R+ T' A
  30.         }
    0 u' W2 _* }# S" }5 u/ x+ O
  31.   // 填充addrNew、addrOld等
    * h6 J' t& W  C7 t
  32.         // Restore all the fields...- S: q$ Z1 j" J" [) [, Y. u
  33.         // Restore the key
    $ v+ Z1 h) U- p7 [' L4 @! H8 W
  34.         a.key = aJSON.Key1 M4 d# L. m' o5 a
  35.         // Restore .addrNew & .addrOld% r+ r: Y: e2 \' U- y
  36.         for _, ka := range aJSON.Addrs {, |: [  Q, \7 T6 _5 S
  37.                 for _, bucketIndex := range ka.Buckets {
    # ^& T- I4 Q3 i9 x3 a$ O# U
  38.                         bucket := a.getBucket(ka.BucketType, bucketIndex)
    - y% m. @( U7 r" c& a
  39.                         bucket[ka.Addr.String()] = ka4 D6 M' L( q% }5 g' c0 s- O
  40.                 }
    9 x$ w9 J9 j& P4 V: B; B# m) t6 w
  41.                 a.addrLookup[ka.Addr.String()] = ka
    3 N: i7 A' l/ e8 j
  42.                 if ka.BucketType == bucketTypeNew {( K5 ^3 c  o+ s
  43.                         a.nNew++
    1 V: _6 z- O5 a1 [4 ]5 I3 L' {
  44.                 } else {
    5 J4 {  D, Y, \% o4 ^
  45.                         a.nOld++3 O3 W" e$ e8 P& W1 F/ Y: g0 y
  46.                 }, ]/ k7 j7 @6 l; F  _* K
  47.         }
    * Q# }& X. ~& O) I, S
  48.         return true
    + |7 P2 A2 j- C! m
  49. }
复制代码

/ Y3 l9 L: V- R# p2 J/ P0 _7 N! |( _定时更新地址簿$ i/ i2 ]% ^( _6 D- Z
bytomd会定时更新本地地址簿,默认2分钟一次
9 L  c/ E3 d' f( a, V* ?
  1. func (a *AddrBook) saveRoutine() {
    1 u, G" I$ e: A# b4 D% w( I
  2.         dumpAddressTicker := time.NewTicker(dumpAddressInterval)
    : j# Q) _: I) E4 t1 W8 k: f  k& g
  3. out:
    ; L( p- Q* s4 i& D& X+ h5 v$ W1 o
  4.         for {
    1 R* v, a- w6 ~" u! K1 S
  5.                 select {8 r' i; s/ y9 R' H% y2 ^0 ?
  6.                 case
复制代码
( D( B) \5 \+ F, |5 O
添加新地址
5 M& w! p. }6 i, c* K- d0 j当peer之间交换addr时,节点会收到对端节点已知的地址信息,这些信息会被当前节点添加到地址簿中7 l( U* v- E+ X. L" N6 R
  1. func (a *AddrBook) AddAddress(addr *NetAddress, src *NetAddress) {1 ~# B2 [: N- p" b$ j7 q) u
  2.         a.mtx.Lock()
    6 U5 n& T) G- l+ l8 k
  3.         defer a.mtx.Unlock()
    2 H( ~# _9 u+ S6 @3 ]$ F& a2 |
  4.         log.WithFields(log.Fields{
    2 [- Z$ e: n8 t% E9 A$ z4 o0 o, B
  5.                 "addr": addr,0 j: f/ y  {# D: B) b
  6.                 "src":  src,
    ! P9 a/ C  R- E6 C; c, ~
  7.         }).Debug("Add address to book"), i& \1 u6 J) A* t
  8.         a.addAddress(addr, src)* P" \9 S8 a( @1 G7 G' d
  9. }; ~1 f' ~) l: g0 ^5 _$ z
  10. func (a *AddrBook) addAddress(addr, src *NetAddress) {/ r' G$ _! ?: v: w9 O4 ~9 ^4 v! I9 _
  11.         // 验证地址是否为可路由地址
    - m, ~/ c5 }7 |4 G
  12.         if a.routabilityStrict && !addr.Routable() {/ `1 i8 s& t* ^: p
  13.                 log.Error(cmn.Fmt("Cannot add non-routable address %v", addr))
    3 |  t; w6 {* d# m1 [
  14.                 return
    ( H0 l, ?' m( p9 B, @0 m- k: Y
  15.         }& D6 _4 k+ _, ^$ E
  16.         // 验证地址是否为本地节点地址
    % o3 p! F; v- R& t; ]- o
  17.         if _, ok := a.ourAddrs[addr.String()]; ok {: p8 o3 n4 S8 d* o0 d% W& d
  18.                 // Ignore our own listener address.1 m" `* \/ A6 G' u# V
  19.                 return0 J. g" {  N9 N- p5 L6 M0 L
  20.         }6 L  `# z1 M' U' Q' K2 L* a  Q
  21.         // 验证地址是否存在地址集中
    8 `. V: [7 g% e/ m0 u
  22.         // 如果存在:则判断该地址是否为old可靠地址、是否超过了最大buckets中。否则根据该地址已经被ka.Buckets引用的个数来随机决定是否添加到地址集中
    9 Z/ t( I$ m! R+ D5 v2 U3 q
  23.         // 如果不存在:则添加到地址集中。并标识为bucketTypeNew地址类型6 ?# k9 g6 z( ^2 r* J
  24.         ka := a.addrLookup[addr.String()]
    % @/ t7 F# l) e; O
  25.         if ka != nil {
    ! n: o* ^( \2 b7 i  [; p* U1 V, a( z
  26.                 // Already old.
    ! B' Q4 k; O* o; f- G  w/ b( k5 X: Z; O
  27.                 if ka.isOld() {
    1 z- a$ |8 ^1 C; }4 l
  28.                         return
    / \6 d% w& y6 q
  29.                 }1 L, i1 g1 p8 F3 |6 l, f6 [6 b
  30.                 // Already in max new buckets.: ]0 h- _3 ?$ r
  31.                 if len(ka.Buckets) == maxNewBucketsPerAddress {( W/ t7 J% J5 @: }( `: K1 A0 z
  32.                         return+ e  p  s( a7 k9 J
  33.                 }
    6 t, R6 l4 C1 V" W" S  Y! X
  34.                 // The more entries we have, the less likely we are to add more.
    8 x8 h9 f  X6 J( W- \+ G3 D2 e
  35.                 factor := int32(2 * len(ka.Buckets))
    * X$ _% f2 I+ r8 {  M
  36.                 if a.rand.Int31n(factor) != 0 {
    0 S- x& ?7 j3 t( P6 `
  37.                         return0 n3 Y6 p; e( z. X
  38.                 }( i) w& \9 A5 C; L
  39.         } else {. A' R! }! E' ]( l$ |1 t
  40.                 ka = newKnownAddress(addr, src)0 |* v- F3 [! A0 T# q7 E+ A
  41.         }4 b3 k" t- H) l" g/ z$ ~% r* N
  42.         // 找到该地址在地址集的索引位置并添加- E- {- b! U& |9 ~$ N. m$ J% r  A
  43.         bucket := a.calcNewBucket(addr, src)
    5 ]1 V) {* O  d6 t+ D
  44.         a.addToNewBucket(ka, bucket)
    5 z! W) j" ^2 f( o* R# E7 o- L
  45.         log.Info("Added new address ", "address:", addr, " total:", a.size())
    . R! V$ w% @2 ~8 z/ `
  46. }
复制代码
3 ]. W( Q# h# f( l/ \, O' d/ I. T
选择最优节点
% s8 Y# R' ^2 C! r6 b# [% X) ~地址簿中存储众多地址,在p2p网络中需选择最优的地址去连接$ c8 \( x# X; G1 G$ b- a0 d. U
PickAddress(newBias int)函数中newBias是由pex_reactor产生的地址评分。如何计算地址分数在其他章节中再讲
- ]( ]7 T) ?) X, L) b根据地址评分随机选择地址可增加区块链安全性/ c) N) @  `" ]8 {( T
  1. // Pick an address to connect to with new/old bias.
    # z1 k; _$ l5 }6 V, ]
  2. func (a *AddrBook) PickAddress(newBias int) *NetAddress {! c! f6 S( w" h; K. U
  3.         a.mtx.Lock()- B. {2 _5 ^% U, t6 G1 U: J
  4.         defer a.mtx.Unlock()
    6 r" `( q/ m- ]1 @
  5.         if a.size() == 0 {- o" Z0 ~9 @: D" t; g
  6.                 return nil
    + n" W; I0 q9 D- m4 s1 z
  7.         }
    " K" x. X1 E& e+ X% @
  8.         // newBias地址分数限制在0-100分数之间
    6 L' G$ X$ ~  i7 M- l! O- {- G
  9.         if newBias > 100 {
    * g7 c2 ^" ~# l/ _
  10.                 newBias = 100
    # N, B; T8 a# L7 l$ I
  11.         }! K: m& i. n4 z( S0 y
  12.         if newBias
复制代码
* b) X) v6 h+ ~+ t) s$ c# e# k
移除一个地址: G; \& g$ {! N, z  K* f1 Q
当一个地址被标记为Bad时则从地址集中移除。目前bytomd的代码版本并未调用过
1 i' n6 C4 H, {9 {: v3 I
  1. func (a *AddrBook) MarkBad(addr *NetAddress) {$ M6 d+ p& Z7 c0 \
  2.         a.RemoveAddress(addr)6 f6 V& [9 R& n( G- i' C
  3. }
    1 [: N, S* |# a* R7 f' K$ ^/ A  w
  4. // RemoveAddress removes the address from the book.
    4 O9 @3 y! m; Q& N
  5. func (a *AddrBook) RemoveAddress(addr *NetAddress) {
    , d  j6 O2 l; K! I. Y/ B
  6.         a.mtx.Lock(): f8 a# ~! I) Q0 R* \# @6 v
  7.         defer a.mtx.Unlock()" T: r. s/ L9 f" k: P
  8.         ka := a.addrLookup[addr.String()]
    + N5 ]9 h* Q/ V$ e+ P
  9.         if ka == nil {* M% N2 @( G3 s* W
  10.                 return, z7 E, H5 O+ F, Z
  11.         }
    " T  \2 R" W* Y" E0 N
  12.         log.WithField("addr", addr).Info("Remove address from book")" o7 z4 c6 R1 k3 D
  13.         a.removeFromAllBuckets(ka)
      a2 h3 z3 o' w8 m
  14. }/ h6 Y, L( L' n
  15. func (a *AddrBook) removeFromAllBuckets(ka *knownAddress) {2 K+ X8 U) Z) L0 S5 ^8 e
  16.         for _, bucketIdx := range ka.Buckets {" G0 `- m5 g/ J
  17.                 bucket := a.getBucket(ka.BucketType, bucketIdx)9 l' c* ]. H) V% {8 R' `2 f+ u
  18.                 delete(bucket, ka.Addr.String())
    ' K( W& ?" @, p; o
  19.         }
    : d% T% v. V, G+ A% ~
  20.         ka.Buckets = nil4 s4 ^/ b: D. U" W: K7 v8 Y# k
  21.         if ka.BucketType == bucketTypeNew {
    ; ]8 F- {# o" V4 T+ j2 I: }
  22.                 a.nNew--2 k  s7 b, Z9 g) U* T3 D$ m8 r
  23.         } else {
    ' u! ]0 D! S+ B/ }
  24.                 a.nOld--9 q- K' T% d9 ]
  25.         }
    ! [- Y1 f+ ~% h4 D- L# z' U3 K
  26.         delete(a.addrLookup, ka.Addr.String())
    ( [; e% q, K, a' U; V
  27. }
复制代码
: _& `. D, v8 F
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

excel436 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    7