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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

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

/ [2 O7 x6 r+ T! L4 E
  v7 V4 C+ Y) U0 B2 }4 @5 P, _' C% d
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等8 |5 E6 {! M$ B
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3

' Z( w; E& k4 o" W+ X
% [, |, X  X$ O/ N& G+ ]( b8 [
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)

8 k0 D5 G9 ~+ P) C5 T9 C4 S* I9 ~. h5 W' C8 e7 u
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)
8 s3 T; Y# }- i" @, ~
2 h( u( O. S9 q7 H8 O. c
# j$ s6 E- r2 M& b" y
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
' A2 `6 q; ?* V" _* `% N
0 M  ~- {0 m! ^; S5 N0 P1 ]4 E
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>6 {; O/ u& T9 j5 _5 S, u

  2. + l: t# U! @& D3 g4 K
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理

2 s) P) R2 X3 _. _& O- e& c9 i    readset  用来检查可读性的一组文件描述字。

9 z/ K, `+ H: ]/ ?, O- G    writeset 用来检查可写性的一组文件描述字。

& K" f% y3 O7 O+ F6 t+ H    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)

9 m2 {: H# E; l    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。6 O4 \4 }( n& q( }" T: r- a. A

5 ~; S# G7 l" d! q' }    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
) K- C! i( q; R0 V

; ~% j7 K" y- B4 y% |2 @
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)7 g5 @$ h# |1 e
  2. ; s, T- i4 I3 q- N& c. l
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)6 s8 c5 U) _. l2 c6 P! p; i0 U; a5 Q

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

9 p; B! r# b9 T4 {1 c   
; n& k6 E8 `0 K# a/ B: F" m: ^5 t
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集
    5 ~$ E; D* H4 v, ]% z' x% l4 I
  2.     % D5 F1 p' i+ K$ j
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd
    . s: B- Q7 f1 ^, c

  4. 0 Y  A" i/ P: J
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd
    ) e1 k; p* T- o0 |- f5 ?2 @0 s
  6. 3 x& o4 N; H9 K  d2 l* q0 }
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;
    % i$ I8 R$ S( S5 }6 m' p
  2. .....
    $ B: b  p. H4 f/ U
  3. fd_set set;6 B% r8 k& h) L
  4. while(1){" j7 T, q, e/ a7 G% T
  5. FD_ZERO(&set);                    //将你的套节字集合清空
    & Q2 `( _# h1 f: X; U
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s
    / l: T6 ?- N+ ^: z
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,& I* f8 q% [9 z5 J2 D' l  `
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,
    $ A' g" I, s9 b% N6 i+ S, ]/ l
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    2 ]! u& W0 p- \* s/ U) y7 {4 }
  10.                                 //只保留符合条件的套节字在这个集合里面3 I# G* Y5 g- y0 z! b2 c' l
  11. recv(s,...);
      c  A) ?5 N8 A
  12. }4 T& H( @" e) k
  13. //do something here
    1 x$ F' `; ~$ X5 I2 f/ l& s7 Y
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。
    % ^1 G5 F  E1 u

  2. ; g' [, f( p& C" [
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)
    . P$ N5 j0 E' I' Y' s+ m# U7 r

  4. , ^2 N# V6 n2 _
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,00110 M0 |+ K9 w# h5 M$ j" z& n

  6. 0 p. `3 l3 r3 `1 }* d+ {
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待
    & o1 D( f+ d' b* \5 e. ^
  8. 4 \4 h0 |  I/ g; z* r& M9 ^9 c7 ]
  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.具体过程看代码会好理解
  t' }  K( k( Y' v4 F+ b- y0 O

8 b" S1 F5 W7 N
使用select函数的过程一般是:
# u  m, j5 H& e/ [9 M% C  D
    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。2 c9 H( t2 Q3 X/ B3 D% s; G+ T$ V
8 I; M2 w& Z2 D3 y) b
客户端:
  1. #include <time.h>( Z3 s1 L: N0 X! \& X  b
  2. #include <stdio.h>+ r" w# M$ k" o
  3. #include <stdlib.h>
    3 @# L+ s5 s; z$ w0 O; [
  4. #include <string.h>
      N9 l5 o$ R  I4 d
  5. #include <unistd.h>
    2 B* b# O  {- M* j5 l
  6. #include <arpa/inet.h>
    1 B- u! W% G3 z; z6 m7 T% _
  7. #include <netinet/in.h>- F& n# y$ ~" [* L
  8. #include <fcntl.h>) ?+ [6 S+ I! E+ E# q
  9. #include <sys/stat.h>! @0 o2 g2 ?$ K% V! O  V2 Z2 D
  10. #include <sys/types.h>! C5 g$ f3 B1 d' l0 h, K7 M9 E
  11. #include <sys/socket.h>
    2 f# Z  t& X( R1 }$ q

  12. 4 x, O6 q& Z% {+ k/ X
  13. #define REMOTE_PORT 6666        //服务器端口
    / u$ X* i1 Q9 Y4 D
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址
    & N- Q; h4 g0 B3 d! t$ i

  15. % K+ m: w% S1 i
  16. int main(){+ F7 _* s1 \3 m) G. Y5 o$ `1 k5 j4 x0 M
  17.   int sockfd;& o1 A3 y' ?# \) u4 ~. N( G
  18.   struct sockaddr_in addr;& q+ h1 ?6 J% p' L/ Q. R! l3 Y% f
  19.   char msgbuffer[256];
    . |7 y' u% |% O
  20.    + }9 y3 ]! q2 y* E; u& g" t: y
  21.   //创建套接字
    + y; S0 {3 s  y; D5 H, \' h% S
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    , V  c3 o$ s/ c* |( }
  23.   if(sockfd>=0)9 ?6 e6 \) r; s
  24.     printf("open socket: %d\n",sockfd);
    - _6 Z( g0 D* E1 U( `
  25. ' \) O( r+ r: p( W; C
  26.   //将服务器的地址和端口存储于套接字结构体中5 _, x7 D5 `7 g0 D
  27.   bzero(&addr,sizeof(addr));7 T; L9 m* Z2 ^  U. v9 r
  28.   addr.sin_family=AF_INET;
    " h9 {0 H) V* T1 H* u4 a2 F1 I
  29.   addr.sin_port=htons(REMOTE_PORT);: h/ J/ I9 o1 `( B" B2 a
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);. g$ g2 l+ U4 G  V2 B
  31.   : `3 h7 a' U% i
  32.   //向服务器发送请求
    , u% p" l# `0 `6 R7 d
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    7 V* p7 U" d# `$ b& i
  34.     printf("connect successfully\n");
    4 ]* d# z! K5 F- b7 E, }0 `+ F
  35.    3 D/ r$ L+ X0 L; L; x5 v/ N8 M
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
    1 A) ]& k: N0 y* [( _- n7 l
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);4 J. J3 d! s1 S& n1 f
  38.     printf("%s\n",msgbuffer);
    " T4 ~5 D) S6 i1 ]4 p
  39.   
    / K# ~4 s8 P! o1 ?# ^
  40.   while(1){
    * K2 x$ z) U  S/ N1 a0 g
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
    ' H: k% B. F! m, N- Y7 \
  42.     bzero(msgbuffer,sizeof(msgbuffer));
    ( U# f: q+ n. o( m1 j. q
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
    * E3 `/ w5 n; S$ s+ G3 }5 e4 e
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)' V; E8 |  D* n+ B; {& ]) O7 i# i: K
  45.       perror("ERROR");3 M/ d0 K% f8 f9 q- c
  46.    
    7 f* n7 @) I0 {/ S
  47.     bzero(msgbuffer,sizeof(msgbuffer));- x& d' V# {' T) f$ M6 q1 j7 C% I
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    : y' P$ L( k+ X9 T, B, z7 l
  49.     printf("[receive]:%s\n",msgbuffer);5 q. ?& Z" X4 T3 Y! a
  50.     / T6 T# T0 A* k
  51.     usleep(500000);9 x3 R' x" V8 A+ Y. q
  52.   }0 K9 X. h3 U; z
  53. }
复制代码

4 o- P1 r, Y& \; L7 x& G
$ ?7 H1 c) D9 Q9 z+ R( t0 i! n, }; [
服务端:
  1. #include <time.h>
    * v/ [5 ^6 ~& ]; S& }
  2. #include <stdio.h>
    7 k0 M8 Z% Q2 l% ~; z; y' s& Y4 \
  3. #include <stdlib.h>8 E, y" a) r0 j" [3 a
  4. #include <string.h>' F8 e7 Z8 T+ d% {! @
  5. #include <unistd.h>
    - i0 ~$ n: b7 r2 P( _7 R  p
  6. #include <arpa/inet.h>+ ~* f8 B9 N# q% M* A- y* T0 e
  7. #include <netinet/in.h>
    9 d: s8 p! z6 n4 P2 |
  8. #include <sys/types.h>' L6 P! O: Y# X
  9. #include <sys/socket.h>
    * f) S4 X; d6 `; `" V2 T/ m" i5 f
  10. ( A/ G; w. R) M* p. z
  11. #define LOCAL_PORT 6666      //本地服务端口* i8 D; C& C$ ^$ ^' a1 t0 S
  12. #define MAX 5            //最大连接数量
    ! u! S  X: `9 H  Y/ T9 A

  13. ; p' k# S  _' W" x
  14. int main(){" H8 u# q4 h! K
  15.   int sockfd,connfd,fd,is_connected[MAX];
    8 O$ E- W; u) b* s- F
  16.   struct sockaddr_in addr;* o- u5 h. m& |
  17.   int addr_len = sizeof(struct sockaddr_in);
    7 e0 ]3 m7 [) ^% z. r
  18.   char msgbuffer[256];( Z3 ~4 `# \( V9 z( K$ N* U
  19.   char msgsend[] = "Welcome To Demon Server";0 d& X' b7 G; j- i
  20.   fd_set fds;
    ( v+ X% G3 Z- P! |) f% S* C( Z# i
  21.    8 r" l3 m  g4 n6 _; a7 x- a
  22.   //创建套接字
    9 X. x0 n7 u( t  K2 S1 N7 P" z
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);6 R0 t* Z; T: `  L! E7 R! b
  24.   if(sockfd>=0)
    % v! _, I" z! v+ B5 T% B" l( N
  25.     printf("open socket: %d\n",sockfd);- W$ s# W% K3 j' N
  26. 9 R5 s; L5 u; G* k
  27.   //将本地端口和监听地址信息保存到套接字结构体中9 z& ~4 W: a6 m' L% Y1 ?3 H
  28.   bzero(&addr,sizeof(addr));& l4 c: S2 J( G" a  E  n! Z$ {
  29.   addr.sin_family=AF_INET;+ G/ d4 B. Z# A$ f2 }/ I2 [9 ?
  30.   addr.sin_port=htons(LOCAL_PORT);( r5 s) ^( ]( K2 q# d
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
    + n3 p. t0 i2 N" h/ C, [2 N
  32.    
    1 z# u6 ~# j) H; E5 j. ]
  33.   //将套接字于端口号绑定' R6 E# S0 y* l6 O& o* Y: p% U
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)4 t$ w5 X* s) N8 B
  35.     printf("bind the port: %d\n",LOCAL_PORT);" g& s2 f6 J$ e5 ]$ m; L
  36. 5 l8 m7 U8 `, O9 O. q: d& q: V' y  ?
  37.   //开启端口监听, [$ C( {# y2 C. N
  38.   if(listen(sockfd,3)>=0)
    + E) g* g/ U' k/ R
  39.     printf("begin listenning...\n");5 q. q3 |1 u& ?4 k

  40. 1 U, H( K) q; n0 j8 x; m
  41.   //默认所有fd没有被打开1 n; I2 [) W7 ~0 t8 z
  42.   for(fd=0;fd<MAX;fd++)
    ) A5 @7 _4 }  _3 p5 U% h
  43.     is_connected[fd]=0;
    3 w' ]5 B: p1 O. t4 @$ j
  44. 3 `8 t$ Z+ |4 D" J
  45.   while(1){$ a  [" m' U% A2 p' ~. [
  46.     //将服务端套接字加入集合中; N/ a) h0 G* a! `6 y7 [
  47.     FD_ZERO(&fds);' J4 J  B: G" ]" ~' k9 O2 e9 u
  48.     FD_SET(sockfd,&fds);7 Q  c* f* A5 |1 Z. R
  49.      
    " g; r1 }6 R7 P  r% v6 h) {
  50.     //将活跃的套接字加入集合中" u) Y; i$ z7 d6 a6 }+ m. U4 G
  51.     for(fd=0;fd<MAX;fd++)# f) O, |8 f5 u4 y( ]
  52.       if(is_connected[fd])$ u- D+ i+ s1 v" g8 `6 i, S
  53.         FD_SET(fd,&fds);2 [9 m8 \; ]! i( u1 J8 g
  54. # w" V. H; D! z% b  P( e
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为04 j6 }! V3 \" X, c
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))  m8 J# `, }- b4 `5 g9 {
  57.       continue;6 j! b2 h/ a2 x

  58. 6 }; O0 ]1 Z( d# U- V( F
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字$ ?9 V% c$ |6 r( A* _
  60.     for(fd=0;fd<MAX;fd++){
    & S) s- {8 I4 Q! b' E
  61.       if(FD_ISSET(fd,&fds)){
    ' H4 V6 H& o) q, q( j
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接* Q# u7 }# r6 S0 N( D
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);9 R3 g7 t0 \7 p1 r# ?9 P
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语
    8 ?. ]8 i+ H- I7 b' @
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用
    ! I6 r- _5 o) T# V. C& G# Y
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));
    : o: H/ c3 x% n3 p3 W/ F! Y
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
    * T/ g7 i7 l' C2 t5 s7 }
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ % I+ @8 e# D7 t4 Q' z
  69.             write(fd,msgbuffer,sizeof(msgbuffer));. ]9 W! h6 p, `* l4 x& p# o1 ^
  70.             printf("[read]: %s\n",msgbuffer);
    ) e0 P3 f" p6 _; o" Y! u
  71.           }else{7 Z1 {! v3 t9 o
  72.              is_connected[fd]=0;
    / ]/ R% N8 \. ~6 [* s1 E9 e
  73.              close(fd);% ^- b3 [, Y! L/ N2 P+ b  Z) `" m+ b
  74.              printf("close connected\n");
    ' K7 Q1 S9 ?" b- @# g+ q2 H, n/ t
  75.           }
    $ V( g. {1 ~6 `* r' y
  76.         }$ T) {' V+ y6 i6 @/ }; C
  77.       }
    0 G2 m- Y2 f: R4 e. Z$ F; ?
  78.     }
    $ R2 X# s9 [# i* c+ T+ ^1 S
  79.   }* ], k7 @" V; F5 \# Y; p+ X: K% n" }; H
  80. }
复制代码
4 f4 C2 |, r! B0 [7 R% f

" w, r# p5 Q" i- \! X/ k9 v2 ~: x+ Y6 o* X" e

8 S7 t" F5 }% r# {4 A# \( y; ^4 H" k( X* V% r( C+ y& j* |8 |, N! ^
. s  Y4 ^# b1 l# C  M: a' e
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2021-9-24 16:15 , Processed in 0.137272 second(s), 23 queries .

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