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

QQ登录

只需一步,快速开始

 找回密码
 立即注册

QQ登录

只需一步,快速开始

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

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

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

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

) f/ ]7 z8 @0 f0 l: E
/ p) |1 s& d0 ?/ e' H' k. w
如何标识一个SOCKET:
       如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
       SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等( E6 y/ a  G" z$ ^5 ]8 P% o
       描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
       述符前三个标识符0  1  2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
       当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3

* d: J4 L% v* r4 [# ^' r% l# l
, F% `& D/ c1 `9 E, X1 Y
服务端实现的流程:
       1.服务端开启一个SOCKET(socket函数)
       2.使用SOCKET绑定一个端口号(bind函数)
       3.在这个端口号上开启监听功能(listen函数)
       4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
       5.接收或者回复消息(read函数 write函数)

, V; o) _# _4 o/ f' M9 I, h6 j
: t- M, K/ z. I+ f$ C) h" L4 s
客户端实现流程:
      1.打开一个SOCKET
      2.向指定的IP 和端口号发起连接(connect函数)
      3.接收或者发送消息(send函数  recv函数)

$ U8 I3 u: T1 S
* q! I, Z4 z* x9 l6 c( w/ a' v
9 T8 e: {/ G5 e& P0 @  O4 H4 u: W  z+ d
如何并发处理:
      如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
      直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
      一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
      有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
      单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
$ V, z+ f2 Q; F  R* g& R

9 G0 Y4 A" E, f+ h
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
  1. int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码
所在的头文件为:
  1. #include <sys/time.h>
    1 @. V5 a7 |1 l2 U8 X6 o2 `
  2. 3 `. l' R) I" ]- J6 D/ _" x
  3. #include <unistd.h>
复制代码
  功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
: W" O$ N% |5 a. A$ O2 _6 d- s6 ^
    readset  用来检查可读性的一组文件描述字。

- w1 h9 Y' F( M, P5 B    writeset 用来检查可写性的一组文件描述字。
* d, Y$ W* k! K9 v3 ~2 d1 t
    exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)

  G- b$ O/ ~2 [# u& Z    timeout  用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
" x0 ~+ w; n- u& N9 q6 J* l8 y; N% y6 v' v1 F
    对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:4 P; S# g# p  k
6 y$ N: n* _3 E4 A
  1. 1.timeout=NULL                 (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件); t* ~; r( X  {  c- K3 i- G& K
  2. + y, Q# P; A4 o- d/ ?
  3.     2.timeout所指向的结构设为非零时间  (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)8 B2 ?9 r$ ^4 N& r9 W( T

  4. 9 ~  E$ l/ N1 ~5 {" Z1 i7 {) r$ D, R
  5.     3.timeout所指向的结构,时间设为0   (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码
   返回值:
    返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
   你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
   现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
   其值通常是1024,这样就能表示<1024的fd。. ]8 O+ a* e4 t1 H0 v' d$ p, O5 z/ F
. r  S" q8 e, B4 d8 B: E" J* t
   
0 M7 B# {2 X: Y
fd_set结构体:
     文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
       可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
  1. FD_ZERO(*fds):     将fds设为空集4 o) ]: A3 z3 ^: L4 q8 `
  2.    
    - N, O7 C# B* A3 v' R5 E2 j
  3. FD_CLR(fd,*fds):   从集合fds中删除指定的fd9 L! V5 R4 s7 J
  4. % t* h' B; {) G3 n* T- a
  5. FD_SET(fd,*fds):   从集合fds中添加指定的fd
    ! b- e# P1 j, d- D3 x
  6. ( C- e# m. M- }- R5 N% }/ p3 Q
  7. FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码
步骤如下
  1. socket s;4 f9 ?$ d% d4 Y- ]: M, f
  2. .....
    4 p% g: |- Q$ U2 x7 [- V9 U
  3. fd_set set;
    7 f+ D# U% y& q
  4. while(1){
    2 }# ]% W9 s! z/ M: O" U3 x
  5. FD_ZERO(&set);                    //将你的套节字集合清空' m5 w/ o; n* B
  6. FD_SET(s, &set);                 //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s# u5 Y1 S! M" N0 v
  7. select(0,&set,NULL,NULL,NULL);   //检查套节字是否可读,/ o8 {5 C0 r* T
  8. if(FD_ISSET(s, &set)            //检查s是否在这个集合里面,
    ) B9 i+ G! y' _  Y
  9. {                               //select将更新这个集合,把其中不可读的套节字去掉2 ?) F/ m6 I2 D! F
  10.                                 //只保留符合条件的套节字在这个集合里面
    & u- p  |/ z$ p8 {  s* d- A+ O8 Q
  11. recv(s,...);, \- `! W( I4 S& b9 f5 r$ i
  12. }2 v+ s' x, X0 v+ D
  13. //do something here
    ! p) t9 d0 {7 j$ M: T# j
  14. }
复制代码
假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
  1. (1)执行fd_set set; FD_ZERO(&set);  则set用位为0000,0000。
    2 C. V. j- p7 C

  2. 0 w* U2 w0 A% j) X& x7 _8 {* L, _, N
  3.    (2)若fd=5,执行FD_SET(fd,&set);     后set变为 0001,0000(第5位置为1)
    4 U# M- x8 I  ^5 k& H8 {
  4. 1 A: `* z& a! |0 h4 H! P+ s
  5.    (3)若再加入fd=2,fd=1               则set变为 0001,0011+ d9 C3 {# }7 e2 r3 t
  6. 8 q9 [! G  p) m6 j
  7.    (4)执行select(6,&set,0,0,0)        阻塞等待( W. v8 r7 {! Q+ V% r1 u; y

  8. . B9 c8 G, u( p7 v! O4 b, \5 N( A
  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 }6 S* A' T2 J- W0 e% f4 r
9 e& F: x1 U2 P: G
使用select函数的过程一般是:

2 ~4 J1 V6 K+ ~0 g# a& D    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
    接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
     复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。. \7 O. N. m! k% }% V' }
+ d3 D) A5 h0 u! }- l9 J7 V
客户端:
  1. #include <time.h>0 _/ y  G/ P$ p# r
  2. #include <stdio.h>' @" v. L6 T4 ^8 z  r/ \
  3. #include <stdlib.h>+ B6 v% W% K1 P& P
  4. #include <string.h>3 R5 n0 v" _  L0 z
  5. #include <unistd.h># Q4 @  ]* q* m! c- H( `4 U
  6. #include <arpa/inet.h>
    4 k& b7 w6 u2 n2 N8 X8 ]7 }) o
  7. #include <netinet/in.h>
      c/ _6 g4 @* r+ E- j6 T
  8. #include <fcntl.h>5 a* j9 q- M. w$ w- n8 A
  9. #include <sys/stat.h>/ p' m+ w% W5 N( Z% x
  10. #include <sys/types.h>
    ' n  a8 h: M  Z; v" q# f
  11. #include <sys/socket.h>, C' X  `$ g' E, m7 S  d. d

  12. 0 J- e; o' l) s1 ~" _
  13. #define REMOTE_PORT 6666        //服务器端口
    ' m5 x2 L. c' }$ _( {
  14. #define REMOTE_ADDR "127.0.0.1"     //服务器地址
    7 q% V7 [: u; h  {& w

  15.   T& S$ E$ i& V0 ]- ]# S6 _/ R
  16. int main(){
    6 s# n) `& @& W$ t2 F5 b
  17.   int sockfd;9 ]. z# v, [7 C. j* s( n
  18.   struct sockaddr_in addr;
      z# `% D: E+ V# w! E3 z: P
  19.   char msgbuffer[256];" N3 ~# v5 h6 X2 Z
  20.    $ d8 m# e* u8 k5 A& B
  21.   //创建套接字. b/ T- i+ T( X( _: U. M
  22.   sockfd = socket(AF_INET,SOCK_STREAM,0);, z" ?! v6 `1 G) ?$ `! H
  23.   if(sockfd>=0)
    % y, B1 Q  S- W& N( G+ G! C1 G5 @; x
  24.     printf("open socket: %d\n",sockfd);  M" F0 L" @3 @+ b& M/ K
  25. ' |" V, ^4 N! n! ~6 q' u
  26.   //将服务器的地址和端口存储于套接字结构体中
    % U8 Z1 l  G1 ?; _' y4 ^1 z3 F
  27.   bzero(&addr,sizeof(addr));# a5 ?; |3 l! r' k+ i; |
  28.   addr.sin_family=AF_INET;
    " n7 _5 @" y& T: U" A
  29.   addr.sin_port=htons(REMOTE_PORT);; X& ]7 x) \& `. ^
  30.   addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);+ _0 _1 N5 l& x
  31.   # u% ~( A3 b& n& L) M  V
  32.   //向服务器发送请求6 G& Q- J& H0 W$ V$ B, j
  33.   if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
    0 e# E; U( |, z1 r- z$ P) b. {
  34.     printf("connect successfully\n");
    ! R! u% o! Q, g* m; J
  35.    
    1 d6 ^6 i7 |* z+ X9 ?" f
  36.   //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行); b; J9 q$ y$ p: N4 s! E1 F- \7 H
  37.   recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    1 t" w0 N3 y5 a4 e; y! \
  38.     printf("%s\n",msgbuffer);# z3 O5 a$ D5 u( E/ e
  39.   
    * C8 x& a7 J( R" \4 L
  40.   while(1){
    ) ?, n0 K: N- N9 ]1 E4 q
  41.     //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息& p7 c+ z/ ?3 m- ?% ~
  42.     bzero(msgbuffer,sizeof(msgbuffer));8 _# T6 W9 d. q( D5 l3 ?
  43.     read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
    ; X; X! T3 n) n4 ]' n: i  J
  44.     if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0). J$ ~/ I3 S: J# A) E
  45.       perror("ERROR");
    # W6 G  B( N" f/ t4 N, l) y
  46.     . _. m- E5 Y, [# O+ M5 H! H/ h
  47.     bzero(msgbuffer,sizeof(msgbuffer));
    : w, Q+ W+ F: C$ {$ A; Y- p2 Z
  48.     recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
    ' F/ l% v0 j' \7 V
  49.     printf("[receive]:%s\n",msgbuffer);
    + t7 g) t; R3 l* y6 R2 H: X
  50.    
    ( P8 @+ [& n8 O4 ~; ]5 J
  51.     usleep(500000);
    , {; `: B; C/ B% C# n
  52.   }/ K+ M0 {- u% Q
  53. }
