Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

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

excel436
133 0 0
简介% ]  g' C6 \1 M8 ^- [) v# @6 @, S
https://github.com/Bytom/bytom( r' t; b" B, q. L7 Z
本章介绍bytom代码P2P网络中addrbook地址簿
& h' P0 u1 }: @& q- M( ]
4 {/ W5 t& K& a- ?, d& ~作者使用MacOS操作系统,其他平台也大同小异" L9 Q! z% a0 q8 ]
* K$ V( ^+ {& u8 q
# |1 c5 S. E4 v1 J1 y7 M, u
Golang Version: 1.8# e" F  p) m/ l! @, a- i

* d4 g% j3 c4 W' p9 g  saddrbook介绍3 k# t2 H2 c: _3 O: Y. t- o
addrbook用于存储P2P网络中保留最近的对端节点地址
. ~& d% H' s  H3 k: n" b在MacOS下,默认的地址簿路径存储在~/Library/Bytom/addrbook.json
1 u9 h( l6 M4 W地址簿格式: `. ~0 {1 y+ j
  1. ** ~/Library/Bytom/addrbook.json **6 {8 ]' s: K9 L
  2. {5 M) Y  P4 U+ k- D- T5 W; r
  3.     "Key": "359be6d08bc0c6e21c84bbb2",# V. u5 m1 L0 z, P9 b/ z
  4.     "Addrs": [
    ! R2 _  |7 i" ^5 v, H2 @
  5.         {
    * i# b* E: k- g4 {! p9 N" r% c4 I4 p
  6.             "Addr": {
    1 W! f* }, C7 a/ l* Q2 x1 n
  7.                 "IP": "122.224.11.144",) j$ K( P6 A& Y4 ]" ?% I* j
  8.                 "Port": 46657+ A; M9 a: Y9 ]
  9.             },9 g7 |, Y6 l3 H4 u( g
  10.             "Src": {) C) V/ m% ^. E7 Q4 B2 Z
  11.                 "IP": "198.74.61.131",. c! r' ^1 c9 Q6 B' @2 i: J7 h
  12.                 "Port": 46657
    & T- M% O% o! f! {! r$ @3 w' z* H
  13.             },) d+ `0 k  e9 e( M' p5 Y
  14.             "Attempts": 0,9 m: V+ G" Y% M- c3 R2 l
  15.             "LastAttempt": "2018-05-04T12:58:23.894057702+08:00",: d9 X  f3 ^( J& Y; x9 C
  16.             "LastSuccess": "0001-01-01T00:00:00Z",7 w/ F! s4 h+ I' W- _9 B! R0 a
  17.             "BucketType": 1,
    - ~8 c9 m7 O5 R) r- Y
  18.             "Buckets": [3 b  {0 l  ]' Q4 o
  19.                 181,
    6 i0 |0 \' }+ N0 q7 C0 s  {
  20.                 10" |( }3 S. O9 P
  21.             ]: X6 h9 J9 `3 `% v5 j! D
  22.         }
    : r+ ^/ h* s* r7 l& b  t# |' v7 c# Y
  23.     ]
    $ q, J) \, W- B' s& t8 B; B
  24. }
复制代码

% r0 f9 Y( T. ^. w, M地址类型
- U. A* R% f6 G; X0 \. O7 @在addrbook中存储的地址有两种:( F, d+ ^9 d( E$ S+ l- M
  1. ** p2p/addrbook.go **! a/ h6 B, x2 W' U; I
  2. const (
    2 l$ o; \- _( a3 e5 K+ v9 s# `
  3.         bucketTypeNew = 0x01  // 标识新地址,不可靠地址(未成功连接过)。只存储在一个bucket中, s" u  l3 b" ?3 V) }: ~
  4.         bucketTypeOld = 0x02  // 标识旧地址,可靠地址(已成功连接过)。可以存储在多个bucket中,最多为maxNewBucketsPerAddress个
    ( x! c1 r+ \4 n( f; Z9 Y$ X
  5. )
复制代码

$ j$ H, @4 W- Z% r" Q9 O2 Q5 J; _. q
# `# S0 y9 S3 S) R8 w+ j" o注意: 一个地址的类型变更不在此文章中做介绍,后期的文章会讨论该问题
0 o5 e2 ]- e7 h3 D! [* x3 D
! r. q9 v  I- {( |  N! Y地址簿相关结构体
: `+ M0 |& X- g! c; b8 c地址簿
! y/ p# {0 u% A) n
  1. type AddrBook struct {
    8 y- J: _1 R  _
  2.         cmn.BaseService
    * F, e/ }4 `4 @: ]8 ^' F
  3.         mtx               sync.Mutex
    9 @; E$ ~% w# w- ]- g; I
  4.         filePath          string  // 地址簿路径7 ]/ u& P! I+ ]7 \2 Q( j
  5.         routabilityStrict bool  // 是否可路由,默认为true
    . p6 o1 }; e& S) Y, \# X
  6.         rand              *rand.Rand 6 b- u+ y. c& a3 ~# ~1 a+ ?
  7.         key               string  // 地址簿标识,用于计算addrNew和addrOld的索引
    " F* K) w! B3 `  L+ ^' p
  8.         ourAddrs          map[string]*NetAddress  // 存储本地网络地址,用于添加p2p地址时做排除使用3 H/ ]+ ^5 G7 m4 b3 O$ h
  9.         addrLookup        map[string]*knownAddress // 存储新、旧地址集,用于查询3 S/ V2 ]- S, D6 V2 S
  10.         addrNew           []map[string]*knownAddress // 存储新地址
    8 y- ^  P$ e9 i$ _7 X
  11.         addrOld           []map[string]*knownAddress // 存储旧地址
    / t  Q* \9 I0 X6 n6 N# `
  12.         wg                sync.WaitGroup) [5 V5 Z. y% p0 r! w
  13.         nOld              int // 旧地址数量9 \, _7 k6 }% c/ `6 C
  14.         nNew              int // 新地址数量) `' |; R; }4 A# Q) r( g. D+ e
  15. }
复制代码
& q- J1 g" U' E) V+ V* u- Z
已知地址+ G6 t5 j0 \' q, Z  _7 @4 m! _
  1. type knownAddress struct {
    % n( s: J4 M& _& I7 K, v' T8 {
  2.         Addr        *NetAddress // 已知peer的addr
    4 Y, J( i" I4 I% L7 q- v
  3.         Src         *NetAddress // 已知peer的addr的来源addr
    # ~6 d& d4 R# ^& e9 I& m4 a% l
  4.         Attempts    int32 // 连接peer的重试次数
    1 k0 u7 G  j% i
  5.         LastAttempt time.Time // 最近一次尝试连接的时间9 b6 l. b' l& z2 K& c
  6.         LastSuccess time.Time // 最近一次尝试成功连接的时间
    3 R8 t7 a0 S! I* ~- m8 N
  7.         BucketType  byte // 地址的类型(表示可靠地址或不可靠地址)
    7 h" T& y" O& p3 a! c
  8.         Buckets     []int // 当前addr所属的buckets
    3 l& M8 C6 z* @$ _4 `# S8 T+ M
  9. }
复制代码
8 `4 y4 t( ]; n
routabilityStrict参数表示地址簿是否存储的ip是否可路由。可路由是根据RFC划分,具体参考资料:RFC标准
) S5 u- A+ _8 t5 ?, e初始化地址簿
5 {+ O) y' ~, t$ L
  1. // NewAddrBook creates a new address book.
    ! f5 R8 N) Q" p( r5 E2 z
  2. // Use Start to begin processing asynchronous address updates./ c0 Y4 ?6 u1 K: Z& F
  3. func NewAddrBook(filePath string, routabilityStrict bool) *AddrBook {/ m/ k* J# `9 w% E- Z
  4.         am := &AddrBook{+ s: T. k: F4 ~9 t
  5.                 rand:              rand.New(rand.NewSource(time.Now().UnixNano())),- Z5 p7 |* \) B% b5 L' r; }" t
  6.                 ourAddrs:          make(map[string]*NetAddress),
    # g% e; `1 Y1 P2 C3 {" H3 E
  7.                 addrLookup:        make(map[string]*knownAddress),* n" q( |8 {/ F
  8.                 filePath:          filePath,
    % L% T9 ^1 r, e( R
  9.                 routabilityStrict: routabilityStrict,0 q" J) {4 v: W/ S7 N0 u
  10.         }9 y, R" D* m# h, {& [: ]5 w
  11.         am.init()! u  ?" s4 Y' Y
  12.         am.BaseService = *cmn.NewBaseService(nil, "AddrBook", am)
    ) W0 C% P: |: ~, h
  13.         return am
    ; x+ o8 b# T& Q0 t5 c; k2 }
  14. }
    % d3 G3 N( J, T) R; c
  15. // When modifying this, don't forget to update loadFromFile()+ q5 g* w1 ]8 T8 I7 u
  16. func (a *AddrBook) init() {: c6 a, A3 p' ^: r  l! I3 R
  17.   // 地址簿唯一标识
    2 e# _- I. k$ y# H
  18.         a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits
    5 d  J+ e- g' G: I
  19.         // New addr buckets, 默认为256个大小7 o& N% _+ ~6 j6 T5 X
  20.         a.addrNew = make([]map[string]*knownAddress, newBucketCount)  k/ D6 B! X& V( ^% B, v5 x
  21.         for i := range a.addrNew {6 `) `" t# ~- x5 B9 E& v
  22.                 a.addrNew<i> = make(map[string]*knownAddress)
    3 }6 \& M& g# A5 }3 a$ @7 {# R" c* s6 y
  23.         }$ \. A+ b0 c7 q$ X  @% k0 }
  24.         // Old addr buckets,默认为64个大小- H, N4 x0 ^7 T  ?1 m
  25.         a.addrOld = make([]map[string]*knownAddress, oldBucketCount)
    5 i& L8 K/ }% r, w3 n
  26.         for i := range a.addrOld {
    . ]/ G- u8 s9 |: K0 e
  27.                 a.addrOld<i> = make(map[string]*knownAddress)/ m, L+ y, y+ ?/ g
  28.         }
    ) Z  |* W& y$ c( X5 ]# ?' N  @
  29. }</i></i>
复制代码

; a% k3 F4 R! k  x' V4 h9 a: [; Q- k- Sbytomd启动时加载本地地址簿! Q4 a2 x7 R" G" t' x$ _% g
loadFromFile在bytomd启动时,首先会加载本地的地址簿, v0 O+ m: A. c' g) H6 J; w
  1. // OnStart implements Service.
    1 K  X# r! ^+ Y: i2 b8 V$ Q
  2. func (a *AddrBook) OnStart() error {9 n- {( y1 [) E9 b
  3.         a.BaseService.OnStart()* o$ Q+ ?- J8 V  C8 _+ i
  4.         a.loadFromFile(a.filePath)
    + |4 }! M# S* i3 d
  5.         a.wg.Add(1)) Z' }- X% Q1 ]% R- M
  6.         go a.saveRoutine()3 P! Y. q! x! W
  7.         return nil
    / e' `. G8 S6 ^/ Q* H2 N
  8. }& ?8 R8 S1 A  ^7 M+ A4 P! ^8 B
  9. // Returns false if file does not exist.
    $ n( q: Y9 H+ W9 C0 [+ o8 i
  10. // cmn.Panics if file is corrupt.: d( e& X+ a( e) p! V
  11. func (a *AddrBook) loadFromFile(filePath string) bool {" ]" ^9 G7 _* `* q( D2 S
  12.         // If doesn't exist, do nothing.* T5 o. d' D9 _( ~; ~3 n3 h
  13.         // 如果本地地址簿不存在则直接返回
    ( x3 Y; _( U. k1 p
  14.         _, err := os.Stat(filePath)
    " e4 H" n( J( J* d4 O; A6 s
  15.         if os.IsNotExist(err) {. M5 T8 |& b# h( K# _5 w1 T
  16.                 return false! Y9 J/ I: x: C" U8 \# E
  17.         }8 L' Y4 D' L. @- w' A1 f
  18.   // 加载地址簿json内容  k; B; Y4 p: v4 e2 l1 q, n  Y
  19.         // Load addrBookJSON{}& m) K6 }! D# o! u7 c* q
  20.         r, err := os.Open(filePath)
    % m/ J+ |5 Q4 r" ?
  21.         if err != nil {
    5 ]* f* i) ]8 S& o3 I$ d
  22.                 cmn.PanicCrisis(cmn.Fmt("Error opening file %s: %v", filePath, err))
    3 A( h5 T7 p# E4 _
  23.         }
    0 I; t) D9 _* K) l! G) o. k7 ?+ @, ~
  24.         defer r.Close()
    ) D" e! a% r1 w$ Y
  25.         aJSON := &addrBookJSON{}+ ~& C# x4 D+ g9 {+ a% {
  26.         dec := json.NewDecoder(r)- {  k# D8 j; {, b$ S1 T4 m4 u
  27.         err = dec.Decode(aJSON)0 R1 r) C; \; [9 b2 v+ e# `. J
  28.         if err != nil {
      [3 A4 x1 l* R% A
  29.                 cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", filePath, err))
    ! W- i( k+ X4 N/ T$ {, }
  30.         }4 a" b: x  e% X' K' T3 `2 I
  31.   // 填充addrNew、addrOld等: H+ O6 ?" N) ?* \9 v) [8 t
  32.         // Restore all the fields...6 ~0 E- u# L6 u$ V* M% i5 D/ W
  33.         // Restore the key
    & b* U0 z( ]6 O
  34.         a.key = aJSON.Key7 P' }: z, \0 x5 @6 g# b
  35.         // Restore .addrNew & .addrOld
    / q$ c7 [5 R1 A
  36.         for _, ka := range aJSON.Addrs {1 W% M- B: _" C9 |4 j  j8 Q
  37.                 for _, bucketIndex := range ka.Buckets {, {. e& ?! `3 [4 z0 J2 b; s
  38.                         bucket := a.getBucket(ka.BucketType, bucketIndex)9 f& I) H1 S/ ]. M
  39.                         bucket[ka.Addr.String()] = ka2 Y# t. y/ n. G% Z* ^
  40.                 }* A7 n1 j3 L+ }4 q
  41.                 a.addrLookup[ka.Addr.String()] = ka
    7 e9 m& u  k% h2 C
  42.                 if ka.BucketType == bucketTypeNew {; W( e5 ^7 m9 w+ P* R3 N5 C- b
  43.                         a.nNew++
    ' k, p2 x# X- D' a! ?
  44.                 } else {
    9 W' }  u0 o: ~  R- c! i
  45.                         a.nOld++& Q- R7 @' |7 I
  46.                 }
    : v( b1 X( L1 C( ?# A
  47.         }
    1 _: K( O8 _  Q! D- q
  48.         return true/ z; d' m1 K( A* G/ P. o
  49. }
复制代码
/ a. k1 t' p% k: h" p- W5 P
定时更新地址簿
4 }. U9 V3 {' R  w  a1 S, pbytomd会定时更新本地地址簿,默认2分钟一次
- X/ [4 Y, ]) @, O+ x: h
  1. func (a *AddrBook) saveRoutine() {
    , e, ?! z! Y9 N' T
  2.         dumpAddressTicker := time.NewTicker(dumpAddressInterval)
    + h# ^1 w& m4 {5 Z$ n
  3. out:
    ; `, K; a+ b+ Z2 q5 x! U4 n5 U3 p
  4.         for {7 @  R1 c$ F( m- E) U) N* B
  5.                 select {4 N" N; h) R& N2 P: e
  6.                 case
复制代码

$ O3 l) e! J. n5 P7 N添加新地址4 D# }3 N3 a6 ~
当peer之间交换addr时,节点会收到对端节点已知的地址信息,这些信息会被当前节点添加到地址簿中8 s- E7 |8 \0 d! r* \1 c: A. a
  1. func (a *AddrBook) AddAddress(addr *NetAddress, src *NetAddress) {- B6 `$ n6 Q! b7 z  p0 x+ W
  2.         a.mtx.Lock(): M5 i" ]: M! \  j- V0 D
  3.         defer a.mtx.Unlock()
    6 o3 j" i- u. D
  4.         log.WithFields(log.Fields{
    , `" [+ t: ^5 C! ~/ A
  5.                 "addr": addr,! r( B. d: v. x1 \
  6.                 "src":  src,
    ; a5 L0 e2 z- `! h# e& E5 ]0 D" _% k
  7.         }).Debug("Add address to book"). Y! Y; j4 \6 K! y9 e* x
  8.         a.addAddress(addr, src)
    1 |( t, e' m) Q
  9. }
    * Z/ F3 }- S0 s( l) N
  10. func (a *AddrBook) addAddress(addr, src *NetAddress) {
    , f. {7 G  Q4 L0 q: O5 ^
  11.         // 验证地址是否为可路由地址5 j& E' d. U& @8 \4 q
  12.         if a.routabilityStrict && !addr.Routable() {
    0 W; ^# ^( V5 y# h3 \! @
  13.                 log.Error(cmn.Fmt("Cannot add non-routable address %v", addr))3 v( P* p  P" s/ n% g2 |2 ~
  14.                 return
    * u7 C: \: G* a5 [! _" [2 Q* R
  15.         }
    - B! R4 L0 |& K7 G" S: k
  16.         // 验证地址是否为本地节点地址- ]! _8 @1 P" d; l5 P
  17.         if _, ok := a.ourAddrs[addr.String()]; ok {
    8 f% t  L4 u+ w; U+ V+ S$ F9 H4 V
  18.                 // Ignore our own listener address.
    . ?; X" j3 _; J: m$ k
  19.                 return% m8 N) {+ R8 N  d' x
  20.         }$ J: O7 m: y; z; C9 e7 i3 [' N
  21.         // 验证地址是否存在地址集中
    : _/ r# l- m5 {
  22.         // 如果存在:则判断该地址是否为old可靠地址、是否超过了最大buckets中。否则根据该地址已经被ka.Buckets引用的个数来随机决定是否添加到地址集中
    8 F$ E8 x( H2 T0 u! s" E
  23.         // 如果不存在:则添加到地址集中。并标识为bucketTypeNew地址类型
    9 q! j7 Q* s' ^
  24.         ka := a.addrLookup[addr.String()]
    ( D9 p* t, m- o: h. J" z* \% Z9 t1 b
  25.         if ka != nil {8 D; p; |! p4 {, Z  d
  26.                 // Already old.
    , {5 y/ z8 \, |) L
  27.                 if ka.isOld() {
    ' T0 u2 ~0 P/ l: D0 }4 N. d
  28.                         return- `# Q* c$ ^; G, X8 p+ c
  29.                 }* n3 J  M6 V: o" M( C% g2 m8 J4 l5 [# ~* E
  30.                 // Already in max new buckets.- p0 m/ V" k; J1 C) j- [
  31.                 if len(ka.Buckets) == maxNewBucketsPerAddress {, j: C. R* _4 e' G0 v
  32.                         return
    ! t+ P0 K8 T$ a6 X8 a* s
  33.                 }& N" o5 ]2 M* k/ I* z) l1 V* @
  34.                 // The more entries we have, the less likely we are to add more.# h2 ~. N: M7 r6 o+ p* E$ @
  35.                 factor := int32(2 * len(ka.Buckets))
    2 W; S1 \% R; }- a7 I! E7 j3 h" X4 |
  36.                 if a.rand.Int31n(factor) != 0 {
    " A/ m+ Y4 a4 O! E5 z! e9 T- |/ L+ o
  37.                         return- I$ b6 i  J) D; i* c+ C* y
  38.                 }
    7 G- O- \' c* s5 E
  39.         } else {
    & Q( r% y! y0 ?
  40.                 ka = newKnownAddress(addr, src)) j3 o. i. G- ]8 @8 H
  41.         }& y4 M% \& K- l0 W, h
  42.         // 找到该地址在地址集的索引位置并添加
    , m  h& Y: V+ B+ ^$ E4 ]
  43.         bucket := a.calcNewBucket(addr, src)
    2 S7 _8 x+ Z: k, S) z* D6 ?
  44.         a.addToNewBucket(ka, bucket)
    + F: d- ]* w  q- p- ^9 u
  45.         log.Info("Added new address ", "address:", addr, " total:", a.size())
    9 J% ~! {2 ^2 b- o
  46. }
复制代码
  V6 l6 h+ B4 D
选择最优节点
* h4 R( m( u) M! E1 c; j1 V8 \地址簿中存储众多地址,在p2p网络中需选择最优的地址去连接  h9 F  I' d$ Z$ {5 }
PickAddress(newBias int)函数中newBias是由pex_reactor产生的地址评分。如何计算地址分数在其他章节中再讲
0 T* {. S, K* J* E4 [: y根据地址评分随机选择地址可增加区块链安全性
* a% E) M: n5 t" q
  1. // Pick an address to connect to with new/old bias.( y' T$ l9 z( B/ \0 l4 i0 Y) F
  2. func (a *AddrBook) PickAddress(newBias int) *NetAddress {$ r8 N1 \9 c3 B
  3.         a.mtx.Lock()7 B( m5 E7 E6 }; c6 Y+ I
  4.         defer a.mtx.Unlock()
    7 W! ~6 n, X7 B$ w- X
  5.         if a.size() == 0 {$ h) Q4 F9 ]  `$ Z2 s( r
  6.                 return nil
    & w6 r7 f  ]$ j* W/ S
  7.         }" x  d! Q- r% \, P
  8.         // newBias地址分数限制在0-100分数之间
    2 H) L) F. `8 M; R; p
  9.         if newBias > 100 {
    ) [0 A. I9 D/ D/ Z* _- L5 d! s0 N; Z! q
  10.                 newBias = 100- \5 E" U- b% ^! ?" k0 q5 {
  11.         }
    5 ^$ O1 Y: O, A1 s" O% R; j
  12.         if newBias
复制代码

8 Y9 |- [, w& [! Y移除一个地址; T& y  t) r( p8 E* z+ S/ W
当一个地址被标记为Bad时则从地址集中移除。目前bytomd的代码版本并未调用过9 F& ^# i8 m; a- k, J
  1. func (a *AddrBook) MarkBad(addr *NetAddress) {# @! m& d2 }- U9 V9 B- T  F
  2.         a.RemoveAddress(addr)
    * E$ i( `: F  ]* v5 \/ B2 T1 k
  3. }
    ; i# W% k6 G/ p7 [/ z6 [
  4. // RemoveAddress removes the address from the book.
    9 ?& }" i8 S* q0 H3 y2 ~# k1 H$ S1 e
  5. func (a *AddrBook) RemoveAddress(addr *NetAddress) {
    8 W. ]& D2 O! b3 R# }+ h0 `0 P  E- m7 c
  6.         a.mtx.Lock()
    ! k' F; E9 {  B* @3 u7 |
  7.         defer a.mtx.Unlock()0 Z2 Q: U- }8 J/ T
  8.         ka := a.addrLookup[addr.String()]6 x) B3 I2 I( D6 @
  9.         if ka == nil {
    , f* g' o$ B' b
  10.                 return, S' [( Y5 l# p5 k: M0 C
  11.         }) X' y# `& [' L# }
  12.         log.WithField("addr", addr).Info("Remove address from book")
    ! ^5 s$ {# U6 ?# b# B8 f" i4 a
  13.         a.removeFromAllBuckets(ka)
    + n$ S# n: Z6 x) v6 l
  14. }
    / W! K0 m! Y9 q  m4 o, }8 @
  15. func (a *AddrBook) removeFromAllBuckets(ka *knownAddress) {4 v% f2 k' f2 W) A0 O3 I9 [
  16.         for _, bucketIdx := range ka.Buckets {
    $ \" I, g1 s' O" W" H
  17.                 bucket := a.getBucket(ka.BucketType, bucketIdx)+ {, X- m* \7 L. b# i- @
  18.                 delete(bucket, ka.Addr.String())
    & z0 k6 c. h, G
  19.         }4 j% X4 q9 m7 h
  20.         ka.Buckets = nil0 G9 j) |, K, r- \* _9 |
  21.         if ka.BucketType == bucketTypeNew {- o) K) N, [# }# D* n( E
  22.                 a.nNew--3 W$ F( v3 J% g* h4 H  [. w
  23.         } else {
    1 _* r/ O) r# \& [  m1 ?8 |
  24.                 a.nOld--
    ) e3 r7 S" C4 R  H/ q' R) W
  25.         }  p) M3 X; a& `
  26.         delete(a.addrLookup, ka.Addr.String())) H. N2 P& [. c) k! v$ v
  27. }
复制代码

' Y$ t1 r% y# H- p( Z
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

excel436 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    7