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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

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

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

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

) W- u; ^: U/ N
什么是SOCKET(插口):
     这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。. h/ I5 x7 F' i& {, Q, v4 ?4 t
     "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
      对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
      具体其他高级的定义不是这里的重点。值得说的是:
      每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
      应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层  ->发送(接收的话与之相反)6 T& h) O' ^$ o6 M

6 M4 ?1 c7 U, |+ H& [# }( `
6 v& W% [0 N8 O2 D' r
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
+ A# h; d: l: j* }0 z* i7 Y
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3

$ D% ~4 W, v/ O. J
0 G2 J0 ~0 g3 i
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)

) w' Z# Z! O+ i! H/ U* ^6 v3 V" N# i$ G* u% V4 d
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)
. w- O& F3 B5 d/ I* N4 J9 H" ]/ `

6 X1 o9 n4 I# S* a7 S2 [
3 r' C! S  R$ M9 q$ l% E
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
0 g+ q3 a/ \3 ]& E
' }% ~7 Y6 n* M; m3 _& W' {
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>8 W( h' c5 a2 _1 k! A3 ]4 s: m
  2. " {' {; U. u7 a6 w7 r
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
. \! w% z: E: S+ E6 g
    readset  用来检查可读性的一组文件描述字。
1 V* l, U- M5 y; V- C8 |1 m, f
    writeset 用来检查可写性的一组文件描述字。
% [% v, W5 o* u& R& D
    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
8 f8 N. J( Y/ Q; K- V
    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。& P9 [, B. R6 k/ J

8 Y! f4 h$ ?; Q; [    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
% V. ?( G% e4 [5 ]/ w* ^" |+ r: u
( j1 p3 `7 M9 T( G6 ?2 t
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)9 S# F5 W$ t6 ~* N0 A# f
  2. 8 B9 l3 i3 W8 M1 |
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回), U9 o4 c) V: g

  4. . f# d$ @% j7 o
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。
- C# L6 |- d+ L3 O- U+ {5 |5 L' @6 l  J- l; h6 M
   
% j, \6 ^8 e" v7 M4 ]4 |3 w4 g) Z* R
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集/ f$ s8 p/ F* d! w! @0 w
  2.     $ J1 N3 G; j' Q9 E, `" v( e- p! F
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd! m& ~& o; O5 p( |

  4. 3 p! M0 d" Y  b3 M2 ?2 o
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd
    , q( ]+ P* A+ b- O& Y6 {7 T

  6. * H  n2 {% ~6 k  i- }) b0 F0 z( q
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;- I+ F% z/ x# `7 ~" s
  2. .....% @$ h: x. ~6 Z6 u
  3. fd_set set;
      k5 g: V7 C% ]2 C5 t/ G5 d
  4. while(1){
    4 {/ `) G8 ]% e3 Z
  5. FD_ZERO(&set);                    //将你的套节字集合清空* `. v( R: x, j6 `5 J
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s7 I- w4 X- V$ J6 `2 {! i
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,2 I* h- W3 G, B) @+ e: u
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,
    ( g  F* S: s# i7 z% y: ]: M
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    - x6 Y- z5 x7 f4 }0 i2 }! l
  10.                                 //只保留符合条件的套节字在这个集合里面; s" ?$ E1 L0 }) D, e
  11. recv(s,...);6 v9 i& j! u0 H$ p
  12. }
    7 e& v% }, _. R9 `2 u) @
  13. //do something here
    6 n/ D2 ?/ i- n' L8 M4 {
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。
    ( @9 c2 q1 Y. k! L9 P# t& O

  2. ; o. D: e$ Z4 r; ?7 }: ]* x
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)
    9 y2 I) ~3 q, {* i0 [
  4. ! P# t& D1 B6 q" q
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,00114 V" o4 C3 v9 E1 x
  6. , v$ p( b& G/ f: Y1 Z7 S
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待  |8 j9 P, ]% f  @

  8. ! Z- ^' S4 X; H% _. Y* G/ B3 _8 N
  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.具体过程看代码会好理解

8 u, q  \8 w5 r, Z3 \. j# {8 n+ a6 ?/ p% f6 O) |6 W* l% t
使用select函数的过程一般是:

; ?2 ?9 v$ S" \    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。, z1 F) f. r) s" l. \+ D- \1 f0 W
; M; ~: y/ C+ L, X
客户端:
  1. #include <time.h>
    5 l7 l9 T! f7 r) t/ v
  2. #include <stdio.h>
    $ u( _. x6 |/ Z6 A+ n
  3. #include <stdlib.h>0 \# I/ N$ z+ i" X  ^. q
  4. #include <string.h>
    & B! o, m$ W& N% b2 X
  5. #include <unistd.h>
    - ?3 W; [1 H" T
  6. #include <arpa/inet.h>; b5 t, c0 U1 n. |1 S7 s4 v0 f
  7. #include <netinet/in.h>7 u- \  s: S9 w5 Q" t+ O
  8. #include <fcntl.h>% m, M) R$ O- f" q/ y$ Z
  9. #include <sys/stat.h>' _% ^% I% [# x  T+ N3 S
  10. #include <sys/types.h>
    6 i: l# i1 J' B9 }3 J; w% E$ y
  11. #include <sys/socket.h>
    8 Z: `7 }5 K, s7 Y. n# n

  12. 6 a; `! F8 m/ B  @4 \' H
  13. #define REMOTE_PORT 6666        //服务器端口8 L& h8 q3 S" G
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址1 ?1 v; s$ \4 W
  15. 1 P8 a% k! r- i- {9 v4 v1 f
  16. int main(){/ i9 U; H3 w# r9 Y# J) ~
  17.   int sockfd;
      h  D( K5 N0 Z+ y; v# v; M: Q0 ~
  18.   struct sockaddr_in addr;4 p/ d/ L' K% H
  19.   char msgbuffer[256];) U8 q! F! i3 [8 {
  20.    
    4 ]& ?; d& s  ]7 P  Z
  21.   //创建套接字
    9 J: m1 d. V6 D+ u  Q' w
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    - h+ R- D4 C1 W
  23.   if(sockfd>=0), q2 O1 X1 h8 z* T- b6 ?# `5 o
  24.     printf("open socket: %d\n",sockfd);4 `( b( V8 J, O! U  I' c
  25. ' ?! }+ {+ B& J0 [
  26.   //将服务器的地址和端口存储于套接字结构体中
    & |& M- e; G1 k1 l. r* W& `
  27.   bzero(&addr,sizeof(addr));
      R. Z5 @- ^8 @- r( I2 B# h
  28.   addr.sin_family=AF_INET;0 M8 k7 z* C  u& a; }
  29.   addr.sin_port=htons(REMOTE_PORT);7 y" C2 I$ }  j
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    1 |% r0 F8 F: F% }6 ]- }5 }
  31.   5 C, G& a/ M3 ^0 v$ \- w5 Z
  32.   //向服务器发送请求
    + d% _& M* r1 S
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    9 {  Y2 m4 u4 V4 M2 f. a
  34.     printf("connect successfully\n");
    % s( U; R1 O, F6 [# k& h8 L
  35.    
    + d, R$ L) b7 }- x8 P+ F
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
    5 M8 U- ^7 O7 R9 h
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);# U8 n4 s' U4 r! ]1 ]0 `* W8 K/ O
  38.     printf("%s\n",msgbuffer);* L# k/ y$ K; ?4 C) V+ p$ |
  39.   # z  \& O- y1 Z$ L: E0 t( f, j
  40.   while(1){9 @( Y( E$ D7 }; r5 G6 K5 P
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息; n$ w- l# i, f8 H; v
  42.     bzero(msgbuffer,sizeof(msgbuffer));
    + |" ~6 a1 }) z! O
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));$ v' [! |) H" S/ T) k
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)5 a$ W3 j; w. @9 t. u' S
  45.       perror("ERROR");9 d+ L9 }; b2 E1 R) a5 Q! W
  46.     , v! {3 w( t& I5 S' Y
  47.     bzero(msgbuffer,sizeof(msgbuffer));( a: |* ?% W0 }6 e
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    0 y7 c+ z; p2 u. h5 Z
  49.     printf("[receive]:%s\n",msgbuffer);9 B1 a, g; c7 V8 C6 d1 N5 V* L
  50.    
    7 R# V$ `- X9 H* Y. E% l" x
  51.     usleep(500000);
    ( u/ R. x1 q5 s8 I- d
  52.   }0 t; B2 J$ U, t9 I- U
  53. }
复制代码

; x- V; U+ f9 B6 O
3 B1 y% {6 B$ E
服务端:
  1. #include <time.h>: f7 V) d% i/ s; p8 M
  2. #include <stdio.h>, i+ Q# t9 A$ K/ Z& O: F; J) I5 R( ]
  3. #include <stdlib.h>2 a3 q0 T& V. ^0 G& r
  4. #include <string.h>) ]7 V4 {4 I7 y6 p$ M
  5. #include <unistd.h>
    ( A5 a/ q' _4 L
  6. #include <arpa/inet.h>" z$ G( U" t: D9 J8 h: x3 s' r3 k
  7. #include <netinet/in.h>5 l: \" u: A- ?& f% s% ?
  8. #include <sys/types.h>
    ; V4 d7 [) x& W7 V1 C% b
  9. #include <sys/socket.h>
    . }2 N9 X! _0 `$ x1 q5 F4 N

  10. ) `5 R) L: b9 T: V+ `5 W
  11. #define LOCAL_PORT 6666      //本地服务端口
    ) Y& j* u0 \7 |( h1 i, e, s* i
  12. #define MAX 5            //最大连接数量
    ! R# K4 b6 ~$ }4 B8 K) ?
  13. ) q1 r" z# C6 t' X
  14. int main(){. n0 g  r/ }9 J8 o( f2 O# B
  15.   int sockfd,connfd,fd,is_connected[MAX];+ @; k6 e$ J& B7 G$ T$ f# a: _8 }5 n3 J
  16.   struct sockaddr_in addr;3 A# r! P% I: A; {3 ~' j
  17.   int addr_len = sizeof(struct sockaddr_in);
    3 H: ^! A( X  G  p
  18.   char msgbuffer[256];/ Y, Z, J5 N! k$ \& U1 ^
  19.   char msgsend[] = "Welcome To Demon Server";* B8 Z: e  ~6 T3 f; j/ o0 b/ B
  20.   fd_set fds;
    4 W' y, j0 V' @: h$ v- @0 f
  21.    , Z. }0 i$ T; i; n+ r. {0 J
  22.   //创建套接字
    + A4 k. z: U+ I# G
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);% I' G5 v  {5 ?1 }' D
  24.   if(sockfd>=0)
    6 R% Z* y; J& q
  25.     printf("open socket: %d\n",sockfd);
    . d+ Z! e, L7 r: q3 c. C1 F
  26. ; M, i6 R, w& b
  27.   //将本地端口和监听地址信息保存到套接字结构体中
    . e1 R: A, Q7 s' c- ~2 r
  28.   bzero(&addr,sizeof(addr));- H8 g0 p. S4 _0 T4 g2 `+ H9 Q% @, U
  29.   addr.sin_family=AF_INET;& ?) f+ b! v9 t4 M" Y  N' a5 c
  30.   addr.sin_port=htons(LOCAL_PORT);
    ! v. k/ Y5 F1 i, L# Y4 {
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
    : ^5 j- D1 x  X8 \  Z
  32.    + d! c% {5 F# a1 B
  33.   //将套接字于端口号绑定
    % ~6 N- k4 `+ I/ B
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    # G2 k- G  a9 s7 |+ V2 m9 \9 m! Z
  35.     printf("bind the port: %d\n",LOCAL_PORT);) X  D( ^7 h  S# @" |4 Q% L

  36. ) O* O) \+ |' d
  37.   //开启端口监听
    4 ?% W7 ?2 W& r2 `* U, E4 P' N) v
  38.   if(listen(sockfd,3)>=0)
    / C$ ~, z9 |& Y7 _' a- r  S6 ~
  39.     printf("begin listenning...\n");, L- Y3 b7 v2 z* Y; N: t1 X  W* z
  40. ! V& W- Y. `5 R- c. J# c0 d
  41.   //默认所有fd没有被打开
    2 A2 H. D  P7 s' d/ x' j1 n2 h  |
  42.   for(fd=0;fd<MAX;fd++)
    ) O4 U& {9 R0 V, Q# J" q4 d+ D
  43.     is_connected[fd]=0;4 q/ q5 }- m: f& C5 J0 o$ |

  44. 5 u" |' J8 Q( r8 ~; J
  45.   while(1){% {2 d5 ]( l* j0 \/ a
  46.     //将服务端套接字加入集合中
    ( E0 m; C1 r! O+ n
  47.     FD_ZERO(&fds);: J+ p. U( X" [, [. u  L1 E
  48.     FD_SET(sockfd,&fds);
    ) l/ }$ ~! j1 P0 i
  49.        h4 g. g3 `0 j
  50.     //将活跃的套接字加入集合中; |/ n! T" t5 r; f1 b3 O8 D# c" n9 l2 e
  51.     for(fd=0;fd<MAX;fd++)4 Q- U' L2 B$ s0 v
  52.       if(is_connected[fd])
    * E. i+ E, U2 {' z% Q0 k3 f0 Q/ @9 }) o5 z
  53.         FD_SET(fd,&fds);
    . f0 Z4 _' d* y  [; Q$ ?

  54. 9 d; D) R$ j( d( J# Y
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为04 N6 j" n. C: \$ h6 I
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))
    $ C% @* l. Y9 ~. J! y9 `$ _% @6 h+ a
  57.       continue;
    # d4 G% M* _, S' ^

  58. * V3 R+ M5 m6 u3 [3 Q
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字
    ; l% g/ F7 _3 A: j' i
  60.     for(fd=0;fd<MAX;fd++){4 Z6 N2 n) b- @" m. \7 a- F7 x' [
  61.       if(FD_ISSET(fd,&fds)){
    9 ^$ S) R7 X7 G
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接% {, _( ~0 V7 ~3 z  o% [  i- O0 G
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
    ; v- K1 z, \8 L& R' ^7 U3 f
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语# @! L: V& s4 W# \
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用
    1 ~- P' u" [6 a! m* X: N
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));6 I* d: u/ A$ z) n+ k; l& ~7 t, P
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
    6 {8 P5 q: Y7 E; r, L' i
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ 4 U" g( P% b" Z3 J3 b  c+ K
  69.             write(fd,msgbuffer,sizeof(msgbuffer));
    3 O  G5 |( v5 F7 w" ^) U1 U3 q
  70.             printf("[read]: %s\n",msgbuffer);
    ' L5 E( f0 X- `& c! J
  71.           }else{' y0 v  h+ Q+ ?8 c
  72.              is_connected[fd]=0;+ ?* x5 s( Y! \0 @/ h
  73.              close(fd);8 |- d: _3 N) ?. Y- ]- i. B2 S
  74.              printf("close connected\n");. {2 G/ g- e! J' \& O7 M
  75.           }
    * R; t4 U' P' Z
  76.         }
    - c. J) X6 C7 A  ?
  77.       }5 v. ]8 S  s8 p2 I3 O
  78.     }  M( ~/ ]; y0 Z
  79.   }
    1 ~* g$ n: B! ~3 {* {
  80. }
复制代码
# G" o' h1 r0 [- \4 ]/ o

$ d0 |0 y# e9 w% u" a, x( n- z; U6 x# l: g  |5 ~! P' ?- S2 s" J
, Z  F4 M8 j, x) M3 z% `7 v

8 e. T" y& K" d( g7 ^) {* ~0 w: k2 u; b; }+ V1 R7 a
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2024-5-19 02:26 , Processed in 0.128559 second(s), 25 queries .

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