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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

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

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

[复制链接]
跳转到指定楼层
楼主
发表于 2020-5-9 01:53:20 | 只看该作者 |只看大图 回帖奖励 |正序浏览 |阅读模式
实验环境是linux系统,效果如下:
1.启动服务端程序,监听在6666端口上
2.启动客户端,与服务端建立TCP连接
3.建立完TCP连接,在客户端上向服务端发送消息
4.断开连接
实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
% g0 K. z, @' R( f
什么是SOCKET(插口):
     这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
! {1 o; z) v8 ^, _2 l
     "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
      对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
      具体其他高级的定义不是这里的重点。值得说的是:
      每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
      应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层  ->发送(接收的话与之相反)
! ^( a1 j4 x$ C' r6 Z! r( o1 s
3 n1 T$ r- F% Q% }# K4 ]

; d/ y3 n5 p( ?' |) {) s
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等2 f: \( }) A9 t/ H' Z" y& g3 |
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
$ l: Z8 _' M0 F% C$ Q8 M1 V; _# |

) ~: T! L4 w3 b  ]  N, n
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)
; T; j9 s. T) O6 k/ E( |! i
5 X# U6 V; q& M( A) a1 t2 j( J6 Y
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)

: C2 S1 d4 R1 B1 r9 _* H$ |0 R4 s
$ H! p" m9 s0 q" s
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
+ \+ \1 A9 q2 J- g, V
# z* n5 B, f3 ~. U2 B% Z+ K
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>
    4 k0 I$ Y+ P! }( |
  2. , X, o3 x0 S) E9 e- w$ S- q# P
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理

