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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

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

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

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

5 Y% b5 M7 |, f/ h: T
5 ~5 k& \8 K7 `8 c
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
/ N3 @# S4 t+ ~, w
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
& G6 g3 Q, S( E

9 U, w! R9 l" ^6 Y3 J
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)

: `& k: z& K2 X, N4 g4 `
2 H8 y% v3 }% Q' X7 r! E5 T
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)

0 y# k! I, E5 ]9 U& m2 K7 \: S& E% ~+ b5 G. H
- V, `5 _2 R  Y, p- t& W
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。

2 f# m% |* V" ^- U
- Y3 d( {- k# r5 ^  i. D0 X# @4 t1 N0 _
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>8 t, F; g1 J8 v+ Q
  2. 8 z6 b+ ^8 U) \4 E9 z
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理

3 }( Q: S( s$ v: Z9 o- d    readset  用来检查可读性的一组文件描述字。

7 F5 U0 ?+ l8 z7 F  S4 P, r- A9 o* M    writeset 用来检查可写性的一组文件描述字。
7 e* T) F5 B5 j
    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
2 {* P! e' b, C/ {0 [
    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。8 C& Z3 _1 _* [( V6 l' L

6 }( k; X# y: o3 X    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
) j6 V" y8 Y" N" ~5 L- O9 y
# m: `8 p  M4 M3 \4 R3 T
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
    0 h2 q) B# Q% N/ B4 J) P& w

  2. 6 [5 X8 ]/ O4 }( _* f. @% \
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)$ `1 k" d9 m- _( [: m. Z+ M

  4. + a  t* h7 I" [3 |5 i
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。0 d- W* }3 [5 P

3 Y$ S8 }" i1 t2 O   
7 h8 H+ U6 C9 y/ g1 z# h# i
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集0 b% `6 ~# j9 n( ^% f
  2.    
      B/ q' [. v( G. h, y" Y
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd
    / X" L7 f3 s$ l) U7 v* S/ p

  4. 0 V( i" c7 O7 @
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd
    5 r" I& u7 i# C* ~$ O9 ]2 k4 J, Q: A
  6.   j8 q) E7 r/ r* {2 o) o& A: T
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;+ a' E1 s0 @- T0 l! c8 t2 C
  2. .....8 M. X/ \5 _8 E. l# [7 a9 [. h4 h
  3. fd_set set;1 H0 ?7 z( }/ e" a; j
  4. while(1){( ?& Y" R1 d. ~3 a
  5. FD_ZERO(&set);                    //将你的套节字集合清空* ^; w6 l$ ?3 U" g1 @: }, @
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s5 ?; e7 q9 N. B6 X) `& Q
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,
    * w/ \( y- B8 f1 w) [
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,
    ' p& X/ ?0 M% S7 N
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    1 _1 f) {2 b( ^, u# |; ~
  10.                                 //只保留符合条件的套节字在这个集合里面
    , a8 N! c) b- ~9 Y! W$ ]( X8 d
  11. recv(s,...);6 O' J: v0 T( t$ M6 g# h
  12. }* [/ W" J6 l0 j' P9 p
  13. //do something here
      D0 c; H# ?' n. r3 a0 U) p
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。# \& ~- g4 J3 v" j; i6 s
  2. 9 {; B9 a5 u+ |5 f. f
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)# H4 U, X2 Y8 h6 S3 g# `
  4. 1 ^6 C- W; h/ T5 B/ {
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011
    , }) M0 q3 y) o6 c" s* ?6 k
  6.   X* X( |5 M- B9 |
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待9 X# o% }1 z7 z, E! ?3 G
  8. . B. m* W8 w: s9 z5 R
  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.具体过程看代码会好理解
: S8 F( ]' D+ K. ]* l
9 z( w2 J5 \& I+ R& p: z+ Z
使用select函数的过程一般是:
; Y0 j- X3 @- J1 F
    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。! n  G. d- v6 b2 a: [9 O
, B4 m# h4 T# ]: Q% Z, Z
客户端:
  1. #include <time.h>& C2 q1 Y0 L, J6 k
  2. #include <stdio.h>! e2 u' L/ W2 S& _$ T( L! v
  3. #include <stdlib.h>8 A- p( p1 P# I, d7 Y& m% w
  4. #include <string.h>, N3 Z3 X; D0 o2 c: a7 k
  5. #include <unistd.h>
    2 }3 R# K1 r  S/ }  X" x
  6. #include <arpa/inet.h>- k* y, i% `4 o" A- C; S, w$ f
  7. #include <netinet/in.h>0 v* [' M' K) |- Z% e
  8. #include <fcntl.h>5 V7 v$ ]) [! a7 z1 ~3 o
  9. #include <sys/stat.h>
    5 R% g0 ~. t. o6 L
  10. #include <sys/types.h>1 Q2 ?1 L. y' ]0 V2 `2 w
  11. #include <sys/socket.h>
    * W4 T# E) h/ H1 ]9 s
  12. 1 A8 `& |" a2 q0 p8 b& A
  13. #define REMOTE_PORT 6666        //服务器端口
    9 W( E8 R8 f: H7 z% R6 y$ @5 ?0 m2 u
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址
    , S6 v. s" Y+ @* Z( V; O9 j
  15. , M3 P, f8 q) I
  16. int main(){
    4 \1 L1 G# ~; r* M2 e% D( P
  17.   int sockfd;  Y- T5 z1 E* `8 W' B* T
  18.   struct sockaddr_in addr;
    7 `; ]& O% @6 |% Y! M( e
  19.   char msgbuffer[256];+ v1 Y! z- F- K
  20.    
    & E# }6 z. |: M+ F2 V4 c0 z+ Z
  21.   //创建套接字! |- E& M% K$ P' }# q$ |
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    8 A' \, q3 G0 g  X
  23.   if(sockfd>=0)
    + G5 z$ e* z8 r" S9 p  m  m0 I
  24.     printf("open socket: %d\n",sockfd);
    ( i: z5 J  V" N4 X# N9 d1 [  L
  25. ( }# g1 q8 }# h$ H
  26.   //将服务器的地址和端口存储于套接字结构体中) N( k& H( n  `1 l( c
  27.   bzero(&addr,sizeof(addr));
    0 M, c) j: o5 M2 Q! z
  28.   addr.sin_family=AF_INET;
    2 }4 e4 }6 p3 L
  29.   addr.sin_port=htons(REMOTE_PORT);- J1 S+ P5 i+ ~2 ]3 s
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);3 z3 d+ d/ F' Q7 T
  31.   
    * ]* b) \/ y. F  j
  32.   //向服务器发送请求# k2 t$ ?. B# a+ J. y
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)- h# D3 O9 X1 ?- z* E2 q" I
  34.     printf("connect successfully\n");1 Y9 ]2 F3 P* L: ~$ I4 t2 H
  35.    
    4 N! m- K) _- z* ]/ M
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
    3 r" F- |/ w6 T5 N
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    " Z8 ~$ S- u+ k& l0 @1 W- Y7 U% r
  38.     printf("%s\n",msgbuffer);
    ) v7 M2 l& \) S9 F; \5 b: G. S
  39.   % s* s; N# q' t2 k% E+ x/ b1 S. }: E
  40.   while(1){) C0 e1 ?3 w6 Z7 y% r6 C7 i
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息( ]8 E8 w. x4 D$ ?
  42.     bzero(msgbuffer,sizeof(msgbuffer));% F4 c! k& Y& _# N" P  P
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
    ; K' G, j; ?( |2 W
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
    ; f2 A4 ^, P7 q4 _( @% r
  45.       perror("ERROR");# `" Y" r6 t/ }0 ]* A3 o
  46.     , G* }& X$ [2 ]! ?! s9 N
  47.     bzero(msgbuffer,sizeof(msgbuffer));9 W; p' x: H% s7 q" C# z4 w; V
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);9 Z& p1 K8 `1 G
  49.     printf("[receive]:%s\n",msgbuffer);8 x0 W, E! p& G7 P! @5 b! |" z
  50.     0 `1 L9 |( Q) W& A# A* O
  51.     usleep(500000);
    ; i- c  E8 P, g' e% F: R  ?2 U# s
  52.   }; Z& \5 Q' F, v9 x
  53. }
