cncml手绘网
标题: 编写一个简单的TCP服务端和客户端 [打印本页]
作者: admin 时间: 2020-5-9 01:53
标题: 编写一个简单的TCP服务端和客户端
实验环境是linux系统,效果如下:
1.启动服务端程序,监听在6666端口上
2.启动客户端,与服务端建立TCP连接
3.建立完TCP连接,在客户端上向服务端发送消息
4.断开连接
实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
|( p- Q: G! ]0 K3 V5 w
什么是SOCKET(插口):
这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
- E. b' Z O+ w' A
"套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
具体其他高级的定义不是这里的重点。值得说的是:
每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)
% ]- U$ m2 a1 q: s# o$ C3 j
- V0 t3 [) A1 b: G8 i A7 F# n
$ ~( _0 W3 D$ T% `如何标识一个SOCKET:
如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等0 a4 v p6 G6 N
描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
; k$ G- [0 b. D% a( ^
8 [' l' `4 ~+ U M" d
服务端实现的流程:
1.服务端开启一个SOCKET(socket函数)
2.使用SOCKET绑定一个端口号(bind函数)
3.在这个端口号上开启监听功能(listen函数)
4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
5.接收或者回复消息(read函数 write函数)
, [9 o) S$ p3 n! h6 @2 o* x) U; M* V+ m; Z; g$ F) k j, S
客户端实现流程:
1.打开一个SOCKET
2.向指定的IP 和端口号发起连接(connect函数)
3.接收或者发送消息(send函数 recv函数)
- J* I& o2 M9 j& S) H" K7 b
3 ~/ Y n! S% E8 s* z8 j) j6 \+ M
. j0 o: L4 l4 {( l ~- g; J如何并发处理:
如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
) y0 r1 H5 h6 L( D. C. U, [: ~. s
% ~; Z0 ~; W" L5 \4 v3 Q, M1 Q如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
- int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为:
- #include <sys/time.h>1 M- [) `4 X. X1 m; d# Z0 z
- ' \/ u$ g& ~& q9 W; S5 u& x
- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
3 o% w& j& y1 b1 [ readset 用来检查可读性的一组文件描述字。
8 i5 N9 C6 m# i+ M9 H writeset 用来检查可写性的一组文件描述字。
# s4 R8 w6 {7 z, v! D0 y8 D
exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
* B' M$ d! Q1 T- w0 q/ u7 M
timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
# i, S; ~2 A* B/ p, D) q: b1 p5 F# C9 e0 P0 g, @2 P& _) X8 U
对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:8 d5 k$ {: L9 C/ C4 Q- O5 i C& E* F1 I
9 A- m# D1 C. t0 O& v
- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
* ^( x3 h" N+ g! ` - 7 q- u" Y0 U! C1 Y" d. F
- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回). T$ o6 o' |6 h4 T. b, _
% g1 _7 A' {8 n! h/ l- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值:
返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
其值通常是1024,这样就能表示<1024的fd。
2 h+ M/ ^. H) W8 p5 z- j J( G% \) _
+ o. A- e* m# w8 Q6 X
% y( G+ g! _" s, T
fd_set结构体:
文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
- FD_ZERO(*fds): 将fds设为空集
& ~9 ~. d& V0 `5 G; T -
" A. H2 ]8 U5 @4 W, B - FD_CLR(fd,*fds): 从集合fds中删除指定的fd
- f; F9 R; p4 b- p: T% t - % \& q+ s. c |2 n
- FD_SET(fd,*fds): 从集合fds中添加指定的fd4 ], W. u( N: z: T, r) n
) X' j+ v3 ?. H8 l( @- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下
- socket s;* _+ {/ V: G& `/ d7 O u* M
- .....( s1 k$ ]' M2 l; i7 W) ]: l
- fd_set set;0 p; ~ n. U, ?0 J' Z
- while(1){( w0 A: X/ {! D- B4 `! |2 H
- FD_ZERO(&set); //将你的套节字集合清空8 u: v4 G% Q+ \! m6 A
- FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s
9 d: v2 g6 p9 q - select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,
1 Z+ X; t6 O! c8 D) R- e* n- R - if(FD_ISSET(s, &set) //检查s是否在这个集合里面,1 `8 X9 o- S+ g4 V' J; J. x
- { //select将更新这个集合,把其中不可读的套节字去掉
' ]2 ^( R1 _9 g% S& L+ N - //只保留符合条件的套节字在这个集合里面# X/ k* ?& w4 i; L) \$ B
- recv(s,...); N7 l- |( K [
- }% U/ I! W4 L) M t9 U
- //do something here7 V( s7 @. M. G# @+ m+ r
- }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
- (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。7 \, @6 \% V1 y3 _
1 E0 C5 j/ a8 y$ v4 v- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)) S9 V+ u/ v- F" g; Y Q
- - X, x" V* B* ~8 _' N# Z7 O+ Y D
- (3)若再加入fd=2,fd=1 则set变为 0001,0011+ E9 ]# Q* e0 w9 [5 e5 M
$ x8 E0 `0 n; r9 e4 ^* } `- (4)执行select(6,&set,0,0,0) 阻塞等待3 a. i, g% S$ v( |
- + e+ Z: \ r; z+ N; _
- (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.具体过程看代码会好理解
; C+ }5 ?/ \7 d: A) \( Z; P' @2 p$ p$ J7 p8 q: F/ w+ Z% ~0 E
使用select函数的过程一般是:
. D6 s! I# H5 E! D( ?# N, _ 先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。! K# p! k) A0 v t5 |5 i
% X9 }* ~/ T. k9 f: e! L
客户端:
- #include <time.h>
% \! M2 l6 H+ m1 u, V - #include <stdio.h>' w; e! R# C' V0 O" ~
- #include <stdlib.h>" _3 z! d. l7 w
- #include <string.h>
' F w6 }! \+ L" c - #include <unistd.h>) f0 Q. ^9 G5 O) E* _
- #include <arpa/inet.h>
O3 L* |; V3 H1 l5 R8 z. ` P - #include <netinet/in.h>+ N1 k1 T/ Y" j
- #include <fcntl.h>
/ l; O, s) X" F: c* x% ]4 | - #include <sys/stat.h>6 Q2 f. }4 L! a0 H0 r$ x
- #include <sys/types.h>- m/ Y: o# ^7 q R9 R) _, a3 q
- #include <sys/socket.h>
6 m2 W* t3 J. c$ l* A -
0 K7 l5 k( N* ] - #define REMOTE_PORT 6666 //服务器端口
: p6 k) ~' U8 f) c' R) k - #define REMOTE_ADDR "127.0.0.1" //服务器地址2 p) I2 {: M T$ L2 |( g
- , x6 w. ^: a- ^5 b# q3 V! Q* v
- int main(){: b: S# `% D0 R' V1 A
- int sockfd;
& v, e7 K( }4 }$ S0 M - struct sockaddr_in addr;( W- F6 a' } f2 [/ z9 m
- char msgbuffer[256];2 p6 }' ?) R1 _0 X! h
-
9 q% M% v. H" j; o h - //创建套接字4 K& K' B9 m M- S- \
- sockfd = socket(AF_INET,SOCK_STREAM,0);
5 D! X* P9 [+ t5 R' r# l5 L - if(sockfd>=0)3 n3 h, ]4 l. |- I7 k% V f4 q: H
- printf("open socket: %d\n",sockfd);
* X S0 O- F3 b - 4 X2 D3 T Q A/ ~1 @( ~
- //将服务器的地址和端口存储于套接字结构体中
4 j4 S" z+ f u5 L! U1 O4 _5 D: P - bzero(&addr,sizeof(addr));4 u# p5 V( W9 r" Z4 c6 X
- addr.sin_family=AF_INET;& L4 m" Z/ F% Y
- addr.sin_port=htons(REMOTE_PORT);
' ^) m4 @- w! u# e1 K2 v - addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);& ^: r$ B+ C# e! l- g/ v: p+ Q
-
) A& k! ^+ n0 T% b - //向服务器发送请求' ?- ]& T0 d& y! ]7 ?3 h
- if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)' A" r0 N! @2 i8 c
- printf("connect successfully\n");0 n, V' \5 |' w) V3 u/ h- k
-
$ e$ Y. i) m$ P; R - //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
' {( _' w' l0 o8 | - recv(sockfd,msgbuffer,sizeof(msgbuffer),0); Y/ z. n. t# T- g4 r3 t" s0 w
- printf("%s\n",msgbuffer);( ^% f7 x5 \# W8 T$ x# I
- 2 \& z. N- u. G! Q. r
- while(1){
: `0 K* ? C% Y5 q - //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
% i' H" [5 A6 G n, W: h - bzero(msgbuffer,sizeof(msgbuffer));
6 ~/ O" R( s k0 H - read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));% N& H- k% u( |! K. C3 _
- if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)! b6 _: S4 ?% O6 N: B2 ]5 ^
- perror("ERROR");
3 M. }- ^! m( n0 C" { -
) e8 c& I3 C* M$ e: u/ d; s0 n - bzero(msgbuffer,sizeof(msgbuffer));
0 B- ^; Q8 T9 y0 H - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
; w( Z( L+ W' ]# F4 ]- F7 G - printf("[receive]:%s\n",msgbuffer);. h- v, v: d0 x
- # T+ \' w) p: L" b; ~# }9 F: u' h \
- usleep(500000);
9 Y3 i' g0 }8 f$ Y! y2 d$ _ - }+ b9 m5 z# ?( \5 r S
- }
复制代码 * K9 S: U' v+ n! J0 T
5 |; n& `; l$ {' e& u服务端:
- #include <time.h>
% c9 {2 ~/ H9 v: k6 ^ - #include <stdio.h>
- a% O+ Y ~7 t9 M - #include <stdlib.h>" I7 f0 ^# C6 b
- #include <string.h>
* v: z) G$ ]! N: m - #include <unistd.h>5 `* l7 M) E- |$ b
- #include <arpa/inet.h>" p+ P# v0 Z9 L) u& |4 n
- #include <netinet/in.h>( C* J8 K9 a8 Y' {. |
- #include <sys/types.h>, l4 }0 Q$ N- Z3 H; f2 s- O
- #include <sys/socket.h>
$ i" ^) f! x. v7 h3 f5 p$ s -
) h' ]! i; K3 W - #define LOCAL_PORT 6666 //本地服务端口, t8 M, U; E+ F3 l" @
- #define MAX 5 //最大连接数量7 I6 _8 U/ {1 K$ `2 _
- ( M2 `* v }7 }) _" o) k' ~+ b6 }& r- E
- int main(){4 g. n+ G1 C. m4 z7 L% ~/ q
- int sockfd,connfd,fd,is_connected[MAX];
; I& S, E3 s, x: |& F6 `4 R8 f - struct sockaddr_in addr;/ V6 d9 _6 O* ?; i( i; l8 B; U+ J
- int addr_len = sizeof(struct sockaddr_in);* H$ F! p. x8 w) g
- char msgbuffer[256];
- k9 e$ m L7 J' F9 I! I1 x ?- V4 r - char msgsend[] = "Welcome To Demon Server";
, v- h, W* Z }2 ^+ Y+ M/ ~. T - fd_set fds;
2 H" x5 r5 H; P& G- u -
3 U4 m8 L& l: J" p# o+ d - //创建套接字
- u& H1 M. q, {5 D6 W - sockfd = socket(AF_INET,SOCK_STREAM,0);
) J n; F+ T+ W - if(sockfd>=0)
9 O# e+ W! M3 [5 x - printf("open socket: %d\n",sockfd);! _9 j! `5 ^# Q3 B! o" F( x+ D
-
6 u, {8 u4 {+ s - //将本地端口和监听地址信息保存到套接字结构体中6 D2 S6 X# h- t( T+ N4 p$ ]. s- ^
- bzero(&addr,sizeof(addr));
5 h4 W3 |" e0 s) L5 B% k- Z8 L - addr.sin_family=AF_INET;
+ ?; N0 D) P" b4 i3 d# D8 ^ - addr.sin_port=htons(LOCAL_PORT);1 d1 w Z. S# g" x* j3 ]
- addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.07 Z+ e$ r$ ~/ s4 g+ A
-
- |8 G+ P" T2 ^1 A( Q& t- ? - //将套接字于端口号绑定
5 s& `. ~- d/ N - if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
8 \1 L1 O+ E B7 T - printf("bind the port: %d\n",LOCAL_PORT);" D g$ Q I u/ Z8 J
-
; G2 F5 W2 z2 R P - //开启端口监听, d9 _! i' o5 q0 u/ l3 A
- if(listen(sockfd,3)>=0)
# _- a4 @5 c" E2 f - printf("begin listenning...\n");, ?1 P0 y2 W! }
- : W9 J& N, ^$ v0 B0 N: K4 R
- //默认所有fd没有被打开4 z6 j6 |! L# }. z3 m
- for(fd=0;fd<MAX;fd++)- i: L- i* u& H- B
- is_connected[fd]=0;9 w6 b' {2 o2 B
- 8 k2 ^0 o q1 T* [- m' V' d; d' ?
- while(1){
, c# O4 q, G) w( T# I/ G6 W6 r - //将服务端套接字加入集合中3 I8 h7 k+ K$ k/ M: A: K
- FD_ZERO(&fds);
8 R0 I4 M" a3 K, l/ t/ M0 M; ]7 ]9 _ - FD_SET(sockfd,&fds);5 D& E" p; A7 X5 Z9 a
-
& _" J. |: m3 F - //将活跃的套接字加入集合中9 B) K% T0 z' |9 M' g
- for(fd=0;fd<MAX;fd++)
6 X: D/ f' O' H - if(is_connected[fd])
0 @. D3 r8 M/ @: m6 _3 V - FD_SET(fd,&fds);
3 V" _, }# R8 \ - 3 O3 K3 T/ b2 E M( h
- //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
. C9 e U. C8 e, d - if(!select(MAX,&fds,NULL,NULL,NULL)). n' _1 P; i u" H! \0 G
- continue;9 T9 |' `( d( N% m
- 7 f4 z6 X! r4 o; c, K) F: S
- //遍历所有套接字判断是否在属于集合中的活跃套接字
' R& B" Y% t7 I! U F1 \$ @ - for(fd=0;fd<MAX;fd++){
8 Z' V. c* K. l( ^! r - if(FD_ISSET(fd,&fds)){
6 q. B8 R) ~+ ?$ M - if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接7 m4 |) y( u5 A/ N: W" M" M
- connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);, k2 ~9 ^2 y h9 R
- write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语
& S" A* U* {. b# _- `7 B3 k5 f3 a - is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用% n; O, {8 H6 |1 [* m" W
- printf("connected from %s\n",inet_ntoa(addr.sin_addr));' R' U! | C3 f0 n
- }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字, f* p2 t3 O' M* o( d
- if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
3 {% d' _( I( i! a% z9 ]" A - write(fd,msgbuffer,sizeof(msgbuffer));4 o$ K$ n/ F% V( l' J; |
- printf("[read]: %s\n",msgbuffer);
) ~+ e. A! q$ w2 C( U) R - }else{
; u2 T+ L( B: V1 ]& p. H - is_connected[fd]=0;
9 \+ r# `3 V5 R/ o% W - close(fd);% ]1 X5 X" j9 L X
- printf("close connected\n");
5 b8 l$ ]+ W) Z+ v - }
0 B; |& v' g! R+ s/ |+ m& [ - }$ J2 _; j/ B/ { B' x
- }
$ O/ c! q9 b7 H' K. ]: t$ q - }" _" c) H4 X- v) o, b4 G" I9 N/ U5 [/ L
- }
9 D D/ t/ N4 r7 m* U: y1 H- K+ d - }
复制代码
* X) `8 @5 r/ |2 F$ J5 z+ u4 W! _, G& `
2 n W3 w! I4 m- l% r) f. ]+ |. F- _4 F9 W1 A
0 Z8 ?) ~0 K5 |+ L; L7 A g$ ~' B$ i1 D/ G$ W8 L: U! m! k- L
欢迎光临 cncml手绘网 (http://cncml.com/) |
Powered by Discuz! X3.2 |