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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

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

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

[复制链接]
跳转到指定楼层
楼主
发表于 2020-5-9 01:53:20 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
实验环境是linux系统,效果如下:
1.启动服务端程序,监听在6666端口上
2.启动客户端,与服务端建立TCP连接
3.建立完TCP连接,在客户端上向服务端发送消息
4.断开连接
实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点

* J2 n- h0 R5 p+ F: a
什么是SOCKET(插口):
     这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
/ Q+ E" P! |' N3 o$ U) t
     "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
      对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
      具体其他高级的定义不是这里的重点。值得说的是:
      每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
      应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层  ->发送(接收的话与之相反). `$ y9 h: S  V- T0 B0 z
0 M" S4 A9 }2 X, K( v# r  Y# o0 d$ g
: z. m' j% v2 \
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等( C7 q% v: I; o
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
! ^7 a# [3 R3 @9 _! b& \+ u4 F
  M9 c, N5 |, A8 ~
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)

. o- E% F3 q/ l9 s  H2 r- j, T
( u4 E" }6 a& m8 M
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)
* _2 w* M; J( Q- K  O
: y0 f1 r7 z" K! d* X% F- S

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

' _9 [8 R( O2 e, l6 d5 y! d8 W) z
9 U4 `. Q2 P9 l5 G, }
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>
    % ^, X8 ]: \! R% }) }
  2. 2 x$ p8 j/ o" P, ], T, C5 M3 {6 \4 Y
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理

, P2 X1 ?, r: S# f3 A' ]& R    readset  用来检查可读性的一组文件描述字。
  e: y  d" r' H& x7 K
    writeset 用来检查可写性的一组文件描述字。
+ {9 C. G  @+ G' N. _. \( Z& h
    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)

5 z5 K3 |8 c; w- W& d  i    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。$ q0 o4 d, `$ a" n' w
) l2 X) N$ V4 n! O6 q+ c
    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
; T+ C/ ~2 Z/ z1 k

3 J) C& P$ o  x& d7 j
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
    , w9 t  F3 z  j4 @# T! ]

  2. 9 z  b- z0 i9 o! K1 {3 ]
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)5 t7 `/ `8 S: s+ S

  4. 8 s9 p$ J/ l. N/ _9 A$ ?9 p* T* f% q
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。
/ p8 |! K9 q  p. r' W0 ~) V! a& Q6 D( B: b9 V
   # C! O: \1 S" H! ]
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集' W& J- B2 ?& j; L2 t1 d
  2.    
    * x0 o% X7 l' a, d  i5 `: p
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd
    % A2 E! e% W& a5 ^

  4.   F( m$ M. H/ F) V8 V: i# s
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd, R. Y4 C& p3 B6 W
  6. 6 K5 V/ Z6 x9 Y/ a) d  s! x1 h0 o
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;- n' i' h, F6 \" x2 s, q9 T1 _
  2. .....2 {) d+ h! ?1 G: v
  3. fd_set set;
    0 g1 u$ t1 _" C9 y# H9 O
  4. while(1){& x4 t- Z2 r# ?, C; [
  5. FD_ZERO(&set);                    //将你的套节字集合清空- c# E9 E8 [/ }; ]# J- Z! D! y
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s/ _- D1 D+ O# |, ~6 k' e2 A
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,- X1 Z; u& x/ W  g' Y$ e
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,
    1 v% G  i: N6 ?2 y( o7 ?
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    2 o& f! f% M2 M. m/ r) X
  10.                                 //只保留符合条件的套节字在这个集合里面
    " k; `6 j% Q& j9 `
  11. recv(s,...);. Y7 l) [$ u, h& ]
  12. }# A: n4 q7 P$ y; y1 W. j7 `
  13. //do something here7 _8 J9 y5 L& E8 h8 K
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。' K6 b/ O6 u  y" `& `
  2. $ ~3 P" }, e  }+ E) R7 h# _: {" d
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)
    ' j4 e2 K# F* i6 D. k3 m: R

  4. # o, M/ h& J0 x! V+ e' X! e! ?
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011
    6 F5 ?- ^: \* o/ @6 ^7 j5 w

  6. 6 f: ?! @' H5 p
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待" D8 {& c. n5 e. h2 z8 L, {
  8. # }8 _' a0 K- B
  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.具体过程看代码会好理解

