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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

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

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

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

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

5 m4 N/ o/ h5 w+ I; q- O. d* I
2 L4 e$ I1 w; h# P: p  i) h" ~: e
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)
- m' H( g7 W. f7 o; c
' P3 q, z+ @* i$ y: ^
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)
5 C) V0 \! d  N& o2 Y
; t1 c& p5 Z7 T7 b, J, X# y

7 y, M  F2 w! Z2 K2 B- ^, l; w
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。

) X# {: v1 d' _! Z5 j: |/ s* L8 {' _& y- x& j1 \
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>
    0 Y7 i: U6 Q4 D6 c
  2. ' X+ `8 ]5 J6 f( _$ S+ K" F
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
8 p, w5 S3 A" K; Z. T2 q' R1 k* \
    readset  用来检查可读性的一组文件描述字。

- J- b/ I- A$ O, E    writeset 用来检查可写性的一组文件描述字。
% p) I4 y! P5 X1 c0 ]
    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)

# E7 \) \" a5 i    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。8 {3 e/ U$ I4 e. q6 M: P- l
) a6 r4 R6 q3 Z) j
    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
! I  i) W+ Y: _! @: R
& V  N% U9 o0 @7 L. _" l
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)7 o9 u/ j$ Q8 N1 M# V, w1 U

  2. 4 i! ?) g, _+ k- l7 g; [) M% `$ E7 K
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
    0 o' T5 Q1 v- N  u  G2 }1 H
  4. 4 A4 t- s9 r/ l' G
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。
" X! A' ^, i6 K) p7 \9 f
) N9 \% C! V& m' w! y   4 L& ]* L: ~* `# K+ @8 O6 ?
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集; b' P5 W6 x' Q' ^" i: G
  2.    
    ; V  v  a# o  _! T3 E, O0 L
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd1 S1 {8 x! ]( D( m; I
  4. & F( ^  g+ Z# V6 ]& q# L2 n/ D
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd/ O! H5 I; c1 I* R" K& b

  6. * _. L% Y7 Y+ a1 q( Z
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;
    . D5 S( j" S+ m$ `" E/ N& \
  2. .....5 ~4 D9 ?3 \8 c3 c2 p5 e
  3. fd_set set;3 H; U  T7 {/ c. L
  4. while(1){
    8 ~% G' C4 j6 _5 V
  5. FD_ZERO(&set);                    //将你的套节字集合清空9 A$ n% Q7 L1 B+ I) S  h4 ]
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s4 U4 w5 D1 B/ s7 _7 T( N
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,
    " x+ i  ]& X6 h  D# o  k+ q" `9 u
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,
    # P0 f) r5 {5 v1 m
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    ( t# b9 h& X% U6 _
  10.                                 //只保留符合条件的套节字在这个集合里面
    : X+ H6 Z+ |& s# v$ O2 ?
  11. recv(s,...);" R. x8 u& H( ~  G
  12. }0 `: r6 y( ~. ^8 s: e
  13. //do something here/ z- {- i6 ~- M5 E# l4 P, K
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。
    # K# s! j4 A# k! a3 t* f2 n" l9 U3 Y
  2. , `  c9 D+ ^! y5 f
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)* X( f$ r7 ^1 G, L5 z+ A* F; @
  4. & M$ P+ [) W& b9 y+ v! C
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011
    ! }3 s0 s, y$ I8 K' ^1 a8 [2 e

  6. ' f$ a3 n& i' y
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待5 x( [  r2 R4 d1 Z9 p4 K; k+ u
  8. 4 W0 X( o1 a( m
  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.具体过程看代码会好理解

" q) `1 o! {2 _4 P' P  m5 ]% ?  ~0 ]% [! C6 \7 A  k$ ?& F
使用select函数的过程一般是:

3 S( L7 i8 l8 P: \) O    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。/ u, L1 T. L3 F3 R! R: P$ E  T9 l
( R6 f7 W' s! d" J
客户端:
  1. #include <time.h>
    ' k3 s4 }5 _3 x  A3 P7 j
  2. #include <stdio.h>
    5 `9 ~2 ?. z* N: q* @/ {
  3. #include <stdlib.h># f! g$ H& A" x2 L% Q0 f
  4. #include <string.h>7 A9 ^4 \0 }4 x7 Q$ [7 z2 B2 ?
  5. #include <unistd.h>! E: j3 a9 S: Y1 D" W) k
  6. #include <arpa/inet.h>* _6 [* b) u# Y7 V! G2 V% W+ H
  7. #include <netinet/in.h>/ }0 x: w3 o# a$ u4 i3 X
  8. #include <fcntl.h>
    : |. |4 w6 ?# ?3 ~3 N3 a
  9. #include <sys/stat.h>$ K( N; a+ S& o4 C& J4 a  D
  10. #include <sys/types.h>3 ]7 r: B- \8 f. V
  11. #include <sys/socket.h>
    % q; y0 C  _' p

  12. ; P" V  s' j" _) r
  13. #define REMOTE_PORT 6666        //服务器端口+ Q* O" h/ u7 ]0 p8 P9 `" R
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址; b% V$ b$ V, g% r+ A# v% P
  15. 9 v4 T1 q' m! X
  16. int main(){4 ^9 t* u( W1 R
  17.   int sockfd;  O  j& [6 i1 ~4 j6 l: _
  18.   struct sockaddr_in addr;
    5 G( \0 m$ K- K, S4 |  [& ]
  19.   char msgbuffer[256];2 }  a" e" [- Z/ P& @# U
  20.    4 b/ k! E  ]/ {8 ^2 }1 t* b
  21.   //创建套接字
    + O# E# b3 t& N
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    + d* p0 I( f, a  p: X9 ~
  23.   if(sockfd>=0)" l  p; w2 Q$ ?& n3 |- m- X8 n
  24.     printf("open socket: %d\n",sockfd);
    9 m& l: r) G$ m( T
  25. 1 I2 ~- j1 _1 b2 n
  26.   //将服务器的地址和端口存储于套接字结构体中2 ?- E  `6 k% N: m( c* u  Y/ W
  27.   bzero(&addr,sizeof(addr));
    ( t# p+ p. L1 q9 K% f
  28.   addr.sin_family=AF_INET;, T$ C) {0 c/ w" j6 Y
  29.   addr.sin_port=htons(REMOTE_PORT);. J* L& N+ M7 t
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);+ H5 L" ^$ J( [' Z7 g" F, M$ K
  31.   " ^" p5 l8 n1 R; W  v
  32.   //向服务器发送请求8 c* k9 P4 n  H2 c. k, {% n
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)0 f7 w! B* K% r
  34.     printf("connect successfully\n");6 P9 n/ Q3 M/ M
  35.    , f+ i  S, [1 G: d. V
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
    , A: j, ?1 l/ T
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);0 z1 y9 q1 q' L( p4 W3 v- X1 J6 j
  38.     printf("%s\n",msgbuffer);/ O* R, j; V5 X$ @! L5 A
  39.   
    + e+ ?  n' S& c8 [
  40.   while(1){
    5 L9 R& X# Z) L' n0 z7 f
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息& Q" b6 N' E/ L1 ~
  42.     bzero(msgbuffer,sizeof(msgbuffer));
    ; Y- ^4 O$ A: C" a' o! T
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));+ h. e" }# t6 P; O4 \
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)# q, |# }! C: C6 a  X" j1 o
  45.       perror("ERROR");, X3 V+ p* E9 X# l
  46.     ' b; M, j7 Z3 R# R# H
  47.     bzero(msgbuffer,sizeof(msgbuffer));4 v: _& H  s3 j! H) s. G  T
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    2 d/ z+ V# `) f0 M/ }! c
  49.     printf("[receive]:%s\n",msgbuffer);
    5 X6 C' ^- T4 I  O3 S
  50.    
    , U2 e7 @- Q6 S( c1 N: Y4 a/ A
  51.     usleep(500000);/ J7 E3 Q0 {7 D7 r/ I
  52.   }* H  u% N3 i: T, E
  53. }
复制代码
9 _* |  W) b) a3 l% |' }

- c+ Z8 M. i5 r* @  ~: c
服务端:
  1. #include <time.h>
    4 }, [2 w! {1 n1 o" ?  W
  2. #include <stdio.h>) a" P: M+ J# z* M$ |$ L
  3. #include <stdlib.h>! a- n; g/ J9 Q4 s
  4. #include <string.h>
    2 |: O# S" _# j- A9 X+ _/ Z, }
  5. #include <unistd.h>
    : f5 @$ S3 P' P, k0 o
  6. #include <arpa/inet.h>7 n' |( T3 R, H1 l8 k9 V/ R4 J
  7. #include <netinet/in.h>
    : e" S  x( r3 r8 H# Z& G8 Q7 {% T
  8. #include <sys/types.h>: x3 ?) B" w9 M$ j4 t% F
  9. #include <sys/socket.h>
    " ^8 a, H* q0 ]
  10. - f& u5 b1 \3 H  m" _: A( r/ f
  11. #define LOCAL_PORT 6666      //本地服务端口
    2 ]+ C5 E9 G+ @( J9 t
  12. #define MAX 5            //最大连接数量% T% t" X4 y1 |4 T
  13. ( Z& k0 K3 J5 z' ?& O
  14. int main(){" O! s" }+ X4 N3 V) j- D) X& W* z
  15.   int sockfd,connfd,fd,is_connected[MAX];( O) m9 k& k/ P$ y
  16.   struct sockaddr_in addr;
    * g0 D& Y6 M1 U7 e7 |9 E" u
  17.   int addr_len = sizeof(struct sockaddr_in);5 s/ ?' ^% ]0 X& y, O5 m: }3 L  M5 N6 h
  18.   char msgbuffer[256];
    ! C, n: X7 j, q: {+ f
  19.   char msgsend[] = "Welcome To Demon Server";8 ^0 ~" I  W* N; M; [% Q$ B; o+ \
  20.   fd_set fds;
    , \$ J, i8 {8 U  z, y% g+ b4 G( ?
  21.    
    $ U6 I" v; i. D  }# c  E4 Y
  22.   //创建套接字, b" w- y- C* i+ }4 n9 n0 a3 `. V
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    . E0 g' j. q" l" Z  U+ k
  24.   if(sockfd>=0)$ r2 A* }9 |  o, ^& F$ Z% C
  25.     printf("open socket: %d\n",sockfd);; f7 Y$ n" ^, r4 V! |/ r3 ?& ?0 ?

  26. ! s- }/ H; j1 P+ Q
  27.   //将本地端口和监听地址信息保存到套接字结构体中
    . u$ V& ^  D7 N' _8 ^  w7 ~
  28.   bzero(&addr,sizeof(addr));
    6 A# ~, ^: V+ v' B* G$ I* l
  29.   addr.sin_family=AF_INET;: m3 `. q/ q4 |/ n* B. ?3 u. \
  30.   addr.sin_port=htons(LOCAL_PORT);* q0 N. @+ h: O6 ?" Y
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
    " N& u- ?3 l4 `4 X! E/ q2 X
  32.    
    $ j7 B* c* q1 Y' r
  33.   //将套接字于端口号绑定
    3 A& I5 N" U2 a7 C/ {- g3 x
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    % K! T  C; s4 C" k4 Z
  35.     printf("bind the port: %d\n",LOCAL_PORT);
    * r7 K5 |8 v2 R

  36. : G, @. ]0 U& O- f" @5 F: x+ N
  37.   //开启端口监听
    $ Q' {$ t. p+ e: `4 F+ y
  38.   if(listen(sockfd,3)>=0)3 }" y6 n3 ~, v- V: m/ D/ |0 {
  39.     printf("begin listenning...\n");/ h/ u+ Z1 p5 I+ G, J

  40. % o7 I/ k) g& V% s6 k6 }8 n
  41.   //默认所有fd没有被打开
    ( `( d3 b$ U5 y4 v
  42.   for(fd=0;fd<MAX;fd++)
    ' Z6 v4 S$ ]% {; z* ~8 h
  43.     is_connected[fd]=0;1 N) c  V, i  ?9 N( M
  44. ( o, d# s8 K8 R7 ~# v
  45.   while(1){
    ( N0 r* K( h9 ]; Q; ]) N' R" u5 U
  46.     //将服务端套接字加入集合中
    , R$ I7 I: g9 ^  `5 A
  47.     FD_ZERO(&fds);
    . Z! u. T* f  C4 y; S
  48.     FD_SET(sockfd,&fds);. f( q, {, F5 o5 X5 `
  49.      
    # l0 ~& @9 m0 p% S: c
  50.     //将活跃的套接字加入集合中( G* S  R& j+ a1 b
  51.     for(fd=0;fd<MAX;fd++). x5 }1 z4 G% N+ G0 }- a. j
  52.       if(is_connected[fd]). {" J5 q) w2 w8 e
  53.         FD_SET(fd,&fds);" C. N" F7 s9 z9 _8 A

  54. * h: u" ?8 f" p& q3 h
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0& q' F% L# k2 g! X- @7 Y) |
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))
      H7 s* _# x4 N* h  z5 k
  57.       continue;" m! p1 ~. E6 x2 c
  58. ) h  Y- t; q* I8 a* q
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字. v2 U* b: b2 I' y+ t7 w5 a6 U
  60.     for(fd=0;fd<MAX;fd++){6 ~% h) k% y, O/ L% q
  61.       if(FD_ISSET(fd,&fds)){
    * C& O) [9 Q$ g$ h8 I
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接
      U  N$ z% q: p2 ]8 \
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
    9 k+ O: d9 v- {$ R
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语
    % f0 |' l) a4 G- T1 ~. |- Z
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用
    ) m& J# @1 l' c/ I
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));( o9 O; G/ m8 w+ c* }5 h
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字' k& k( o6 E" }, E
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
    " d( O% Q7 E0 u; }
  69.             write(fd,msgbuffer,sizeof(msgbuffer));
    8 |; t& t; \* M
  70.             printf("[read]: %s\n",msgbuffer);, m2 z) Q% s8 U6 h8 a
  71.           }else{# H) ?3 G5 c; Q7 T. K* y4 i  ^# J9 e
  72.              is_connected[fd]=0;- G* j& G: _+ t2 p4 o! j
  73.              close(fd);7 A+ m  r; v4 _& ]
  74.              printf("close connected\n");
    1 o& }- J; ]1 M2 k2 S8 f7 Z
  75.           }) |  W/ f" K1 i. p. m
  76.         }
    ' \9 s) S& q! v& J; ^
  77.       }
    ! r( l2 d1 V9 A! N5 K# m
  78.     }
    1 I. T: \' o: Z4 \: Y
  79.   }
    ( I) H2 k5 T$ n% o4 c5 @
  80. }
复制代码
0 N% w: }1 x; o8 f
% C( @2 b8 ?' x" ?; z9 \6 X

4 E: I& z/ S9 r6 c0 Z# L$ r  c2 j( ?4 }: V# B4 F6 j

1 M$ n! b: u. g; S0 e/ h9 _; E' x
1 j+ S) d* x, ?1 ^5 ?' G8 {: I0 }
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

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

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