复制代码

6 x" q3 h4 b2 ]5 {/ }9 z9 Q  U2 N$ V1 S3 h
服务端:
  1. #include <time.h>
    , ?* n& L' F- i) l
  2. #include <stdio.h>
    4 X1 ~2 j& R* C& O
  3. #include <stdlib.h>9 y* Z9 Q5 a7 R& p6 e7 D4 S
  4. #include <string.h>; A0 W% X. d6 ]: ?# S$ u
  5. #include <unistd.h>
    & j3 c* U$ G: b8 Z. X8 W2 S: h
  6. #include <arpa/inet.h>' R" g' h* ?' K' |$ h/ e$ I' L5 ~
  7. #include <netinet/in.h>
    + y; Y+ }( p7 A/ }: B0 M
  8. #include <sys/types.h>/ A) r: G7 B: Z
  9. #include <sys/socket.h>% a7 \1 A9 S0 f9 \* I

  10. 3 {) n0 P) n2 N1 @
  11. #define LOCAL_PORT 6666      //本地服务端口
    ' y2 h+ [9 K& ?* h/ N
  12. #define MAX 5            //最大连接数量
    : F+ W1 I! s# _- e8 K- _4 M

  13. ; |9 Z; x* o8 W
  14. int main(){( I2 Q3 Z0 s6 \, i# B8 g
  15.   int sockfd,connfd,fd,is_connected[MAX];
    $ h0 a4 P: `0 H$ O& d7 J
  16.   struct sockaddr_in addr;
    / j& O% y; [* E0 m4 h: q7 N$ ~8 o6 \
  17.   int addr_len = sizeof(struct sockaddr_in);
    : }6 k. f6 y/ y% Y3 {9 ?' }* o
  18.   char msgbuffer[256];
    * ^- ], E! u/ \5 Z  S
  19.   char msgsend[] = "Welcome To Demon Server";/ M1 r/ E. }! u- t* u* ^
  20.   fd_set fds;% e4 f! @1 T8 T$ u# c
  21.    
    3 R% ?) C! Q' a3 \  K- D" Z6 f9 P
  22.   //创建套接字  R, a7 \  J; r4 m" J8 G
  23.   sockfd = socket(AF_INET,SOCK_STREAM,0);
    . B; ]3 K8 _% }3 w
  24.   if(sockfd>=0)
    - ~. j$ p2 Q# ]( i: t+ f( k# S
  25.     printf("open socket: %d\n",sockfd);
    - Z1 [" J+ Z: i9 t' a5 f$ q

  26. " g9 V' Y. E8 `' F9 H: _4 b
  27.   //将本地端口和监听地址信息保存到套接字结构体中
    0 i. c- P6 i3 C5 E4 k+ Y: x
  28.   bzero(&addr,sizeof(addr));
    4 F$ @4 F% l$ u& q0 i2 g' O
  29.   addr.sin_family=AF_INET;
    , w! C, B* t# C. |' W; ^6 f
  30.   addr.sin_port=htons(LOCAL_PORT);/ e; ^9 l, s8 x7 i$ j5 |
  31.   addr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
    & ~( a9 n+ N0 p( }1 G
  32.    5 P( {9 E' W* N  N# j; @! X
  33.   //将套接字于端口号绑定
    : ?6 s2 C# u% P: A* v8 x9 D
  34.   if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)0 f, ^& @1 V. y: W* A/ E2 A
  35.     printf("bind the port: %d\n",LOCAL_PORT);
    5 I% Z6 g3 l2 w2 v; I- U. s5 p& ~

  36. : W0 m- \- ~. _7 k  B" D" u
  37.   //开启端口监听
    2 q+ l6 \: T( I# }+ y) i7 Q
  38.   if(listen(sockfd,3)>=0)& `5 k( o3 x$ }6 h( D& F
  39.     printf("begin listenning...\n");3 X6 C$ b7 U6 Q  h# A

  40. " `4 D& c" ]9 B" a8 L
  41.   //默认所有fd没有被打开6 A0 G; r2 r8 w. t$ U7 ~3 j
  42.   for(fd=0;fd<MAX;fd++)
    * m5 Z- F1 Z5 k! D
  43.     is_connected[fd]=0;
    ) Y* J$ L- Z9 `% Z$ i; O

  44. # M9 L5 }6 u: Z
  45.   while(1){3 [/ v/ G6 V0 Q2 I9 {5 [
  46.     //将服务端套接字加入集合中
    , {+ l0 \( q( ~( V. L4 ?
  47.     FD_ZERO(&fds);
    ( t  l% q$ C4 S; X8 e& _' D6 }6 k
  48.     FD_SET(sockfd,&fds);+ T, v5 P& z; j4 P
  49.      8 e$ s% p% f5 ?, P
  50.     //将活跃的套接字加入集合中" g+ U( [' c" L* j+ r+ \0 ?8 b/ ^: @
  51.     for(fd=0;fd<MAX;fd++)& [, x, |2 Z: ^* Z
  52.       if(is_connected[fd])
    6 c5 Q' z! \, `. [
  53.         FD_SET(fd,&fds);0 ^( H: V  R( F7 J7 h, u
  54. $ x- q7 y# G+ J! y
  55.     //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
    ) L& c: g3 W. Q5 D
  56.     if(!select(MAX,&fds,NULL,NULL,NULL))9 r9 z. V' g* f: \
  57.       continue;
    $ @+ i! e+ n" z& \3 S

  58.   M% L4 U5 |* `8 c
  59.     //遍历所有套接字判断是否在属于集合中的活跃套接字) G+ [. H# f' t
  60.     for(fd=0;fd<MAX;fd++){
    5 i. y# D; c  k: q
  61.       if(FD_ISSET(fd,&fds)){
    ; ^3 y& u# f* @4 }- L0 o1 ?* E
  62.         if(fd==sockfd){                             //如果套接字是服务端,那么与客户端accept建立连接
    2 d* n* [& {. U
  63.           connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);* S6 z; ^- ?/ z. ^7 j. o' [
  64.           write(connfd,msgsend,sizeof(msgsend));    //向其输出欢迎语
    . G; Q8 B) P0 [
  65.           is_connected[connfd]=1;                   //对客户端的fd对应下标将其设为活跃状态,方便下次调用
    5 D. k9 ~3 ?. ]
  66.           printf("connected from %s\n",inet_ntoa(addr.sin_addr));  c# O8 ~7 l- Q% X
  67.         }else{                                      //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
    , {  g6 ?& [( V% Z4 o  s
  68.           if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ 7 P6 N* I2 y: P
  69.             write(fd,msgbuffer,sizeof(msgbuffer));
    4 k* v: h$ Y7 ^$ W
  70.             printf("[read]: %s\n",msgbuffer);
    5 k: i9 q* n& P( h9 n
  71.           }else{
    , z2 z4 l/ V7 J0 Q
  72.              is_connected[fd]=0;# c! `4 @# @# z8 j, a
  73.              close(fd);) K6 u! V9 d& |% P
  74.              printf("close connected\n");
    4 R0 S" f1 o' N# i6 ^) c! v
  75.           }
    " _& b# i. Z7 a
  76.         }
    & h! u1 P  n0 E! y- d
  77.       }
    # b8 y) o5 U- c* ]! |' @$ P
  78.     }* o& H8 H& j0 A: j, z
  79.   }
    5 C* g* V3 U1 h0 N) p+ T
  80. }
复制代码
% Q+ D& E8 u1 h

8 \1 k0 c) o. B4 Y& w5 \
" c# @, M- ]1 g. o$ ^) t
+ [5 w! [, J3 B0 A
+ o" s& d3 E7 @' f$ h) o& ~: d  J* w" N$ F" Z, x
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

GMT+8, 2026-3-16 13:36 , Processed in 0.082525 second(s), 24 queries .

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