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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 1712|回复: 0

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

[复制链接]
发表于 2020-5-9 01:53:20 | 显示全部楼层 |阅读模式
实验环境是linux系统,效果如下:
1.启动服务端程序,监听在6666端口上
1006989-20170811220309273-324593640[1].png
2.启动客户端,与服务端建立TCP连接
1006989-20170811220504273-1102389198[1].png
3.建立完TCP连接,在客户端上向服务端发送消息
1006989-20170811220710367-260545598[1].png
4.断开连接
1006989-20170811220732663-1219798729[1].png
实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点

! a) J: b; T* f4 k2 y& ~
什么是SOCKET(插口):
     这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
" L/ g3 @8 W- k$ ~/ _$ ^4 s! C
     "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
      对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
      具体其他高级的定义不是这里的重点。值得说的是:
      每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
      应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层  ->发送(接收的话与之相反)% o& |* ?& F9 K) j3 ~
/ a' d& P: ^1 ~1 @2 H: w

3 f3 `9 G$ R- w0 P
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
, R- t: f) x* R! F% N2 m% v; V( k
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
9 V- C0 {0 A( x& ~/ L% H$ W* l( P

) D. P- v: p- R
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)
' q& }2 K- w# o" o8 X. y
0 ?# `+ m1 q$ o8 N! _; h* {% h
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)

2 s6 m2 K* D) F& r$ W) f' w4 O  u6 V% `: V' L

& g) z& v+ v; b5 p9 w9 h/ ]
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
! \9 a8 @; ^. M5 `

. i* {% s* P8 P  K3 X9 T% l$ A
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>; n0 R; N! U8 g- F

  2. 8 G8 B7 d- @1 D+ }2 _+ F
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
; i8 N0 R5 K$ n5 `* `7 [8 ^9 A" n/ x
    readset  用来检查可读性的一组文件描述字。
! P. S$ J3 k9 m# N
    writeset 用来检查可写性的一组文件描述字。
# C4 h( b6 ]3 ?- ^2 S' ]
    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)

# t" t  f6 L" @! q  p* s; I! Y4 E    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
5 z+ I1 ^/ V7 {, W$ s$ P
5 p+ V3 H$ s8 z7 y4 a1 R    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:) k( j. J% d$ O+ W  a6 o

* [$ W$ t, h( M- e! j& S8 x& n1 b1 s- K
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)2 l1 o  Q* A& X0 n, z
  2. 0 q/ }  s5 A6 O9 o/ Q1 E' x
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)! o; L* Y# q, f" {4 s( m$ f# n. w

  4. $ W/ v5 H! ]; ]  [+ c
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。
( {3 \0 |( ~3 A2 K& C$ x! H+ V
; Z+ ~% k% e7 Y' G1 L8 k) z/ {6 ~   , V2 S/ Q& c  x4 K' j
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集' C/ s! I0 z/ x- k. D
  2.    
    0 }! m. ?- V) @; Y! I
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd
    ! V5 P4 d$ J- [' N/ f7 a# Q, d* m- \
  4. 8 G0 d5 x, ~& ~" E7 k8 a
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd; {5 ?- `1 Y8 z% B! E- z) ~* r

  6. / }* u0 G6 V) f& X
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;1 u7 F  i; G% d5 K4 f0 n4 [$ ~
  2. .....
    ) f+ x! B( X- O0 y, l+ n# ~8 [8 [
  3. fd_set set;
    # T( S8 |7 [- K
  4. while(1){
    5 B" R+ o4 V3 ?1 A1 f
  5. FD_ZERO(&set);                    //将你的套节字集合清空
    4 D  N  P. ^" q" `, u/ M
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s$ w  \2 |1 c6 y: Z3 D8 @- u+ z
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,
    & ^/ ~* h6 r% C6 r
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,
    ) z  n8 q6 }& q) Z( ~+ B0 O( f% n- e
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    ( g7 L  e0 a, @1 H& |: F, s# c
  10.                                 //只保留符合条件的套节字在这个集合里面
    , T2 A8 z4 q, M, @5 j6 j
  11. recv(s,...);3 g3 c9 _, E% p( s* D0 D/ B; j, K  N
  12. }+ M4 `) T( ?5 m4 e
  13. //do something here% l3 P5 ?3 ?, }6 [
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。+ u* M* r# w1 \* w0 X4 F
  2. - B( K, ]% Z. [- G2 s' Y6 b1 I
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)0 x8 Y) b, Q  M! m' }

  4. # I$ P: g2 s* I. W1 a3 Y, D
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011
    $ @+ ]  F; n0 Z5 _4 J4 V6 f: b5 _1 a
  6. ! K8 @3 }( f. ?. m
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待
    # ~- o$ M0 G1 O3 x2 [" P/ G

  8. / G' G5 r# ?6 @& |; @- q
  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 V, J) @0 h3 [2 }: ?* E' x

