cncml手绘网

标题: 编写一个简单的TCP服务端和客户端 [打印本页]

作者: admin    时间: 2020-5-9 01:53
标题: 编写一个简单的TCP服务端和客户端
实验环境是linux系统,效果如下:
1.启动服务端程序,监听在6666端口上
2.启动客户端,与服务端建立TCP连接
3.建立完TCP连接,在客户端上向服务端发送消息
4.断开连接
实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点

/ c) ]; Q; k: E9 l( ~
什么是SOCKET(插口):
     这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
1 f# ~; v, @+ X3 t! u
     "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
      对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
      具体其他高级的定义不是这里的重点。值得说的是:
      每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
      应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层  ->发送(接收的话与之相反)3 q& N" O9 z  U* L( ?, x1 Q8 J
" S7 \$ u6 O1 A: x& J

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

/ i* D$ e6 Z3 P* _8 k1 |; W% `- q; x2 u0 {4 Y1 @1 I  }
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)

4 C  L) k5 P7 Y8 }3 o  _6 J$ @3 d2 @: y# l( P
4 @. p7 e: ?: `' G1 F: C$ ^1 Z' {
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。

$ x; E$ k% W6 E3 t+ t4 T& ^, U, {! L* |4 u
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>/ v" g. N6 N) ?. v+ k

  2. . ], ]1 @5 C8 z
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
5 i5 t6 ^- L8 |! ~, A
    readset  用来检查可读性的一组文件描述字。
6 k6 z) i0 b1 w: m( r+ m
    writeset 用来检查可写性的一组文件描述字。
+ ~9 c% N. ?4 x& \- m8 b
    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)

