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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

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

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

[复制链接]
跳转到指定楼层
楼主
发表于 2020-5-9 01:53:20 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
实验环境是linux系统,效果如下:
1.启动服务端程序,监听在6666端口上
2.启动客户端,与服务端建立TCP连接
3.建立完TCP连接,在客户端上向服务端发送消息
4.断开连接
实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
$ X2 @& ~2 K! y, ?: R+ n3 u
什么是SOCKET(插口):
     这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。* a9 K2 j) O0 i
     "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
      对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
      具体其他高级的定义不是这里的重点。值得说的是:
      每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
      应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层  ->发送(接收的话与之相反)# }% i( f/ f- Q
$ Z* ]! u% x/ d" `/ i
- D% Q3 Y( a* g% _* o  H
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等. D: l# [  t5 q7 [4 l1 H" C
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
, p. h0 O! M' K9 ~+ i" D1 U

  _8 e4 U7 d( J8 \
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)

" I  n$ S7 |" J: F1 s9 p, y/ T' k8 x( J' G+ C
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)
6 K% h4 a5 @+ H7 @2 Q0 ~
& n9 X" L: ^. r9 T
7 t) m/ ~2 L- [7 F+ x8 y% x* ]
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。

9 w" f9 k! S8 R4 r& z9 `9 s6 R( p, B1 r- ?: U! e: B2 w
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>
    & t. b, X0 D' X6 ^6 u

  2. 8 Z( R, M! b& ~" x6 ~$ C
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
! P2 C( F6 o0 w  \" g5 p4 N% r
    readset  用来检查可读性的一组文件描述字。

( z% @% M* b4 v) d4 `    writeset 用来检查可写性的一组文件描述字。
& S9 H" w9 C5 _8 l% O7 M6 Z
    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)

; J& D- E+ N- X* q    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。( o" m, x0 p. a2 N& M) }5 r

4 C, X4 y# i* K. p* M    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:7 `0 V' ~. v) K/ Q6 N4 k$ q
- G; T6 p/ h) g; N5 j5 ?7 k4 F
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)" F4 c& w! W! ]! s6 `" c& i

  2. 4 N+ S/ \! f# z
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)+ J+ L, E- H2 g1 E; }

  4. ! O7 X+ B: ?, C
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。
9 P9 k8 O& ]. [7 U3 t
* p# |: ~. `, h1 d   
6 o3 `6 a. }" n* p
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集
    ; d5 m2 M5 M$ X. r/ x* b
  2.     , v# u) ^/ Q7 L7 R
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd( |" z& E, J6 S" s  X
  4. 9 w8 R* a: ?( v1 x
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd$ c& }$ v5 B8 A5 J% Z% o
  6. 6 \5 N7 H! t- R' g
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;
    3 m( k' S4 s1 F
  2. .....8 W9 S, h8 H, D# w$ w1 U* J4 @
  3. fd_set set;
    9 x9 V( X8 f, M! j; L- E
  4. while(1){
    # z8 h% w. |; F8 v* n& l
  5. FD_ZERO(&set);                    //将你的套节字集合清空
    1 p7 S! }8 `. `  b8 e) P3 I
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s  D& g: [) s1 r$ f
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,' j* J( H" i" ]! i; D: k+ T8 H
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,
    : H# H; K2 ]( H( \6 n4 k
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉4 {( t" M/ t/ a6 z! [" U3 A0 [( y- a
  10.                                 //只保留符合条件的套节字在这个集合里面/ K0 K  H& k2 e% E" `9 o& d+ Q
  11. recv(s,...);9 _0 j# B; [% ?6 @- G* c
  12. }
    - S) a4 d+ _1 }, C% z+ f  Z2 \0 q
  13. //do something here4 Y. d" S: m* N
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。
    3 `& j+ v; }  U) D/ S) C
  2. ; m, ?% {9 I1 Y8 ~- x
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)
    " p8 V+ @, H9 m/ N4 F% I- i
  4. , b; `9 p# G8 w; C3 {
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011
    ! n  w+ d, f9 D" z: w% Z$ {
  6. 4 b( w* E8 I/ x+ k: x. P5 |) Q
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待
    " L# I) L& q& t
  8. 8 _8 m. F3 s, P" S6 o; }
  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.具体过程看代码会好理解
