cncml手绘网

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

作者: admin    时间: 2020-5-9 01:53
标题: 编写一个简单的TCP服务端和客户端
实验环境是linux系统,效果如下:
1.启动服务端程序,监听在6666端口上
2.启动客户端,与服务端建立TCP连接
3.建立完TCP连接,在客户端上向服务端发送消息
4.断开连接
实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点

8 |1 t- E& D8 Q( a: P' l
什么是SOCKET(插口):
     这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
% ~! O0 K% W7 i! m  G1 r: Z2 K; R
     "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
      对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
      具体其他高级的定义不是这里的重点。值得说的是:
      每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
      应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层  ->发送(接收的话与之相反)
2 w8 Y4 Q" ~# ~8 C! H+ c& m
: K- L4 v0 ?2 [( a

; W* I  @& U; |$ T! g
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
4 k' O: s" ]: `3 B# I: c! y: o
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3

1 u* S' {$ p2 ]9 M3 F! K! s) `, v& x9 [& x
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)

/ h' w8 i! j# m6 Q3 Q- I: U& ~1 K0 I8 P9 R0 Q) d8 r7 b
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)

* U9 z: K9 H, ]. u) V$ K, C5 q  N  ]3 O7 c8 a) E

- @. F0 T3 n- b- Q2 u2 C
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
; c- t- x7 x, b. E) Q+ x- b% g
4 D# N8 j# e) z: S3 k: @
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>
    # n+ ]% A  _6 m; Y2 f

  2. 3 q. ~  B) `+ ?
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
- z/ ]# F. N( Z" A+ h
    readset  用来检查可读性的一组文件描述字。

! V7 ]4 Z8 j5 |. X# }0 _" v* K    writeset 用来检查可写性的一组文件描述字。

' a* l' n' _$ V7 c0 j    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)

+ T- R7 ~5 }" G' q0 V% [    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。8 s* q* O' p( T% W! N2 E9 l
: F, I% R. M* g  E* z9 r* c
    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:0 C; B' ?7 b- y/ [8 }
; l- N. z# S5 r$ z1 A% s* J
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)9 v( [6 Q* N3 e$ V

  2. 6 w5 }  g6 `9 v, K, \4 _
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
    + _+ c% |+ H7 S% h

  4. * w+ x  @8 q- U& m1 \
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。! f: t( n  x% q* o* v. S
. t& b/ K& f6 ~0 x+ K9 n  A) y
   
& D; @. i3 S# `$ F8 q5 S; V
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集8 p5 {( ^( L* ?% u
  2.     2 d: W$ Z. \0 i& Q
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd) B& c0 L) |+ T) S. X+ p* x

  4. 0 s1 y& ?3 i! k3 `9 u
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd& h! E* l( g2 b2 M# u- }

  6. 5 |# A' u' D8 X8 b! N
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;6 a0 E+ @  o; c& j. Y
  2. .....
    ' B! m  B. A5 F& V- \# x! v' N
  3. fd_set set;
    3 G; ]( o% ]/ \( |
  4. while(1){
    4 h( F- N3 S/ m
  5. FD_ZERO(&set);                    //将你的套节字集合清空  z* R) L. K& k/ ]/ j% x
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s
    , U* x% z0 L0 n7 v# K, }
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,
    1 K( J+ i: Q* v% |& m' F* Y
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,/ \( U: c. b1 ^1 b' ^) P
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    0 ?* ~1 o6 g; g' F
  10.                                 //只保留符合条件的套节字在这个集合里面1 u, ~+ I2 K2 G5 c& w* h
  11. recv(s,...);
    $ A$ ?) q' Z6 {" [+ l% A. T% d
  12. }
      b! m% m* {  @! W* i) }
  13. //do something here
    - m0 c9 V$ ~, Z5 x# k& `8 ^6 d
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。
    4 m: Z7 Q2 p( z- _8 L% A& m

  2.   n. f2 B( G9 G8 U8 V! b
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)
    # k+ S; z9 v$ X* d0 R

  4. 2 }+ \0 M2 B; u6 X# R
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011
    ( E! z% m) E6 I$ y/ t  ~
  6. % O1 n) o1 }) B1 N
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待
    " x; L$ y/ F8 `$ j: L
  8. + z- K5 D' j9 ^6 V3 [2 G
  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.具体过程看代码会好理解
