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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 17816|回复: 0
打印 上一主题 下一主题

[C] 编写一个简单的TCP服务端和客户端

[复制链接]
跳转到指定楼层
楼主
发表于 2020-5-9 01:53:20 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
实验环境是linux系统,效果如下:
1.启动服务端程序,监听在6666端口上
2.启动客户端,与服务端建立TCP连接
3.建立完TCP连接,在客户端上向服务端发送消息
4.断开连接
实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
" @* A( n+ N4 F$ s
什么是SOCKET(插口):
     这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
- G3 e# ~8 {+ L/ V/ Y
     "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
      对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
      具体其他高级的定义不是这里的重点。值得说的是:
      每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
      应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层  ->发送(接收的话与之相反)
! M1 q% f% r3 B
$ U/ V& K1 A0 a' h

0 O% q) [1 j! Y3 H8 u+ S
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等: a& M: o5 v, k5 x. F
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
5 a2 n2 W, x* o. b
4 H% n( a& a) X- U# ?
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)

& M$ n9 ~! [; K3 C" k2 ]9 U/ g$ |& H) }/ w( i9 X
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)
' ^: @! ]) H3 j1 m6 n6 f
0 [' z7 j! ^$ [; c# E4 @

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

& p5 V" O; @& r* F% Q+ o! r  I5 [; X% \* m$ U+ @0 B9 N/ d: G0 r
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>/ Q; F. X5 U! b7 r

  2. $ |7 q- V0 Q* b+ H/ X0 B) o
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理

( b* s; b1 W3 ]! n! c3 A3 [" Z; W    readset  用来检查可读性的一组文件描述字。

7 S- Z3 h; z. \! J( ]    writeset 用来检查可写性的一组文件描述字。
4 A3 U* R4 N" }3 }$ t5 y" A
    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)

, }% o# ^7 [* B" M& u" @    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。2 J4 V+ q* x2 ^  c7 G& D
% k/ y+ [6 C' l1 _; d4 {- \. S
    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:2 C9 m0 ]) c% }! D8 C# ~( i  m! u: K, `

