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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 2566|回复: 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
实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
  }% l: E$ t/ b0 v
什么是SOCKET(插口):
     这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。7 @; m( |  _2 a" X$ u1 S# L
     "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
      对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
      具体其他高级的定义不是这里的重点。值得说的是:
      每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
      应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层  ->发送(接收的话与之相反)
: ?5 N# h' S: B  J/ @

/ r& H: n; ~, p$ f6 \3 |4 }2 _) W9 @/ q
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
- F& O- s4 U8 j0 I$ O1 h
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3

7 G9 \' E8 Y" y4 O# ?. c2 c, _& w5 Y7 _# G4 B4 L; X
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)
! r. j3 [  f. T1 f: Q. A

+ ]. j  F$ g5 V+ u. A
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)

1 g( N0 I$ A& p! C
$ P1 j3 n$ t. ?6 A; s1 V: u
# I* }# X& k! T5 [
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
  ?. _: s" o, H2 k9 |# m, o

4 K+ e+ x9 U6 ]5 J* H$ g
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>
    , X8 y+ `4 u, k8 F6 W' V5 o- z4 v* ^& A3 @
  2. 5 D: S# ~, J; _# G
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
' T& z# _* o1 K- b( k" C
    readset  用来检查可读性的一组文件描述字。
  Y7 x4 O9 t) C6 R9 m- ~8 K' ^
    writeset 用来检查可写性的一组文件描述字。

3 U/ p1 i6 Y* I* i9 R, h    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
9 ^$ q  ]' @# h6 c. N
    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
1 r1 c1 U# B5 N- q; q, d, |. U
    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:- R6 _" c, C+ p1 E& u. l9 @
: L, F5 N- [; f" p/ x- w& N
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)/ s  X/ e6 A6 n! U! q: K0 H7 l, ^

  2. , R1 ^, D* t4 D" S6 ~* E4 F4 E, ?- y
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)+ K! d- H" l. ]2 D: A9 j
  4. 1 @  N/ O- |0 j* d
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。9 R' o! K2 x) e9 y9 \4 s% J# w* Q
) o" U" A% L3 |8 \/ r( K- c- U
   : v) g/ l$ L# A4 |' V6 s
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集
    ( W4 o  }: g# R8 ~" S: q
  2.     % p& u; Y7 O( |- x- ]) k" F) U
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd
    8 \1 L  k9 W* s* Q
  4. % D* e0 Q2 R% ]: N4 n
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd
    * g7 z; T3 I4 _1 R. v! V$ _) b

  6.   Z* x! o. f8 K7 ^
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;& s* u% z2 J2 W# n
  2. .....
    & {" e2 o, ^7 |. m6 S
  3. fd_set set;0 T8 c; b( \: f. f5 q, I/ W' Q! n/ `
  4. while(1){6 T7 `: G2 Y& t2 f
  5. FD_ZERO(&set);                    //将你的套节字集合清空
    6 N1 d1 V9 z7 D3 T; J5 d
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s3 U1 ?3 R- |0 [( a0 k! V/ c) |2 V
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,% y4 n* k' W% G3 k' o! Y1 X
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,# y/ A4 s+ ~  M% ~0 I, y
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉3 m# G5 x3 J/ L4 l
  10.                                 //只保留符合条件的套节字在这个集合里面- {4 Y+ U' e- ]8 N' M
  11. recv(s,...);8 G4 C- q$ h, D  {4 x3 l
  12. }% ~7 D  m. q$ f( V
  13. //do something here
    6 g& @; F- ~( t, a$ t( y6 W; M
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。6 T4 o1 o  ]+ q( `8 ]' P: C

  2. 5 \" t! c* M  }) F  w  o0 W. @* b1 j
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1), {) E2 I  W' s; {: y' j

  4. ( a/ `8 M) {$ b# n$ Q9 s& b
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011
    ) g- t- B+ ^+ q9 Z6 @% S1 H

  6. ' C9 L  M  N) v5 k
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待3 m2 t1 i: t$ [2 l0 f6 T
  8. 6 E! Y/ ]; ]+ J7 d) X& G4 |. n0 D' d, @
  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.具体过程看代码会好理解
- U% ?) M% G$ C$ L
) n% C; s! W; V! I1 I' x! Q& T1 h
使用select函数的过程一般是:
! {: G: j, V7 P! G
    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
& K. u8 V! z7 s; \
6 ?' x" d& C; E+ k
客户端:
  1. #include <time.h>
    2 e7 K4 g& K* R8 Z. C) t; p. _7 V9 s
  2. #include <stdio.h>
    ' u- q, V7 V7 \4 T
  3. #include <stdlib.h>& r. `3 ^$ ~( W2 o4 R- L
  4. #include <string.h>; y- E* M9 Z# R/ A& q: j
  5. #include <unistd.h>( I- D& E5 g: ~  w; V
  6. #include <arpa/inet.h>; E$ ~6 @2 H% G$ ~% k
  7. #include <netinet/in.h>
      A' j% n( b: C+ d
  8. #include <fcntl.h>; j! o: K! {+ P/ \  Q/ r$ K
  9. #include <sys/stat.h>1 J+ q8 }5 z, b2 w6 ]
  10. #include <sys/types.h>
    " }* t$ p. L6 U% K  X7 |
  11. #include <sys/socket.h>
    8 {" c" a- O% Q9 K  R; C
  12. # c+ a5 Z9 u  g, y
  13. #define REMOTE_PORT 6666        //服务器端口
    & X: K% W3 v9 P/ w0 z
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址% k3 V- K2 C/ ^& _

  15. 8 N. d, c" J  i3 T7 _/ [) X
  16. int main(){
    : x. w1 J% I5 N" x8 p7 `; a0 T
  17.   int sockfd;% ^+ A" D) X5 T- }7 h/ f" a
  18.   struct sockaddr_in addr;5 g8 `( c6 L* a8 I+ y
  19.   char msgbuffer[256];
    ( ~. L! O# O8 j+ Y7 A  s
  20.    ( r' \4 M: v8 L" y
  21.   //创建套接字$ i0 ^8 r; E1 I3 m) n) Q6 j
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    + |4 K: w- u, ~! h
  23.   if(sockfd>=0)8 ]4 ?- c! |1 }/ q$ H4 z
  24.     printf("open socket: %d\n",sockfd);
    4 j1 K, B0 q  Q( k2 A$ B4 Z

  25. : @! f( z; i" K
  26.   //将服务器的地址和端口存储于套接字结构体中
    5 c5 G2 E" a, m, V8 j
  27.   bzero(&addr,sizeof(addr));
    & i+ R  O, K9 }
  28.   addr.sin_family=AF_INET;/ R0 g5 b7 R+ C$ W% z5 P
  29.   addr.sin_port=htons(REMOTE_PORT);
    * O4 ]. w4 k9 `' Q5 K5 E
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    4 z, O2 f. Q5 X* v3 R) O7 c
  31.   , `) {! Z5 [$ m/ Z; n
  32.   //向服务器发送请求% T9 ^! m, ?9 q3 @9 O9 a  h
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    : d, p  n+ l+ A3 B9 u/ I" ]/ R+ R
  34.     printf("connect successfully\n");
    - ~. |& k) y9 e) C" U
  35.    
    0 V3 z3 _9 c2 x
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
    2 f% u4 \3 @4 c4 D0 D3 P3 p& ]: p
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    / k& \: A# c" \
  38.     printf("%s\n",msgbuffer);" }6 G6 _; g  N) f7 l7 p$ P0 Y
  39.   
    8 p  Y0 A: f/ G. A7 i7 Y
  40.   while(1){5 Q6 C; ?% [& v* j. a2 Y
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
    & S3 L1 p0 D* m7 d+ O1 w
  42.     bzero(msgbuffer,sizeof(msgbuffer));
    : ^0 \7 \6 Z; I4 m0 D
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
    / w1 C1 \  ^% ~! Q0 T
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)8 [- }5 A9 R) B
  45.       perror("ERROR");
    ) g7 w3 R& d3 _' ]
  46.     ' }6 i! ]+ R  [& q
  47.     bzero(msgbuffer,sizeof(msgbuffer));
    " t8 p9 a% \! q, Z& o8 ^
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
      C) b  }7 M& }  y$ B% c0 f6 w
  49.     printf("[receive]:%s\n",msgbuffer);
    % _5 T( d+ B* W, ]. u2 b
  50.    
    + R, H& c0 E& G
  51.     usleep(500000);2 N; R" ]: l$ T' a& C5 ^0 T7 ^
  52.   }& X8 e. r/ l, N1 ]
  53. }
复制代码
+ L# |) g6 u: h! w5 v! \+ ?6 L0 m$ ^
' h* f! L) F& p3 [  y# C) E# H2 G, p
服务端:
  1. #include <time.h>
    5 t# q+ W5 R$ h6 n8 D
  2. #include <stdio.h>7 X0 p7 x; I7 E4 Y7 X
  3. #include <stdlib.h>% s: j3 Z  `  d, f. w
  4. #include <string.h>
    ' `$ r8 z( C, i1 O) j. A) R
  5. #include <unistd.h>
    : B$ q% \* r0 \- ~0 _1 r8 q) l
  6. #include <arpa/inet.h>
    4 C. q0 B. \( s; [' C) l% F6 \& ]
  7. #include <netinet/in.h>
    / ?. ]/ _2 ]' L5 C0 [
  8. #include <sys/types.h>- l) ?- m) C) b& T6 b, ~
  9. #include <sys/socket.h>9 ^" N; |0 {9 k% M

  10. * c% w) p& ?6 |% f
  11. #define LOCAL_PORT 6666      //本地服务端口
    0 }3 U! y: f) ~( c: ^  {5 \8 @/ T
  12. #define MAX 5            //最大连接数量$ H# R2 ^8 R) `" P& F( l
  13. ; j5 b$ W( K( A9 e3 z' S  B
  14. int main(){
    / B# }! U, [4 A
  15.   int sockfd,connfd,fd,is_connected[MAX];* ]8 ?# o! }' U
  16.   struct sockaddr_in addr;7 ^+ g# F8 ~; O7 Z7 A# g
  17.   int addr_len = sizeof(struct sockaddr_in);6 Z+ r2 I. {, r! x7 s3 {
  18.   char msgbuffer[256];
    . F; k; F: E3 d9 o- M
  19.   char msgsend[] = "Welcome To Demon Server";5 ^. Y$ i9 i2 y/ R
  20.   fd_set fds;
    . c3 e( R4 l/ s3 D/ ~
  21.    $ s0 U3 o5 {3 F  M0 K8 U2 i
  22.   //创建套接字
    ; L2 X* H/ x. u& }: c, z
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);3 f3 }! t) z! x
  24.   if(sockfd>=0)$ T8 A% \# I# k3 {9 T
  25.     printf("open socket: %d\n",sockfd);2 D. w+ }3 z$ l1 a; p

  26. 5 f' p; O, U! l- C0 _) v
  27.   //将本地端口和监听地址信息保存到套接字结构体中3 @$ V- @( j/ J/ {! J
  28.   bzero(&addr,sizeof(addr));
    8 x2 K; x" h3 g7 @0 o: @+ m4 C. E& Y
  29.   addr.sin_family=AF_INET;( l/ G; C; @( _# r% V* f- S
  30.   addr.sin_port=htons(LOCAL_PORT);0 K$ V0 U* L8 h: A- u# o, m& Q9 q
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.03 Q, h$ }1 \( I4 ?- f& i
  32.    
    6 F  [1 W2 M$ m0 Y
  33.   //将套接字于端口号绑定
    8 v. `( }$ {8 F( W; G* x7 A
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    0 L% k  u; H5 K3 j; I5 S! ?& Z
  35.     printf("bind the port: %d\n",LOCAL_PORT);! {+ p8 n0 p( S* m$ q& M3 [% N
  36. 3 i; u" E% C: B: ~5 j( \
  37.   //开启端口监听0 |9 f5 [6 l% F5 o: i# D
  38.   if(listen(sockfd,3)>=0)
    / t/ s$ y% o  Z
  39.     printf("begin listenning...\n");; J; C- ^* E, ^1 ~9 N, p2 K! [
  40. + Y0 r  c2 u7 m* G$ c* U
  41.   //默认所有fd没有被打开
    : z' P- }$ K1 D% E9 r
  42.   for(fd=0;fd<MAX;fd++)
    2 z/ a. [  c2 q: X
  43.     is_connected[fd]=0;$ V# }' f# v2 z6 A  S3 o

  44. 1 ]6 u# Q. W0 Y0 ^. M6 {3 U
  45.   while(1){+ B) m. i, w, B2 t2 y+ A( U4 h; I
  46.     //将服务端套接字加入集合中
    & b% M. B- W- {6 o- k. ^
  47.     FD_ZERO(&fds);
    5 W0 D, r, ]* \/ L5 U# d
  48.     FD_SET(sockfd,&fds);$ J* ?8 F4 q6 a  p. @
  49.      
    # D, L) K  K1 a" `5 n! K1 o
  50.     //将活跃的套接字加入集合中( G" Q, L7 e$ T; B9 x
  51.     for(fd=0;fd<MAX;fd++)
    : x, i7 L$ a, T$ r5 V
  52.       if(is_connected[fd]). f, r8 w+ @5 a+ H- j/ C' d
  53.         FD_SET(fd,&fds);
    # A/ d1 V( t% [) |: O
  54. 8 w$ x5 e! H1 \% n+ C4 h  L2 p' z
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
    ( o* b; }4 C; o: }6 G' A% Q
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))
    8 p0 o3 l2 e+ C9 E2 z5 J; D
  57.       continue;
    , u/ L0 y0 {9 _
  58. 2 c, X9 L1 M4 N& F
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字! E: v  X) T/ s& v
  60.     for(fd=0;fd<MAX;fd++){1 H9 P4 t8 Q5 t  A( r. e5 j
  61.       if(FD_ISSET(fd,&fds)){' ?0 p0 t7 z4 S/ j+ R0 V; }
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接2 ^+ w, u9 ^( i: Y# n, ~/ Q
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
    " v+ {1 Z/ y% |0 d
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语$ J$ }5 `# n7 ?5 z# C/ C
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用' c- D# r4 e. B% Y
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));
    : }" j' h# P2 q2 S* i6 Q3 D7 y
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
    * q1 j8 ~! n1 z% i6 m
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ 8 e; d$ k- Z( I- y) ?3 J1 D) w
  69.             write(fd,msgbuffer,sizeof(msgbuffer));
    9 Q$ b0 p4 K+ Z( i9 f1 H
  70.             printf("[read]: %s\n",msgbuffer);
    2 W+ R. D8 D' x' k1 q
  71.           }else{( [/ X$ F% G; z) R+ v
  72.              is_connected[fd]=0;
    ' m6 S( {$ q! D( S$ k" w
  73.              close(fd);' C0 C7 x  U4 i9 u
  74.              printf("close connected\n");
    6 _7 B, M/ D7 c
  75.           }. X6 w$ m* Q  Q% s
  76.         }5 H0 H8 B9 i3 l5 L7 d
  77.       }
      H9 w  T" {! i+ L4 Y. {. F9 f
  78.     }, G! H1 j$ c, @$ c7 ^% g
  79.   }0 {6 i  v+ R# b
  80. }
复制代码

6 [0 Y% Q, R3 z7 _0 i" c- O: g! {0 e( T

+ J+ U2 V( O# ~0 _: Y+ e/ A2 c; Z0 w/ d9 b* f

9 n. w% h8 O1 |5 a& }" @8 y- k
8 ]: Q0 t2 f% a. G
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2021-5-6 14:42 , Processed in 0.203099 second(s), 23 queries .

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