3 V' ~/ m& b! P9 l, V5 ^# \
( u5 H, D7 B, i# H$ B+ k- _
使用select函数的过程一般是:
- ^1 `* Z6 f& Y9 @
    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
* X1 t5 t9 C1 m6 [
0 o  v! N8 m' c6 n$ |2 f& I9 g3 ?
客户端:
  1. #include <time.h>
    7 q  x# t# d7 s! P
  2. #include <stdio.h>5 a% c9 w' d8 @! f$ Y3 D
  3. #include <stdlib.h>
    7 z4 Z% ^1 w0 P0 G: n% @0 l: x6 `
  4. #include <string.h>8 J8 r3 c( [) C7 Y  n
  5. #include <unistd.h>
    ' Z" J$ R; N6 `3 O* u2 m* W" I
  6. #include <arpa/inet.h>
    1 j& A& u4 V& K
  7. #include <netinet/in.h>
    & s( ]) n" T% D. Z* ~6 n8 U# r  W" M
  8. #include <fcntl.h>
    * K( a, O7 k  P! L- B
  9. #include <sys/stat.h>9 K; S4 G( m. f9 o: ?& H2 k2 ?) N
  10. #include <sys/types.h>
    2 R6 O, n0 Y+ T+ a9 W
  11. #include <sys/socket.h>
    . \6 c' Z, H' u- V5 O

  12. " d8 k8 o% `/ f3 N# F- y% m- V
  13. #define REMOTE_PORT 6666        //服务器端口- ^! M/ h* S2 b0 K0 A
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址
    7 B! ^. Z/ l1 x
  15. / U! V: K7 _3 h: x7 @6 z/ X
  16. int main(){
      e  W- f1 h& a6 V* i% H3 K9 A8 z
  17.   int sockfd;! K4 Z2 A" h* l6 k( C" J
  18.   struct sockaddr_in addr;( @5 r7 y. z1 ^% P
  19.   char msgbuffer[256];( T6 y+ i  V% K/ C: p' J! A' s1 }
  20.    0 @! R! Y# [9 l/ ^. S
  21.   //创建套接字& o# L9 L" Q3 V1 I, I
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);; I7 a' j7 Q* q- V
  23.   if(sockfd>=0)& v  ?7 ?% A/ T9 v( v% J
  24.     printf("open socket: %d\n",sockfd);; G  f* s' P6 P; V6 a
  25. # W% H! j. F$ d% H) g
  26.   //将服务器的地址和端口存储于套接字结构体中* S, t3 N, Y% `! r  c% [
  27.   bzero(&addr,sizeof(addr));2 m7 o5 d1 A8 z- [4 b7 D
  28.   addr.sin_family=AF_INET;
    8 g& M& [$ \% k
  29.   addr.sin_port=htons(REMOTE_PORT);3 i0 L1 y% V& f5 G5 T7 [. a$ [
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);; w% q( n7 q* C' |$ d3 b. C
  31.   $ m' E9 ~# K6 l
  32.   //向服务器发送请求3 L# H, E! i/ v
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    1 ]% b2 c6 n$ Y, b$ G; [% s) C. d/ D9 k
  34.     printf("connect successfully\n");1 o3 Y% H# G7 W" C0 l
  35.    
    ; g" A: ?9 `, G# N; t3 N1 L
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)9 s8 M2 g8 B. o9 H' H0 }
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    6 S  r. l  Y. s: _% d" O" ]
  38.     printf("%s\n",msgbuffer);' p$ v$ T- d) r2 g- o# G. {
  39.   
    , R+ }) w- C/ ~9 D$ x: F1 B2 H
  40.   while(1){
    8 J( m* l$ o6 z
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息. j& G' J1 r  k/ a. O
  42.     bzero(msgbuffer,sizeof(msgbuffer));5 Q2 z& f- X' U; @* U& {" C/ z
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
    ! M0 Q" |  P7 b% v% k
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)/ a, h  j7 }( S$ K# c% R, Q+ p$ O) T4 N/ x
  45.       perror("ERROR");
    $ S" q  Y4 ]: h. c" t$ z5 T# N) C
  46.    
    / _' x7 t& e# r3 C8 A& N
  47.     bzero(msgbuffer,sizeof(msgbuffer));
    : }) `! ?4 s8 }8 @' F! E
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    . \( d# B9 d  p( v
  49.     printf("[receive]:%s\n",msgbuffer);, I) G- R& c; e  u- Z
  50.    
    7 e# |) q5 e) ]5 U2 y, T: O# t- A
  51.     usleep(500000);/ b" E# W3 C, \% ]
  52.   }% s# \4 t. `9 T  K& D9 F7 y6 s# @
  53. }