5 Y& v" o. q0 H5 y1 w

6 ^: U- B% L; y3 D9 K
使用select函数的过程一般是:

! x9 ]- b  \! l    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
6 ?/ |: s6 x! s. t3 o1 T1 U* ]

( @/ _# X: n, _9 b1 Q) t/ u) j
客户端:
  1. #include <time.h>5 ]! L0 m/ Y' l" i+ F# ^
  2. #include <stdio.h>! X8 P) I( E; M9 m; m, Y- }
  3. #include <stdlib.h>
    5 E/ q' k* J% n( B( }; \
  4. #include <string.h>: t3 D  p' S( d3 ]! ?, V1 }) q9 B$ ?
  5. #include <unistd.h>: c5 B" `8 W* |
  6. #include <arpa/inet.h>
    + ]0 m$ |: K0 {+ _
  7. #include <netinet/in.h># o: L! _1 ?9 `. D
  8. #include <fcntl.h>
    , j0 ?3 c8 X4 s9 B/ z+ G
  9. #include <sys/stat.h>
    # L5 f0 a/ }9 h& B3 U9 X1 x& E
  10. #include <sys/types.h>
    7 R2 ~# |( [( p# l. x* J' M
  11. #include <sys/socket.h>. W) k  N. e4 a0 [- O8 i

  12. : j: `- V& C2 [2 L  i# v
  13. #define REMOTE_PORT 6666        //服务器端口
    % m/ i$ ]6 J  i7 \
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址) Y5 N! m4 ~7 Z8 s$ b9 \/ g* i$ o, g
  15. 9 ^. F2 Z2 h2 h& |: J" B3 C
  16. int main(){
    7 z1 o0 M" C4 t0 W& H% v! P
  17.   int sockfd;
    / X5 X9 ]+ t( L4 d$ T
  18.   struct sockaddr_in addr;: ~( @) ]$ a) @  j7 t9 s
  19.   char msgbuffer[256];+ x2 o& H7 x3 Y6 b
  20.    
    " X  T' @  [& Q4 M' h: z" S0 U  b
  21.   //创建套接字( s$ O, ], A1 y3 D
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);9 E$ v  e/ D1 X' Q9 o8 i' r
  23.   if(sockfd>=0)
    / Y$ P% d) w( u6 G8 L( |+ F
  24.     printf("open socket: %d\n",sockfd);
    2 H7 Y. K; t) w& l# [

  25. 1 `; o' J# a1 W7 R
  26.   //将服务器的地址和端口存储于套接字结构体中
    # ~0 g) ?+ U- X/ q- t2 \  s
  27.   bzero(&addr,sizeof(addr));: B1 r/ x9 F3 ~" o
  28.   addr.sin_family=AF_INET;
    9 ]8 y  d$ W7 C9 k: w" s
  29.   addr.sin_port=htons(REMOTE_PORT);
    ) c+ ~" e3 P$ S6 a& f
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    9 h5 l; |4 G  n8 Q! T$ h# A
  31.   
    ) ~, p/ S0 X" ]- P
  32.   //向服务器发送请求
    ; G. ~* U% R& n: f1 ~4 m
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)* }4 i9 D9 \; ^' S
  34.     printf("connect successfully\n");
    ) U/ E4 w) X/ j  o0 a
  35.    9 T' u  e9 ?: B$ @8 `& h. `
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
    4 ^+ ]7 K( e6 S' D4 M0 j. {
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    ; J. N" d9 F$ s1 Z0 b, V
  38.     printf("%s\n",msgbuffer);. C, }9 @5 x0 S+ ]4 T1 X9 f- H/ b
  39.   
    . f1 S) P3 [  q; G0 K5 _
  40.   while(1){
    8 p6 a0 D7 q- ^0 X+ n5 |: C
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息* w7 `: {4 H  u
  42.     bzero(msgbuffer,sizeof(msgbuffer));
    1 ]; p) y7 j) z2 |
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));- u% j, _$ f7 s+ h$ k! _* U
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
    1 Q. x, P4 }( u. t2 b
  45.       perror("ERROR");& ?8 F! j* W. C+ g+ q2 w
  46.    
    5 A- c9 t: R: h$ s9 J& [. L$ t
  47.     bzero(msgbuffer,sizeof(msgbuffer));
    * [5 N) u$ K, B6 n
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);3 ?" t9 t$ l! g( {% L( A6 V) {
  49.     printf("[receive]:%s\n",msgbuffer);
    # N( d; J, y4 ?( x0 \
  50.    
    ( _% r9 x. K: x9 x
  51.     usleep(500000);
    0 F5 \7 z+ `8 t- L# d
  52.   }
    4 t* ~5 r) i0 y
  53. }
