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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

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

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

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

$ u% F- ?# [9 h
什么是SOCKET(插口):
     这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
# N6 j3 e' g& R: S
     "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
      对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
      具体其他高级的定义不是这里的重点。值得说的是:
      每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
      应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层  ->发送(接收的话与之相反)& z) v1 [$ c1 p
; l4 D% f6 b! X) }
! y* N5 t' r0 P5 Y; V
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等8 r6 S3 ~, L: C! I8 z9 L
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3

1 u; R/ b% W0 |4 S' w( ?9 i5 r) [3 x
8 R7 E8 i/ K/ P  u+ {: z2 s3 }
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)

8 x# v6 F! D  e0 B3 d' ^$ J0 L% s6 u" i
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)

: S! y- p) q' `! W
- ~: }" ]$ o0 h. P) e3 u0 @  v; ~: ~5 }3 J8 W. x) i
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。

3 m( R) E+ o5 U! t9 f' q3 e. W6 Y9 i
6 B4 R8 C( `3 ^
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>* S9 \- X8 j1 z3 Y5 g

  2. : Z' J/ K1 m6 B, v- b9 G8 Z
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
: c+ Q2 k+ j8 z/ X6 ~( T
    readset  用来检查可读性的一组文件描述字。

0 ~6 _6 {0 V; Q) M    writeset 用来检查可写性的一组文件描述字。

2 J" g6 _6 E$ b$ k5 w7 q    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)

- i+ u& j  R0 _% e, e    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。6 ?8 ?, `7 Q. u+ s1 j) m9 c

  a$ I" h# b8 ^- b  C    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
