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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

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

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

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

/ S$ o( K) v; D  }/ T8 B
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
! g  d7 p! p0 K! d/ I0 f$ n
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3

) E8 Y; \7 E! p0 r' M
" L6 v. `& s, o; \
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)

8 k% `- Z! b% h0 W, Q) r1 k; w1 C0 L9 r& i$ l+ ]
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)
) A8 W1 q+ L7 W

. x% V' y# A1 c# u& q3 @% k% V4 V2 {" Y
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
" ]$ n1 J; r( s# ^, h- G7 z5 }3 X

5 t+ b& e" j  c# V1 l& h
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>' h: I8 Q! Z2 S% S( \* l. c2 f
  2. : L/ z2 U6 U4 p2 d2 |* R* I' t
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
7 c; w# A! d% O: j2 Y
    readset  用来检查可读性的一组文件描述字。
0 u. s' Z, x/ M$ z5 b
    writeset 用来检查可写性的一组文件描述字。

- b5 A2 U$ K- ?% P    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
. B2 {$ O* |$ C4 J7 X) O
    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。. b! @6 X( z, Q) @: \# n

: [. ^8 H! S  ?1 N& j9 o5 x    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:# W* S2 {0 x3 @
! _) ^7 c( a  v, x( r$ n  g
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
    . q; a7 q+ L4 `7 F

  2. 5 K3 u) e; {& b* D0 c
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
    0 l  D1 o0 L. l/ i6 z" R

  4. ! ]: w$ }  [1 }% n4 l% f
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。
& P0 `4 [) Y! k0 ?1 y, O' x$ R
& ]' K( o' j5 Z1 J/ c! p6 J: W4 f   
5 C/ G; I7 X. l7 E
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集- b/ r0 [% H' ^5 F, L
  2.     : |- X) s/ a. n# L% q, ~
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd
    / V. \8 h. N& s" Z8 D7 f  V3 n& A- p

  4. ; R. I/ L9 y% Q: x' ?: F7 @7 B
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd7 q7 x: e) n5 x
  6. + A- L1 Y: `8 s: c2 |
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;
    " k+ i/ P  Z# W. a5 ]  J8 r9 c
  2. .....1 ?9 V+ f# M7 H  `2 m8 o
  3. fd_set set;( d  B+ q# _# p0 i* g
  4. while(1){/ O" l3 u5 o! _0 y
  5. FD_ZERO(&set);                    //将你的套节字集合清空
    / K$ `! e3 f7 g
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s9 B/ J" C6 G& F/ ~. Q" I
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,
    ! a% j, D$ r2 g( @1 t
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,
    ; B  a7 ?  L- e+ k
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    + Z* k3 E  B: q2 O
  10.                                 //只保留符合条件的套节字在这个集合里面
    3 h: M% i/ d# @. d
  11. recv(s,...);
    . d* O1 d" |9 m/ [3 u5 h  R- V
  12. }
    6 z* y5 k/ W3 U' O
  13. //do something here
    3 T; Z5 C; S. f. g7 G
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。
    - G( L: G+ w) E
  2. - ~- R9 n9 U) v2 k
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)& u) ]1 A0 Q, j  N% H

  4. ; O% X- r: Q. _$ I$ B2 O
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011& v( t- {. F" a( @" V

  6. " v) E0 L4 d; D) c. U! w4 W
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待4 t/ }, N* [- E' E5 p0 Y( L
  8. + o6 d, b- l9 q; ]1 p# T
  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.具体过程看代码会好理解

0 J+ a- V8 g' `& s- z) ?% Y" x! V) f: _0 i' |3 b
使用select函数的过程一般是:
/ H! {1 @! e- _9 k
    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
5 Z0 ~! e) r( V5 u; {, ~: h
( ~0 a4 W& m' J/ t) y, \
客户端:
  1. #include <time.h>) L* O0 h  [' j
  2. #include <stdio.h>
    1 p  ?( v& P, o7 Q5 K
  3. #include <stdlib.h>
    7 Z7 s3 g: I) _+ Q) d) K, m
  4. #include <string.h>
    ' D( c5 F# c* z- ^8 k) i0 |; a; J( `
  5. #include <unistd.h>" h. |$ Q1 u8 }8 c7 m$ b$ S
  6. #include <arpa/inet.h>" h. e% l% s  s% V0 R2 q1 M! p; ^3 b- E
  7. #include <netinet/in.h>
    4 @* t) u# f+ [  w, Q8 [
  8. #include <fcntl.h>
    0 M; e( {. S. Q
  9. #include <sys/stat.h>
    1 S% _1 N4 L; k' B/ c
  10. #include <sys/types.h>+ f% O4 [# b9 \4 R2 k# Q* Z% U
  11. #include <sys/socket.h>  n  D  }" Q8 u! ]
  12. : M! `' Q  j+ p
  13. #define REMOTE_PORT 6666        //服务器端口
    2 J) ^0 x, w# O7 V( }: R1 R+ D
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址
    . M6 u! E# F6 `- w8 V

  15. ! ?+ i1 m: W: _0 s* J- W; W- X
  16. int main(){8 i8 z; e( R6 p7 A  E. N4 W; @
  17.   int sockfd;
    # f0 F7 E2 L) l! C
  18.   struct sockaddr_in addr;
    7 C8 m  {& e( e/ r8 u" n' _% }
  19.   char msgbuffer[256];0 Z2 r4 [9 e4 Y
  20.    ( X' S7 d# I: `) i
  21.   //创建套接字% c" Z( y. w. ^
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);- u) x7 ^+ N% X: f! v: _/ b8 @; l
  23.   if(sockfd>=0)# m( ]! z5 I/ S. Y: l
  24.     printf("open socket: %d\n",sockfd);
    + a% Q) e$ z! j$ c
  25. . M2 y# H& {: Q
  26.   //将服务器的地址和端口存储于套接字结构体中
    - F( B4 y9 @% Y# d
  27.   bzero(&addr,sizeof(addr));
    $ v" T+ E2 p3 I2 R$ F$ b- b
  28.   addr.sin_family=AF_INET;7 x( n$ R  \% W6 P5 |' s/ n
  29.   addr.sin_port=htons(REMOTE_PORT);: D, c" N5 ~+ I$ Q
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);3 T6 M1 t* |+ j( ^! ?5 T$ _1 c
  31.   
    2 [( e# I/ V, M9 ^7 h
  32.   //向服务器发送请求
    % `5 J% {0 W2 h' F9 J
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)8 g$ `: P2 P0 v6 O
  34.     printf("connect successfully\n");
    5 F) ]9 E4 i1 Y- z
  35.      l2 \7 h* p' n0 L* |. {
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)+ m& v" w7 F; b* ?1 B
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    : K  `4 X4 z0 S) T6 n; s
  38.     printf("%s\n",msgbuffer);
    # x( S0 n* s3 x3 |
  39.   
    / A1 h6 {0 k7 I2 s6 C+ R
  40.   while(1){
    3 h" {: H% t$ _/ ?# m4 I
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息, i% V' x: r- t4 E
  42.     bzero(msgbuffer,sizeof(msgbuffer));+ E1 m5 ]0 h3 J# ^& w5 Y# g) h! B
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
    7 D2 }( z: B/ d/ ~$ s' G- ^+ C
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
    # K9 ^$ S! R4 E7 T0 O
  45.       perror("ERROR");
    : L8 h, N4 b' m" ^  \
  46.     1 O7 t/ E5 h9 s; y
  47.     bzero(msgbuffer,sizeof(msgbuffer));
    - ]+ f6 J0 P% r9 Q! }
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);" V, x) j5 T3 S4 `4 L" R
  49.     printf("[receive]:%s\n",msgbuffer);  b$ W1 v: \8 d, G! f( z
  50.     9 r9 g) u, n( \' @) q# b' }9 }
  51.     usleep(500000);
    9 n) g% _( v2 S) p; X3 _4 Z0 s
  52.   }+ W5 l3 E1 [% v) c; F; Y1 z" z+ H. r
  53. }
复制代码

. M0 [( G' e2 p0 u" L4 d/ {' n% L* t) n' T) T' ~) T* U
服务端:
  1. #include <time.h>
    - l0 ]* @9 l5 W! E" E, c% e+ L2 }
  2. #include <stdio.h>0 w; D; G/ @( h: B: [8 f4 P
  3. #include <stdlib.h>( d, Y4 M& d/ R5 G9 X
  4. #include <string.h>" b# @9 x' R6 C* l; b- [8 F7 J
  5. #include <unistd.h>
    ! [6 c2 A0 k/ z
  6. #include <arpa/inet.h>- D# e* W# L  m" m0 d) W) }% y% i
  7. #include <netinet/in.h># A, _9 \( H! J( n8 x- u
  8. #include <sys/types.h>3 K1 p+ {7 m" T
  9. #include <sys/socket.h>1 T: s: O( e  R9 L2 C

  10. / Y( |, V9 s  x
  11. #define LOCAL_PORT 6666      //本地服务端口6 n* K/ n5 f* V. ]' P+ Y% ]3 B
  12. #define MAX 5            //最大连接数量6 v! t6 C5 Y4 E- \" y

  13. 4 i- T& G2 n( \4 C9 F
  14. int main(){/ P( t& d0 B$ h) S
  15.   int sockfd,connfd,fd,is_connected[MAX];% }) I, D+ Y* D! v  u* g  J0 p1 r3 {
  16.   struct sockaddr_in addr;% l* m1 r& [+ P) y6 s6 t
  17.   int addr_len = sizeof(struct sockaddr_in);+ f9 i) r( g3 L; ?: _& i
  18.   char msgbuffer[256];
    4 ]3 {$ @9 Y! d( n' h) Z0 f8 e/ r
  19.   char msgsend[] = "Welcome To Demon Server";
    * P: z* [6 g# ]* l
  20.   fd_set fds;
    7 ?) W$ i+ K6 `, T
  21.    
    # w  M" X$ b+ i# W
  22.   //创建套接字
    ' ~, d' h& ?  }$ f6 G9 r: j
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);, [' M) }# ?. ^' I9 J
  24.   if(sockfd>=0)" N5 M2 u( N1 k$ j& u
  25.     printf("open socket: %d\n",sockfd);
    ! M3 {9 c: I1 I6 {

  26. 1 z; z! x5 _. F5 K
  27.   //将本地端口和监听地址信息保存到套接字结构体中# `. Z5 {" w. m# C5 [
  28.   bzero(&addr,sizeof(addr));4 R! C: g0 u* H! T9 Z- p( [
  29.   addr.sin_family=AF_INET;6 Y% L' r8 i' B; P% v" S
  30.   addr.sin_port=htons(LOCAL_PORT);! ~3 b! Q1 Q7 m3 D/ s/ [
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0  B" a9 I* @" a1 D7 i0 g
  32.    
    ' d, z% X- N' c: F; K2 [  e7 a
  33.   //将套接字于端口号绑定
    5 @/ Q% _3 t! ~
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)( i6 ^0 z2 p0 i
  35.     printf("bind the port: %d\n",LOCAL_PORT);- j1 V1 {7 j, ]
  36. 3 `7 k4 l$ Z7 D) n/ i2 U3 B9 U
  37.   //开启端口监听7 J! K. E; K  t) @6 H. f) H
  38.   if(listen(sockfd,3)>=0); B/ J2 b: z; w5 T+ K  |  N
  39.     printf("begin listenning...\n");  C& s& a# Q' p4 M9 p; H) d  _) w

  40. # N( X- D7 @2 A; x$ [: t- K0 u+ N
  41.   //默认所有fd没有被打开
    ' ?* B% @9 q4 n4 b9 L
  42.   for(fd=0;fd<MAX;fd++)4 c- |* Y( u# P
  43.     is_connected[fd]=0;
    ; J6 T6 ?" O. B  ^. e& T

  44. 7 S4 s8 M5 Y' B, Y
  45.   while(1){
    : b4 ?7 d4 f  V. K% h
  46.     //将服务端套接字加入集合中
    5 W9 [* U/ w% `) @
  47.     FD_ZERO(&fds);0 d6 C- _0 h+ L* D
  48.     FD_SET(sockfd,&fds);
    6 v% u; {7 z, a) F0 S& h/ S
  49.      ; R# N) g, E8 v, P) w
  50.     //将活跃的套接字加入集合中
    * V( |4 S& T$ b, A! m
  51.     for(fd=0;fd<MAX;fd++)7 @' F- \5 M: A/ E. A
  52.       if(is_connected[fd])( J; n& f5 X+ H# q
  53.         FD_SET(fd,&fds);3 X$ N" O3 M! T2 F) Z
  54. . H' }4 `; q5 [6 P
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
    ! G) c) L* `, }4 Q& u. U
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))( s  H3 K8 X5 z" T4 l
  57.       continue;
    ) [* p3 C  R8 U6 @9 U9 G

  58.   ~0 x- \5 v! ^* f+ }3 f0 _* M
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字
    + W: J! h  H1 z, r
  60.     for(fd=0;fd<MAX;fd++){
    # B( _& o3 F  `6 z& l
  61.       if(FD_ISSET(fd,&fds)){
    & Y! N6 x+ e5 L9 H: [% ~3 f  `
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接( T( Y# c" I9 |4 S; Y
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);/ |( l: G- E6 H7 V9 J$ M
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语
    - Y$ D/ b6 B2 i( z; s" G3 z) n: t
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用: A! v% P! D: Y6 E8 X: A7 ], C- F
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));/ _  u$ _: p* R! D' _2 P+ }
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
    8 y; G" N( Y% h' U& z
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ ) Y$ v0 T- ?6 [* R
  69.             write(fd,msgbuffer,sizeof(msgbuffer));
      D% f! G3 ]$ `7 x; Y3 t0 v
  70.             printf("[read]: %s\n",msgbuffer);6 v; V7 x% L1 @
  71.           }else{
    ; t! i" O0 s' c5 D- H
  72.              is_connected[fd]=0;
    2 Y$ E8 V  [# T' j6 ~
  73.              close(fd);  B; X% X( k2 z  m
  74.              printf("close connected\n");
    ) ~1 \; X6 G6 S+ v
  75.           }, i6 g9 n8 h$ \) x: J) b, _& z
  76.         }
    0 _$ Y9 @. P* ]' ?' z9 C3 v3 a
  77.       }7 s# Y- C! a# O2 k( b+ B
  78.     }; h( Y5 ]+ B( Q$ u
  79.   }( N) P# d' G  p5 [% ]1 h
  80. }
复制代码

  S0 E6 B/ v, e$ X0 E. t& }5 P! Y, ~5 H

2 {4 f) u: V/ }5 j1 r0 L, \5 Y" O6 E6 a
4 n  a  ^7 K( _3 h  m  i
3 @1 \( U/ O9 F, A% [  I7 l! _
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2026-5-2 11:45 , Processed in 0.069218 second(s), 24 queries .

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