; u4 z: \9 `& N* O7 K  ]( U    readset  用来检查可读性的一组文件描述字。

/ P" I5 l2 f! |, H: f- \- C    writeset 用来检查可写性的一组文件描述字。
7 I3 \: A/ e* z
    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)

- K$ R! V. A$ _, Z9 h4 N. L# j" S3 h    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
" a  f- _9 q  z; v" J
: G8 K/ s- T0 O+ f% Q5 j    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:6 J' _, }+ S9 m: w* m" f

% @# d' q" c  y" [" `, l
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
    & w4 C1 J! x. i9 W! u6 `
  2. 3 R  _! H+ P0 ]& E- L7 s* U
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
    ) C: F& k- J4 l4 D' Y0 R1 k
  4. & j6 I- j4 L: Z# ~8 p- _3 |
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。
5 C; d7 k6 d* I5 `* I. V1 g  z! p5 G, R! `% W2 S6 n
   & `3 z9 c& Q7 B) @& n. k2 s" c
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集
    3 X1 x8 X  c4 D9 s* s; {0 @  [+ ?
  2.    
    : b6 n9 m8 `$ f4 T( K
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd. o& R* q8 f3 U; m6 [

  4. . Z. \+ N( I, c5 W$ X6 O
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd9 d- P' H; Y& S8 ~7 _, e6 r
  6. ( o6 U5 Y/ ~6 a' a
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;
    9 R7 {$ E, M9 ]. q: a$ [+ e  }
  2. .....
    1 W1 X9 Y, _+ n* d: |! X( y/ B
  3. fd_set set;
    9 T7 X3 D" s) i8 h# K
  4. while(1){
    8 F: I0 _5 h: }) n. {, N6 R* `- U
  5. FD_ZERO(&set);                    //将你的套节字集合清空
    8 ?3 y# ~; b  _
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s
    3 J3 [2 ~' T( s( a( M( H; x* Z
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,4 t$ F4 X# z7 d4 W5 _1 h
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,/ j8 c) q: v8 K) {1 a5 B
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉8 h+ D6 w1 X; P( w* l+ ]: m% R
  10.                                 //只保留符合条件的套节字在这个集合里面
    * }8 g3 U0 X$ |4 {5 g7 O1 r0 i5 v
  11. recv(s,...);8 A, k2 @5 k, s1 A
  12. }
    9 u8 ]# C: x5 U
  13. //do something here6 j5 K) p2 I& y4 _+ ^
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。
    " Q8 b/ \7 j" c* h+ O

  2. ' }8 ^5 q1 `- ^) f" F$ ~( P( w
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)
    ( m' S* P( x' ]. z( K5 p

  4. " u" q, u4 _0 ^% W" }
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011" o/ r+ l* k0 u1 [) J% K
  6.   A, W7 s: M% c, {8 N
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待9 G- J4 E! K; \
  8. 9 i! s  n# R# B" O; P
  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.具体过程看代码会好理解

) {! K" n7 o. j6 [; ~3 c& {- g: \& e2 c7 |# y' K& ?
使用select函数的过程一般是:
3 j" n" e9 i* o4 }& L7 x  v" H
    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
  G) k( [; R; Y  j, n  u  z
6 P8 q" R  E/ j8 @2 \
客户端:
  1. #include <time.h>8 E8 b( e2 x* [
  2. #include <stdio.h>; q: P' K! N3 W  \' l
  3. #include <stdlib.h>
    / i- T0 `3 w- a9 z6 |) ]
  4. #include <string.h>6 Q; ]  Z, Y/ E; h1 `( c! H9 |
  5. #include <unistd.h>
    . |& N' t# S2 ~' |0 c. j7 k+ W
  6. #include <arpa/inet.h>7 u7 t$ \2 P. J1 X" b
  7. #include <netinet/in.h>
    % d/ I/ l0 c0 `/ g
  8. #include <fcntl.h>2 E' F! M2 c( N# G6 t
  9. #include <sys/stat.h>
    ) {) L- c* t' Y( B, o( N: N9 ^
  10. #include <sys/types.h>4 p+ y# g8 H  L& Q8 r
  11. #include <sys/socket.h>
      x( p6 e* f+ j7 {7 u. c
  12. 2 n# I" I% A/ f6 Y: c1 B) d4 ]
  13. #define REMOTE_PORT 6666        //服务器端口/ W( @: e0 U+ L/ ]
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址
    1 f# V3 L+ _2 p6 |+ ~  n
  15. : m; R9 s1 h5 K5 w0 }3 @
  16. int main(){
    6 I6 Q4 F7 f: S$ H
  17.   int sockfd;
    ; K& G1 Q5 t& W9 M; w2 L" Q
  18.   struct sockaddr_in addr;) T+ @+ p( y; ~# W5 l- I
  19.   char msgbuffer[256];
    ! U( g9 p: U0 c' |/ M5 S9 D
  20.    6 D  _0 j+ V7 L! V9 A- A
  21.   //创建套接字
    8 v* o' `8 X8 B' J8 d5 l, i" O# X
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    ( e- R& q3 D3 }9 ]% {& ]. v
  23.   if(sockfd>=0)
    ! f' @9 E( x4 X+ `% T# k0 t; o
  24.     printf("open socket: %d\n",sockfd);
    3 f9 c3 i6 [  O6 U/ U! R! \
  25. ; R- S8 o; {" z0 j4 U
  26.   //将服务器的地址和端口存储于套接字结构体中
    / M% j/ M2 Z5 u& K" J4 M( I
  27.   bzero(&addr,sizeof(addr));+ E1 }; y; ^3 K% T5 L2 U) `/ j0 N- Y
  28.   addr.sin_family=AF_INET;9 A/ \  m+ o! l- K' Y$ C2 q
  29.   addr.sin_port=htons(REMOTE_PORT);
    $ R; c, b) x7 F9 T$ c' u/ l
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    . Z1 q8 }3 D  d2 g
  31.   2 T2 r: l" e8 o! X, Z
  32.   //向服务器发送请求
    ! n8 [8 r/ |8 ~! L' a
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)4 T+ J5 @! }/ E8 v3 n
  34.     printf("connect successfully\n");
    ' n( P" H8 Q9 @8 P" l
  35.    
    , E3 t( l& F8 A. G
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
    . x* B( `& I  B- g
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);! J: y( N$ `; x2 m( X: ^5 `
  38.     printf("%s\n",msgbuffer);
    ( v0 y' G! m2 g) v
  39.   
    0 Q, O4 V" J+ I/ j! c8 D2 V
  40.   while(1){; W' _1 W8 b9 S% Z
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息8 J, v8 r& v2 ^. U$ t+ r7 q
  42.     bzero(msgbuffer,sizeof(msgbuffer));
    2 D5 P8 A) U/ _7 }$ O' A# _+ }& P
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
    7 }" ]/ ]% N8 w' r2 ^( J
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
    : X5 |3 Z, o: e/ @+ V
  45.       perror("ERROR");  T' }1 l/ p4 Y& R9 D0 h5 X  j9 I
  46.     # r0 o/ R; q! N# i# v% }2 i
  47.     bzero(msgbuffer,sizeof(msgbuffer));8 r9 w( `8 W& }( Q, O8 }$ E0 [: X0 A
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    - k$ Z6 |( c% W! t
  49.     printf("[receive]:%s\n",msgbuffer);
    * P. H4 }: T: ]7 X
  50.     - H# f% I8 b! v1 g9 C
  51.     usleep(500000);* X0 ^. X( x5 T# P3 A5 n5 C/ D
  52.   }1 K) q. |# ^  `& j2 ^, ?1 p
  53. }
复制代码
& ?) Y/ d4 a: q9 q1 t8 X
8 V8 c  y" Y( Z! t) c9 b, E7 C6 J
服务端:
  1. #include <time.h>
    6 L, i: M9 b1 J# {: W* u8 e
  2. #include <stdio.h>" \1 w+ l7 e2 i9 s
  3. #include <stdlib.h>! {% q" B3 B$ {; Z
  4. #include <string.h>
    ; c: u3 j) A# @  I/ ?! K7 O
  5. #include <unistd.h>. d) @% G) n9 d! \% I+ _% a( P
  6. #include <arpa/inet.h>
    9 Z( V, P. J8 H) ~3 \5 C
  7. #include <netinet/in.h>
    $ k" V; G) f& S( q5 u  Z
  8. #include <sys/types.h>
    ! I, d$ c: _$ V
  9. #include <sys/socket.h>% @# m) _% c* o, g0 I* |# V& S
  10. 7 d- m* b# k' o1 r7 P! I  Q
  11. #define LOCAL_PORT 6666      //本地服务端口9 q3 Z( H, U# S, C3 \3 j) w
  12. #define MAX 5            //最大连接数量
    6 F# S# N" p5 x2 ^

  13. ! {8 M; b" Q& l# m( i8 T( }
  14. int main(){/ S8 r8 J" C! E) q/ i- [
  15.   int sockfd,connfd,fd,is_connected[MAX];/ |( f1 C8 @3 G) a3 _2 ]% d% K
  16.   struct sockaddr_in addr;" ^# q5 l3 @8 u+ s! K& a
  17.   int addr_len = sizeof(struct sockaddr_in);
    2 L3 c) D  k1 ?+ T: W5 l& z# k
  18.   char msgbuffer[256];
    6 h0 G: \" u5 L, `
  19.   char msgsend[] = "Welcome To Demon Server";
    ' B- s: y7 r: g' e6 p$ ?
  20.   fd_set fds;; x0 b) z! i% u3 y  U  C
  21.    2 H  E' M1 \! J0 x: `# m5 b& F
  22.   //创建套接字
    5 K& O; ^; ?) A7 M8 j& c
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    % }: d6 v7 f* Q+ w. N, t2 k3 o
  24.   if(sockfd>=0)* o3 e0 Z/ m6 H1 S
  25.     printf("open socket: %d\n",sockfd);
    ; r& \  l; H5 s6 v6 c9 \
  26. & v6 H/ ]* ?: L. R& D/ r
  27.   //将本地端口和监听地址信息保存到套接字结构体中
    6 }" h$ ~" ^  q# ]& f; ^# h
  28.   bzero(&addr,sizeof(addr));0 L( ?; v- `& U5 V1 V
  29.   addr.sin_family=AF_INET;
    2 K7 T4 g) x/ I2 X1 X
  30.   addr.sin_port=htons(LOCAL_PORT);
    1 h7 X" E# a( O7 X, {& |8 \
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
    ' v3 l* V8 F, |) G
  32.    1 i1 y9 W4 @- G- F
  33.   //将套接字于端口号绑定
    / `* h2 B' s) o7 `- O- n
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0); t2 N4 Q( I0 n
  35.     printf("bind the port: %d\n",LOCAL_PORT);& u8 E8 n% o3 t, O

  36. ! c4 n& h; z9 k: l7 F
  37.   //开启端口监听! u- e3 _: L+ S1 r% [+ R, N0 z% s: g8 s
  38.   if(listen(sockfd,3)>=0)
    9 t* E! l+ u1 M$ x- ~2 I8 i1 f
  39.     printf("begin listenning...\n");
    7 S, \/ u: C. o: w# ?: B
  40.   \8 |6 F2 ]0 ?5 ~7 s; k2 o
  41.   //默认所有fd没有被打开8 Q( |/ S$ A: {2 D+ q1 W
  42.   for(fd=0;fd<MAX;fd++)( Z7 p1 F6 I- m
  43.     is_connected[fd]=0;
    ) ?; \& E  _0 `( F) p7 G

  44. ; u4 c" C* m8 h9 p% Z! x% z; i% y
  45.   while(1){$ \* }+ s" W, R7 f; l5 M0 m) x9 ^3 I
  46.     //将服务端套接字加入集合中& @6 f* ?4 f5 H0 o' N
  47.     FD_ZERO(&fds);
    2 k* o, p- M% R& W: ^
  48.     FD_SET(sockfd,&fds);( @' V, R4 |. X4 V3 N$ {$ ]# r+ t
  49.      
    ( P- C8 T- g. e' ]& T9 L5 S
  50.     //将活跃的套接字加入集合中/ Z5 z3 [1 R/ K# C7 p3 m
  51.     for(fd=0;fd<MAX;fd++). t- {# f; l0 `: s5 ?
  52.       if(is_connected[fd])
    ' _! r8 q) t# C2 b
  53.         FD_SET(fd,&fds);9 C7 p/ \' \( @/ r7 ^
  54. ' U- t; p! }1 x
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
    ! G" C" E5 ?- b! P" C8 S+ B- m
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))
    ; L& {, c6 g- Q) ~3 {( C, H: t
  57.       continue;2 n" ~- b; z% R) B! l

  58. . r! n( w1 b$ _1 {1 u- G7 }5 M9 Q$ W
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字
    . C. q% e7 [# M  f6 j3 j
  60.     for(fd=0;fd<MAX;fd++){
    3 o" {3 f- U1 O% i4 d' F1 ?
  61.       if(FD_ISSET(fd,&fds)){0 h2 |( U" z* r
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接
    / A+ R! k$ f* D2 J9 W! H$ ~
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
    ; z, u9 p  S: m& z9 T
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语
    : J0 }' s5 {% V6 w- ?2 E
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用
    # K) i& S5 f& t* L7 a
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));
    1 X+ L$ S* g9 e
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
    5 F  M$ h4 y5 X* @2 E
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ 0 T8 {  {9 W1 z/ I1 g! ~, o
  69.             write(fd,msgbuffer,sizeof(msgbuffer));5 r6 e$ `" \0 D1 M4 ^% H* I) K% F. J3 b
  70.             printf("[read]: %s\n",msgbuffer);
    % D4 Z6 ~' m$ U# S
  71.           }else{
    / Z# U& p' B1 z( z7 ~* X# x" T
  72.              is_connected[fd]=0;
    1 w: q+ O8 H6 d9 r
  73.              close(fd);1 m! a9 |' l! s% Y+ s6 }  ^
  74.              printf("close connected\n");- N/ e% R" J+ G8 E  t: T% e
  75.           }
    * r: B, u8 |# D2 F) `5 s
  76.         }) f9 F7 u1 D: N% |5 [3 L5 @0 F* b
  77.       }  l7 }& s" V' f
  78.     }! r6 _; N4 z7 c2 x8 w2 u
  79.   }
    ( N6 X% j3 x$ J
  80. }
复制代码
1 n% X9 N- q- J$ x  |7 b- e: t

. Q# [+ g/ B/ |
( I2 ^) g4 R# D3 s, y  @4 j& ?+ V' }- f& M% j

# g* `$ G3 Y, ]+ b' y4 V% G- d
$ `- Z. c. v. t6 n0 j4 a
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2024-5-5 20:29 , Processed in 0.158108 second(s), 25 queries .

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