复制代码
9 ~3 `3 o3 y% [# F
! t# i) s: g/ L2 l
服务端:
  1. #include <time.h>
    # B+ g) a6 l) q! z6 `# j
  2. #include <stdio.h>0 u# m/ o3 ?$ f1 k) E- `
  3. #include <stdlib.h>) [2 c, W9 J. z$ U4 Q
  4. #include <string.h>
    2 S3 {$ x5 e" c" O
  5. #include <unistd.h>
    8 w' d9 R* [/ s1 Z: b5 m
  6. #include <arpa/inet.h>
    . Y* Y4 i3 z7 w
  7. #include <netinet/in.h>  e% a' G' @" }$ e% }
  8. #include <sys/types.h>
    $ c6 h; }) ?! m/ _1 k
  9. #include <sys/socket.h>
    & s2 }1 u  a6 d6 J  j) B

  10. . A# x5 Q1 |7 T- I( F
  11. #define LOCAL_PORT 6666      //本地服务端口: F' U# Z5 S5 }! {4 v* H
  12. #define MAX 5            //最大连接数量& ]; z7 i; z9 H2 F6 H

  13. 1 {! L: V4 o% d& c/ v. S$ @: j& b
  14. int main(){/ O  B4 k6 |$ b6 z2 J' X
  15.   int sockfd,connfd,fd,is_connected[MAX];
      {; w0 _6 p6 M
  16.   struct sockaddr_in addr;
    6 W, S; T+ S8 ^5 d6 H% o, b9 A( n4 X
  17.   int addr_len = sizeof(struct sockaddr_in);$ l9 {9 V5 o( B5 C
  18.   char msgbuffer[256];  A$ v" k; c# i# G4 T
  19.   char msgsend[] = "Welcome To Demon Server";+ U. l+ x( M* D( ^6 H
  20.   fd_set fds;
    2 o$ y2 p) |) n- N. B/ L4 e3 P3 _
  21.    
    + O" ^: E! E* l+ O& P
  22.   //创建套接字$ I! Y+ _; M9 R
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);9 [( d# e8 v! `. ^
  24.   if(sockfd>=0)1 x& l( t1 N, _/ F
  25.     printf("open socket: %d\n",sockfd);
    ; i8 V) B1 g. t# D, Z- S7 ?
  26. ! I9 V' h! _) w9 `6 \' f. N( u
  27.   //将本地端口和监听地址信息保存到套接字结构体中; `- O2 l& J0 a8 F3 Y
  28.   bzero(&addr,sizeof(addr));
    1 t$ y  d6 W/ x  }  L
  29.   addr.sin_family=AF_INET;
    1 v; ]/ p7 n* y" q/ @) }
  30.   addr.sin_port=htons(LOCAL_PORT);
    ; g4 k4 }: ^, P1 R
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
    3 ]( x# Y) P, |! J
  32.    8 L, J# P9 p1 Z# O' B# w& u/ |7 M( p
  33.   //将套接字于端口号绑定
    9 E, ^& d5 `( v. }- h$ _5 i
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    , P. ?1 a/ |9 I
  35.     printf("bind the port: %d\n",LOCAL_PORT);
    8 d: O& i8 @* n1 H

  36. , }0 g0 C6 J2 |6 ~2 f7 y/ @
  37.   //开启端口监听
    ) K. B  Z, i0 j
  38.   if(listen(sockfd,3)>=0)( \: I! R6 W. r2 B* H5 Z
  39.     printf("begin listenning...\n");
    / H3 ]2 p0 ]/ ]6 k& ~* E
  40. ! u% K+ e7 s, r# \' x( |
  41.   //默认所有fd没有被打开/ c0 }9 I0 @: ~9 y, h" ]6 \
  42.   for(fd=0;fd<MAX;fd++)
    . n4 a* a& h. `! v
  43.     is_connected[fd]=0;4 A: m  k, Y+ t  y4 M+ b/ Y" L( _2 a
  44. : z' q8 T: \5 `$ T
  45.   while(1){# u5 }; ^) U* J; e4 {  {  e& z' @
  46.     //将服务端套接字加入集合中/ ], T! Q1 G# S4 T2 O) I4 e" I2 F3 g
  47.     FD_ZERO(&fds);
    # z  t" n: G: V" l# O" M. J' m
  48.     FD_SET(sockfd,&fds);
    % U: L; k) j& t% j
  49.      
    9 y9 }) y9 X" d% n) W$ E- \" H5 }
  50.     //将活跃的套接字加入集合中
    ! |: X4 x) U8 C
  51.     for(fd=0;fd<MAX;fd++)" Y6 M$ k& C& j* N3 h# h+ {) ?
  52.       if(is_connected[fd])  v" \( ]: I9 p8 i, f
  53.         FD_SET(fd,&fds);. {! Y1 P4 b5 {# u0 [/ N
  54. 6 F( E% q" a9 l, G5 m/ U
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0; d4 S. p( W) `# A5 n0 e
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))% F3 m) |7 k0 Y9 U" y" O
  57.       continue;  b+ l0 X6 J1 i& e7 K- ]  \
  58. * Z. N0 C) U: m$ a9 N
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字$ _2 P4 b* B0 e6 e& n. \
  60.     for(fd=0;fd<MAX;fd++){- a0 g8 X! |  @+ K
  61.       if(FD_ISSET(fd,&fds)){" N% W2 `! v6 V+ B' ?4 |5 i; Y
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接
    0 m6 r2 M1 Q& {/ ^; _
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
    & ~5 g% v& v# `
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语# o- i- {2 S$ {: j1 G; U1 d
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用
    0 ?$ j% T- s$ U- w
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));  C$ J4 V$ @; B$ q
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字' |$ d3 E7 x  d
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
    . w0 `# |% \" r3 O) y  ^+ M
  69.             write(fd,msgbuffer,sizeof(msgbuffer));
    4 O0 P4 K! n) K$ Z
  70.             printf("[read]: %s\n",msgbuffer);' R1 Q9 V, g2 I4 R
  71.           }else{' m2 }% t, u+ Z- D" B+ n/ ]
  72.              is_connected[fd]=0;
    2 g& R" V3 z' E* e9 w! p
  73.              close(fd);. c+ j/ R7 x- `8 U) s+ ?3 L  z
  74.              printf("close connected\n");2 U  ~) w/ B# D4 B6 o' a7 c
  75.           }
    5 E! v+ U& K0 j
  76.         }
    # ^, O& P" T) o. o. P- ^
  77.       }
    9 l" c: E- ^9 `. d8 Y3 I4 F! |
  78.     }
    / x- S: z  L* Z% `
  79.   }! }5 j/ v2 H' q- c; _1 M: S9 g
  80. }
复制代码
3 l* L4 A! o, k9 |  }, ~3 ^3 z
: P6 [* l( J/ G: T
6 P7 E2 k" W" Q

3 Z  L* M# x6 u6 Z) |# p
1 i( S3 N& N2 g& Z5 c5 A4 ]( o2 ^+ K- w' G. @& N0 J. e
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2026-3-16 17:21 , Processed in 0.061019 second(s), 22 queries .

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