cncml手绘网

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

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

7 ?. {- n$ c( f
9 a/ B* k" W4 R& Y( C+ D) B; I
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
6 x. z# `3 M( f, _# F
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3

/ M$ k! K2 p* n! i3 g6 p6 B/ n/ _' c: Z6 q
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)

# w! i# Y2 g$ t* t. O/ v) p" ]+ Y) u) x, ^5 b9 m
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)

, c  c) u0 W' H
0 X7 n, k0 D7 Z4 Q' u$ {7 F# y! y1 h! H+ ^  _
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。

2 W8 K0 B& W6 z' m* S$ c5 j; B8 ?. X0 A. J
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>+ K$ D9 I2 i* T" q7 ~' Q

  2. 2 V. v! `+ W7 U- b, w
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
& R! s7 K" g; H
    readset  用来检查可读性的一组文件描述字。
: B1 A1 s: g9 O. o  I& F
    writeset 用来检查可写性的一组文件描述字。

/ w# L. x. N  F* Y    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)

2 K; h4 W% y+ ^    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
* }" `" D' N  }3 t
  l& [/ J# P: Z% |0 g  d    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:, _% y( C% }" A' `2 h: b7 H( E. ?
: k' a. a  q; l/ J) L
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件), V  K! J* [, o; c% m, z9 Q" l

  2. $ H* ]& W7 F3 _( L
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)! |+ a  ~: K+ ?/ X. r  a
  4. 9 w0 V- W" D, K2 e- U3 E6 P
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。  ~1 V: B: n/ i; i

