您尚未登录,请登录后浏览更多内容! 登录 | 立即注册

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 17821|回复: 0
打印 上一主题 下一主题

[C] 编写一个简单的TCP服务端和客户端

[复制链接]
跳转到指定楼层
楼主
发表于 2020-5-9 01:53:20 | 只看该作者 |只看大图 回帖奖励 |正序浏览 |阅读模式
实验环境是linux系统,效果如下:
1.启动服务端程序,监听在6666端口上
2.启动客户端,与服务端建立TCP连接
3.建立完TCP连接,在客户端上向服务端发送消息
4.断开连接
实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点

! J! f* g3 H7 ]0 m
什么是SOCKET(插口):
     这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
0 K  Y& e  \0 _" k
     "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
      对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
      具体其他高级的定义不是这里的重点。值得说的是:
      每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
      应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层  ->发送(接收的话与之相反)
( z, n$ Z/ e: Y" d+ S" ~
* C8 V, H" Q, @/ p
0 g! F; l# B6 O! [3 F
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等1 O6 Q5 T" `% T+ H
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
- ~4 n2 a6 T/ u# v9 T: M  M

5 S5 p& @) ?7 G8 J
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)
% M) A: C6 d# ?/ l0 Y) ^: M

  ?/ w( q7 L! K
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)
, m: I; w* X6 w( j

% @! @7 `3 E* I# ]! m: \! S8 ?4 T/ T* ]" H# ?4 I! i! r
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。

% e3 `: M7 h! O& |; d, _  W) t- _
4 e/ V; {, N: d, X- L
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>
    2 J/ @$ f0 L; T
  2. 5 B8 m9 G6 @% Q/ r
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
- M$ w; \8 t2 K: K6 ^$ W9 O
    readset  用来检查可读性的一组文件描述字。
* L; y; k- V4 f4 ^
    writeset 用来检查可写性的一组文件描述字。
. B$ J# d  E+ ~3 V$ I
    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
1 u) h7 H! P, M: C  z8 ^8 U( \
    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。7 u* l" C2 T* A' h0 J# V
; G" i& `9 k6 C  h6 d- k
    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:+ _) n( h* f  m" m6 |/ w0 O6 W

9 O$ _! @  E; T. l) Y! E  k; b
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
    ! L' J5 U4 C) _/ n: R) h+ ?

  2. ( x8 O7 e/ `& o+ G
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)( G: Q3 _. E1 P  Z
  4. 4 c, q5 f! n0 m  ^, ]$ C$ `
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。) q6 p; c$ K5 v5 `
7 k% N) J% A' t+ Z  N* C6 Q
   
3 ^2 I# |8 U. S- c- n5 v$ C) ?
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集
    8 ]# R( a3 k) I# U+ d: v
  2.     $ F. ]) ~  b( b& y* [# {6 ^3 S
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd
    ) v* g$ ]4 l  ]3 n$ P1 c
  4. + U# ~% }; l7 s, P+ t) f
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd! u8 z8 \! u( N6 u) o8 `" O! h
  6. 0 x) |8 B0 X! P( X! D
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;
    ' M. [+ w* H( u+ l2 y
  2. .....
    0 {3 o; G8 y# A  q9 v+ a& v& w; x& B
  3. fd_set set;! l8 v4 x* n! G* g" X
  4. while(1){8 F/ }, x& Q  }! }0 E7 E. e1 a5 t
  5. FD_ZERO(&set);                    //将你的套节字集合清空
    9 D9 ]0 O9 h+ h+ s: L! l
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s! s. m/ M  }: `% T
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,
    + O* [8 o. Q  z; S
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,
    ) l2 @& z$ o1 P: C
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    2 Q  J+ m* a8 ^) t* @+ s
  10.                                 //只保留符合条件的套节字在这个集合里面; @& @3 ]& v5 W. }8 i2 x
  11. recv(s,...);
    8 r1 c* V4 I+ Y0 U7 u: g8 L1 w
  12. }' s% J4 l1 z) ?" {0 Q" z: B2 J
  13. //do something here
    , U1 n4 y8 p. f' Y* }1 _
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。. p1 w" f& i$ x% F
  2. # |7 q8 @/ ]8 H
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1); n! w* @: H1 o* `% O
  4. 0 m& v' ]$ h& e& e+ O+ {9 d
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011
    - S: w5 b5 Q* |3 w

  6. 5 g9 v% g& X9 }! ?9 H. x
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待* U1 |+ V  o" j4 e0 \9 @1 T3 ~

  8. " c2 Y; o4 M) x
  9.    (5)若fd=1,fd=2                    上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
