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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

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

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

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

. D: s/ o! D: \5 N" b
5 J0 I8 O- O5 y" x1 T9 E
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等" A" X. C# z% g. K/ a8 D+ b
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
& o% Y. I9 b7 t& Y
( e# ]# e& c; j
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)
1 q/ [& O7 L$ n# I
* T0 P" d; V% c: p7 b
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)
& H8 R/ U9 }) o' Y" @8 F, Q
! P+ W6 d6 \& M, M9 Q2 z) h( ~
1 F$ K: @, Z+ q7 ?% D$ X
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。

, P: `& P) D: d3 E
2 I% E) N2 ~9 P2 q$ E
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>
    4 w  q# L$ ^! h' l# q4 ?+ e& b0 L( N
  2. + R/ Z- z2 C* ~2 h/ u6 g2 H
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
- O/ |2 V. F1 A. E
    readset  用来检查可读性的一组文件描述字。

; b; O# l) {- P$ u9 f8 D    writeset 用来检查可写性的一组文件描述字。

% I; |' i* N4 ?) k    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)

7 N0 x& l$ ?6 e! M+ k% a) O2 R$ ]    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。2 y. T8 k6 g+ z# _. C

# Z% ]+ U) i- g7 ~/ H. J; h! D: t1 K8 n7 W    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:, i( m% h9 N$ `2 K
+ @9 s) P: i8 y) K* k5 w
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)  B9 G( m$ i9 N% x# E7 J
  2. & U: X* Y& {. k  n
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)5 {3 g& P( F% N/ t
  4. + T$ |5 a  S7 G/ Q3 C
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。
- g9 P  P' S+ L, l4 _/ O* v' L# r' z/ F1 o$ s
   
8 O0 e# x/ P5 U7 j1 B
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集
    9 j9 l* ^9 R1 r+ n! z$ a
  2.    
    6 G" M* ?0 u; }* x9 G! m
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd
    9 n0 ^: n/ |- f0 G5 T+ `; A& S
  4. 6 G2 d, [  _' Q- ]/ n
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd
    % U( f  e( p$ _( {
  6. " u' i8 n7 T3 D- }9 R
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;* t6 p9 M- A7 h4 P5 A9 f* o4 C! N
  2. .....+ F8 c) d+ O7 @9 s- y
  3. fd_set set;
    4 b4 ]% p, l( L
  4. while(1){
    $ q( Y+ y, Q! |$ Y# @* Q
  5. FD_ZERO(&set);                    //将你的套节字集合清空5 F. x; F9 V* O" ~1 e5 Z$ _# b. \3 K
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s
    , D2 ]1 E. g! ~/ J3 G
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,# e/ Y9 l3 q* e+ G" G: [
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,$ ~: Z; {' P' ]0 F! ?" R
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉7 `  S7 k3 t7 P5 D& |
  10.                                 //只保留符合条件的套节字在这个集合里面
    % \; T3 P! I" d8 ]* N" |1 |4 R
  11. recv(s,...);
    ) |$ ]: r, k: n7 S1 I
  12. }( N# v8 G3 @2 ?8 M
  13. //do something here
    ; l5 d  W9 l; _: A0 U+ T- J. p
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。8 Y( {, `2 n- y) A

  2. . u& u3 P5 y+ ~) K& n( C' Y- h
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)
    ' z- D6 |8 r# L/ `
  4. 6 S& B  A2 a$ F3 E$ A
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011
    : s" a5 E5 W4 a

  6. 1 ^; O, E, z9 d- j. T: j5 J' o) R% a
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待2 A+ e2 i# d. n/ [* N8 [* X0 a

  8. % ^) K1 q! S  I( ]/ L: i" h
  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 x" J9 B, N* n- X
! w( g0 }' q7 {+ G
使用select函数的过程一般是:

/ W6 Y7 C2 b5 k, w! I3 U    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
0 m1 }. a. ~" G
- v  e3 n5 o. c7 P( r
客户端:
  1. #include <time.h>
    # A4 r# k9 U: S/ z" E
  2. #include <stdio.h>1 x( c7 X6 Q# _. X8 w' y8 H
  3. #include <stdlib.h>$ d( D9 f1 \- ?: u* d
  4. #include <string.h>
    1 I( b! h( A3 ~) W, q' B( W
  5. #include <unistd.h>
    . h# e1 E8 h" B; t- p
  6. #include <arpa/inet.h>
    $ ^4 f3 }) y- i% Y
  7. #include <netinet/in.h>0 t: q1 s9 J- Q+ F6 e# U
  8. #include <fcntl.h>. }3 e+ H6 N+ H/ _7 P
  9. #include <sys/stat.h>
    7 ?: g8 X1 b4 }
  10. #include <sys/types.h>& J* l" g, n5 a, l
  11. #include <sys/socket.h>, K& U) d' b1 v! o: g: v3 p
  12. " n9 A9 {- {" I, C! M. s
  13. #define REMOTE_PORT 6666        //服务器端口
    " ]% ]0 b4 y, |6 I* t
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址, m  d6 T- d# |3 a
  15. 4 ^+ L0 ]% `6 W
  16. int main(){2 L. t6 q. k9 K3 {
  17.   int sockfd;
    & i! V- m) p/ l  {. B6 q  b0 b
  18.   struct sockaddr_in addr;% K( n7 X. C* t% c
  19.   char msgbuffer[256];* D# E0 }/ H( e  ?, c- a: S3 f( r0 G- W
  20.    4 g- B, E+ ~" c, i' d! b
  21.   //创建套接字( ~; X& X! ^8 I
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    , g# D5 C; g( O- }' s2 `
  23.   if(sockfd>=0). {: g8 m8 h2 m5 g) A2 P+ e: }
  24.     printf("open socket: %d\n",sockfd);
    : \% ]% M9 r1 D! T7 Q" I

  25. - l% I# }# R3 M4 u; F" |1 e
  26.   //将服务器的地址和端口存储于套接字结构体中+ T% s& q" q, X4 \, k- @
  27.   bzero(&addr,sizeof(addr));
    , U5 n4 a' H9 g* e" \2 d
  28.   addr.sin_family=AF_INET;2 l  B2 A* ?. ~; o/ o
  29.   addr.sin_port=htons(REMOTE_PORT);0 d$ V) X; d% E" m1 F
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    % Q. \# H  Q- E/ Z) x% ?
  31.   
    7 Q; x, N+ N: ^* x% q
  32.   //向服务器发送请求
    1 W! Z: X3 {1 G# \
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    . G# a, J% t& i
  34.     printf("connect successfully\n");
      E5 \* ?( I2 `  z3 u  K: ]
  35.    6 d( i- M$ k; h+ r+ t
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
    5 ^, s% [. g- Q! E2 s
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);' [9 V5 j/ H. c4 n3 }& P
  38.     printf("%s\n",msgbuffer);7 M) Q  z( f- [: r. ~/ W" u
  39.   - w* s) C9 S1 J1 K& G* D
  40.   while(1){
    8 k# ?1 ^# @6 Q1 R6 S/ P
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
    - s3 o+ B" F, _% o, B- a
  42.     bzero(msgbuffer,sizeof(msgbuffer));
    , m  W2 k# Y! h7 F
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));" N1 b9 V: W. |1 U  S
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)9 C. U- v" Z/ R" X
  45.       perror("ERROR");: {$ H) M1 f0 X4 o" @
  46.     9 A8 @' ?% t( A* U* ^) n
  47.     bzero(msgbuffer,sizeof(msgbuffer));, h* W2 K! z1 I9 v" V* Y/ d  a
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    0 R4 R( O% O+ z
  49.     printf("[receive]:%s\n",msgbuffer);
    0 {, v6 |' i  o9 Q) n
  50.    
    9 K9 b7 ^$ y: I. Z1 G
  51.     usleep(500000);
    6 b' u) m+ n: D8 ]* e$ ^- N7 N$ ~4 x* o
  52.   }
    : D4 m0 a8 F" w6 q- X
  53. }
复制代码
9 X1 v* O: d- E5 r) I
# ]% ^; @0 L. _1 d9 W5 P
服务端:
  1. #include <time.h># T' W, n* V$ B7 z1 C
  2. #include <stdio.h>
    & N' c9 w8 v8 g6 a" g/ W
  3. #include <stdlib.h>
    1 b/ p& O. W; l+ G1 m
  4. #include <string.h>
    5 ^! I" T5 q# U" p# G- D
  5. #include <unistd.h>
    , |! x$ A6 M/ `& q9 h8 X8 G$ w' |  @) |
  6. #include <arpa/inet.h>
    + u4 Q6 o' S4 O7 O# t8 h
  7. #include <netinet/in.h>
    ) s4 ~8 Y) Y& @+ U
  8. #include <sys/types.h>
    + M) j, h  V8 k  E) m
  9. #include <sys/socket.h>
      r' l/ r1 b* R( X0 S: E- O) V7 V
  10. 7 x0 {' X1 k# J# I
  11. #define LOCAL_PORT 6666      //本地服务端口' k5 k, x8 h  }7 k- S: Z+ P
  12. #define MAX 5            //最大连接数量; O9 h: `  l/ J- W4 C6 l' ~" }

  13. 4 i$ E0 C2 \  j
  14. int main(){3 {4 l6 Z% a. T. Z( v9 v- Y# K( b
  15.   int sockfd,connfd,fd,is_connected[MAX];6 l1 w' t6 D( Z" P" n7 o: [  B
  16.   struct sockaddr_in addr;5 R! R% {% ]% E/ d/ {3 M8 z1 o
  17.   int addr_len = sizeof(struct sockaddr_in);  K! w8 Y1 w% u* b# ^5 F
  18.   char msgbuffer[256];% c. l' @8 W# F7 m0 N
  19.   char msgsend[] = "Welcome To Demon Server";. R, x+ j. o# h/ F
  20.   fd_set fds;
    & U  @7 x. E8 @4 K
  21.    % J2 k% @+ u7 u+ O
  22.   //创建套接字: D3 b) d% T/ y3 \3 U7 U( x8 k
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);- Y) N6 t$ S/ H) `1 @/ r
  24.   if(sockfd>=0)
    ( H/ _% t6 h" ?6 ?0 i* I
  25.     printf("open socket: %d\n",sockfd);
    9 n+ a# K+ o5 h3 n2 e
  26. - V6 `2 ^& q2 G
  27.   //将本地端口和监听地址信息保存到套接字结构体中7 t/ V* i2 @* S( [
  28.   bzero(&addr,sizeof(addr));
    2 h7 _* n( U7 d  C. T2 _
  29.   addr.sin_family=AF_INET;* ?$ j/ v* P# h4 z0 W/ {; W
  30.   addr.sin_port=htons(LOCAL_PORT);
    & d7 j6 m+ Q6 T% U" l, L0 I
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0  X: Q# X6 Z$ H8 W  ]8 x" h1 l, ~
  32.    
    . z1 r, W3 r. a# o0 \. `( v
  33.   //将套接字于端口号绑定5 R8 {; d2 |6 t* w* D: Y
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0). W5 b# Y% E! v  b1 [
  35.     printf("bind the port: %d\n",LOCAL_PORT);1 U8 f! d+ ^8 J, Y" N

  36. 6 g8 Y" F2 \8 L' H6 w7 }6 m1 r
  37.   //开启端口监听$ b. ~$ u3 {7 H; z; t3 }+ L
  38.   if(listen(sockfd,3)>=0)3 d8 p  F0 T5 N7 m+ z. I6 n
  39.     printf("begin listenning...\n");
    $ Q; S0 ]* U- \6 b+ S8 ?" |
  40. # ^1 ^+ ~) @" ~' w4 a$ s! t9 q& y+ \
  41.   //默认所有fd没有被打开
    8 W% E% L  ~. N9 e2 j: @! G* _3 Q" ]
  42.   for(fd=0;fd<MAX;fd++)
    7 }. @1 J0 l' g8 l5 M
  43.     is_connected[fd]=0;
    6 {- W4 K$ B4 ?3 H% V
  44. % Z' K1 [4 t+ V- D+ R3 j$ b: I! ]6 a
  45.   while(1){
    4 ?- `, I6 k4 Y+ U& D% N, ]
  46.     //将服务端套接字加入集合中( h7 v& [" l# ~) Q% u
  47.     FD_ZERO(&fds);
    $ M* m3 o7 R) X; R
  48.     FD_SET(sockfd,&fds);5 w4 Y; i- s0 p
  49.      9 o- K  D$ R2 v" ?2 R. k
  50.     //将活跃的套接字加入集合中
    # G% j) @# C. D9 ~; B3 s
  51.     for(fd=0;fd<MAX;fd++)
    2 l& a" R  F* }; n, D& k
  52.       if(is_connected[fd]), P& e) q+ r! G5 U: P
  53.         FD_SET(fd,&fds);
    - a# m; N6 [# e$ ]

  54. . E* y# m/ @" x6 a
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
    5 ^  m/ I% K- t! J) e
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))
    8 _& V; [! A. G9 C7 n0 ~0 J& m7 M
  57.       continue;
    . d/ S  |# d7 b& v$ L
  58. 0 e6 ~! q# Q5 u: }
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字
    ) b2 m9 t6 ~7 v8 i1 E% @; T1 W
  60.     for(fd=0;fd<MAX;fd++){
    7 h+ r8 V6 K# @: ]
  61.       if(FD_ISSET(fd,&fds)){& X& o+ z5 ?+ T% {6 X
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接" q) B7 g9 F4 c! h) Z& {
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
    , R& |9 g$ G% z8 N2 O" B" u
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语
    2 n! O+ M2 q; s& S$ a
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用
    & R+ o! L$ v: Z1 |5 Z
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));6 ~& e) f( g" a+ \: t+ ?
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字! d6 k) |- R$ b- f" A' A
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ $ \; V; U' n$ X3 D  X  E
  69.             write(fd,msgbuffer,sizeof(msgbuffer));4 ]; d* a+ Y9 D- f
  70.             printf("[read]: %s\n",msgbuffer);3 p1 v' y( m6 U- P& `: c) y
  71.           }else{, k8 p* I: W" v
  72.              is_connected[fd]=0;
    " p% z' Z9 \% e/ N/ t
  73.              close(fd);
    / @- a' s# J# F+ p; b
  74.              printf("close connected\n");
    3 d. ]! q$ p8 J
  75.           }; H) b0 h9 l- i. I1 U
  76.         }
    * {# h% v. g: \$ S8 _) m
  77.       }0 S. s$ j9 |1 J4 i
  78.     }
    " {" L: @4 B( s: l
  79.   }
    9 l9 P7 U2 v0 D
  80. }
复制代码
# P6 w: ~4 ]7 c% _

; Y( v3 `# I4 ~& s; j1 u+ a0 _+ G( L5 O0 b

- n  \5 b  \! E0 m
, |1 h- J0 _) y. D- {' T& r; X
# n2 M! y/ v  _7 @
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2026-8-24 19:35 , Processed in 0.074297 second(s), 22 queries .

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