5 w6 d' j% U$ S9 b* d. p6 R   
% C) M2 I2 _% }+ x1 d% q
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集8 s' @0 e. |. ^" O" `) X/ ]
  2.    
    1 M% R* g! C0 b, {' h) t* g( J
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd
    " t' `4 }9 }4 |" x) M& r
  4. : o, Q8 t5 M( l; s* p( [2 T
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd
    ( O4 A  ^$ S" n/ Y+ u0 r4 ]
  6. 3 J8 @) `, o7 u
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;
    , E) }, M' _% U6 L4 @& t
  2. .....
    & S4 g" B7 a( |) g5 s& H
  3. fd_set set;& V, b5 G" I0 F9 l% w5 \
  4. while(1){& k0 p8 n. Z* o. m1 m
  5. FD_ZERO(&set);                    //将你的套节字集合清空
    ! }6 i! R" D# {$ N# A6 Z6 n
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s3 c3 P/ C2 y5 L0 ]. c( {2 `
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,( p4 T5 B! s3 v, @( y
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,% T. `7 G4 C+ g& U, o. c( ]
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉+ T% u* k6 [9 K3 y/ U7 H1 D, `
  10.                                 //只保留符合条件的套节字在这个集合里面
    " n. q3 s4 ^! {+ x4 o$ I
  11. recv(s,...);' r1 D+ q1 J: V  w" O9 T
  12. }% J% o3 p- {+ h* T, k2 T4 X
  13. //do something here8 i- a9 x- {' C: |5 J4 G, S7 N* t
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。/ O& c. ]% _1 P/ R

  2. 4 `+ {' g* J# W
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1), t8 ^3 J8 u( g
  4. / s! B" {6 c! ~5 w
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011
    # c0 p& c5 f6 ^" s

  6. 6 F! B6 I6 v- p- o+ u
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待
    & L7 j7 u  v- \# n) e$ u
  8. ; g. Y( i: i' 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.具体过程看代码会好理解

8 t$ U( W$ I; ]; j& s6 e. ]3 M, v1 m, R: r. H/ A
使用select函数的过程一般是:
/ J) b/ c% W- e8 U' ~; N
    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。( I3 h$ ?) ~/ F* c: [9 R8 H
6 ?5 ]7 h, |# \! J0 @( ^& ?
客户端:
  1. #include <time.h>! w# _: ~  [) k% c  L& o
  2. #include <stdio.h>9 l% Q0 g/ `# V% }
  3. #include <stdlib.h>
    ' o9 R4 C, O3 N* q0 W) Y. f
  4. #include <string.h>( h. ^: |. d% L# X( a
  5. #include <unistd.h>% k  }8 H: q7 b) u
  6. #include <arpa/inet.h>
    ' S; B! ~3 a2 u" N3 n
  7. #include <netinet/in.h>( Q  e) i( Y" @# v$ ^5 p0 F# o
  8. #include <fcntl.h>0 a: }7 W5 n  V9 c; M9 g: C+ P
  9. #include <sys/stat.h>" `7 i8 i+ e: J5 Z1 O0 v
  10. #include <sys/types.h>
    ' ~/ S9 F! F* x1 a. h$ a
  11. #include <sys/socket.h>
    / q7 Y1 |; D' P+ [; a+ ^1 |1 F
  12. 5 G" e! ~: ^, h! v
  13. #define REMOTE_PORT 6666        //服务器端口3 X, Q* h0 T4 w1 _* w* V. e
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址* @8 d% x) V2 s* }9 h
  15. 6 K  @( R- l" t& x8 D
  16. int main(){
    ) u) |/ m+ s5 F5 m$ ?2 P
  17.   int sockfd;  O- T0 \+ R# g4 o% n
  18.   struct sockaddr_in addr;8 t2 l3 q+ p- m# v
  19.   char msgbuffer[256];
    ' v# R& e+ O) k/ O
  20.    
    6 k( {1 ~$ ~. B2 s- y) P2 O+ K
  21.   //创建套接字
    ! C& w9 D+ y8 r/ M3 S6 w; U' S% k
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    . C! K! e( P( q. X, g
  23.   if(sockfd>=0); [" [! h# k( w  m' s- u4 C9 A. {. y
  24.     printf("open socket: %d\n",sockfd);
    1 w5 U* l! }& @& l
  25. + u) Y: _4 e% z& y" i. m* u
  26.   //将服务器的地址和端口存储于套接字结构体中6 W' b( a, q- X: ~- P
  27.   bzero(&addr,sizeof(addr));
    6 t' _- J4 v4 J' z) O! F
  28.   addr.sin_family=AF_INET;9 E( Y9 }, ^( ~: B# P4 H1 A
  29.   addr.sin_port=htons(REMOTE_PORT);) B, [% Y" W" J# T: T* N
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    1 N$ o" D$ h! a8 G; l1 N. Q
  31.   
    ) ^5 ]- ^0 ~4 c/ V# u
  32.   //向服务器发送请求
    : V: O$ d+ P! {* h2 Q
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0): W6 e% h$ K6 w# e$ y9 w" W
  34.     printf("connect successfully\n");
    1 y3 r& `/ t% w) m$ [
  35.    
      C6 i9 F/ u% v0 R3 T, [$ @6 |
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行); L3 r( S* B, E! R/ w2 a
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);! w4 G) S8 p1 f$ c4 E9 c* z% M
  38.     printf("%s\n",msgbuffer);
    7 S* b' X" j) ]
  39.   
    ! O4 F* v/ P& J! u' C, q! \
  40.   while(1){* [4 f4 i& h/ a1 L7 ^
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
    : n% i3 @- L8 s: j0 b
  42.     bzero(msgbuffer,sizeof(msgbuffer));
    # |3 e3 V# g/ O1 A
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
    , U/ W3 o" b6 K# c- H
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
      t$ e9 F; U% J6 v3 [) m8 F
  45.       perror("ERROR");
    , W/ Q  G$ N3 H" z" y
  46.     9 U/ n0 G; |2 M+ {' M
  47.     bzero(msgbuffer,sizeof(msgbuffer));
    & o; D" |" R: ^( y7 R( p% _
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);% K1 A1 z9 j$ k" Q8 o5 [! P4 N6 k
  49.     printf("[receive]:%s\n",msgbuffer);
    2 S: c. t/ x4 U' w
  50.    
    7 t% Z% u9 y; ^# o/ X2 t# e
  51.     usleep(500000);' i4 g; I* M4 F6 ?' X+ F9 [
  52.   }+ s9 x( ]8 [4 M
  53. }
复制代码
9 G6 [6 ^: T2 ~8 R  Y. `

6 K) U  P5 X! F, q0 D8 ^
服务端:
  1. #include <time.h>
    , M4 x' ^6 M) j- ?7 d" t, a
  2. #include <stdio.h>
    8 g# K& D$ {! G; B8 a
  3. #include <stdlib.h>
    " ~# u* {- d$ X
  4. #include <string.h>
    ' B( m# [* w, W, k3 c' ?5 N' w$ e
  5. #include <unistd.h>6 |4 K, S7 @0 D1 |3 ~* K9 k6 P
  6. #include <arpa/inet.h>
    ' o+ ^9 q" A1 y1 G
  7. #include <netinet/in.h>
    / d) t: h+ b- C( E: l& L% f
  8. #include <sys/types.h>/ X9 y) S8 S2 j: \* j6 k9 O
  9. #include <sys/socket.h>
    - O( e* n. r$ o! U. M
  10. , K7 l/ R: V$ T
  11. #define LOCAL_PORT 6666      //本地服务端口8 y3 I  B( g  @9 M( y$ Z
  12. #define MAX 5            //最大连接数量
    0 F% |' r) @" u: ^

  13.   u- Q. M( u5 w5 }3 ~
  14. int main(){
    2 {$ e6 s% b3 [+ |% ~
  15.   int sockfd,connfd,fd,is_connected[MAX];
    # j6 W9 \6 y4 t2 L( H
  16.   struct sockaddr_in addr;
    / o) o- N* v- G& c% c
  17.   int addr_len = sizeof(struct sockaddr_in);8 p8 `8 G7 E: R+ }' E. R. R  R
  18.   char msgbuffer[256];& s/ U5 R  ?: E) j! @
  19.   char msgsend[] = "Welcome To Demon Server";
    2 z. y5 p5 L6 Q! S( S( ?" _
  20.   fd_set fds;' H( c, U" P' @  ]( M" E' M* s+ ]0 i
  21.    
    ; s& t+ B* @, i- {- T6 P$ m
  22.   //创建套接字& T( X: _) ?$ I
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    " j1 O" O: `& O& d
  24.   if(sockfd>=0)# p2 F' M5 O" Z
  25.     printf("open socket: %d\n",sockfd);
    0 l( |) a3 L  }5 R: e% p( @3 M

  26. 2 h" D+ {: |9 `8 K4 [, ~% Q6 M
  27.   //将本地端口和监听地址信息保存到套接字结构体中0 _; Y" L3 S; _7 B
  28.   bzero(&addr,sizeof(addr));
    + S3 ]# N2 |$ v4 P# S6 j: w1 X' }
  29.   addr.sin_family=AF_INET;
    3 \% K& t; _; y$ {) Y2 j
  30.   addr.sin_port=htons(LOCAL_PORT);4 ?3 |  {5 H. Q9 L4 `
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0) @* k- l# l" {3 B/ q/ t
  32.    
    0 y9 B$ q/ M* `. W: a
  33.   //将套接字于端口号绑定, g$ w2 a  i: ?
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    7 Y# J  n" m; x
  35.     printf("bind the port: %d\n",LOCAL_PORT);, v9 i- V; Q% {' S

  36. # p% W2 [1 [; D5 [0 u4 S8 R
  37.   //开启端口监听6 G: l; o5 w7 Q8 N! T
  38.   if(listen(sockfd,3)>=0)* d! o; ~: W3 R
  39.     printf("begin listenning...\n");2 A" H8 V+ g* p* n

  40. 2 T4 g- B  Y8 L+ y
  41.   //默认所有fd没有被打开
    # J! S# V* _7 r; D- E9 H
  42.   for(fd=0;fd<MAX;fd++)8 c9 e. I7 K, T/ I/ g
  43.     is_connected[fd]=0;/ |7 z+ W2 N0 I/ `, x3 V

  44. ( }. v$ Y: F7 _0 G6 e
  45.   while(1){
    2 S$ G( Q3 v( }$ H/ a/ p1 ?) A; Z
  46.     //将服务端套接字加入集合中
    4 `3 `) L! S3 ]4 ~( N
  47.     FD_ZERO(&fds);, u5 ?/ [& H) B
  48.     FD_SET(sockfd,&fds);8 N' |5 j$ Q% ^% R
  49.      
    % m0 o7 `7 D9 i& c6 @8 V4 Q8 Z
  50.     //将活跃的套接字加入集合中
    # e8 m" d+ }1 S7 c3 V% }: @
  51.     for(fd=0;fd<MAX;fd++)  v: q4 }7 t# d0 J
  52.       if(is_connected[fd])
    ( F. m4 C% {9 Z
  53.         FD_SET(fd,&fds);
    6 L- R& d& e$ p7 c

  54. $ k! p2 T# a8 ~0 z
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
    5 D1 ~* c/ _6 Z+ R- K- x
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))5 V" e* i: k- V* {! c
  57.       continue;$ R( R& s+ e  s  h8 z/ X
  58. ; S- I0 s. L6 @8 {) v2 O
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字8 d/ A* a; I# D0 q0 a4 Q
  60.     for(fd=0;fd<MAX;fd++){
    2 E# N6 t# `# }7 G" M! d
  61.       if(FD_ISSET(fd,&fds)){) f: l6 i9 Z! c
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接
    2 ?. }3 f* W) q" [+ ^" w) g
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);1 d6 s/ U5 j/ C. V
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语
    ) a% j2 ~/ Y/ W( x# }
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用3 `1 K$ y9 ~) H$ Y
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));
    " T& `- i4 M3 N% y+ U' u
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
    / D5 s) s4 a7 p! W8 z
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ 7 h9 x# s/ t7 `! Y) m
  69.             write(fd,msgbuffer,sizeof(msgbuffer));
    2 A$ E: g+ R8 R- B
  70.             printf("[read]: %s\n",msgbuffer);
    3 {7 {, X7 l* R/ j* _+ u" T: C
  71.           }else{' z* N/ g. S( T+ {. q6 @5 }
  72.              is_connected[fd]=0;
    % e; g, l+ W' d
  73.              close(fd);
    2 ?5 C0 h2 d' C+ m2 }
  74.              printf("close connected\n");/ [/ g: {5 U0 l, Y0 F+ b
  75.           }
    4 n1 V$ s* @9 ~
  76.         }
    2 W) g6 B, N6 o- H% d
  77.       }
    % O9 j) `$ ^8 r  u, o7 U
  78.     }
    , \1 m- ~. E2 Q$ N- V3 z2 ^
  79.   }
    7 p7 N2 K1 B  `( W) G3 X
  80. }
复制代码
3 t) U5 m, Y, e) b2 l, O! `
' E) l5 r8 m8 l+ N
- c( ^+ G2 d: w
6 o" m1 Y1 L3 p& T% x

( `4 A2 [3 I+ o  W) Y6 X) X- K' V. l& @, [! i& e) c' p4 t: X





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