7 G- W! y7 E5 r
9 Z$ J4 x4 h  ~$ ?# J* z0 a
使用select函数的过程一般是:
* R5 \! v) V  o
    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
- z" ~9 M" v; |

9 E7 t; ?; D: j1 o  K
客户端:
  1. #include <time.h>
    % B, ~( X4 _1 f8 \6 ]+ X: F# A% n
  2. #include <stdio.h>* C8 j& s# w) [
  3. #include <stdlib.h>
    5 E' L  m; k( c2 N
  4. #include <string.h>% F9 T6 R# b2 q8 F) f$ c
  5. #include <unistd.h>& \) e6 F7 Q. j7 n, Y$ p6 E
  6. #include <arpa/inet.h>
    * G  h7 Y! c* D; ]7 M! p
  7. #include <netinet/in.h>' p6 C" d3 S& K& R& \1 k4 z
  8. #include <fcntl.h>
    $ g  D2 i% _4 n# H( f* `
  9. #include <sys/stat.h>
    7 k6 K( s8 D, z3 B6 d# C% `3 F
  10. #include <sys/types.h>0 ?4 U( M. H- X
  11. #include <sys/socket.h>
    : H/ P& |0 f; e: b
  12. % g- X) @7 I8 o/ P0 Q8 r
  13. #define REMOTE_PORT 6666        //服务器端口" L+ u$ G" t! M( U; x
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址. w  [8 n! U4 r1 E. p

  15. - d) p9 E6 @) J% \
  16. int main(){
    5 t$ c+ y0 b3 S0 O
  17.   int sockfd;
    3 s& v$ P9 Y, s" \
  18.   struct sockaddr_in addr;
    4 F! A! P0 N$ P* m
  19.   char msgbuffer[256];. @5 W( o/ X1 ], h. z
  20.    2 _1 g0 p2 y, L3 G7 N" U) t7 K
  21.   //创建套接字9 ~) R8 V, h: q" ~' W: l
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);3 L3 T" o; U- K2 z+ S- x1 I+ ^
  23.   if(sockfd>=0)
    2 Z. F9 r% |* x
  24.     printf("open socket: %d\n",sockfd);
    ) X: L9 z. x/ m+ T
  25. " k" G9 T$ e# m4 S5 Y
  26.   //将服务器的地址和端口存储于套接字结构体中
    , k: S! [" C% X4 m2 P1 I9 f
  27.   bzero(&addr,sizeof(addr));  G1 C3 S! q' B$ P% H  B0 P
  28.   addr.sin_family=AF_INET;
      f1 _; K" I/ s8 x
  29.   addr.sin_port=htons(REMOTE_PORT);
    0 r; ]9 z- a7 }1 Y  @' X1 ~) C  d
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);" I; I! x) V0 A5 R
  31.   
    ( X0 s) v$ V0 g3 I% L8 o8 [
  32.   //向服务器发送请求! E$ o0 S- a8 S; r% k
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)+ f% q& M0 `2 I% f
  34.     printf("connect successfully\n");
    . U! e  v1 @8 o8 q8 D0 Z! m8 y
  35.    * p% A! o% `' R3 x: v
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
    3 d# T0 W  E( m# a/ v$ ~! w
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    4 Q, q3 p& r) v
  38.     printf("%s\n",msgbuffer);
    5 [7 p* I( Y6 E
  39.   
      P% `7 t! M( e
  40.   while(1){- \/ C0 H1 D+ D8 W$ C
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
    + ]" h" U  Q: K
  42.     bzero(msgbuffer,sizeof(msgbuffer));) y( M. f- ^: b. t, C; K
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
      J$ n1 t7 C1 c3 G
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
    6 P7 i9 M' F5 s( i  f
  45.       perror("ERROR");
    & K4 `) T/ i4 w, \  Y6 T% ~
  46.    
    2 O3 m- E. R  z9 B! q7 W6 H- C
  47.     bzero(msgbuffer,sizeof(msgbuffer));
    7 h9 u+ B: ~/ L; F( R
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    ) J# ^; z7 [% q  _
  49.     printf("[receive]:%s\n",msgbuffer);7 ]$ x# d: e! \# v2 R
  50.     , c) b8 ~) _5 ~. U1 W0 D
  51.     usleep(500000);
    " n5 @% n: O4 _: `' u7 a4 h
  52.   }) `7 m  B5 R7 w6 Z. Z$ U8 _
  53. }
复制代码

, S% k; A6 U$ q& M7 A* m# w& Q, D8 F4 T0 i9 E7 \0 U
服务端:
  1. #include <time.h>' a8 y: n: T3 b( @0 W" M
  2. #include <stdio.h>
    ! H6 q5 P: Y8 t! `/ o- B5 P
  3. #include <stdlib.h>
    : d! k8 H+ Q6 i2 x" N; K1 u: L3 |
  4. #include <string.h>1 W, {9 F8 w: Y3 {; }+ c
  5. #include <unistd.h>3 ]$ Z: o# P9 k& ?. b- @$ r0 C
  6. #include <arpa/inet.h>4 r" z+ c/ r: I% q9 ^" G1 i
  7. #include <netinet/in.h>
    7 n" Z. i4 p; O) Z0 R$ t+ G
  8. #include <sys/types.h>
    " G9 S& \% J8 C% v; ]0 Q
  9. #include <sys/socket.h>
    ) p* S  F# [. A9 A2 p
  10. / m- p# S7 Y2 H$ Q7 o) R
  11. #define LOCAL_PORT 6666      //本地服务端口
    - n! n( r& g; d/ L: R$ l) @( j
  12. #define MAX 5            //最大连接数量
    9 A# y9 v% E5 z, o9 c

  13. 5 k  u; d/ }; y/ X
  14. int main(){+ k# C( [$ V& [
  15.   int sockfd,connfd,fd,is_connected[MAX];" S2 C1 t. Y" p1 ~% B- \: Z
  16.   struct sockaddr_in addr;
    / M- P8 }) D( Q  U
  17.   int addr_len = sizeof(struct sockaddr_in);9 [) L6 p6 ^) u* c3 B& Z
  18.   char msgbuffer[256];
    % D8 G4 Z: s$ h" Y+ F* }
  19.   char msgsend[] = "Welcome To Demon Server";; y9 C2 L8 e' P, y
  20.   fd_set fds;
    2 R" o) o8 _9 U* H3 c# v
  21.    
    ( L/ _! _+ s4 {  [; @* D
  22.   //创建套接字
    1 r( _8 J+ A2 p4 A5 p) f; z
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);2 K' c) g) i4 Z8 Q, ?) H0 N
  24.   if(sockfd>=0)
    % [2 T9 d$ ~0 X8 e/ n: |, ]# T5 ?
  25.     printf("open socket: %d\n",sockfd);
    & g( D& w* N  O- [, V* z. z
  26. - ]" G9 ~5 F6 u4 q8 I' o; `; U! i6 W$ b
  27.   //将本地端口和监听地址信息保存到套接字结构体中
    2 {8 @+ a  l9 Y4 l8 e
  28.   bzero(&addr,sizeof(addr));) _2 B. |1 S- O
  29.   addr.sin_family=AF_INET;) w5 Z, C8 T) C  P9 d& V) T
  30.   addr.sin_port=htons(LOCAL_PORT);
    ( Z% p; \8 ], k# Y) M* d+ f* `
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.01 D( T- r: K9 ?
  32.    
    + J) r9 P9 s5 u+ B. {, C. r
  33.   //将套接字于端口号绑定
    ; r. U- J/ Q7 U) }4 j$ T
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    ) S6 ?- S; N9 g& O! u1 U
  35.     printf("bind the port: %d\n",LOCAL_PORT);
    7 Y6 h! r- Y) P" U: O. z

  36. " Y- I) B+ i# q( t
  37.   //开启端口监听; L/ i3 A+ C0 o3 n
  38.   if(listen(sockfd,3)>=0)' h1 m9 Y( T8 `
  39.     printf("begin listenning...\n");
    7 V  i! V! E" d1 G4 F& O* q

  40. 7 w& z0 {; ^  ?, b
  41.   //默认所有fd没有被打开( q2 l4 e* X- D3 b5 |
  42.   for(fd=0;fd<MAX;fd++)
    . e0 e8 O; }8 B& Y+ N
  43.     is_connected[fd]=0;0 [! \& C% U/ l6 t! f
  44. ' m" P3 v0 z2 q3 J5 C; o
  45.   while(1){
    4 Y' [+ K/ x9 m- J- N$ F
  46.     //将服务端套接字加入集合中( _1 a# p' H  a# _- J& ~
  47.     FD_ZERO(&fds);
    3 Y  |& T; p. u2 M
  48.     FD_SET(sockfd,&fds);) V  ]4 Y+ y- \( h6 D$ L. L8 B
  49.      3 P# m" T3 K( W: x$ l6 }+ @
  50.     //将活跃的套接字加入集合中
    ! T' E& {/ D8 o& j7 J; E
  51.     for(fd=0;fd<MAX;fd++)0 \+ M; k" O! a- c+ [
  52.       if(is_connected[fd])6 }. }4 Z4 l3 k4 J
  53.         FD_SET(fd,&fds);
    & \/ n# O7 q! i4 I. `
  54. & Z7 n; O. P' K+ C; V6 f% W3 u( Y
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0* J- E% x1 g$ N, Z
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))/ N8 ^- E1 }( Z8 t. x' l9 x
  57.       continue;
    2 d9 @5 ~4 x: f$ L2 U: ]
  58. 9 d/ e  p9 M4 }8 O
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字
    7 Z1 f+ X$ @  g
  60.     for(fd=0;fd<MAX;fd++){' y9 L# w3 |$ d, s4 ^* y
  61.       if(FD_ISSET(fd,&fds)){
    * q) \5 _+ ^+ f0 R9 l
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接
    + M; {% \+ H* A1 s
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);8 [- b" |. [" R% D3 z
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语5 f+ l3 \/ H# R4 M. G! ^$ G
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用
    3 K# G: o  M3 L* G: j. t
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));* l, c; `$ f+ E% Q* T  _  v
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字" ], a6 Y" `0 o% ~. r/ Y
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ : H# `1 `+ N% |" H
  69.             write(fd,msgbuffer,sizeof(msgbuffer));
    ) m- _* Q; t+ d# d% [" p
  70.             printf("[read]: %s\n",msgbuffer);, a% s' D+ X0 D; l0 y. _: Q& m
  71.           }else{
    - X4 q4 g. V4 Z. q% B
  72.              is_connected[fd]=0;
    5 T9 d$ x; q4 t: T- r6 W8 u
  73.              close(fd);% ~% L' ?' Y7 z! R+ m( W* L+ a
  74.              printf("close connected\n");( y8 C+ p  T: i/ D' Q; }- S# N( Y
  75.           }2 A3 t& z( x1 Y" J4 e+ E
  76.         }. N6 p0 c1 J0 h, V) ?& z
  77.       }( B  M  ]) f9 c
  78.     }7 c4 L* d* I* ~8 W
  79.   }
    9 N( [$ q7 R! G3 d  l" ?
  80. }
复制代码
! z! U1 a4 C: f; a7 y& T

  x6 t7 M$ ?! J7 Y* v5 y: F
! V% T9 i- E9 |5 u0 Y' _! X: u9 ?" ]; O+ j$ T, `0 Z, @# S9 G, {
, O& a- ?# P+ e0 s$ v

  {% T' l, I! p3 V# m" D
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2024-5-19 03:36 , Processed in 0.123773 second(s), 24 queries .

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