* B7 \. M, B! H/ w
! B) \. S2 S! m4 o% V' Z$ Y+ r
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)) m* T6 A7 ~: H9 e* i
  2. . I# e5 r0 J, v8 R2 O! t, i
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)5 I: d/ E. Z. h7 r, ?. F
  4. ( K! @& V* z( q$ _$ O  m0 v
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。
% u9 R' [1 M# L1 N% r0 @% H; s2 ]$ B- d2 O
   * O& X9 x; F* _0 O7 x# w9 G
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集7 O& k! {7 _( K3 I
  2.     / `# c; r% B0 Z
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd
    ; a0 u" `2 C. V1 b

  4. 5 H9 F" }: _4 I
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd
    5 F/ h8 V/ B3 o

  6. 2 f/ M4 _7 k3 @+ Q
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;
    + b2 I+ v! Z5 {1 Y. W% [2 b# Y
  2. .....
    - ?+ j: q/ n/ |8 ~# ^, l( `
  3. fd_set set;
    % D7 n4 ^7 e8 w! o4 T+ U
  4. while(1){0 G1 ^+ D: A4 J# ]4 `7 x( N5 U, [
  5. FD_ZERO(&set);                    //将你的套节字集合清空: n3 a6 w) z, m; \1 l
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s
    ; T6 P4 i; n7 i+ \2 M
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,
    ( s- @5 N& P# Z( v, b1 I  V
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,  t* D6 w! ?6 Q9 ]
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    ( k, s5 N) B/ _
  10.                                 //只保留符合条件的套节字在这个集合里面
    , Y8 c, [( c; ?
  11. recv(s,...);- I2 A- U% N* ]3 m1 r7 e
  12. }
    2 `' D8 d! a7 |% @9 o' U8 i
  13. //do something here
    3 H3 d. w" P" a* p6 u3 [
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。: d, N  Y* B: \# O, ^# }3 k6 u

  2. 6 w+ Y6 ^0 M! W1 y& \- D) ^5 S
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)
    5 u% q9 K$ {! ]

  4. ! d) V4 M" ]2 C" J
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,00110 Q% Y5 O' ?% e3 S) \
  6. 7 Y8 E! k+ i: c" W3 T# D
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待
    : y- p9 K6 V, G  A  y, y$ a) m
  8. 6 I  v4 c0 Z( Y1 |7 M: r+ v
  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.具体过程看代码会好理解
+ v* s/ B2 X. y7 {3 ]5 N1 I

4 w' c, t# b; s, h4 ^
使用select函数的过程一般是:
( m0 {6 B- Z; R) v6 w" W
    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。0 a" T" U# ^5 Y
  R+ f+ @2 s. Y0 ?! g0 }% c
客户端:
  1. #include <time.h>
    , u( J3 b& J5 [
  2. #include <stdio.h>6 }1 b; m& D1 {" i# k: g
  3. #include <stdlib.h>
    2 T1 S2 N  J5 U; Y3 P7 I' u: J( f( u
  4. #include <string.h>9 l* Z2 k4 `7 h1 @* ]9 f1 l
  5. #include <unistd.h>: g  ]3 ?* n* h( I
  6. #include <arpa/inet.h>
    8 Z. E7 K6 L/ T9 {# t; u3 P  |
  7. #include <netinet/in.h>
    , z0 ~* K) ^6 C* v9 x
  8. #include <fcntl.h>) `4 `& Q$ X5 U$ y- e8 U6 |
  9. #include <sys/stat.h>: O* C# u4 p' F+ g4 O; E% B: T' e. U' w
  10. #include <sys/types.h>4 B+ V/ ]# y2 ^+ ^7 h
  11. #include <sys/socket.h>9 Q8 Z( i8 G5 n! O! r
  12. 5 M7 b3 v+ }- V* c
  13. #define REMOTE_PORT 6666        //服务器端口& n" W9 v; P* K5 T5 O
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址
    , o  R3 x' N3 t
  15. % n, q2 \, f' o1 x4 n" J3 F
  16. int main(){; N. w  g- A9 [/ c  V
  17.   int sockfd;" f) R, f& n  w" H4 }
  18.   struct sockaddr_in addr;( L0 @; Q% z0 S% R6 F! y- I
  19.   char msgbuffer[256];
    % e5 h; L8 @: t* D
  20.    
    ; z" f% z' \4 u/ r
  21.   //创建套接字* I) D; I+ c# d; E$ P5 U
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    $ A! l* w+ D8 o8 _6 L) U0 a
  23.   if(sockfd>=0)8 \; D; j! j% Z9 W! B4 ^3 p, \/ X
  24.     printf("open socket: %d\n",sockfd);
    7 |& d8 w2 I/ _9 \+ i4 B

  25. 7 |: \# C- \3 q% R% M
  26.   //将服务器的地址和端口存储于套接字结构体中
    & {- Y/ `% Q5 ^+ V6 v4 h; b# u
  27.   bzero(&addr,sizeof(addr));1 }* j$ j6 a) E* L, L( [* |# a) M
  28.   addr.sin_family=AF_INET;
    2 c& S) J( z4 u3 O9 |+ {, W
  29.   addr.sin_port=htons(REMOTE_PORT);
    6 \, A9 k8 J% v% g0 j
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);7 _3 f8 C1 i+ p5 i3 l  T
  31.   
    " C. m% Y* |  B0 o5 E
  32.   //向服务器发送请求3 @: r, Y+ x5 e  M4 T
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    ; C% k# {' {- d7 O
  34.     printf("connect successfully\n");
    0 T1 i. v, V  ?
  35.    + ^2 n( \- G' J0 Q; z! e& z5 a. V
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)3 q8 F) ~5 |: V5 e* a6 Q! i: C9 y- ]' e
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    ' {  A/ _3 H% X. j( w
  38.     printf("%s\n",msgbuffer);. n7 Y" E: q- g' _$ |
  39.   
    ) f6 W1 h8 h& j; D& a
  40.   while(1){
    - E8 ]3 h' X3 c( j) {- g# C
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
    * V+ z3 r6 c. ^+ {/ Z' z
  42.     bzero(msgbuffer,sizeof(msgbuffer));: A% G( k6 S. @) b8 C* Z) b
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
    ( O+ f( ?( w0 y0 C1 y# _' |
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)* O! J- I- Q4 r  g" W! d; i2 \
  45.       perror("ERROR");2 h2 r# i- H- g  o' L7 s5 i5 g6 W
  46.     ) m5 D. r" X9 ?: p4 g* Z  s
  47.     bzero(msgbuffer,sizeof(msgbuffer));
    ( M( d- ]. A8 g  ?5 O
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    ' v' \3 T1 h' ]" }
  49.     printf("[receive]:%s\n",msgbuffer);
    + H0 {' \* b7 ?; @5 W% S: y
  50.     4 h. C; V$ P' G/ @
  51.     usleep(500000);
    % G0 E! }- W! V4 N1 s; \
  52.   }
      _8 A- l4 S% c" ^/ i: P. q7 C
  53. }
复制代码

5 d2 C0 U5 I4 w+ v  T6 b
- D; h5 l& w9 t$ P+ N
服务端:
  1. #include <time.h>
    2 C9 ?# @; e$ Z0 n  x2 Y
  2. #include <stdio.h>0 `9 a2 i. w" j- V, G
  3. #include <stdlib.h>! N; G  s6 k8 t- v+ F) I. H
  4. #include <string.h>3 A7 c2 i2 v4 P/ s% @
  5. #include <unistd.h>
    1 l0 p! X8 v$ t" l. z6 I- ?! k
  6. #include <arpa/inet.h>
      c1 L  T0 k. c; o; ]; a
  7. #include <netinet/in.h>
    % g5 \7 h9 S' o, `0 T
  8. #include <sys/types.h>
    2 v7 H3 a2 y; {7 b
  9. #include <sys/socket.h>
    * z: y, M9 [. \/ k3 N0 e. C
  10. 9 X# P2 w2 C" Q0 u5 _9 s. o) f7 w, B
  11. #define LOCAL_PORT 6666      //本地服务端口* L2 T' c* S1 z4 w# x
  12. #define MAX 5            //最大连接数量3 N. Y9 S; i2 B/ u5 k) q2 \

  13. , }& o( U, @4 N/ r9 }( @
  14. int main(){
    7 y9 n+ [; o1 t! s/ i+ y# u
  15.   int sockfd,connfd,fd,is_connected[MAX];( R0 {5 M! {: ?* J: H( x: _  x
  16.   struct sockaddr_in addr;( L& l% Q8 E. X- T$ T
  17.   int addr_len = sizeof(struct sockaddr_in);6 v7 t1 E3 h0 z2 X7 B2 E
  18.   char msgbuffer[256];2 t% w2 K0 X- L: z7 y9 y% l
  19.   char msgsend[] = "Welcome To Demon Server";* t2 H: l+ I: a
  20.   fd_set fds;6 j% E; F8 z  W6 _4 `4 Y
  21.    
    ; B$ x/ K. g9 ~1 w
  22.   //创建套接字
    $ q$ f. C- s+ _1 k3 |5 {. u" M1 R
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);' P& i1 W8 A0 w( H
  24.   if(sockfd>=0)
    ) o! a/ q' }3 Y5 ~+ p$ ]
  25.     printf("open socket: %d\n",sockfd);
      i0 e* ?, {* g9 F- J; A* q
  26. , ]+ f' z2 s! x
  27.   //将本地端口和监听地址信息保存到套接字结构体中2 A2 A4 k: ^+ ^) \' c9 H$ ^- Z' ^
  28.   bzero(&addr,sizeof(addr));
    9 j& B8 Z  I! ?0 \, ~
  29.   addr.sin_family=AF_INET;% @- g3 `+ r% `# Y" E
  30.   addr.sin_port=htons(LOCAL_PORT);
    + ]) o6 v( V  T5 u6 B# E5 [, [
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.09 G  J" j+ x6 O7 |) k7 O: ~/ d
  32.    8 t3 C( P. [( w1 v; l% B, F
  33.   //将套接字于端口号绑定
    & Q. q+ o  L! c0 V8 Z4 P0 [( L: a+ E
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)# {* F! f$ P8 a. |; L( V
  35.     printf("bind the port: %d\n",LOCAL_PORT);
    & t6 `9 Y) ^" Z- g# p/ g) Q2 D

  36. & b2 N, ?- O: i% |3 _8 [
  37.   //开启端口监听
    & M* b6 A9 h8 G- P
  38.   if(listen(sockfd,3)>=0)
    7 b/ V2 x! F  U' h8 E0 L/ Q7 G
  39.     printf("begin listenning...\n");
    / z4 i3 v% b+ ~  m0 T$ z1 @, `( o% K7 b

  40. . s1 R3 Z- ]# S& o3 ]/ U+ Y: R7 d5 A
  41.   //默认所有fd没有被打开
    2 E' i" L9 Z  T- q
  42.   for(fd=0;fd<MAX;fd++)3 t4 x5 C6 r; q% W
  43.     is_connected[fd]=0;2 e/ R( x# f8 {/ A0 b6 H
  44.   B* \2 t. W7 q% r- z' t, g% P
  45.   while(1){
    ! {0 u' w; @/ i
  46.     //将服务端套接字加入集合中
    # s+ ~- D! V6 R. b; T
  47.     FD_ZERO(&fds);
    # f, t1 y7 c' y2 q/ F
  48.     FD_SET(sockfd,&fds);9 t6 s0 L  K1 K
  49.      
    ; l: y* @! ~; h$ {' O- s
  50.     //将活跃的套接字加入集合中1 ?+ l9 p- w+ {! k2 J3 T/ U
  51.     for(fd=0;fd<MAX;fd++)& k- \! l" R3 t! l1 g9 g8 M+ S7 U
  52.       if(is_connected[fd])% _* a' N% [- z3 z) n
  53.         FD_SET(fd,&fds);  }" `- a3 l/ c# A9 A

  54. : o# e$ u/ z" w+ V! W
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0+ b( j" H( j6 j/ M/ ^
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))0 b5 t$ K0 {2 I: w' h
  57.       continue;' D6 Y- F' M0 V1 w
  58. 6 a/ M' s1 N; O3 M! t" L3 V7 @
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字
    ! z/ q: g+ }$ p
  60.     for(fd=0;fd<MAX;fd++){) l- M5 f: j( v3 N  K/ r' ~) N
  61.       if(FD_ISSET(fd,&fds)){
    . k: q3 Q( X/ R4 P4 ~+ \
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接
    / k& A+ z/ z! b& _1 q$ N
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
    - ]0 j% Z! i: w1 B' d; H
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语
    ( _+ m  g# z1 ?& ?5 b& e
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用
    ( B' H2 L4 p! O1 L' x6 m2 {, |
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));3 w1 `+ j1 q  b
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字  c8 p* B/ e6 b9 f  t$ A( W
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ ' W- ]: L8 K) L2 ^6 l* ?6 n
  69.             write(fd,msgbuffer,sizeof(msgbuffer));% A! q( p; Y& O$ J+ T; ^
  70.             printf("[read]: %s\n",msgbuffer);9 T& M* e' S1 u$ [8 Q* g8 n2 t
  71.           }else{
      a9 x' ~3 K, l" G' Y' M
  72.              is_connected[fd]=0;1 u5 _* T1 A$ ]/ X0 p5 u7 \
  73.              close(fd);
    ) i9 {5 N' ]' W8 \: N( Q. F! |
  74.              printf("close connected\n");
    4 i4 T; H' K/ m& R8 j
  75.           }
    9 q6 F' h! a6 C0 m; d
  76.         }9 s& L6 t  T& R6 J% B/ S. b) C
  77.       }; J* N- |" [* D; b9 v
  78.     }
    0 X! V6 n! b. ]
  79.   }, K& r0 M% l3 A9 \7 W. Y
  80. }
复制代码

& p5 C2 F( C, q; _$ x7 D0 V+ s/ o* V2 P" t! D

% o- j: [. Y+ R- c) F, w2 c
. I' ^5 t8 D( |0 f
. o7 s0 r+ j0 V! n9 h5 m' Q; L
. R, d/ l+ L0 g/ i$ J( x# z# c) \
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2024-5-19 20:15 , Processed in 0.139613 second(s), 22 queries .

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