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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

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

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

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

5 v/ m# i1 t) H; C
什么是SOCKET(插口):
     这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。) W1 ]# m% L# [! U
     "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
      对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
      具体其他高级的定义不是这里的重点。值得说的是:
      每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
      应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层  ->发送(接收的话与之相反)/ |+ |+ W- e' n  Y
& B6 j  [2 m$ t4 I

, r9 {4 j: H" Z5 [
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等) e( A, A* o4 U7 q+ F9 f9 I
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
( {) o% S/ U  U4 r, v4 t0 u
& [+ E& e2 Z) c$ e& b9 @: n8 v
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)
. y3 |, O6 C% _- _

; J6 O0 F' {+ r1 q. \9 J
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)
) ]! X, y  }: l. [0 g" k) V  w5 q

& @* v. e3 m3 @/ u+ }
, {5 M* N" _2 Q* C4 [
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
, w  l2 W) M# \4 P8 t5 O/ r
9 b! h8 s  }6 A/ v, k
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>
    ' |% I4 Z$ f, e8 W; o& s4 e8 j+ y

  2. # S# M$ h# H5 m3 a0 j
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理

+ v( p. J6 K. q# m- V' j    readset  用来检查可读性的一组文件描述字。
9 {* e# Z9 u. V$ k! T" d
    writeset 用来检查可写性的一组文件描述字。

7 D* U& d- x; N( J; t& q6 k/ a/ u    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
7 L7 f5 m2 f( z* C5 }) _4 W
    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
% ^5 e, \& q' i/ W; h! {4 I1 @; b6 G9 S) g% S. I( O7 g, ?
    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
% p# `3 U  w& C, M4 i
* w! D6 x" Q7 q" {: |4 X) n
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
    % x( ]7 m0 y) G- X: E* h5 {

  2. % F& k* u! |9 O2 I1 H
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
    " y& D6 Q. _6 E' i! P) F( ?

  4.   I& l# E  v% n! E" c
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。
: ?- d7 [  S$ J+ w' x$ @" d1 l
3 j0 ]: O, `6 U. v1 _3 O- I0 V   
1 I/ y& t8 y' e3 k  }
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集* q$ c  W; O5 `% C9 G- M* H
  2.    
    " l( I9 d2 A/ |6 ^& q4 ]8 X% N
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd
    ' R) S' N1 _3 k7 D% W" o+ O8 P# I$ G
  4. 0 F% M  V' r, t5 s( ~! N: }
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd$ j/ A% a- J6 J- Y$ P

  6. % Y- {- G! {6 a, f. w6 h
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;
    # t  ^4 O! F' h$ V% ]8 Y# D
  2. .....; M4 g! z$ q% g. [( K0 l
  3. fd_set set;' w* Z/ A" q# B
  4. while(1){
    9 [. o' i8 x4 e* D/ e$ Q% X
  5. FD_ZERO(&set);                    //将你的套节字集合清空/ {9 ]- D  @4 j) h
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s+ M0 Y' {4 h6 R! ?- F
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,
    0 d1 j' l3 \3 g; P* V
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,9 t! a' I% k& ^5 s
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    ) K  s) i7 ]( c. C/ f9 W6 u
  10.                                 //只保留符合条件的套节字在这个集合里面
    0 e8 ^! ?; v5 g8 m5 g& N1 L
  11. recv(s,...);9 k' n6 G( L* ]9 f
  12. }
    9 P# H( ?# m2 b6 K- {7 m
  13. //do something here9 E9 w/ t8 m8 D+ d. |8 Q/ ?8 Y' i
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。
    " g% @7 H1 |5 b. P; |

  2. " S7 C3 r# y0 Z0 r
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)" o  y& F9 S; B! f1 K) s) \6 U' ]
  4. 5 N3 m0 \. i8 r, P
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011
    # P7 e! K+ r. y9 x/ M
  6. * |5 k2 f& L4 n2 y/ |
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待
    " v+ d" N6 R# ^' T2 _
  8. 7 K- b3 ]( |! M' ^" t; 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.具体过程看代码会好理解
' C. Z8 X" J( N( e/ J
; i9 G, a; o; ?9 s% ~% t
使用select函数的过程一般是:

, i! A! \' E8 i6 b3 c    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。* @9 v* n1 Y- c, ?. }

. J/ q4 x9 b% b3 N1 L
客户端:
  1. #include <time.h>
    4 Z8 w, y- Z* W' D* {; R, `  f0 N9 r
  2. #include <stdio.h>
    ! _0 ^/ P6 n7 Y# g
  3. #include <stdlib.h>
    , D% E/ I4 t! j7 V& p4 I, @7 u
  4. #include <string.h>
      t0 E0 J1 e! C; w( C
  5. #include <unistd.h>
    . S" i5 Y6 V  _5 l* Q* N1 }
  6. #include <arpa/inet.h>
    3 L( K9 s; p6 t! S+ c
  7. #include <netinet/in.h>+ O$ q9 Y. ^* p8 J  G  `
  8. #include <fcntl.h>: e5 G7 `1 Y5 }  q/ U# t$ {+ S
  9. #include <sys/stat.h>
    8 e. u8 _% ]! v0 m; o
  10. #include <sys/types.h>
    8 Q( j3 L" H# i; A$ a& U% B5 c3 x
  11. #include <sys/socket.h>9 h& F' n' {, [9 q
  12. / A: o0 Z. D9 O& K! }* U5 A6 Z
  13. #define REMOTE_PORT 6666        //服务器端口+ H9 t  r: F+ _. R" q4 D
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址6 `8 A: v) }+ G/ ~
  15. / z- m% p% M' K1 x+ w0 d, [
  16. int main(){
    * f- h& s( K) S& r
  17.   int sockfd;
    " \" M$ x# H& x0 n6 n' o
  18.   struct sockaddr_in addr;
    & d( E% ?, T' L+ i
  19.   char msgbuffer[256];% l3 H/ a( B% T0 C0 }5 W5 ^! w
  20.    
    : K  ?; x2 i: R  k1 C
  21.   //创建套接字+ [5 p7 t0 l" X# R3 L
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    ) m: D7 C! U9 v
  23.   if(sockfd>=0)
    1 J: K% L. z+ b* M8 g1 D8 n
  24.     printf("open socket: %d\n",sockfd);
    $ W1 {* Z7 H! j7 i9 P

  25. 5 Q! q4 H! F8 g: Y$ }6 E
  26.   //将服务器的地址和端口存储于套接字结构体中
    - D& o# u. H8 C4 y% H
  27.   bzero(&addr,sizeof(addr));
    : H! A7 _: @" b' C: P& [2 t
  28.   addr.sin_family=AF_INET;
    " i6 H+ J! E2 y& h" q5 J
  29.   addr.sin_port=htons(REMOTE_PORT);) ~8 U* m4 M- I0 y2 m8 Q4 |6 p
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);4 g$ r2 H" q8 V
  31.   
    9 a0 I# T! c( E
  32.   //向服务器发送请求
    5 J7 ^# w5 h# ~* }0 y& I
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    * ?0 n& l' R4 }8 l0 _
  34.     printf("connect successfully\n");( \* u+ N7 m9 E: H- J) D
  35.    2 t2 [& H4 m- x- |3 t3 H5 L
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行), T' E3 r: g' c, [
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);2 R, O7 w+ S1 z, x6 w
  38.     printf("%s\n",msgbuffer);
    ' d- X  U% m& t0 J9 L+ T
  39.   4 O) t) H1 i& q( ?% n- y0 h% x
  40.   while(1){
    & e- g  B0 L, U: o6 p
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
      ^7 g  X, ^* z  N! y3 s+ L
  42.     bzero(msgbuffer,sizeof(msgbuffer));
    2 T: l1 X1 M' H( Z& [* f
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));5 ^1 \  l+ G( P; ~3 b+ j1 i
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
    4 E. V7 I$ s' X) w4 V# d& d
  45.       perror("ERROR");
    ! {/ ?$ ~0 R" L0 {# l8 g5 {& E
  46.     ; g6 E3 c( j" d) m6 Q
  47.     bzero(msgbuffer,sizeof(msgbuffer));$ ?* b5 v, E& k$ d9 J( x
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    & o- }+ Z9 O- d6 Q. {5 O7 j# d
  49.     printf("[receive]:%s\n",msgbuffer);
    7 K+ I9 s. u) M+ T1 S. g
  50.     . J; ?5 T0 }7 Z7 W& t1 {
  51.     usleep(500000);
    * E: j3 R2 O* q2 w6 G; J
  52.   }* ~, F  g" v2 @# Z* ]) K
  53. }
复制代码
& c# j& p3 A6 @5 K" h5 {2 X

5 d" Y' i* h" d# H; D6 ?( Q
服务端:
  1. #include <time.h>
    $ Y( t  X: Q$ W: p( T0 e1 y# A. |) T
  2. #include <stdio.h>
    3 K* v+ q( V/ x  ^+ ]& Y( |
  3. #include <stdlib.h>5 j$ ~. Y" i/ @, Q
  4. #include <string.h>
    9 `: H$ c5 E' G7 S, Q5 Z8 T' ]! Z
  5. #include <unistd.h>7 d" U1 u$ {0 Y- `- U% Y: g$ b3 Q
  6. #include <arpa/inet.h>, L% k  @; N/ t$ B' m
  7. #include <netinet/in.h>/ c8 L" n: Y# o; n# [
  8. #include <sys/types.h>  j$ L4 a% `( p' x! v/ V$ L
  9. #include <sys/socket.h>3 O# V7 f) D' |# j' G
  10. 3 I' g: J0 V# n$ d) ?  ^
  11. #define LOCAL_PORT 6666      //本地服务端口2 S6 {9 q9 M  P6 ^" Y: ~% F
  12. #define MAX 5            //最大连接数量
    9 o' r- M; H" }  I
  13. ( c0 s( S! v  V1 O3 B8 e% H
  14. int main(){: R8 W. N2 }$ \9 M
  15.   int sockfd,connfd,fd,is_connected[MAX];& F0 L7 f9 A, k, W$ j; w8 V8 J
  16.   struct sockaddr_in addr;  h* O  n( [3 D0 ?8 N6 Q
  17.   int addr_len = sizeof(struct sockaddr_in);) z9 i, ^4 D0 W- f8 w  c
  18.   char msgbuffer[256];
    . M6 |4 r9 R) Q% I5 H
  19.   char msgsend[] = "Welcome To Demon Server";
    % i& k8 }$ w7 S* M
  20.   fd_set fds;6 P* w1 p2 b1 p
  21.    * o) L4 L& J  |  k
  22.   //创建套接字! a9 F: d  K1 I  B/ l. i, q( A
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    * Q0 n1 E# `; t" d
  24.   if(sockfd>=0)
    ; q4 p6 v6 k) C. a7 j
  25.     printf("open socket: %d\n",sockfd);( `# [5 a4 [; C7 |6 J0 l( j3 k

  26. 3 v' W3 l3 S0 d7 @$ R
  27.   //将本地端口和监听地址信息保存到套接字结构体中7 [( L( p4 B0 W: E3 d/ @% p" b
  28.   bzero(&addr,sizeof(addr));
      g# R/ t* A- N
  29.   addr.sin_family=AF_INET;
    0 \9 u9 C. y) b
  30.   addr.sin_port=htons(LOCAL_PORT);6 \6 ~" R. N  q1 `' m
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
    / i' _3 e; u% l/ n8 N: w7 B0 s, M
  32.    , Q% ~6 g- _( `6 \
  33.   //将套接字于端口号绑定! u# N0 v$ t3 U
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    1 {5 M+ l! g1 F" u, Z' ^+ }
  35.     printf("bind the port: %d\n",LOCAL_PORT);8 L2 w. B8 |& n. S8 q$ p* _
  36. ; p( `& M  P! e' s0 H1 k
  37.   //开启端口监听% b/ Z4 I; O5 E/ L# |  }5 C  ?
  38.   if(listen(sockfd,3)>=0)
    5 Q& O9 K! M6 t* B& r+ ^0 |! c
  39.     printf("begin listenning...\n");
    / R3 U3 J- |4 O8 |$ O* a

  40. ; ~  e0 H  z. C) f3 J6 o, s
  41.   //默认所有fd没有被打开# J" e( V0 _- @
  42.   for(fd=0;fd<MAX;fd++)
    / ]7 }4 v5 i( n* ^3 e$ `
  43.     is_connected[fd]=0;
    3 v# i3 c9 {8 Z& o$ K
  44. ! v# Q7 p; B0 \% n/ F
  45.   while(1){
    " T# p, K9 Z0 _) h
  46.     //将服务端套接字加入集合中3 O4 g2 d6 b& l. {% d; X; x' n$ C
  47.     FD_ZERO(&fds);8 g. i. `9 B+ n  }
  48.     FD_SET(sockfd,&fds);
    6 O" B& C2 r. G& ]! k0 x
  49.      $ a. ^* d& p: B
  50.     //将活跃的套接字加入集合中
    % O  F- o: L3 u, f% s: {
  51.     for(fd=0;fd<MAX;fd++)
    $ }2 \; T3 N- P; n
  52.       if(is_connected[fd])
    ! n' I9 b) \" W7 W' D7 V
  53.         FD_SET(fd,&fds);' z3 g, C6 g7 ]3 r9 r6 P4 C  `3 \5 v
  54. , n5 M* e4 \  e% c! r9 Y4 ~/ {8 r
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
    2 R6 B1 b, O- E3 W, j; L) i
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))
    , {* r: f+ \" j
  57.       continue;) u$ `# O( {* T! L

  58. " t. A% R# F! d* }
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字. j2 O# B, L( t9 p2 }* P
  60.     for(fd=0;fd<MAX;fd++){
    ' P. l6 F2 @* S6 g) |2 `9 i/ e) _
  61.       if(FD_ISSET(fd,&fds)){8 a, Y( _/ b$ y/ F/ N; Z' G
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接
    3 k. D2 D0 O4 p( C* g2 Q
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);5 d1 f: z1 x, a# v
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语+ N+ ?. b- F! K1 ]! x
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用' c9 u0 W& r. @4 V1 A4 S
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));) ~1 V! R3 F. g* o  F
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
    8 r+ o$ t# ?1 c5 S$ X
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
    2 C4 F6 Y$ Z1 Q3 j) D* k' Z
  69.             write(fd,msgbuffer,sizeof(msgbuffer));# m# j9 c! Q; Q! g9 p) y
  70.             printf("[read]: %s\n",msgbuffer);! U! Z2 c& O5 y7 V  b; A9 l$ b6 K, e) O
  71.           }else{
    ' b$ s# N7 d% O+ z
  72.              is_connected[fd]=0;9 N$ J8 v) @  c. F
  73.              close(fd);$ D' P- j/ w! G5 |5 O3 M! A
  74.              printf("close connected\n");* B  g% k# l' X4 X  e. h) z
  75.           }
    6 z+ ^( @  w+ `5 l
  76.         }
    $ @! v0 V1 r+ ]' X
  77.       }
    . P. t/ {* e3 O+ M, u! C
  78.     }, ~7 w1 Z( Z! v' i: J
  79.   }' t4 b5 P/ f8 o+ s
  80. }
复制代码
- F6 k! U) a& n( t% Q
5 t* |- A: {$ H- X* o! M- z

( @* ?7 O6 ~. M7 [- d3 f/ K* |+ O) x
3 S( Y9 W& _" ]! A4 E/ |
) A; y# t8 n4 Y  A% ]) A/ i/ `& g# I' |: [
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2024-12-31 01:42 , Processed in 0.171662 second(s), 25 queries .

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