4 O1 m  E+ ^7 ], R7 [% Q    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。. N# n! P4 k1 `& ^7 f

5 c9 E9 [5 U, U/ Q    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
) O1 n  m9 p9 i$ u/ J4 X! C9 w% f

: i( W( a" s6 C) Z: l. Q  {
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)/ j  D" ^5 `4 {; R1 P# Y

  2. / Q0 r- t' X: B/ Z' Q
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)  w& y) n( J) H' i7 \: B

  4. $ P7 O9 X& g, S6 ^: x  [. x
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。" k0 D( }9 ^& Y3 S9 g" }
* {4 \- r1 w+ J  j2 g/ ~- Z
   & U* I$ x% [& b/ N6 s
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集- \; [( c/ c4 `" B6 J
  2.     " i9 {6 O" d5 g0 F: c, v& b$ e0 o
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd2 ?+ a& t. x8 w& P$ o5 F% W
  4. " s; Q3 b3 N, O' r; N
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd
    : }- g6 B% a' ?/ j3 @& L
  6. 6 a  D9 z$ X2 k' m3 s
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;
      q# H( e; \" |" F9 X! W
  2. .....
    9 F9 \' M% G0 F; E* u
  3. fd_set set;/ [/ z* H( k6 B' z
  4. while(1){
    4 s  v) \9 o4 P& D; I
  5. FD_ZERO(&set);                    //将你的套节字集合清空# w3 ]- D' y0 A
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s: U/ {3 Y" @1 S2 h  ~4 a1 z
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,4 d3 i% v+ E6 {7 h
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,
    - q. p8 r5 n( R; V& u) b, Y, l) Y4 c2 P
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉
    6 E/ W% I8 t, V) m# A8 a8 A% B0 f
  10.                                 //只保留符合条件的套节字在这个集合里面$ e7 e8 B9 Y4 S& \( x
  11. recv(s,...);
    ( y( N  d( z6 {' L6 N) I! @# H. p
  12. }
    ' N7 G) ~# B# I
  13. //do something here/ U% f3 _' X3 v  |- G( e: b
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。4 d/ m0 j& ?, y: w$ q

  2. 2 Y5 S: u+ |5 y
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)( g1 p- u- V" r& S( [( }
  4. - J) Q$ F" d' \  E
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011- E3 [  ~: }% E9 E- z; x  u

  6. 1 x- C7 K+ \% s2 m
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待: ?; j2 E8 B* _' ]3 \" i3 a  y4 r3 M
  8. 5 t/ _4 ?' [3 Y  N; h0 N3 J
  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.具体过程看代码会好理解

6 s& t1 d& I1 X9 ]: {9 L  F* H1 I* @* a/ A' U- g% i
使用select函数的过程一般是:

2 \/ t4 }. W$ m. N4 ?# [$ S: L    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。% H, u( C9 ?9 h5 M

. T8 t1 ?9 t. ^) V$ ~/ f
客户端:
  1. #include <time.h>& D3 Z" v" h& S' w' D
  2. #include <stdio.h>
    : j1 m5 o( ~! B: q$ h9 o. s: @% [7 F: m
  3. #include <stdlib.h>
    4 X& P( t2 ]8 u1 b$ A/ I
  4. #include <string.h>1 i  \8 u& U/ U' d& d, Y
  5. #include <unistd.h>) @* V' m0 ~. ?% ^- O) _
  6. #include <arpa/inet.h>
    ! K2 Y2 k" E6 G% R# B; z6 b/ \8 x
  7. #include <netinet/in.h>( M2 g+ P0 O) m
  8. #include <fcntl.h>8 i  _& a7 h. y2 q+ t
  9. #include <sys/stat.h>
    ! F! b0 p- |+ J/ E$ P
  10. #include <sys/types.h>
    - G3 Z% g% h: a' ]* s
  11. #include <sys/socket.h>
    5 P; H' o. f% p1 _& J3 n5 }. F
  12. 2 W1 t0 X4 |4 r8 b# T3 ~9 ?& N
  13. #define REMOTE_PORT 6666        //服务器端口2 R' o3 M$ o2 A& a6 N7 X
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址
    / W/ y( D  D) P' L0 E( O: b
  15. ! v8 s0 K2 ^, _. ^, E8 f
  16. int main(){
      _' |/ ?7 T/ P
  17.   int sockfd;
    3 S7 p: i: Y1 [9 U1 A  X4 _' X
  18.   struct sockaddr_in addr;! e$ f$ N, h- R
  19.   char msgbuffer[256];. |$ i$ m+ d9 C( M6 y/ v
  20.    
    * E, Q4 ~2 E- n! k1 {* i1 }+ T
  21.   //创建套接字
    5 o1 x& M3 R: v9 T
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);/ Q* w9 R, q2 W3 d" N7 o& T
  23.   if(sockfd>=0)0 b( J$ W0 Y& K6 ]. e. w, y3 S
  24.     printf("open socket: %d\n",sockfd);
    - ?: R, S, M7 S5 n

  25. / w9 }9 e: r! C0 R" T- r( b  W
  26.   //将服务器的地址和端口存储于套接字结构体中% X! Y8 [  e( O6 p
  27.   bzero(&addr,sizeof(addr));
    ; N7 C+ J. Y: H8 c( z" T% f  E% @
  28.   addr.sin_family=AF_INET;
    # \) V3 n6 e4 z& g0 e
  29.   addr.sin_port=htons(REMOTE_PORT);
    . B4 V$ c5 v$ W+ s
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    + K7 O5 q( H9 O* a0 f
  31.   
    9 }3 u% p- i/ [  O1 Z- ?8 L
  32.   //向服务器发送请求! D+ D/ e& g' U# G+ I8 c$ P4 i5 q
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)0 A3 Z; R5 N0 D. e0 o- y' m0 E
  34.     printf("connect successfully\n");+ o/ A$ R4 P" P" u& F% R- R
  35.    5 i( ^- Y" O* d6 y( a' X6 x5 `
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)# C+ O  o' O% d- ]. f- q
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);6 ?9 }5 |, v$ Z* k$ i
  38.     printf("%s\n",msgbuffer);: l+ o  S# K* }$ w' R, R- `
  39.   
    - M: ]1 R/ P+ R, E7 C' `
  40.   while(1){) _4 m& {+ A5 _7 S  ~$ H
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
    . S$ L' O% u/ V4 W
  42.     bzero(msgbuffer,sizeof(msgbuffer));
    6 T$ ^  g8 c5 Z7 n
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
    ) T5 V1 Z6 Y/ |& y7 y' I; _
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)9 g8 R/ N0 b7 o
  45.       perror("ERROR");/ h  }4 `* D; U# M$ q. a6 m; k
  46.    
    : q4 p, }# Y+ w; x! \7 |
  47.     bzero(msgbuffer,sizeof(msgbuffer));
    % |2 L4 }2 ~5 I' ~; U
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    + D3 L! ]0 }  k7 z3 M" R+ i
  49.     printf("[receive]:%s\n",msgbuffer);
    5 {* J5 C% y. }; C) U3 Q# y
  50.    
    & \: y% h6 i  J; B9 ^
  51.     usleep(500000);
    . |/ ^4 [' Q# q3 r( g$ F2 ^
  52.   }- D7 z; _0 ?6 U' z: Z) J  U: d
  53. }
复制代码

1 L& n: ?! v% H! z1 N3 m. q4 n5 T4 i$ @0 A! h
服务端:
  1. #include <time.h>
    - i) n  U; \3 Q, j0 Z
  2. #include <stdio.h>4 G% }3 B3 w+ ~& C( p
  3. #include <stdlib.h>
    $ z. n; K1 U: q, _$ p' u
  4. #include <string.h>6 ~$ W% `) X6 W, t8 {
  5. #include <unistd.h>7 W* e0 T' O1 a% a; d
  6. #include <arpa/inet.h>  B0 P: y: Z: W8 X8 n! F2 p# Y
  7. #include <netinet/in.h>
    ( N/ o3 O9 u0 ?6 U0 y5 c0 D
  8. #include <sys/types.h>
    1 K9 X( @- C8 L
  9. #include <sys/socket.h>
    7 d4 d; g( I6 z# h2 k& q

  10. / M* O! e, f7 W9 k* [
  11. #define LOCAL_PORT 6666      //本地服务端口, W7 ?2 I2 _; J3 }& y' \: g
  12. #define MAX 5            //最大连接数量  o0 _2 K( U  C* U9 I8 w. b2 U
  13. 2 B3 {1 ~: ~8 D0 b1 j/ }
  14. int main(){
    ; M# {" b$ \6 B  h  o. l# ^
  15.   int sockfd,connfd,fd,is_connected[MAX];
    4 X* j5 B& O3 r# q2 t# N$ f/ `) g
  16.   struct sockaddr_in addr;' C0 z$ F: Z# B0 y
  17.   int addr_len = sizeof(struct sockaddr_in);
    * N: ?5 Q1 m1 f
  18.   char msgbuffer[256];) I3 h  Y5 v6 K+ S" F6 z% @
  19.   char msgsend[] = "Welcome To Demon Server";5 P) h! y, \9 C. |
  20.   fd_set fds;
    , N1 i/ A- c/ D* F# ?" ^- _
  21.    
    % S+ Q! n# O' [9 ~, J0 Y
  22.   //创建套接字
    0 T1 M% g" ]$ O) B
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    3 [% r  b- F; }5 \5 \+ s0 \
  24.   if(sockfd>=0)
    9 s2 m( J0 z9 c
  25.     printf("open socket: %d\n",sockfd);8 }$ o8 S. a. C: a: ^" r+ c
  26. ; x9 w! {3 y2 j0 e. T. {# J) d. }
  27.   //将本地端口和监听地址信息保存到套接字结构体中7 y. }- y- P- u
  28.   bzero(&addr,sizeof(addr));9 s" ^0 W2 Y6 G  S0 M2 j# R
  29.   addr.sin_family=AF_INET;7 M5 o1 L5 B) I1 L
  30.   addr.sin_port=htons(LOCAL_PORT);
    1 Y& e: Y  U8 T" L
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0. D9 f! l* `! m1 ]* M
  32.    ; p! I  g5 j5 @' I# e0 _/ y
  33.   //将套接字于端口号绑定' T: D' C4 x5 P( u9 {$ e
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    , t( l0 @9 A/ s/ j0 s( W
  35.     printf("bind the port: %d\n",LOCAL_PORT);4 z! q1 k4 T& |2 X
  36. 6 x3 W3 K3 }6 W" P: u8 s8 }6 z
  37.   //开启端口监听
    - S7 S( a" v# c) P- T* c2 w
  38.   if(listen(sockfd,3)>=0)
    6 ^$ s  x# ]7 ^& B, l
  39.     printf("begin listenning...\n");( j1 M; ?" m) B6 c- E3 W6 V
  40. ( x( Y$ r* |/ {/ G0 l" W
  41.   //默认所有fd没有被打开
    6 S# h1 c  r! j# U# R" g% U7 E$ c
  42.   for(fd=0;fd<MAX;fd++)( p/ t3 ~3 e$ G! D
  43.     is_connected[fd]=0;$ {$ Y6 L( _) r4 h1 b
  44. * Q/ e, L2 `) _: }
  45.   while(1){. \- d! ]5 F0 c4 v
  46.     //将服务端套接字加入集合中
      F) q4 \8 L# R
  47.     FD_ZERO(&fds);
    7 r  T  G1 t$ K$ y+ C
  48.     FD_SET(sockfd,&fds);
    : @5 [) N8 k: I% d( M2 K# E
  49.      7 ]/ F9 u. C2 E) t+ [
  50.     //将活跃的套接字加入集合中, A" N. y8 Y8 R7 G4 t
  51.     for(fd=0;fd<MAX;fd++)
    7 ~+ k+ w/ y3 G- E9 B% Z
  52.       if(is_connected[fd])
    1 o/ C; ]& ^- u
  53.         FD_SET(fd,&fds);
    # X0 F- t2 [; }& n3 w2 F& B1 I9 h( J
  54. ( N( |3 Z0 l' d6 t- b! E" R' f
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
    . J6 ]" s0 K( n5 L
  56.     if(!select(MAX,&fds,NULL,NULL,NULL)); u4 Z2 q- ?$ E
  57.       continue;, Z0 s6 A* {0 e* d) B3 L

  58. ) v: I; b) E; P5 V  n$ o9 |
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字
    * y" G  \. Y5 b. I+ O( Q
  60.     for(fd=0;fd<MAX;fd++){
    5 }$ J, w  y4 s/ [. n+ L
  61.       if(FD_ISSET(fd,&fds)){5 \0 X3 n) Q8 j7 E2 ^% g- Y) B1 g
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接
    / j1 O- ~) z& N4 B7 b' X4 `* j# Q
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);, {- Y7 \' M: h7 e
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语2 u7 H3 r8 g$ c7 b) M) N) \
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用
    * ?4 |+ {! H5 H# v
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));8 u$ H. h: y# B6 R; t5 ]- R4 n
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字3 O! K% S) b& ?
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
    : A) O2 R  U/ |1 w4 @  @, N5 I4 I
  69.             write(fd,msgbuffer,sizeof(msgbuffer));
    ) A9 |1 S- ~8 j' u3 z  W0 ^
  70.             printf("[read]: %s\n",msgbuffer);1 {. s5 y' m  T( n
  71.           }else{5 j& d1 V/ ?2 F% G" i
  72.              is_connected[fd]=0;
      m, w' X& n9 q- G2 O9 Q& m* _
  73.              close(fd);
    2 U0 y- y+ S# M8 l) ~* g
  74.              printf("close connected\n");
    ! J( ?3 [7 E$ {) A# u. {
  75.           }
    & d6 [" E5 K, Y; T- V
  76.         }8 b  Q- u$ H8 G4 v. s( j& B5 `. r
  77.       }
    ' R. E& M- r. l7 V; S9 [
  78.     }! C7 N! q& L' \) {0 o
  79.   }- ^0 S& b( j" t  E
  80. }
复制代码

3 v* w9 r2 i9 H
% ?( H6 ]$ T2 E. u& @* n- {& ]$ _" p+ ~- t: w/ U( C
1 U& @, t7 m0 r7 t) K

& i! I1 r6 V7 J$ ~- s+ l% ?
( {0 }: s7 a% e( U




欢迎光临 cncml手绘网 (http://bbs.cncml.com/) Powered by Discuz! X3.2