cncml手绘网

标题: 编写一个简单的TCP服务端和客户端 [打印本页]

作者: admin    时间: 2020-5-9 01:53
标题: 编写一个简单的TCP服务端和客户端
实验环境是linux系统,效果如下:
1.启动服务端程序,监听在6666端口上
2.启动客户端,与服务端建立TCP连接
3.建立完TCP连接,在客户端上向服务端发送消息
4.断开连接
实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
  |( p- Q: G! ]0 K3 V5 w
什么是SOCKET(插口):
     这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
- E. b' Z  O+ w' A
     "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
      对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
      具体其他高级的定义不是这里的重点。值得说的是:
      每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
      应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层  ->发送(接收的话与之相反)
% ]- U$ m2 a1 q: s# o$ C3 j

- V0 t3 [) A1 b: G8 i  A7 F# n
$ ~( _0 W3 D$ T% `
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等0 a4 v  p6 G6 N
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
; k$ G- [0 b. D% a( ^
8 [' l' `4 ~+ U  M" d
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)

, [9 o) S$ p3 n! h6 @2 o* x) U; M* V+ m; Z; g$ F) k  j, S
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)
- J* I& o2 M9 j& S) H" K7 b
3 ~/ Y  n! S% E8 s* z8 j) j6 \+ M

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

) y0 r1 H5 h6 L( D. C. U, [: ~. s
% ~; Z0 ~; W" L5 \4 v3 Q, M1 Q
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>1 M- [) `4 X. X1 m; d# Z0 z
  2. ' \/ u$ g& ~& q9 W; S5 u& x
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理

3 o% w& j& y1 b1 [    readset  用来检查可读性的一组文件描述字。

8 i5 N9 C6 m# i+ M9 H    writeset 用来检查可写性的一组文件描述字。
# s4 R8 w6 {7 z, v! D0 y8 D
    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
* B' M$ d! Q1 T- w0 q/ u7 M
    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
# i, S; ~2 A* B/ p, D) q: b1 p5 F# C9 e0 P0 g, @2 P& _) X8 U
    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:8 d5 k$ {: L9 C/ C4 Q- O5 i  C& E* F1 I
9 A- m# D1 C. t0 O& v
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
    * ^( x3 h" N+ g! `
  2. 7 q- u" Y0 U! C1 Y" d. F
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回). T$ o6 o' |6 h4 T. b, _

  4. % g1 _7 A' {8 n! h/ l
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。
2 h+ M/ ^. H) W8 p5 z- j  J( G% \) _
+ o. A- e* m# w8 Q6 X   
% y( G+ g! _" s, T
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集
    & ~9 ~. d& V0 `5 G; T
  2.    
    " A. H2 ]8 U5 @4 W, B
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd
    - f; F9 R; p4 b- p: T% t
  4. % \& q+ s. c  |2 n
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd4 ], W. u( N: z: T, r) n

  6. ) X' j+ v3 ?. H8 l( @
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;* _+ {/ V: G& `/ d7 O  u* M
  2. .....( s1 k$ ]' M2 l; i7 W) ]: l
  3. fd_set set;0 p; ~  n. U, ?0 J' Z
  4. while(1){( w0 A: X/ {! D- B4 `! |2 H
  5. FD_ZERO(&set);                    //将你的套节字集合清空8 u: v4 G% Q+ \! m6 A
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s
    9 d: v2 g6 p9 q
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,
    1 Z+ X; t6 O! c8 D) R- e* n- R
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,1 `8 X9 o- S+ g4 V' J; J. x
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    ' ]2 ^( R1 _9 g% S& L+ N
  10.                                 //只保留符合条件的套节字在这个集合里面# X/ k* ?& w4 i; L) \$ B
  11. recv(s,...);  N7 l- |( K  [
  12. }% U/ I! W4 L) M  t9 U
  13. //do something here7 V( s7 @. M. G# @+ m+ r
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。7 \, @6 \% V1 y3 _

  2. 1 E0 C5 j/ a8 y$ v4 v
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)) S9 V+ u/ v- F" g; Y  Q
  4. - X, x" V* B* ~8 _' N# Z7 O+ Y  D
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011+ E9 ]# Q* e0 w9 [5 e5 M

  6. $ x8 E0 `0 n; r9 e4 ^* }  `
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待3 a. i, g% S$ v( |
  8. + e+ Z: \  r; z+ N; _
  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+ }5 ?/ \7 d: A) \( Z; P' @2 p$ p$ J7 p8 q: F/ w+ Z% ~0 E
使用select函数的过程一般是:

. D6 s! I# H5 E! D( ?# N, _    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。! K# p! k) A0 v  t5 |5 i
% X9 }* ~/ T. k9 f: e! L
客户端:
  1. #include <time.h>
    % \! M2 l6 H+ m1 u, V
  2. #include <stdio.h>' w; e! R# C' V0 O" ~
  3. #include <stdlib.h>" _3 z! d. l7 w
  4. #include <string.h>
    ' F  w6 }! \+ L" c
  5. #include <unistd.h>) f0 Q. ^9 G5 O) E* _
  6. #include <arpa/inet.h>
      O3 L* |; V3 H1 l5 R8 z. `  P
  7. #include <netinet/in.h>+ N1 k1 T/ Y" j
  8. #include <fcntl.h>
    / l; O, s) X" F: c* x% ]4 |
  9. #include <sys/stat.h>6 Q2 f. }4 L! a0 H0 r$ x
  10. #include <sys/types.h>- m/ Y: o# ^7 q  R9 R) _, a3 q
  11. #include <sys/socket.h>
    6 m2 W* t3 J. c$ l* A

  12. 0 K7 l5 k( N* ]
  13. #define REMOTE_PORT 6666        //服务器端口
    : p6 k) ~' U8 f) c' R) k
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址2 p) I2 {: M  T$ L2 |( g
  15. , x6 w. ^: a- ^5 b# q3 V! Q* v
  16. int main(){: b: S# `% D0 R' V1 A
  17.   int sockfd;
    & v, e7 K( }4 }$ S0 M
  18.   struct sockaddr_in addr;( W- F6 a' }  f2 [/ z9 m
  19.   char msgbuffer[256];2 p6 }' ?) R1 _0 X! h
  20.    
    9 q% M% v. H" j; o  h
  21.   //创建套接字4 K& K' B9 m  M- S- \
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    5 D! X* P9 [+ t5 R' r# l5 L
  23.   if(sockfd>=0)3 n3 h, ]4 l. |- I7 k% V  f4 q: H
  24.     printf("open socket: %d\n",sockfd);
    * X  S0 O- F3 b
  25. 4 X2 D3 T  Q  A/ ~1 @( ~
  26.   //将服务器的地址和端口存储于套接字结构体中
    4 j4 S" z+ f  u5 L! U1 O4 _5 D: P
  27.   bzero(&addr,sizeof(addr));4 u# p5 V( W9 r" Z4 c6 X
  28.   addr.sin_family=AF_INET;& L4 m" Z/ F% Y
  29.   addr.sin_port=htons(REMOTE_PORT);
    ' ^) m4 @- w! u# e1 K2 v
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);& ^: r$ B+ C# e! l- g/ v: p+ Q
  31.   
    ) A& k! ^+ n0 T% b
  32.   //向服务器发送请求' ?- ]& T0 d& y! ]7 ?3 h
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)' A" r0 N! @2 i8 c
  34.     printf("connect successfully\n");0 n, V' \5 |' w) V3 u/ h- k
  35.    
    $ e$ Y. i) m$ P; R
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
    ' {( _' w' l0 o8 |
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);  Y/ z. n. t# T- g4 r3 t" s0 w
  38.     printf("%s\n",msgbuffer);( ^% f7 x5 \# W8 T$ x# I
  39.   2 \& z. N- u. G! Q. r
  40.   while(1){
    : `0 K* ?  C% Y5 q
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
    % i' H" [5 A6 G  n, W: h
  42.     bzero(msgbuffer,sizeof(msgbuffer));
    6 ~/ O" R( s  k0 H
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));% N& H- k% u( |! K. C3 _
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)! b6 _: S4 ?% O6 N: B2 ]5 ^
  45.       perror("ERROR");
    3 M. }- ^! m( n0 C" {
  46.    
    ) e8 c& I3 C* M$ e: u/ d; s0 n
  47.     bzero(msgbuffer,sizeof(msgbuffer));
    0 B- ^; Q8 T9 y0 H
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    ; w( Z( L+ W' ]# F4 ]- F7 G
  49.     printf("[receive]:%s\n",msgbuffer);. h- v, v: d0 x
  50.     # T+ \' w) p: L" b; ~# }9 F: u' h  \
  51.     usleep(500000);
    9 Y3 i' g0 }8 f$ Y! y2 d$ _
  52.   }+ b9 m5 z# ?( \5 r  S
  53. }
复制代码
* K9 S: U' v+ n! J0 T

5 |; n& `; l$ {' e& u
服务端:
  1. #include <time.h>
    % c9 {2 ~/ H9 v: k6 ^
  2. #include <stdio.h>
    - a% O+ Y  ~7 t9 M
  3. #include <stdlib.h>" I7 f0 ^# C6 b
  4. #include <string.h>
    * v: z) G$ ]! N: m
  5. #include <unistd.h>5 `* l7 M) E- |$ b
  6. #include <arpa/inet.h>" p+ P# v0 Z9 L) u& |4 n
  7. #include <netinet/in.h>( C* J8 K9 a8 Y' {. |
  8. #include <sys/types.h>, l4 }0 Q$ N- Z3 H; f2 s- O
  9. #include <sys/socket.h>
    $ i" ^) f! x. v7 h3 f5 p$ s

  10. ) h' ]! i; K3 W
  11. #define LOCAL_PORT 6666      //本地服务端口, t8 M, U; E+ F3 l" @
  12. #define MAX 5            //最大连接数量7 I6 _8 U/ {1 K$ `2 _
  13. ( M2 `* v  }7 }) _" o) k' ~+ b6 }& r- E
  14. int main(){4 g. n+ G1 C. m4 z7 L% ~/ q
  15.   int sockfd,connfd,fd,is_connected[MAX];
    ; I& S, E3 s, x: |& F6 `4 R8 f
  16.   struct sockaddr_in addr;/ V6 d9 _6 O* ?; i( i; l8 B; U+ J
  17.   int addr_len = sizeof(struct sockaddr_in);* H$ F! p. x8 w) g
  18.   char msgbuffer[256];
    - k9 e$ m  L7 J' F9 I! I1 x  ?- V4 r
  19.   char msgsend[] = "Welcome To Demon Server";
    , v- h, W* Z  }2 ^+ Y+ M/ ~. T
  20.   fd_set fds;
    2 H" x5 r5 H; P& G- u
  21.    
    3 U4 m8 L& l: J" p# o+ d
  22.   //创建套接字
    - u& H1 M. q, {5 D6 W
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    ) J  n; F+ T+ W
  24.   if(sockfd>=0)
    9 O# e+ W! M3 [5 x
  25.     printf("open socket: %d\n",sockfd);! _9 j! `5 ^# Q3 B! o" F( x+ D

  26. 6 u, {8 u4 {+ s
  27.   //将本地端口和监听地址信息保存到套接字结构体中6 D2 S6 X# h- t( T+ N4 p$ ]. s- ^
  28.   bzero(&addr,sizeof(addr));
    5 h4 W3 |" e0 s) L5 B% k- Z8 L
  29.   addr.sin_family=AF_INET;
    + ?; N0 D) P" b4 i3 d# D8 ^
  30.   addr.sin_port=htons(LOCAL_PORT);1 d1 w  Z. S# g" x* j3 ]
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.07 Z+ e$ r$ ~/ s4 g+ A
  32.    
    - |8 G+ P" T2 ^1 A( Q& t- ?
  33.   //将套接字于端口号绑定
    5 s& `. ~- d/ N
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    8 \1 L1 O+ E  B7 T
  35.     printf("bind the port: %d\n",LOCAL_PORT);" D  g$ Q  I  u/ Z8 J

  36. ; G2 F5 W2 z2 R  P
  37.   //开启端口监听, d9 _! i' o5 q0 u/ l3 A
  38.   if(listen(sockfd,3)>=0)
    # _- a4 @5 c" E2 f
  39.     printf("begin listenning...\n");, ?1 P0 y2 W! }
  40. : W9 J& N, ^$ v0 B0 N: K4 R
  41.   //默认所有fd没有被打开4 z6 j6 |! L# }. z3 m
  42.   for(fd=0;fd<MAX;fd++)- i: L- i* u& H- B
  43.     is_connected[fd]=0;9 w6 b' {2 o2 B
  44. 8 k2 ^0 o  q1 T* [- m' V' d; d' ?
  45.   while(1){
    , c# O4 q, G) w( T# I/ G6 W6 r
  46.     //将服务端套接字加入集合中3 I8 h7 k+ K$ k/ M: A: K
  47.     FD_ZERO(&fds);
    8 R0 I4 M" a3 K, l/ t/ M0 M; ]7 ]9 _
  48.     FD_SET(sockfd,&fds);5 D& E" p; A7 X5 Z9 a
  49.      
    & _" J. |: m3 F
  50.     //将活跃的套接字加入集合中9 B) K% T0 z' |9 M' g
  51.     for(fd=0;fd<MAX;fd++)
    6 X: D/ f' O' H
  52.       if(is_connected[fd])
    0 @. D3 r8 M/ @: m6 _3 V
  53.         FD_SET(fd,&fds);
    3 V" _, }# R8 \
  54. 3 O3 K3 T/ b2 E  M( h
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
    . C9 e  U. C8 e, d
  56.     if(!select(MAX,&fds,NULL,NULL,NULL)). n' _1 P; i  u" H! \0 G
  57.       continue;9 T9 |' `( d( N% m
  58. 7 f4 z6 X! r4 o; c, K) F: S
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字
    ' R& B" Y% t7 I! U  F1 \$ @
  60.     for(fd=0;fd<MAX;fd++){
    8 Z' V. c* K. l( ^! r
  61.       if(FD_ISSET(fd,&fds)){
    6 q. B8 R) ~+ ?$ M
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接7 m4 |) y( u5 A/ N: W" M" M
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);, k2 ~9 ^2 y  h9 R
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语
    & S" A* U* {. b# _- `7 B3 k5 f3 a
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用% n; O, {8 H6 |1 [* m" W
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));' R' U! |  C3 f0 n
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字, f* p2 t3 O' M* o( d
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
    3 {% d' _( I( i! a% z9 ]" A
  69.             write(fd,msgbuffer,sizeof(msgbuffer));4 o$ K$ n/ F% V( l' J; |
  70.             printf("[read]: %s\n",msgbuffer);
    ) ~+ e. A! q$ w2 C( U) R
  71.           }else{
    ; u2 T+ L( B: V1 ]& p. H
  72.              is_connected[fd]=0;
    9 \+ r# `3 V5 R/ o% W
  73.              close(fd);% ]1 X5 X" j9 L  X
  74.              printf("close connected\n");
    5 b8 l$ ]+ W) Z+ v
  75.           }
    0 B; |& v' g! R+ s/ |+ m& [
  76.         }$ J2 _; j/ B/ {  B' x
  77.       }
    $ O/ c! q9 b7 H' K. ]: t$ q
  78.     }" _" c) H4 X- v) o, b4 G" I9 N/ U5 [/ L
  79.   }
    9 D  D/ t/ N4 r7 m* U: y1 H- K+ d
  80. }
复制代码

* X) `8 @5 r/ |2 F$ J5 z+ u4 W! _, G& `

2 n  W3 w! I4 m- l% r) f. ]+ |. F- _4 F9 W1 A

0 Z8 ?) ~0 K5 |+ L; L7 A  g$ ~' B$ i1 D/ G$ W8 L: U! m! k- L





欢迎光临 cncml手绘网 (http://cncml.com/) Powered by Discuz! X3.2