复制代码
/ y5 }+ s5 O+ P6 w7 M2 D7 o. O) h
( J1 i3 Q5 {6 C" {) G; t
服务端:
  1. #include <time.h>
    % F( |; ~/ z( _$ t
  2. #include <stdio.h>
    " \; ]' U! S, j
  3. #include <stdlib.h>
    7 w. N% Q9 ]# Q% b: `
  4. #include <string.h>3 w% s, Y; N0 k* \
  5. #include <unistd.h>
    ' V1 ]" A" w: d( P$ W
  6. #include <arpa/inet.h>& n6 U: @: ^' ~' s! t
  7. #include <netinet/in.h>
    " k4 t& `4 z0 k0 E6 {1 }
  8. #include <sys/types.h>" @" N. C$ I' Z5 d( n. R
  9. #include <sys/socket.h>
    6 _1 X8 X4 y+ e9 [
  10. , g1 _& K/ N& x. B; V
  11. #define LOCAL_PORT 6666      //本地服务端口/ t$ `( g4 \: V- [2 B. y$ |# E
  12. #define MAX 5            //最大连接数量
    , b' T. V. G9 N7 s$ e1 |
  13. ) G9 A# X) j* G; a
  14. int main(){4 Z; f2 B2 }! h8 z: o1 k+ c
  15.   int sockfd,connfd,fd,is_connected[MAX];
    ) M3 A! `/ R8 }0 Y
  16.   struct sockaddr_in addr;
    & O& D* O$ `& t/ C. Q
  17.   int addr_len = sizeof(struct sockaddr_in);7 B1 u" I* k' I6 t9 e
  18.   char msgbuffer[256];0 k# W5 [9 o  e$ i6 i+ G
  19.   char msgsend[] = "Welcome To Demon Server";; x7 ]0 X" s# P4 _* c0 n9 _' I3 ^
  20.   fd_set fds;; ^  E& l0 r4 z6 x) g* W8 A$ T# h
  21.    : f. f6 d) {- D: r
  22.   //创建套接字
    ! [+ A$ E* i4 p! ^
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);7 Q. A6 C  G- M& A8 I! G* o! K( `
  24.   if(sockfd>=0)
    ! V1 t: j6 j& m1 e( t" R1 p
  25.     printf("open socket: %d\n",sockfd);
    $ d; ^+ }' x# ?1 [( Z' n4 r  w, e' |" z

  26. " S5 G9 ?" s( E5 L1 v( |! G! O& R3 ^
  27.   //将本地端口和监听地址信息保存到套接字结构体中
    - e" X0 d/ `' b! k( v( {# u
  28.   bzero(&addr,sizeof(addr));1 a1 N, ^% S) W; Z+ i
  29.   addr.sin_family=AF_INET;
    . Q4 v( m1 F1 U! P
  30.   addr.sin_port=htons(LOCAL_PORT);5 G  C, U( X9 A+ E; [
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0* j- K/ |! e0 Y; m) U  c
  32.    
    1 R. I4 @* E1 j
  33.   //将套接字于端口号绑定
    , a8 H6 }. K0 l* }# T, E$ G
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    6 v" L- f6 c; ^3 Y6 v( c# L) h5 K
  35.     printf("bind the port: %d\n",LOCAL_PORT);+ `$ f+ I% y$ O: w2 I) u
  36.   h3 e- b% E1 n/ i' ~( C
  37.   //开启端口监听
    * o8 _9 ^7 q7 r
  38.   if(listen(sockfd,3)>=0)
    ) g8 B. U8 R  ~/ h
  39.     printf("begin listenning...\n");8 ]  n4 l/ n  v, a% O( r# ]
  40. ; W) Q8 r/ h  ]. \+ C
  41.   //默认所有fd没有被打开3 \: t' I* j7 S% J
  42.   for(fd=0;fd<MAX;fd++)
    ) U# t# r* a0 k2 y
  43.     is_connected[fd]=0;8 t; @$ C3 _- @6 H8 H7 ^

  44. , N& p6 t8 g5 p4 s& j0 @
  45.   while(1){
    " l5 `, b" N% y5 C$ L# I3 W1 w) r/ p
  46.     //将服务端套接字加入集合中0 ^8 x* z4 p0 j, q4 t& |$ H  _; I
  47.     FD_ZERO(&fds);
    ( ?3 {& n7 q7 R" A+ \! w# ^
  48.     FD_SET(sockfd,&fds);* ]7 O8 q4 @0 B% c: a0 @
  49.      7 a- F& n$ a+ {5 v
  50.     //将活跃的套接字加入集合中( H2 ]5 Z9 R. N$ h; Q$ `
  51.     for(fd=0;fd<MAX;fd++)" \7 I1 O; J' x( q) f$ y) Z3 v
  52.       if(is_connected[fd])
    " Q- M# C' r0 G, E8 \' U( Z
  53.         FD_SET(fd,&fds);) r3 }# B  o) O! ]; @
  54. 7 N5 I( J, r% b+ h+ p& W
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
    6 z4 O% M2 q, v1 J! b( p" O5 E
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))/ `9 b6 N/ T3 X3 z
  57.       continue;
    " s7 F$ `2 G1 k8 e
  58. 8 d% ?' t7 R# s5 V% \" E
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字
    ( X& T5 J% }; t* m3 D$ c
  60.     for(fd=0;fd<MAX;fd++){
    : V' L' }# Y4 `" H; \& A) v) L
  61.       if(FD_ISSET(fd,&fds)){
    / b% L* B3 B! p- I
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接
    ) y& N9 v; J$ {% v
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
    ! A) U. G( _; t; m3 e3 g6 K% F' m
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语& p. k- e/ M& L3 V8 k) X9 G; |% ~
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用! `# X+ t: w+ o$ M! N+ q4 ^- O8 c* [
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));
    ; d; v. S+ z- R2 P
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
    ; y9 _4 y( H5 l
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
    0 T2 x4 \) P, [& y( M. r) `# L
  69.             write(fd,msgbuffer,sizeof(msgbuffer));) O7 b# D7 }' r8 ~5 h+ g
  70.             printf("[read]: %s\n",msgbuffer);9 A6 r+ M6 \( z- a- T
  71.           }else{& b1 L: c, v2 b) n5 \! a( A2 g! h
  72.              is_connected[fd]=0;/ g; t' u, @# ?/ g+ I
  73.              close(fd);! Y1 s7 v8 B8 I! S8 X' d
  74.              printf("close connected\n");
    : H# A. m  V. l5 e2 B" C5 f8 |- K
  75.           }
    0 |7 _+ i0 p+ G  o8 w
  76.         }! d- S2 L/ c+ b- t
  77.       }
    + D+ D4 [" ~  _7 U6 I
  78.     }+ y1 n5 q9 w& A3 r# t
  79.   }- w( g9 a: e5 T. s; [
  80. }
复制代码

' W  w+ a6 u5 N% g/ P/ F& f8 {% n; `) Q+ h: f2 l

# }" k3 X( `  B2 i- ^. Z: k8 d; S( t# n) E5 O
8 M8 p( [  U5 ?# j1 o
0 E4 G) z. a8 v( }! F
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2026-5-2 15:30 , Processed in 0.076220 second(s), 23 queries .

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