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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

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

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

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

/ C* I8 Y. [# H' ]: C! v
什么是SOCKET(插口):
     这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
' S9 D  ]( k' y  p
     "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
      对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
      具体其他高级的定义不是这里的重点。值得说的是:
      每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
      应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层  ->发送(接收的话与之相反)
; C7 d. ~5 V4 h' m8 D, S; P

+ ]! Y# b7 }3 ]/ X! ^
/ l1 S8 M- f4 a7 L
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
" q* S* R9 H5 \# ?0 t4 [& E
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
* H3 Q7 y+ r* }/ ?/ J
- c' T# ^$ l/ G/ `8 Z9 H8 p
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)

' r, }9 e  m! X( T% e9 ~; k& K" k
: D% c* w7 ^0 \& k, b. r& ^
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)
; H7 D; `+ V' f$ n
, W2 `' u% b! V" d
5 T- N( `) I- {6 D; @) b
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
" u8 ^" |7 d$ L8 `( Y+ z

9 a; n& v" [. @. P6 E% A
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>; c8 _* }- y7 _+ l+ k

  2. $ Z3 c1 X- z, v
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
- E- w' j- U* E2 S6 a
    readset  用来检查可读性的一组文件描述字。
$ @6 k* ?' t1 m0 S/ J' V; z3 H3 ~
    writeset 用来检查可写性的一组文件描述字。

. X1 u! Y+ i$ W" D, x; i: H$ H    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)

4 T" r6 t, t  u, t    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。+ q+ ]0 q. O" V4 h' n  {) Z8 A. H

4 R6 w4 j# k5 m. _( H    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:; ^0 P6 x# P$ L6 E! v# v

/ y' q0 ?; `+ O& H5 \
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)% M: j% f% {* V9 l% S

  2. 2 @3 D, F; ^' y* u/ I
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
    5 Y7 A, b# X# y4 c4 n. r

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

5 B) H5 n' @2 g   " B$ ?  }$ |% }2 A' B1 T  u
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集2 P* l! [9 G) F( S
  2.    
    % l4 O# u0 }, m
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd
    0 A# {1 o. Z& }8 B! `4 D' C6 ?
  4. / {! R8 Q) f5 H- F: C1 r8 E
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd9 H# ~# T' N2 C$ i0 R0 P
  6. & `( {3 v9 F/ l3 o- t5 m, r
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;
    ; @# r+ P6 C! Q. e& ~
  2. .....2 z* ?+ @4 O! `( K. X
  3. fd_set set;
    : A/ u/ w  o; v- H6 P1 M5 h2 `0 p
  4. while(1){! U. {4 T: E: `+ n8 ]# }7 w
  5. FD_ZERO(&set);                    //将你的套节字集合清空% w2 G0 u+ [, F% _; n) b* n
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s5 O9 b# m' E9 z1 o9 p+ o, J
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,- j2 S& w3 L- L; }( [8 T+ ?
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,& \; y9 v  x; H* O8 J) ~8 A
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    # A+ `8 B- H" h( h$ S7 n3 r1 E/ P
  10.                                 //只保留符合条件的套节字在这个集合里面3 U! o( Q1 t5 ?6 a4 a8 |; E3 _- J( q: g
  11. recv(s,...);0 M  ]( ]/ _3 o, Q, A( Y& _: A
  12. }3 ~5 f" S2 V; U* G
  13. //do something here
    * ~7 S3 h; W; `& u
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。; I5 ~5 I3 W+ V( _* i, {! O

  2.   r% Z% }/ u+ S2 n/ \9 u
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)
    $ T6 _6 t: A; S7 l3 E0 O
  4. 3 q/ K. I% c1 t. r" K' G/ D
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011+ Q: T/ B; P8 w" K7 U; G

  6. " G0 S3 P) S+ E+ e* t8 |# x% W4 w0 B
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待
    % Z! g- k2 }9 H
  8. 5 P! L, X- S- J0 A/ a& Y& 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.具体过程看代码会好理解
, v% E5 ~$ |$ c( U" \

/ G) r, R4 Z; M+ h# C
使用select函数的过程一般是:
! v* w( q) w: W1 m1 I/ u
    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
. P. b2 E" b0 h1 d) I5 _) @
6 H& A8 B$ Q7 Z7 H5 y( u6 d
客户端:
  1. #include <time.h>
    / \* l/ {9 e# _3 g8 F
  2. #include <stdio.h>
    6 d& \: d+ D" v+ M# ~
  3. #include <stdlib.h>
    $ N/ ]1 H5 r0 T
  4. #include <string.h>3 x9 w7 W8 F) L3 `+ D
  5. #include <unistd.h>% Y$ n7 |, x: c( ]3 _- Q
  6. #include <arpa/inet.h>' K) Z& |! ]3 X% o6 N
  7. #include <netinet/in.h>; R! p4 N# _' p, O
  8. #include <fcntl.h>' `3 O, A8 L' D9 C
  9. #include <sys/stat.h>
    7 V+ |+ @- v, G2 Q
  10. #include <sys/types.h>  _- H' @8 w( V
  11. #include <sys/socket.h>% F9 g5 ^. X* W

  12. ) i) F' R7 G) S6 V+ t* @8 q
  13. #define REMOTE_PORT 6666        //服务器端口
    ( P( c. t( {* o# l! V1 u6 a1 O
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址% F$ t/ a' I" d+ h5 P
  15. + E- r$ G) G/ ]: w" g( M; _
  16. int main(){8 [4 }+ }" z; O) N  l) i3 N, v4 i8 ~: L
  17.   int sockfd;
    + n1 S4 j3 A- o: I6 N, b
  18.   struct sockaddr_in addr;0 ]9 g* m& y$ V* ^3 ~% D
  19.   char msgbuffer[256];
    5 i1 T, i4 a" Y
  20.    
    + @. O& h# A; i1 V- Z2 d4 h
  21.   //创建套接字3 W0 `0 R# l. R3 x
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    ! v2 g2 ]0 r: w! I; i" I0 Q  a
  23.   if(sockfd>=0)/ g8 M# _  J6 N  d6 x
  24.     printf("open socket: %d\n",sockfd);
    3 n( s5 w# h2 t
  25. 7 h8 g# b8 H% T% f
  26.   //将服务器的地址和端口存储于套接字结构体中" C0 D. D5 I1 L( _
  27.   bzero(&addr,sizeof(addr));3 h) i# S  G2 {
  28.   addr.sin_family=AF_INET;/ y3 P) j' ?( b7 M
  29.   addr.sin_port=htons(REMOTE_PORT);* p+ K* o6 m0 z5 i# V
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
      q' w* r. `- t2 `8 \2 ?9 u5 I& t
  31.     T$ Q5 y) o. V! h" }4 r
  32.   //向服务器发送请求
    - ^" E% v1 [/ ~' D( d
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    ( I9 b) y% L9 ^0 o6 |
  34.     printf("connect successfully\n");" }8 R/ n. |5 D5 @" ?
  35.    6 {6 J# U8 i* Z  J9 k5 t1 c
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
    ; \0 ~0 p8 p, A/ f+ X6 X  Y- C, w) a
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    / O8 h' c8 {' X5 u/ n# a
  38.     printf("%s\n",msgbuffer);* D: I: P+ f& m* ]: Q  \9 ~7 [
  39.   , i- h. o" d" b% E
  40.   while(1){
    5 @$ L( [6 }  r$ t/ ^
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
    0 U4 b, w8 l: K3 b8 z
  42.     bzero(msgbuffer,sizeof(msgbuffer));
    ! W  I1 @8 |0 i$ `( I# r4 ~
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
    - d1 Y0 B/ x% a7 G; i
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
    - e8 S. P# K5 \, B
  45.       perror("ERROR");
    ) W2 W$ m8 x; W( s) w; a% r/ _
  46.    
    ) i8 `4 n, S7 C- N
  47.     bzero(msgbuffer,sizeof(msgbuffer));
    # x2 D9 }( B6 u5 ?
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);" X$ o8 h7 b& f9 [# H
  49.     printf("[receive]:%s\n",msgbuffer);
    8 p6 b4 z& M3 r0 J& A. {
  50.     3 I7 Z. G/ p, k& ]
  51.     usleep(500000);
    ( n5 w4 P7 N2 x+ r& P# j1 Q
  52.   }9 f# R7 E1 ^( F3 r. L7 `% Z+ P7 V
  53. }
复制代码
: O* X$ ~+ d9 \7 b! i/ Q* l# I
0 {" V0 l2 s$ ^! {1 ~9 x
服务端:
  1. #include <time.h>, H% e% ?1 j4 f! o  \- A& |
  2. #include <stdio.h>6 U4 c. \6 }) D7 y; J: [
  3. #include <stdlib.h>! z- P3 M5 m% @* g- S% T
  4. #include <string.h>
    $ u& U9 w; w# M
  5. #include <unistd.h>8 F3 v& z3 c6 J2 d8 y: h7 s, {2 e
  6. #include <arpa/inet.h>, H+ K8 r4 x/ d! |1 f/ k. x7 I
  7. #include <netinet/in.h>
    / H& L% v$ n9 a3 ^
  8. #include <sys/types.h>
    - p% T' {) K" g3 @* `
  9. #include <sys/socket.h>
    / @+ G  r: C0 u4 D; \* P

  10. 7 O' }* @- N0 J$ k
  11. #define LOCAL_PORT 6666      //本地服务端口# c5 U' |3 z( @& Y
  12. #define MAX 5            //最大连接数量2 o2 p1 E* f5 X; V6 ~
  13. : g; {& t; }0 ^# }
  14. int main(){2 b* B) ^3 E9 m
  15.   int sockfd,connfd,fd,is_connected[MAX];
    ! ~6 F) I+ R* ?8 r* P
  16.   struct sockaddr_in addr;
    & \, v- T& A: e( H
  17.   int addr_len = sizeof(struct sockaddr_in);. X9 q4 i/ _* `7 ^
  18.   char msgbuffer[256];6 d, ~4 F0 x9 Y8 p* j- S* a7 _- D
  19.   char msgsend[] = "Welcome To Demon Server";
    ; k0 @& D" J7 w5 M! o
  20.   fd_set fds;
    ; L3 j/ r& D" T) p
  21.    3 _. V) J  T' {9 K& L* a
  22.   //创建套接字# u/ _6 k5 ^5 [, K+ ^! h8 c* M
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    * E8 W6 p) X' w) H0 o7 W
  24.   if(sockfd>=0); b& ^) U6 j9 u& k" y& t* V
  25.     printf("open socket: %d\n",sockfd);
    " w" I/ ~0 }' |
  26. 3 c- v7 E# T' q3 n
  27.   //将本地端口和监听地址信息保存到套接字结构体中
    / t1 \5 b  }5 ]* O  ~* j, h
  28.   bzero(&addr,sizeof(addr));
    ; F5 S2 L2 `* R( s+ l" o
  29.   addr.sin_family=AF_INET;
    : @' M" r; U1 w9 T) d8 V1 Y
  30.   addr.sin_port=htons(LOCAL_PORT);7 z+ Y1 L3 [9 T) q" C3 F0 A
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
    2 o$ t3 C' u; ]4 `& c( l0 w
  32.    : \% }. R' E* \4 W9 z
  33.   //将套接字于端口号绑定- ]4 f5 E) G# d! `& G  b
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    5 c6 v  |$ u0 ^! |& o+ O5 ^
  35.     printf("bind the port: %d\n",LOCAL_PORT);0 V- `* \7 S" q
  36. 7 H$ w1 p9 X: d; x
  37.   //开启端口监听  Q# i/ [( B0 X) I7 i
  38.   if(listen(sockfd,3)>=0)- G* I) j: W5 m5 D
  39.     printf("begin listenning...\n");9 ]; ^  e! y% F/ Z. j& u

  40. - B! s8 x$ E9 n. ^
  41.   //默认所有fd没有被打开# W% }0 N4 C& W/ c- Z! ~; E4 `
  42.   for(fd=0;fd<MAX;fd++)
    0 g6 Y2 |3 C7 `- `* p+ t
  43.     is_connected[fd]=0;
    - M4 n2 h. B5 v' J5 @) J) |

  44. . J# S) G( C0 g9 m* }# s* v, b* M6 O
  45.   while(1){5 A7 d: J# |- |9 [2 b4 w
  46.     //将服务端套接字加入集合中
    , x8 _! ~4 A; R
  47.     FD_ZERO(&fds);! ]6 S1 n" T5 I" B' ]2 V
  48.     FD_SET(sockfd,&fds);& L9 y3 L. K0 l0 j# ?6 c$ D
  49.      
    5 [. M# f7 V' z1 O* S! m
  50.     //将活跃的套接字加入集合中
    % X0 a0 J& @1 U% Z9 J( @! U" G
  51.     for(fd=0;fd<MAX;fd++)
    , R: H# a& G1 S, r4 t
  52.       if(is_connected[fd])
    2 g! K5 [+ d! ~4 [3 X& c
  53.         FD_SET(fd,&fds);
    3 P$ u: `# D# D/ n  T

  54. # z0 G% E$ W, q1 r2 j0 y* [
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
    , Q2 e9 l  H: b7 O4 V
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))7 g% h8 s# j% M: m  S& v& |
  57.       continue;
    - g9 D+ C" n/ d& N: ~' I
  58. ( n8 o" f* M. e' c0 }) {
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字6 T0 R$ W* ?! i9 _
  60.     for(fd=0;fd<MAX;fd++){6 d# z3 P7 m4 n  C+ W+ {6 e% h/ `
  61.       if(FD_ISSET(fd,&fds)){3 h* [4 o! g7 j; H4 v+ _
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接
    / Z% `6 l  z0 n4 \( H
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);" k2 d. F" S$ N3 N3 B
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语' a% V- T& a3 I( X& u
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用
    & `+ v: [; t8 Y4 [! }
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));
    ) h8 J! r! W  t, B0 G2 c. x$ P+ t
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字6 I. L2 j+ s. L( Y9 f% l
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ & \$ I# ^8 P' _+ {9 t' C
  69.             write(fd,msgbuffer,sizeof(msgbuffer));
    # h) J/ o4 x3 v8 e  z. a( R
  70.             printf("[read]: %s\n",msgbuffer);2 ~2 k' v1 u0 J/ a
  71.           }else{; l+ ~  Z7 F+ }( [" @* D, }! f. _
  72.              is_connected[fd]=0;
    3 d/ `; P! i5 }: X9 E& n9 l# |. i: `9 W
  73.              close(fd);
    , L6 ]. N' z1 _3 T2 B2 r4 U* u
  74.              printf("close connected\n");
    ) T8 J" `3 U% W1 B
  75.           }
    ; n- b2 ~. n% o& E) J" c* G. f: Q
  76.         }
    ( s0 p0 U& @( o' K, I
  77.       }
    7 P$ h+ n! S, r1 {
  78.     }9 `) O: Q5 g+ I7 {  o
  79.   }; `2 N/ d% t7 X' F! u9 @. d
  80. }
复制代码

* Z! a( i* j* H! ?
6 q. R$ W. g: i/ Z, b9 i8 X/ D. Q) @7 |3 S

6 _/ o* |: ]3 |$ w6 N+ |2 L( m! y
1 U' E4 F0 `7 ]! g$ ~- _1 s8 w; e
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2026-1-30 13:05 , Processed in 0.054173 second(s), 22 queries .

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