3 S3 }  w" o' K# @
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
    3 I/ X# S7 _# ?7 L

  2. 1 `! F9 }' e3 b+ a  z9 W, E
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)( f0 g1 ^6 Z# w# K
  4. 0 Q% J. |/ y! E. {$ v# n
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。9 u  ~6 {7 g- ^. D+ O2 R
$ k: j6 X2 E2 H/ [' g, h" c
   
& c( N+ g" H6 E9 c, j( n7 |
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集
    ' {8 B5 h& ~. q# a
  2.    
    9 }; N. Z5 E3 J9 i; G! m. \
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd) `. y! g/ l) _4 n! D6 }' S

  4. $ w5 L  G4 h$ E. Y, R
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd
    5 G: d! u7 f7 `% j8 f1 V+ Z
  6. $ r/ ~% D. s9 `' F
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;
    ( N+ B5 K8 y" x( w" L8 [
  2. .....! X) f5 G6 f5 i- \8 c+ N8 R/ o
  3. fd_set set;
    9 Z" r( R. I) V/ k  p8 e
  4. while(1){* T! K7 [  S, g6 x8 Y, Z' r% l
  5. FD_ZERO(&set);                    //将你的套节字集合清空
    4 V( j. O: M+ i/ F* q8 r: G
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s+ L1 @4 X* x* Q% |+ C
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,! A; D0 x  n. G
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,
    : n4 j0 Q0 A( ?% t
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    7 d9 v, l( }8 C! U6 ]2 Q4 J
  10.                                 //只保留符合条件的套节字在这个集合里面  }9 h7 c* x" T/ s, h
  11. recv(s,...);/ M. x8 B( q9 x* N3 c9 ~% e
  12. }
    9 q. Y" k' N! N. Q2 O( p8 U5 W
  13. //do something here
    # t. w" M: d" \% N. k+ t+ {
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。; H0 n1 _, |. r( ?/ V( M
  2. 6 Y0 w9 q- q* p
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)
    . C, R+ M$ Z4 Q& M' h& A

  4. " {) ~# o7 A& g
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011
    8 W( Y2 c9 r: r1 w: J. c* y! ?
  6. : u" a0 G9 y% }# A1 a
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待' b' i6 P) o7 e) y" Z* \

  8. 2 L$ v" d$ |7 D) \4 @2 U+ ^  }
  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.具体过程看代码会好理解
% E2 R' c( Q! O) ^+ `3 z& E
' {$ F  b) w) W5 Y0 H4 H
使用select函数的过程一般是:

/ [- R% e' V# J    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。& t; G: S% a, ~' u' l  _2 J7 O

- o8 m/ l4 S5 U
客户端:
  1. #include <time.h>
    & O+ d/ j$ }4 t1 ?6 a  u
  2. #include <stdio.h>, M3 k0 H: N* H+ h& C
  3. #include <stdlib.h>" w: f, F5 [+ R- a/ A6 r" g. O- Q  U
  4. #include <string.h>1 y  f& T: u* X4 @/ U. Q' r2 }
  5. #include <unistd.h>, g( [9 L& P5 u7 T- t3 s/ T1 x
  6. #include <arpa/inet.h>+ h9 ^* P5 ]: ^, _  E
  7. #include <netinet/in.h>
    ! N9 m! S: b/ A2 K
  8. #include <fcntl.h>, I3 T8 t! R5 q) n
  9. #include <sys/stat.h>2 P( Y% |  u/ K0 t( k
  10. #include <sys/types.h>9 L; e8 E3 L; ~2 r( p
  11. #include <sys/socket.h>  k2 H  u0 g4 n" h$ t
  12. 7 `/ ?. @7 o* {  K; ~  j
  13. #define REMOTE_PORT 6666        //服务器端口+ I6 ]% O% S& e1 @' H5 h+ D) a- M. `
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址
    ' n. t) E: O% t& ^3 k  V, V8 ?: A' U

  15. 2 e# D7 e- e6 ~& F! c
  16. int main(){8 T9 {) |/ t% [3 y" l4 @
  17.   int sockfd;9 o5 f. l" p0 o
  18.   struct sockaddr_in addr;
    ; T( O9 k" I% x. e2 V* T  b
  19.   char msgbuffer[256];; _8 d0 g& m; Z# T, V0 ]
  20.    
    # G# f" F9 N4 O& [" a
  21.   //创建套接字5 g& w0 S- Q0 g( o
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);' M* G  K, ?5 o. S( r  b
  23.   if(sockfd>=0)
    * V4 ~1 S  G3 _( {& b
  24.     printf("open socket: %d\n",sockfd);( H- x$ O3 \$ X) A

  25. 9 ]7 t, L! F: b! [0 Y  {
  26.   //将服务器的地址和端口存储于套接字结构体中4 y+ e+ T1 {* y
  27.   bzero(&addr,sizeof(addr));
    7 ^, g  |- O( J! ~: C( Z
  28.   addr.sin_family=AF_INET;
    , g- M( _# V" Z3 N9 ~7 c
  29.   addr.sin_port=htons(REMOTE_PORT);
    ( c4 o/ X! E# P% n
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);8 g) W7 u. F0 K
  31.   
    , H, `% m% d7 ]+ n' q+ z
  32.   //向服务器发送请求
    , W. L, Y, U$ w; }$ ?
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)3 t- @9 q  l8 G; p' d
  34.     printf("connect successfully\n");8 _$ j5 F/ u* A& H! M6 V
  35.    2 R5 k+ }, ~- p4 `
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
    3 W. l. q! B8 j
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);: X- _# t( M$ \: b* _) V
  38.     printf("%s\n",msgbuffer);
    % Q* a3 t. d( g3 _  @  O
  39.   
    8 {) v/ T& [. r
  40.   while(1){1 l  U& z/ M& [6 _
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息& S+ k- X5 @: }" @" M
  42.     bzero(msgbuffer,sizeof(msgbuffer));/ r2 y) S/ Z2 d4 d1 d; @1 _
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));& D- C  @/ ~3 U& E5 @% @. s
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
    & ?( v. P3 ^6 I2 G
  45.       perror("ERROR");+ s& i* M$ ]# R' ?' h" @3 }8 x8 Y
  46.     5 U7 Q+ [, K" a) Y0 Y* t  R9 n
  47.     bzero(msgbuffer,sizeof(msgbuffer));
    " u& G3 j3 D0 [/ f
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    1 Z/ X! g5 Q4 I4 j+ ^3 X
  49.     printf("[receive]:%s\n",msgbuffer);8 J% p0 g/ b) |; }- F0 ^6 @  s
  50.     - ?* R1 m! K; N/ ]
  51.     usleep(500000);
    3 Z1 S9 }" |- }3 D2 }
  52.   }
    4 ~9 L# h# x% D" p! c- A
  53. }
复制代码
& ^7 d/ b% |* t; J$ a! j) L  n% U

% {9 F  k" R8 G4 |
服务端:
  1. #include <time.h>0 i0 V5 T5 [) w' q6 w
  2. #include <stdio.h>1 T. U" j2 B& u9 d4 a
  3. #include <stdlib.h>
    , z7 e2 C6 ?* i. x6 w2 I2 Z
  4. #include <string.h>0 k8 L, e8 q- |7 X5 ~1 _4 _
  5. #include <unistd.h>9 l9 Z8 h$ ^2 A2 w& `1 x3 |
  6. #include <arpa/inet.h>
    ) M5 S7 t+ g( c( C. U" `
  7. #include <netinet/in.h>
    9 f$ }5 B8 [/ T  [  K0 I$ m! ]* C
  8. #include <sys/types.h>/ P! G* S& v2 j- B" w/ Q3 `7 d/ r8 L
  9. #include <sys/socket.h>0 R1 s  ?! f1 |7 o

  10. ( C& c) s( v+ Z2 M
  11. #define LOCAL_PORT 6666      //本地服务端口
    " u3 O: w8 J9 ^) m+ X9 K
  12. #define MAX 5            //最大连接数量
    # j/ x6 D" ]! f1 e" s
  13. " }* X  G- |7 i4 V
  14. int main(){9 T9 m' I9 U2 v- e$ ~0 o
  15.   int sockfd,connfd,fd,is_connected[MAX];
    3 W0 @; n# E! o1 t/ Y
  16.   struct sockaddr_in addr;
    ! `1 I, j; e) ]( s; J
  17.   int addr_len = sizeof(struct sockaddr_in);
    # R6 i% S/ I& T
  18.   char msgbuffer[256];. H$ p; N: h' E
  19.   char msgsend[] = "Welcome To Demon Server";. o' H7 N& x6 X+ `* m
  20.   fd_set fds;+ y) u, f0 I3 R" }7 y  v5 m* b+ j
  21.    1 a. x, p. N* R: m4 }' H
  22.   //创建套接字8 U; J# H" |2 V* n0 u5 W
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);' u% w; Q1 g9 g
  24.   if(sockfd>=0)3 Z6 Z5 Z: W9 X. Y  x
  25.     printf("open socket: %d\n",sockfd);8 t9 w$ [: E- v- \. W* T  s
  26. 1 ^0 w+ g& Y5 x, [
  27.   //将本地端口和监听地址信息保存到套接字结构体中
      R; \  W1 I. u- l
  28.   bzero(&addr,sizeof(addr));
    ! W' Q2 {) a% O1 j' x% x- Z" L
  29.   addr.sin_family=AF_INET;$ p# I! l8 u# L
  30.   addr.sin_port=htons(LOCAL_PORT);
    4 O9 r, P) W5 {$ M; Z" u- }6 W: T
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0: U* S/ ~2 \$ b: |& }# t- E3 s
  32.    ( P! z4 ]* s7 e0 g' ^1 g( l
  33.   //将套接字于端口号绑定
    ; w) V5 Z6 @' O: t3 n0 X
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    8 S9 e- l( r8 \7 }* S* i
  35.     printf("bind the port: %d\n",LOCAL_PORT);) g; i- Z  S2 ~7 z/ _
  36. 4 z5 i" P; |& w- M9 E5 A
  37.   //开启端口监听3 |9 M: ]( R3 L' ~$ R) w
  38.   if(listen(sockfd,3)>=0)
    * M% [! U/ d/ Q' Y
  39.     printf("begin listenning...\n");
    8 c9 w, T6 ^3 V! B1 X

  40. 7 w" C$ g) f4 l; `* s, t2 }
  41.   //默认所有fd没有被打开
    . b# g4 K* W- u5 ^+ g
  42.   for(fd=0;fd<MAX;fd++)
    * R, q6 x5 s0 W8 \2 K+ z
  43.     is_connected[fd]=0;0 _, @4 r! a$ d* d+ E# i3 k' y
  44. 0 G0 a9 N  D, |$ ?0 t
  45.   while(1){
    . Y) _4 u) U6 M4 n. P
  46.     //将服务端套接字加入集合中
    3 L# W3 A4 V: x. a; q
  47.     FD_ZERO(&fds);
    9 Z" I* \/ j8 P3 @# u: V
  48.     FD_SET(sockfd,&fds);" S. t1 B: W* r: U: F2 c
  49.      / G3 J& u; g+ E' k
  50.     //将活跃的套接字加入集合中) ]# M( A9 H5 S/ m9 d! ^5 ]7 |
  51.     for(fd=0;fd<MAX;fd++)
    4 ?/ Z  c1 ^. Z
  52.       if(is_connected[fd])8 x4 f# c, G% C4 ~2 v
  53.         FD_SET(fd,&fds);
    . x0 h0 d1 D. P: u; ]- m5 v

  54. 2 z+ d- n/ f  v' k6 o+ I
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0$ k/ U& I! E6 f' Y  R  _
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))6 K5 t+ ?( `- t5 r5 ?
  57.       continue;+ ?& r. L) \3 _, s
  58. 0 ^& M- E  C7 _1 O; @' q; T+ ~
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字
    / \# i0 k& h$ n- w
  60.     for(fd=0;fd<MAX;fd++){' z$ o" D, a2 h0 [
  61.       if(FD_ISSET(fd,&fds)){
    6 B& I/ K" T/ K, u5 ?: I
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接
    - G9 ^9 i, O+ q& c
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
    7 }# @/ K- g- D* E1 v( Y
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语" y7 g; t" y$ s  t0 a
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用
    & l/ U" @% g, U) }" t
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));2 ]6 c8 ?' n  x: E/ Z
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
    0 ?0 u1 s* I! I9 P" u- `
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ 0 i7 u/ ~3 R4 t9 n! D
  69.             write(fd,msgbuffer,sizeof(msgbuffer));" f" l6 @6 d6 q- T# R
  70.             printf("[read]: %s\n",msgbuffer);
    4 k4 x1 O  U4 O" U) h% ~4 C" h
  71.           }else{
    1 m, y! [/ L& z/ s
  72.              is_connected[fd]=0;" F  T4 |3 W$ T" p1 O# w& y& h
  73.              close(fd);
    * n: m4 x2 p" F5 |
  74.              printf("close connected\n");" a" j3 D8 F. ^
  75.           }
    & ]6 P! l6 `* \$ m# @
  76.         }
    ) w  O3 _3 `, k* w3 k
  77.       }8 L! r% {7 K  z5 ~' l
  78.     }
    6 u& [2 ~+ M& x; N9 x5 z9 b7 @
  79.   }3 u" m3 x$ [7 f& q" w+ i2 h0 ~; w
  80. }
复制代码

, X0 Z% t: U* t
1 |; ]$ d1 o0 |& u7 m" b7 k3 Q" l
5 v' b/ U3 t- k- L0 M5 G( `! `: C+ j/ z, Y& H- J6 }+ Y
$ e- q% R7 T4 u# n- k

( \1 @4 s. u- p" |7 ^3 b) Y
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2026-1-30 11:34 , Processed in 0.055405 second(s), 22 queries .

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