|
实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上 2.启动客户端,与服务端建立TCP连接 3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点 " @* A( n+ N4 F$ s
什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
- G3 e# ~8 {+ L/ V/ Y "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。 对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。 具体其他高级的定义不是这里的重点。值得说的是: 每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。 应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)
! M1 q% f% r3 B $ U/ V& K1 A0 a' h
0 O% q) [1 j! Y3 H8 u+ S如何标识一个SOCKET: 如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个 SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等: a& M: o5 v, k5 x. F
描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。 述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出 当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3 5 a2 n2 W, x* o. b
4 H% n( a& a) X- U# ?
服务端实现的流程: 1.服务端开启一个SOCKET(socket函数) 2.使用SOCKET绑定一个端口号(bind函数) 3.在这个端口号上开启监听功能(listen函数) 4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数) 5.接收或者回复消息(read函数 write函数)
& M$ n9 ~! [; K3 C" k2 ]9 U/ g$ |& H) }/ w( i9 X
客户端实现流程: 1.打开一个SOCKET 2.向指定的IP 和端口号发起连接(connect函数) 3.接收或者发送消息(send函数 recv函数) ' ^: @! ]) H3 j1 m6 n6 f
0 [' z7 j! ^$ [; c# E4 @
( |1 E8 L5 p" a6 {% |7 x如何并发处理: 如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果 直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端 一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到 有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照 单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
& p5 V" O; @& r* F% Q+ o! r I5 [; X% \* m$ U+ @0 B9 N/ d: G0 r
如何解决: 下面摘文截取网上的资料,有兴趣者可以看看 系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你 - int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为: - #include <sys/time.h>/ Q; F. X5 U! b7 r
$ |7 q- V0 Q* b+ H/ X0 B) o- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
( b* s; b1 W3 ]! n! c3 A3 [" Z; W readset 用来检查可读性的一组文件描述字。
7 S- Z3 h; z. \! J( ] writeset 用来检查可写性的一组文件描述字。
4 A3 U* R4 N" }3 }$ t5 y" A
exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
, }% o# ^7 [* B" M& u" @ timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。2 J4 V+ q* x2 ^ c7 G& D
% k/ y+ [6 C' l1 _; d4 {- \. S
对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:2 C9 m0 ]) c% }! D8 C# ~( i m! u: K, `
3 S3 } w" o' K# @- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
3 I/ X# S7 _# ?7 L
1 `! F9 }' e3 b+ a z9 W, E- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)( f0 g1 ^6 Z# w# K
- 0 Q% J. |/ y! E. {$ v# n
- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值: 返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。 否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来, 你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量, 其值通常是1024,这样就能表示<1024的fd。9 u ~6 {7 g- ^. D+ O2 R
$ k: j6 X2 E2 H/ [' g, h" c
& c( N+ g" H6 E9 c, j( n7 | fd_set结构体: 文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字) 可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作: - FD_ZERO(*fds): 将fds设为空集
' {8 B5 h& ~. q# a -
9 }; N. Z5 E3 J9 i; G! m. \ - FD_CLR(fd,*fds): 从集合fds中删除指定的fd) `. y! g/ l) _4 n! D6 }' S
$ w5 L G4 h$ E. Y, R- FD_SET(fd,*fds): 从集合fds中添加指定的fd
5 G: d! u7 f7 `% j8 f1 V+ Z - $ r/ ~% D. s9 `' F
- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下 - socket s;
( N+ B5 K8 y" x( w" L8 [ - .....! X) f5 G6 f5 i- \8 c+ N8 R/ o
- fd_set set;
9 Z" r( R. I) V/ k p8 e - while(1){* T! K7 [ S, g6 x8 Y, Z' r% l
- FD_ZERO(&set); //将你的套节字集合清空
4 V( j. O: M+ i/ F* q8 r: G - FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s+ L1 @4 X* x* Q% |+ C
- select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,! A; D0 x n. G
- if(FD_ISSET(s, &set) //检查s是否在这个集合里面,
: n4 j0 Q0 A( ?% t - { //select将更新这个集合,把其中不可读的套节字去掉
7 d9 v, l( }8 C! U6 ]2 Q4 J - //只保留符合条件的套节字在这个集合里面 }9 h7 c* x" T/ s, h
- recv(s,...);/ M. x8 B( q9 x* N3 c9 ~% e
- }
9 q. Y" k' N! N. Q2 O( p8 U5 W - //do something here
# t. w" M: d" \% N. k+ t+ { - }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd - (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。; H0 n1 _, |. r( ?/ V( M
- 6 Y0 w9 q- q* p
- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)
. C, R+ M$ Z4 Q& M' h& A
" {) ~# o7 A& g- (3)若再加入fd=2,fd=1 则set变为 0001,0011
8 W( Y2 c9 r: r1 w: J. c* y! ? - : u" a0 G9 y% }# A1 a
- (4)执行select(6,&set,0,0,0) 阻塞等待' b' i6 P) o7 e) y" Z* \
2 L$ v" d$ |7 D) \4 @2 U+ ^ }- (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.具体过程看代码会好理解 % E2 R' c( Q! O) ^+ `3 z& E
' {$ F b) w) W5 Y0 H4 H
使用select函数的过程一般是:
/ [- R% e' V# J 先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1 复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。& t; G: S% a, ~' u' l _2 J7 O
- o8 m/ l4 S5 U客户端: - #include <time.h>
& O+ d/ j$ }4 t1 ?6 a u - #include <stdio.h>, M3 k0 H: N* H+ h& C
- #include <stdlib.h>" w: f, F5 [+ R- a/ A6 r" g. O- Q U
- #include <string.h>1 y f& T: u* X4 @/ U. Q' r2 }
- #include <unistd.h>, g( [9 L& P5 u7 T- t3 s/ T1 x
- #include <arpa/inet.h>+ h9 ^* P5 ]: ^, _ E
- #include <netinet/in.h>
! N9 m! S: b/ A2 K - #include <fcntl.h>, I3 T8 t! R5 q) n
- #include <sys/stat.h>2 P( Y% | u/ K0 t( k
- #include <sys/types.h>9 L; e8 E3 L; ~2 r( p
- #include <sys/socket.h> k2 H u0 g4 n" h$ t
- 7 `/ ?. @7 o* { K; ~ j
- #define REMOTE_PORT 6666 //服务器端口+ I6 ]% O% S& e1 @' H5 h+ D) a- M. `
- #define REMOTE_ADDR "127.0.0.1" //服务器地址
' n. t) E: O% t& ^3 k V, V8 ?: A' U -
2 e# D7 e- e6 ~& F! c - int main(){8 T9 {) |/ t% [3 y" l4 @
- int sockfd;9 o5 f. l" p0 o
- struct sockaddr_in addr;
; T( O9 k" I% x. e2 V* T b - char msgbuffer[256];; _8 d0 g& m; Z# T, V0 ]
-
# G# f" F9 N4 O& [" a - //创建套接字5 g& w0 S- Q0 g( o
- sockfd = socket(AF_INET,SOCK_STREAM,0);' M* G K, ?5 o. S( r b
- if(sockfd>=0)
* V4 ~1 S G3 _( {& b - printf("open socket: %d\n",sockfd);( H- x$ O3 \$ X) A
-
9 ]7 t, L! F: b! [0 Y { - //将服务器的地址和端口存储于套接字结构体中4 y+ e+ T1 {* y
- bzero(&addr,sizeof(addr));
7 ^, g |- O( J! ~: C( Z - addr.sin_family=AF_INET;
, g- M( _# V" Z3 N9 ~7 c - addr.sin_port=htons(REMOTE_PORT);
( c4 o/ X! E# P% n - addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);8 g) W7 u. F0 K
-
, H, `% m% d7 ]+ n' q+ z - //向服务器发送请求
, W. L, Y, U$ w; }$ ? - if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)3 t- @9 q l8 G; p' d
- printf("connect successfully\n");8 _$ j5 F/ u* A& H! M6 V
- 2 R5 k+ }, ~- p4 `
- //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
3 W. l. q! B8 j - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);: X- _# t( M$ \: b* _) V
- printf("%s\n",msgbuffer);
% Q* a3 t. d( g3 _ @ O -
8 {) v/ T& [. r - while(1){1 l U& z/ M& [6 _
- //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息& S+ k- X5 @: }" @" M
- bzero(msgbuffer,sizeof(msgbuffer));/ r2 y) S/ Z2 d4 d1 d; @1 _
- read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));& D- C @/ ~3 U& E5 @% @. s
- if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
& ?( v. P3 ^6 I2 G - perror("ERROR");+ s& i* M$ ]# R' ?' h" @3 }8 x8 Y
- 5 U7 Q+ [, K" a) Y0 Y* t R9 n
- bzero(msgbuffer,sizeof(msgbuffer));
" u& G3 j3 D0 [/ f - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
1 Z/ X! g5 Q4 I4 j+ ^3 X - printf("[receive]:%s\n",msgbuffer);8 J% p0 g/ b) |; }- F0 ^6 @ s
- - ?* R1 m! K; N/ ]
- usleep(500000);
3 Z1 S9 }" |- }3 D2 } - }
4 ~9 L# h# x% D" p! c- A - }
复制代码 & ^7 d/ b% |* t; J$ a! j) L n% U
% {9 F k" R8 G4 |服务端: - #include <time.h>0 i0 V5 T5 [) w' q6 w
- #include <stdio.h>1 T. U" j2 B& u9 d4 a
- #include <stdlib.h>
, z7 e2 C6 ?* i. x6 w2 I2 Z - #include <string.h>0 k8 L, e8 q- |7 X5 ~1 _4 _
- #include <unistd.h>9 l9 Z8 h$ ^2 A2 w& `1 x3 |
- #include <arpa/inet.h>
) M5 S7 t+ g( c( C. U" ` - #include <netinet/in.h>
9 f$ }5 B8 [/ T [ K0 I$ m! ]* C - #include <sys/types.h>/ P! G* S& v2 j- B" w/ Q3 `7 d/ r8 L
- #include <sys/socket.h>0 R1 s ?! f1 |7 o
-
( C& c) s( v+ Z2 M - #define LOCAL_PORT 6666 //本地服务端口
" u3 O: w8 J9 ^) m+ X9 K - #define MAX 5 //最大连接数量
# j/ x6 D" ]! f1 e" s - " }* X G- |7 i4 V
- int main(){9 T9 m' I9 U2 v- e$ ~0 o
- int sockfd,connfd,fd,is_connected[MAX];
3 W0 @; n# E! o1 t/ Y - struct sockaddr_in addr;
! `1 I, j; e) ]( s; J - int addr_len = sizeof(struct sockaddr_in);
# R6 i% S/ I& T - char msgbuffer[256];. H$ p; N: h' E
- char msgsend[] = "Welcome To Demon Server";. o' H7 N& x6 X+ `* m
- fd_set fds;+ y) u, f0 I3 R" }7 y v5 m* b+ j
- 1 a. x, p. N* R: m4 }' H
- //创建套接字8 U; J# H" |2 V* n0 u5 W
- sockfd = socket(AF_INET,SOCK_STREAM,0);' u% w; Q1 g9 g
- if(sockfd>=0)3 Z6 Z5 Z: W9 X. Y x
- printf("open socket: %d\n",sockfd);8 t9 w$ [: E- v- \. W* T s
- 1 ^0 w+ g& Y5 x, [
- //将本地端口和监听地址信息保存到套接字结构体中
R; \ W1 I. u- l - bzero(&addr,sizeof(addr));
! W' Q2 {) a% O1 j' x% x- Z" L - addr.sin_family=AF_INET;$ p# I! l8 u# L
- addr.sin_port=htons(LOCAL_PORT);
4 O9 r, P) W5 {$ M; Z" u- }6 W: T - addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0: U* S/ ~2 \$ b: |& }# t- E3 s
- ( P! z4 ]* s7 e0 g' ^1 g( l
- //将套接字于端口号绑定
; w) V5 Z6 @' O: t3 n0 X - if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
8 S9 e- l( r8 \7 }* S* i - printf("bind the port: %d\n",LOCAL_PORT);) g; i- Z S2 ~7 z/ _
- 4 z5 i" P; |& w- M9 E5 A
- //开启端口监听3 |9 M: ]( R3 L' ~$ R) w
- if(listen(sockfd,3)>=0)
* M% [! U/ d/ Q' Y - printf("begin listenning...\n");
8 c9 w, T6 ^3 V! B1 X -
7 w" C$ g) f4 l; `* s, t2 } - //默认所有fd没有被打开
. b# g4 K* W- u5 ^+ g - for(fd=0;fd<MAX;fd++)
* R, q6 x5 s0 W8 \2 K+ z - is_connected[fd]=0;0 _, @4 r! a$ d* d+ E# i3 k' y
- 0 G0 a9 N D, |$ ?0 t
- while(1){
. Y) _4 u) U6 M4 n. P - //将服务端套接字加入集合中
3 L# W3 A4 V: x. a; q - FD_ZERO(&fds);
9 Z" I* \/ j8 P3 @# u: V - FD_SET(sockfd,&fds);" S. t1 B: W* r: U: F2 c
- / G3 J& u; g+ E' k
- //将活跃的套接字加入集合中) ]# M( A9 H5 S/ m9 d! ^5 ]7 |
- for(fd=0;fd<MAX;fd++)
4 ?/ Z c1 ^. Z - if(is_connected[fd])8 x4 f# c, G% C4 ~2 v
- FD_SET(fd,&fds);
. x0 h0 d1 D. P: u; ]- m5 v -
2 z+ d- n/ f v' k6 o+ I - //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0$ k/ U& I! E6 f' Y R _
- if(!select(MAX,&fds,NULL,NULL,NULL))6 K5 t+ ?( `- t5 r5 ?
- continue;+ ?& r. L) \3 _, s
- 0 ^& M- E C7 _1 O; @' q; T+ ~
- //遍历所有套接字判断是否在属于集合中的活跃套接字
/ \# i0 k& h$ n- w - for(fd=0;fd<MAX;fd++){' z$ o" D, a2 h0 [
- if(FD_ISSET(fd,&fds)){
6 B& I/ K" T/ K, u5 ?: I - if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接
- G9 ^9 i, O+ q& c - connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
7 }# @/ K- g- D* E1 v( Y - write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语" y7 g; t" y$ s t0 a
- is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用
& l/ U" @% g, U) }" t - printf("connected from %s\n",inet_ntoa(addr.sin_addr));2 ]6 c8 ?' n x: E/ Z
- }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
0 ?0 u1 s* I! I9 P" u- ` - if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ 0 i7 u/ ~3 R4 t9 n! D
- write(fd,msgbuffer,sizeof(msgbuffer));" f" l6 @6 d6 q- T# R
- printf("[read]: %s\n",msgbuffer);
4 k4 x1 O U4 O" U) h% ~4 C" h - }else{
1 m, y! [/ L& z/ s - is_connected[fd]=0;" F T4 |3 W$ T" p1 O# w& y& h
- close(fd);
* n: m4 x2 p" F5 | - printf("close connected\n");" a" j3 D8 F. ^
- }
& ]6 P! l6 `* \$ m# @ - }
) w O3 _3 `, k* w3 k - }8 L! r% {7 K z5 ~' l
- }
6 u& [2 ~+ M& x; N9 x5 z9 b7 @ - }3 u" m3 x$ [7 f& q" w+ i2 h0 ~; w
- }
复制代码
, X0 Z% t: U* t
1 |; ]$ d1 o0 |& u7 m" b7 k3 Q" l
5 v' b/ U3 t- k- L0 M5 G( `! `: C+ j/ z, Y& H- J6 }+ Y
$ e- q% R7 T4 u# n- k
( \1 @4 s. u- p" |7 ^3 b) Y |