复制代码
1.可监控描述符的个数取决与sizeof(fd_set)的值
2.文件描述符的上限可以修改
3.将fd加入select监控集时,还需要一个array数组保存所有值
   因为每次select扫描之后,有信号的fd在集合中应被保留,但select将集合清空
   因此array数组可以将活跃的fd存放起来,方便下次加入fd集合中
   对集合fe_set与array进行遍历存储,即所有fd都重新加入fd_set集合中
   另外活跃状态在array中的值是1,非活跃状态的值是0
4.具体过程看代码会好理解

3 P( ^: b4 Z9 e: `) s: P
1 L3 r2 [. G8 e6 c
使用select函数的过程一般是:

+ h  \/ y; w* Q( p4 W+ b/ W    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。$ [, B* ~* {+ I) T8 |
0 r1 Z; Z: ]5 B( \
客户端:
  1. #include <time.h>( x0 c  F/ ]  Z) O  O0 z
  2. #include <stdio.h>
    ; j! b5 j' J, V- \" s
  3. #include <stdlib.h>
    , E1 p- A5 C+ r& G+ W
  4. #include <string.h>. @3 C+ d) q- ?! `
  5. #include <unistd.h>' x4 p8 t0 V+ J# x7 \# O
  6. #include <arpa/inet.h>& ?! P5 h( v5 [: y! S
  7. #include <netinet/in.h>
    # c' w2 ]* W9 E0 J; O
  8. #include <fcntl.h>
    ; N8 Q* P5 p: V
  9. #include <sys/stat.h>7 K; q5 c" f' I: A& o+ k
  10. #include <sys/types.h>
    + t0 ?) o3 n7 v% v3 L' |2 _( j
  11. #include <sys/socket.h>
    $ o" Y% G( F) m

  12. . `7 E& D( T5 h# [
  13. #define REMOTE_PORT 6666        //服务器端口) A2 a' `- I/ b- }  Z: O1 D9 j
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址& j: f$ v$ L$ L1 `. a1 R

  15. # a; _9 g& [+ ?( P
  16. int main(){
    4 s$ K. Y2 R  _% T, ?1 |8 }1 w0 W' h& i
  17.   int sockfd;$ a' g: l# q( u& _5 g+ ?4 ~/ M9 R
  18.   struct sockaddr_in addr;
    9 B6 Q$ F) [( ^  q8 O
  19.   char msgbuffer[256];
    ) ?0 a; B2 V  Z. b- P! F: u8 H
  20.    6 n* a! l% X# R
  21.   //创建套接字
    . x+ _6 M, N/ k: k& k2 a" |
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);; N5 M. m- @( X
  23.   if(sockfd>=0)
    9 v3 h( R9 ?% k2 M
  24.     printf("open socket: %d\n",sockfd);
    3 \( Z: k/ \# l: h3 Y) Z: X5 j
  25. 0 r, g/ ^( ]4 @8 O7 r8 [' v1 f
  26.   //将服务器的地址和端口存储于套接字结构体中( y: p/ J% N+ k5 T* t$ z; @
  27.   bzero(&addr,sizeof(addr));, x. d, L7 A4 s" c1 G
  28.   addr.sin_family=AF_INET;
    & M6 D/ `7 n9 i: H
  29.   addr.sin_port=htons(REMOTE_PORT);% k6 {: p+ Q! v" C: O
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);1 h! ]! S, p. T/ @& Q
  31.   
    ' i$ W9 c3 E6 e3 F, R
  32.   //向服务器发送请求
    + S5 N7 N2 |# V3 ?4 k9 J$ ~! m# W
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)% U& U4 a9 z- w% q. ~
  34.     printf("connect successfully\n");8 H  G& Q4 k8 O& i+ _
  35.      C7 F( D- r6 v1 L
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)  D: J4 S8 L: U0 L! O% X! J1 J
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    ; l3 o+ b5 i$ @. g
  38.     printf("%s\n",msgbuffer);
    ) ?7 n7 Y, O5 E6 J1 C5 B' ?4 t
  39.   % k- G9 `4 p, S3 |6 j+ R
  40.   while(1){
      q6 S1 a- ^0 ]. K+ C8 |. X
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
    ) L- l7 f/ p7 c- _+ v5 |; c/ w( q
  42.     bzero(msgbuffer,sizeof(msgbuffer));8 ^, B& [. x* E( E; F0 f" r
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
    1 j( ?1 T5 G; }* `; b' G3 d
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0), w8 {8 Z( N3 a8 ~1 B$ w
  45.       perror("ERROR");3 s8 k! W$ y9 s5 I. Q! [8 R
  46.     - x$ _( j7 e" j; @; ~
  47.     bzero(msgbuffer,sizeof(msgbuffer));
    1 b0 s. n* ~7 D7 s/ }
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);: ]: W( q6 _9 @, Y6 j5 Q) r& `
  49.     printf("[receive]:%s\n",msgbuffer);
    # |! J5 {( h. u! @+ p5 H& M
  50.    
    8 H* k( ^3 A5 v; F' [- g
  51.     usleep(500000);
    - p. \. H, ?7 a: Q. n. @
  52.   }
    $ N, s4 a: R. M, F, ]3 u
  53. }
复制代码
! l& j( l$ E4 G" [7 W7 ]7 a
9 y1 j$ ~  }" K0 c
服务端:
  1. #include <time.h>
    / L5 ~# E7 {4 ^$ {7 d
  2. #include <stdio.h>5 X+ @6 [  v8 m. `
  3. #include <stdlib.h>
    + W% Y/ [6 H0 y, k7 X& f
  4. #include <string.h>
    2 n: b6 E$ ~5 e4 Y% M4 U3 `
  5. #include <unistd.h>
    " s" K  ~0 M) i: I
  6. #include <arpa/inet.h>
    " g! i+ e/ m; |4 L8 Z  y" [
  7. #include <netinet/in.h>, A/ D$ }* \  J- ^5 o
  8. #include <sys/types.h>9 M  M- ?* s) \0 i! i) ~
  9. #include <sys/socket.h>
    7 K# H. r8 o: Q2 p
  10. 4 U: m& `- p5 O% ?2 L: J
  11. #define LOCAL_PORT 6666      //本地服务端口
    ' _* ?  r2 G# g
  12. #define MAX 5            //最大连接数量6 R+ y  L6 i, s: s

  13. , o0 |; U$ _$ U9 i3 L5 n
  14. int main(){
    : m  f4 Q2 N8 s6 P; N
  15.   int sockfd,connfd,fd,is_connected[MAX];1 y; E8 @  |2 U$ ^5 c1 X7 P9 e
  16.   struct sockaddr_in addr;
    # m0 L9 P+ B( X1 T% Q. W
  17.   int addr_len = sizeof(struct sockaddr_in);$ |7 D8 t/ i  I$ v9 B" Q
  18.   char msgbuffer[256];
    " {. v3 A: H* t
  19.   char msgsend[] = "Welcome To Demon Server";
    9 B; r+ ?3 t  [. Z2 Q' j8 y3 l. |
  20.   fd_set fds;
    " m" S/ l8 ]: S; o# |- G7 L- L
  21.    4 m$ ]' C: ?% P  g% p
  22.   //创建套接字
    - v" o" l" D! B1 G: I; s
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    / d- b% k1 M6 z% n1 c
  24.   if(sockfd>=0)
    7 W; S5 \8 T1 N# [& `
  25.     printf("open socket: %d\n",sockfd);
    : r5 q% h" O, f* z) [
  26. 1 n+ v; |, i0 o5 B
  27.   //将本地端口和监听地址信息保存到套接字结构体中4 F, d! H4 J, M1 U; g( Q" L5 L- S, j
  28.   bzero(&addr,sizeof(addr));
    8 _& v& l) f# E) y7 \
  29.   addr.sin_family=AF_INET;
    . Y& a1 L; Z6 Q6 Z! W2 a6 G+ C* F3 K
  30.   addr.sin_port=htons(LOCAL_PORT);; h' f4 ^% _1 t
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
    % D9 `* `& G3 s# T3 W0 c
  32.    $ n, n- L6 g0 Y" q/ @
  33.   //将套接字于端口号绑定! Q7 z& w2 |0 q5 l
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    ; l# \; J8 J( S3 E+ a1 _! K( ]
  35.     printf("bind the port: %d\n",LOCAL_PORT);
    5 f1 o! M* q4 r" _3 k& N' F5 \

  36. , _  t, P9 L7 g
  37.   //开启端口监听4 z# k6 S. L4 m4 Q. C% j* S" }
  38.   if(listen(sockfd,3)>=0)
    & a' \% V& |, @1 v; c8 U: `
  39.     printf("begin listenning...\n");; e' B  l6 t4 l0 k. J5 ?& y" r

  40. 6 y8 g0 }. K( f$ h. c. `8 N
  41.   //默认所有fd没有被打开
    : w2 j6 }3 n' w! l, t
  42.   for(fd=0;fd<MAX;fd++)
    . H" j1 ]) J! f: M: m3 K
  43.     is_connected[fd]=0;
    # i: [2 x, I# s6 x2 m- S
  44. ! m- C5 G1 w5 s4 ^
  45.   while(1){5 G# J: M1 d. S" e9 B" D
  46.     //将服务端套接字加入集合中
    , |' M; u: x4 x5 ^" u5 M% G# M% X  ^
  47.     FD_ZERO(&fds);! n  v3 K- F6 ?( R6 C
  48.     FD_SET(sockfd,&fds);
    * g3 l9 z( W: l" m+ ^( i# r( i! o, A) b
  49.      . k+ Z& e3 K( S; x, g% M
  50.     //将活跃的套接字加入集合中
    . ]  y2 @9 l( e! y) O. g. K
  51.     for(fd=0;fd<MAX;fd++)
    " A0 J* i5 |+ Q6 B. b5 e2 b% O( X4 W
  52.       if(is_connected[fd])5 B* R/ g% [( C, m( q) E
  53.         FD_SET(fd,&fds);# w) }& n6 p+ K' Z( x) ?
  54. ' i. K7 ^* V# k; G/ o, a% |
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
    & I( r9 O/ p- Z2 b9 `1 S* C: `% e
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))
    ; ~1 g- ]% d5 t7 [
  57.       continue;
    $ m$ }7 e& B7 l* J
  58. ( L" A, S9 H4 G: l& `
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字
    6 E2 ^2 F* v; F! y
  60.     for(fd=0;fd<MAX;fd++){. J  L5 o4 l. ~
  61.       if(FD_ISSET(fd,&fds)){( K0 Y& r; u4 Z; l1 K& p8 ^
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接3 J- z( i% V& F! o+ b* }4 c
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
    ; ]! R, g/ q9 r. j$ n; J; ?
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语- W  b! j& K8 J9 m# ^3 g
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用, T' ~" q0 C3 x  V6 E
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));5 d6 u! M/ n5 ]- w/ ]$ L% j  S  J
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
    ' l8 r6 [5 t  b2 x$ b6 t- y
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
    ! `: Y+ j7 r; Z* |2 D8 z
  69.             write(fd,msgbuffer,sizeof(msgbuffer));
    , a- N6 l1 U! ~- w
  70.             printf("[read]: %s\n",msgbuffer);
    3 L: B8 v/ c/ R5 r1 j
  71.           }else{
    & S" D+ M) u/ c% B$ J/ d
  72.              is_connected[fd]=0;
    " T1 l0 g! t. X# ~$ n5 C, X! v. h) m/ {
  73.              close(fd);" }9 a; I3 B" ^1 j2 b9 `- ~" k6 q# ~/ F
  74.              printf("close connected\n");; q8 l; _- G  ^) x5 Z: O9 h* T4 ]
  75.           }
    2 w8 x( {2 }4 I0 Q6 w* f
  76.         }
    ) P3 F( }2 g# S+ B9 \+ E( \. E/ I
  77.       }
    ' B; W# f! G! v5 Y2 g+ g4 g6 A# b
  78.     }1 X6 [7 O7 l5 F2 |! z. W
  79.   }# y8 d2 T& F  a5 n$ K  y$ W2 G8 f
  80. }
复制代码

9 {$ L* y. ^7 s! q7 x! W- q3 g3 t. V3 J1 A) G6 Y
$ j% Z4 G  w# V" f: e( b: n: a! x+ Q
0 o  v4 d" W* R$ Q

- b5 k( z6 t3 r+ W) Z$ \! V4 W! \. T/ F: c
  H4 i; [7 _9 X- `
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2026-1-30 13:24 , Processed in 0.064730 second(s), 23 queries .

Copyright © 2001-2026 Powered by cncml! X3.2. Theme By cncml!