复制代码
* M8 j& Q3 d3 I5 F7 t3 B
" M" m4 `6 V) w1 V; E4 p
服务端:
  1. #include <time.h>* \9 s& e& U6 {
  2. #include <stdio.h>
      R' G$ q* w" r
  3. #include <stdlib.h>
    " x: F2 z. T% e
  4. #include <string.h>
    ; c" b1 |1 V% p& m
  5. #include <unistd.h>5 k5 v! \3 _0 L' B! O6 [6 [
  6. #include <arpa/inet.h>. K- F6 g4 H' h2 [
  7. #include <netinet/in.h>8 h# X6 D9 b, K) o6 j) S. O
  8. #include <sys/types.h>
    9 ^) {. j4 G( z( E' p
  9. #include <sys/socket.h>
    : U: |" y( ]. u

  10. * q9 A* h  w' |. k: ?& `
  11. #define LOCAL_PORT 6666      //本地服务端口" c$ }5 S$ w, K6 Y# \7 q
  12. #define MAX 5            //最大连接数量
    0 {4 v3 N: ?  `; ^2 U2 J- z/ J5 O- F

  13. ( V4 ?# E+ T* L
  14. int main(){( Y/ ~2 p* s- i- W1 u/ C
  15.   int sockfd,connfd,fd,is_connected[MAX];4 q6 r! G1 J0 g' W4 x& S5 m5 X, n# g+ ]
  16.   struct sockaddr_in addr;1 B9 K: B8 g* t
  17.   int addr_len = sizeof(struct sockaddr_in);
    2 j0 u, Z. G, i2 R% }1 O
  18.   char msgbuffer[256];5 c2 R$ t) H3 P! s$ @" v
  19.   char msgsend[] = "Welcome To Demon Server";7 w; H, [# w/ W5 u
  20.   fd_set fds;; w/ z: |! j% N3 ^0 V# v1 q# e
  21.      ]! }1 a' ?: @. E
  22.   //创建套接字& }3 }- a- j0 T: d* Y
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    1 z( ?  L. |* s: F% S1 M5 }
  24.   if(sockfd>=0)
    + ?) g8 u6 ]% T
  25.     printf("open socket: %d\n",sockfd);' i4 n6 {5 H9 g9 l# d! G( u

  26. % S* B% s6 O8 o3 ^! Z8 w8 d
  27.   //将本地端口和监听地址信息保存到套接字结构体中
    $ D- ~& g( W, U
  28.   bzero(&addr,sizeof(addr));
    7 l, `1 E* |8 M, R
  29.   addr.sin_family=AF_INET;& j: f) c" ^1 k, |/ y$ a3 I' E
  30.   addr.sin_port=htons(LOCAL_PORT);  n$ [9 L, \# `6 h: H" v) {/ e( `) e
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
    & ~3 W% Y6 \9 B, Q: b) Y; ]
  32.    # d; x+ b  d. P4 T% |- Y) h, U9 f
  33.   //将套接字于端口号绑定1 [. I: t9 ~+ S; ^/ t5 L& o5 s% Z
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)0 [7 Q9 v7 [. V2 ^
  35.     printf("bind the port: %d\n",LOCAL_PORT);' _4 r& Z3 S( M2 Q. s* b. ^

  36. / x- d, U# ^% }
  37.   //开启端口监听+ i1 G; [- V0 u: p1 J
  38.   if(listen(sockfd,3)>=0)
    % G  @. ]5 |& G! [' O
  39.     printf("begin listenning...\n");. N5 U$ e) Z: {& w
  40. 1 W/ ^5 j4 h  O6 D; D: `. {
  41.   //默认所有fd没有被打开
    . l0 Q# ^1 a( G, C; X- z! G
  42.   for(fd=0;fd<MAX;fd++)
    ; T, @2 L" w; V: `8 u& n3 ^0 m. |
  43.     is_connected[fd]=0;
    5 V3 a+ b7 e3 t& A4 H4 Q- p' b
  44. + j( Q; V" ^+ o6 q) z
  45.   while(1){) |) k& t0 P- [, p% v  C
  46.     //将服务端套接字加入集合中4 q- Y) ]$ T; ]$ n: z  v
  47.     FD_ZERO(&fds);
    6 e9 {  a5 W9 @5 |3 e% W
  48.     FD_SET(sockfd,&fds);1 B" u+ ]) w8 s- |* r$ {
  49.      
    4 P8 l! v. N! h9 F2 S6 K  E; r
  50.     //将活跃的套接字加入集合中; x' ~% M. F/ l+ p4 S
  51.     for(fd=0;fd<MAX;fd++); f+ Q1 i, T" ~6 F' o  a: r, q
  52.       if(is_connected[fd])
    0 C# n* p/ S$ I  ?
  53.         FD_SET(fd,&fds);2 G3 p( }" n: U' |# O# l
  54. $ ^/ ~9 f- Q7 V( j5 C( }) s+ ^8 O
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
    7 b2 o4 V1 z  {0 f- f0 ^# S) d
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))
    ( J  t& m& M% u& }/ c) ]
  57.       continue;
    . W4 I4 U: T9 q6 E, F( [5 P
  58. . c2 J* T( o! @; [! a6 m) X, L8 b
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字
    . z2 T2 V! d. b* n1 ~* e7 l
  60.     for(fd=0;fd<MAX;fd++){
    * V9 {; r0 w6 f. r+ M# J9 j0 W
  61.       if(FD_ISSET(fd,&fds)){* S( F/ l3 _4 ]1 d
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接6 K/ Q- ~/ K- A- X
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
    - g+ H. N! X- K7 K' G  B* z
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语
    2 C" \# ^6 l/ \& u) b& m! M
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用& s! g$ H. z2 `" W
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));
    ! M# b  K# ^0 d
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字; h/ A1 x, T; f
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
    6 F# T  j2 z! k/ A; t7 B' T. m
  69.             write(fd,msgbuffer,sizeof(msgbuffer));$ S1 L; }* ~: y6 D3 x
  70.             printf("[read]: %s\n",msgbuffer);
    ! k/ h0 z& B( M' Z
  71.           }else{
    : w& o3 Z  B+ U) G6 a! @' g
  72.              is_connected[fd]=0;
    6 Q& n) G& K2 G/ _  h
  73.              close(fd);
    9 k) d4 x. T( B1 L  B2 Y* p) }1 e
  74.              printf("close connected\n");
    * p3 [+ D! R8 I; A4 r) ^  i9 g
  75.           }
    2 T3 C; ~* p5 ]/ Y1 ]
  76.         }! \3 u; q8 L4 i# H6 ^( c
  77.       }
    3 N1 i8 L: R3 c- R
  78.     }
    7 s8 D& V7 k) y8 p; M+ v/ y# ?# z
  79.   }2 K9 b1 v- z) Y) L( C: I2 R6 o
  80. }
复制代码

: |2 B/ c; W' D% o9 P! X# I9 t- ^# @& `" W3 G

$ ^) q+ d, v. Y% P, i
" [9 X8 J; }9 {8 a+ [7 c" `. r2 g7 q; ^4 C* R$ @' ^

& T0 R4 G* _& c0 X




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