" w" e  [1 ]+ {2 \
使用select函数的过程一般是:

+ N4 `. z+ X) ^# _6 |    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。1 l' ~& W: Z2 f8 r# F
1 a+ X( ^' q  X$ t) q( ]  Q
客户端:
  1. #include <time.h>6 y# |2 }0 \7 {( r* y
  2. #include <stdio.h>
    : Y' V* u# K: Z9 d* [: T0 ?# g
  3. #include <stdlib.h>
    5 Q8 ^7 F% i; [6 @6 v. |" X- D3 P
  4. #include <string.h>
    & _6 h( s% }5 ^7 F
  5. #include <unistd.h>
    2 N5 A5 ]/ a. s# \0 N. B
  6. #include <arpa/inet.h>
    , f, |4 o4 I% j" M0 I1 P9 P
  7. #include <netinet/in.h>
    1 }0 p8 Q: F# N
  8. #include <fcntl.h>0 @9 A. l% D# I
  9. #include <sys/stat.h>
    ( i1 z! s, `- {/ D
  10. #include <sys/types.h>
    - o) p# b# J% H, b4 ^: M1 J
  11. #include <sys/socket.h>
    7 F* a% L# N8 \4 |- M
  12. * a6 P3 E' b# G6 m$ F
  13. #define REMOTE_PORT 6666        //服务器端口
    7 b2 m" H- M# ?+ r1 @
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址4 w7 B" Q8 i1 b% n: B
  15. : e! m. }/ a, P3 a0 ?- k1 @! K
  16. int main(){
    7 A6 V1 k9 L( L! k* J8 o
  17.   int sockfd;) t6 x2 p+ S- u5 S% v" R4 y) I/ c
  18.   struct sockaddr_in addr;" r/ }" z' s/ b7 @# N2 f2 {
  19.   char msgbuffer[256];+ R4 D- C: W. ~. D) v+ r
  20.    
    2 B" Q. f0 n- k  y6 {
  21.   //创建套接字
    9 [! s" v) @1 D0 @1 e. _6 r0 \4 P
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    * r. r' m2 N7 V  u% w3 o) ^
  23.   if(sockfd>=0)6 ~. |9 m) t7 Q# x
  24.     printf("open socket: %d\n",sockfd);
    * r- a5 y: i( f' u/ [
  25. + G9 ~& G, j# R7 x7 b( r
  26.   //将服务器的地址和端口存储于套接字结构体中4 [" r6 X8 P* S
  27.   bzero(&addr,sizeof(addr));
    & k$ z7 G$ x6 J% m* a
  28.   addr.sin_family=AF_INET;8 s0 A8 }  j  a& V! i
  29.   addr.sin_port=htons(REMOTE_PORT);
    9 H8 u- S. z  }4 l( s# c
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);! Z% ]' ?9 H% j3 |( ]: H5 \
  31.   6 `" Q+ l% g  m$ j  c1 G6 x
  32.   //向服务器发送请求
    1 p: s' I: n* P) A
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0). p; G& o, u) }$ u1 G+ v, E- h! i
  34.     printf("connect successfully\n");
    * S# P7 [( Z7 K" F$ g
  35.    
    ) \  x& I8 H$ [: q
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
    9 |7 ^! S8 z; _2 `
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    ! J+ i" ?/ |7 x
  38.     printf("%s\n",msgbuffer);. C) U- k8 g# e$ O5 }. R
  39.   
    ; O# y+ t. u3 F1 r1 w+ |: T8 _' n
  40.   while(1){' U2 q, z) z* m
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
    ' e! n2 k% [2 A: V
  42.     bzero(msgbuffer,sizeof(msgbuffer));: Y' I* K- a7 K$ N' ]& j
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));6 u& \0 J9 [0 [! i6 u# X1 ^
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)# {: E& d& ?" S
  45.       perror("ERROR");# b3 H) ~8 v0 m$ h) L& Z
  46.    
    8 S' M7 N( m+ e/ f
  47.     bzero(msgbuffer,sizeof(msgbuffer));" I1 @6 k3 w7 F+ S/ Q
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);5 _$ ~0 d4 A0 ^9 y) W5 x% [; |
  49.     printf("[receive]:%s\n",msgbuffer);2 b: A% O2 E# a$ f, [4 |
  50.    
    ; C- q% B! H6 [$ ^% U) p, P
  51.     usleep(500000);, }/ i) ]/ ^  M2 O4 l3 O
  52.   }% t% v2 H  n  B7 b# D
  53. }
复制代码

9 G9 f( {: A3 ]- ]- V: F" O  J1 e9 }& M1 `. X( g! D
服务端:
  1. #include <time.h>
    4 P: N& p- `0 ^) o
  2. #include <stdio.h>7 f; E( ?4 `3 p( f. u/ J6 }' ]
  3. #include <stdlib.h>/ X2 l( S+ d% [- A. a
  4. #include <string.h>
    ) f0 I1 G; k/ ?! \
  5. #include <unistd.h>$ ]4 g/ ~, d/ W! u8 x$ z
  6. #include <arpa/inet.h>
    " A- d9 x7 j% i$ B  ^
  7. #include <netinet/in.h>/ S1 l- Y0 A) ]
  8. #include <sys/types.h>9 M/ G  w" R8 D9 v; |
  9. #include <sys/socket.h>$ m- _, |4 G. w; X3 n% S) d
  10. 4 ?8 F% ~: o7 d3 d
  11. #define LOCAL_PORT 6666      //本地服务端口
    1 s3 L; H; x/ Y0 ~0 m% s5 g
  12. #define MAX 5            //最大连接数量2 n) {7 \4 f3 y0 H. Z

  13. , s3 n/ y' a3 F
  14. int main(){
    / ?! q2 A* `, D; J
  15.   int sockfd,connfd,fd,is_connected[MAX];
    " B& @( h) }+ E4 ]! n5 W6 {
  16.   struct sockaddr_in addr;0 \$ w9 R' a  u. o
  17.   int addr_len = sizeof(struct sockaddr_in);+ b3 h3 i! K/ i6 L% q7 H/ y
  18.   char msgbuffer[256];$ k$ S# t# r% ?: k# r) n) @* F5 ~
  19.   char msgsend[] = "Welcome To Demon Server";+ P' w9 K: a, B; d& _. d/ Y* c
  20.   fd_set fds;, m! H& G$ {& H  `& ], ]# v: F
  21.    
    - f+ b4 s' w4 a  K" R5 C8 t
  22.   //创建套接字
    1 d! G9 k3 O5 r5 q% m; g
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    & O: r$ O) ^# F
  24.   if(sockfd>=0)
    ) Q, [. |8 z! R. u  ~2 A* l, ~
  25.     printf("open socket: %d\n",sockfd);
    , U2 ?- E4 C8 Z' r* b- m  w
  26. 2 c/ i; h- M/ S3 U- L
  27.   //将本地端口和监听地址信息保存到套接字结构体中
    ( x! r* G0 p$ S4 g9 ?
  28.   bzero(&addr,sizeof(addr));
    : l+ \. \6 Y8 w6 S( t( @
  29.   addr.sin_family=AF_INET;% l0 T8 U# F: K- n; f
  30.   addr.sin_port=htons(LOCAL_PORT);( _/ L  B1 F3 s& J6 L/ s
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.04 n5 B5 K0 `0 T* m3 u
  32.    
    # b7 G" C( g0 D: P+ F; a- {: D
  33.   //将套接字于端口号绑定
    % f5 I+ j# }) ?5 @5 J: f  b
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0), r( U, F9 Q) q4 Q3 w6 C7 e) g' [3 |
  35.     printf("bind the port: %d\n",LOCAL_PORT);0 l! Y; s* D. ^& {& y0 [9 |" {. e
  36. ( @: n  x' B: T$ C; i  z
  37.   //开启端口监听4 ]# B( I/ f+ c' s" `- I
  38.   if(listen(sockfd,3)>=0). c( m3 G6 l6 T8 C& @
  39.     printf("begin listenning...\n");
      O  f6 v( O- H2 C

  40. . o- q  v) ?8 _! n' y1 p; M
  41.   //默认所有fd没有被打开; H  J& X) l# t9 j! u0 u5 c# ^
  42.   for(fd=0;fd<MAX;fd++)* B: u$ s2 L3 l
  43.     is_connected[fd]=0;8 d; A/ S3 X& v
  44. 0 p) f. J$ ?6 ^2 O" l9 I+ o
  45.   while(1){1 ^, E8 Z$ F. j% U  ]8 h
  46.     //将服务端套接字加入集合中$ @( I1 d5 Z6 o# J. S, M
  47.     FD_ZERO(&fds);; v! o. F" C; @3 Y" g& `* x
  48.     FD_SET(sockfd,&fds);
      e% C: j8 F2 `- r6 R3 k) A* C1 ?
  49.      
    0 m* v0 s) G  t9 X# ]4 |
  50.     //将活跃的套接字加入集合中
    4 b+ B% b  `9 g$ ^# p
  51.     for(fd=0;fd<MAX;fd++)! g) M. H5 [, d- f1 p
  52.       if(is_connected[fd])
    3 `. q) t: D' e, y
  53.         FD_SET(fd,&fds);- r+ d- X. X# f

  54. : ~! \1 A) P: p9 m% n' `- j) ?2 l
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为04 K" o& X3 ]! }  S
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))- i: d; Z7 z" p6 A6 w
  57.       continue;
    " S# U* B" m1 ?

  58. 4 J4 E( u9 \7 b- [7 P5 ~) S8 g& \
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字
    ; A0 f, E' v: e& l
  60.     for(fd=0;fd<MAX;fd++){
    : k8 ?/ g; n+ z, f
  61.       if(FD_ISSET(fd,&fds)){
    4 R5 R& x) D' p& B; M8 Y
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接) E% O3 B& F6 f! c" S
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
    ! w* }9 ]2 z  N1 I% w
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语
    5 C  v' x& `8 X4 K1 S9 Q
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用
    ! Z" r) V: c. F; f- B/ z& F
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));6 ?- o9 W% Z6 W" u2 W6 N
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字& Z5 D0 G- V9 d
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
    $ Y- O% o/ G" Y9 j
  69.             write(fd,msgbuffer,sizeof(msgbuffer));' t7 i  b: d% v% P3 ~
  70.             printf("[read]: %s\n",msgbuffer);: ~/ z1 I$ U8 a1 L" O, m
  71.           }else{, `5 A7 [4 j' m0 m, M( H0 E: ^! P
  72.              is_connected[fd]=0;6 v/ V6 M4 S; w5 q1 x. F: t( |  p; }
  73.              close(fd);
    ( X7 n% c5 t; g1 j7 e
  74.              printf("close connected\n");
    9 i# g. b% k, N' H
  75.           }" V2 R4 s. ]9 I* @5 C, T: p6 ?5 T8 X+ l
  76.         }9 Z2 P) y0 g/ c* V, o& C4 O
  77.       }6 K+ k3 M; [& m- L; f" H+ u
  78.     }7 a  t" D( c* H6 r
  79.   }7 e, w1 i: K8 C2 r! s
  80. }
复制代码
. W& {2 V& X/ a7 g
1 k# W: A+ c5 j. o/ C0 o

# h. N5 {" N2 a0 R+ [$ B4 T
- L6 q% x9 ]4 k9 n; g; o
! k" G# D6 u+ ]: U3 A% E- D2 h. \: K3 P0 j; d9 E6 b$ O
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

        在线客服系统
    申请友链|小黑屋|手机版|Archiver| 备案信息代码:冀ICP备18019919号-1

GMT+8, 2020-8-10 13:30 , Processed in 0.159012 second(s), 23 queries .

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