实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上 2.启动客户端,与服务端建立TCP连接 3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点 1 \2 ~( l/ R6 \" O* e7 o
什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
9 |( J1 Z4 p$ ^! ] E "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。 对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。 具体其他高级的定义不是这里的重点。值得说的是: 每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。 应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)+ `4 V1 @- C" w0 a& ^% x
6 v# \; Y! I: B Z4 A4 Q
; E8 Q# ?! q# Y& i如何标识一个SOCKET: 如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个 SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等' _2 Q! ^( @, s! E$ O
描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。 述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出 当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3 1 x0 }6 R) i' {% [) P
" }( w o4 o. s( a, u" @1 J' j
服务端实现的流程: 1.服务端开启一个SOCKET(socket函数) 2.使用SOCKET绑定一个端口号(bind函数) 3.在这个端口号上开启监听功能(listen函数) 4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数) 5.接收或者回复消息(read函数 write函数) ! f7 q2 S( W" v0 x- D, h( Q j
, B% V0 v4 U2 @* m& t& J0 }
客户端实现流程: 1.打开一个SOCKET 2.向指定的IP 和端口号发起连接(connect函数) 3.接收或者发送消息(send函数 recv函数) - m& N1 d E, C- b3 c
; D8 a4 X3 v8 } ^+ h
7 Y/ |, \7 K, e1 q如何并发处理: 如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果 直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端 一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到 有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照 单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。 " }. B4 S. x5 ~3 F5 y
$ N) e7 s6 M. G, v8 g, U* N
如何解决: 下面摘文截取网上的资料,有兴趣者可以看看 系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你 - int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为: - #include <sys/time.h>
: N) w' j" X; J8 a' M% A2 D - + M& |# K2 L! g. @& w7 g" x
- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
; c% w, _, k" O1 }: M) \ readset 用来检查可读性的一组文件描述字。
$ R: w/ ]' f- p+ m n writeset 用来检查可写性的一组文件描述字。
! ]/ c3 m7 M9 C( p6 w) ? exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
0 e& G, w6 i% j' h( `5 _6 }0 a timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。; H" U6 h2 H0 e0 \" m
5 x! t7 H/ |( [
对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:6 U& Q" C0 [- m" u
: \- O; N; t0 j a4 o {
- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件) I' s* m; ^* t9 i! ~
+ K4 T0 W* f$ l2 j- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
2 F& h7 l2 s2 j6 v% W5 @ D* N - ( @: Y- s7 ~. H9 L4 h# F
- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值: 返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。 否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来, 你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量, 其值通常是1024,这样就能表示<1024的fd。$ x/ K$ I) G7 h( ^
! Q4 q( v' ^6 V7 d+ Z5 Y
; A: o' X4 O3 P( }' L0 Y fd_set结构体: 文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字) 可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作: - FD_ZERO(*fds): 将fds设为空集( k( N) \ L, o D2 z* L# J
-
5 K' W& O% c* V; T - FD_CLR(fd,*fds): 从集合fds中删除指定的fd9 H$ P! D d6 R5 ?& }; J4 \
9 K% d: w; y4 ]2 z0 ]+ [2 I* m- FD_SET(fd,*fds): 从集合fds中添加指定的fd# \# W7 G; O. A) V
) A& }( a; U/ `2 y- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下 - socket s;8 c. J. z/ @7 ~, ?: V
- .....' D M$ A" C7 U/ x1 a
- fd_set set;
7 I0 P+ R2 V$ k1 N( j' t - while(1){# x! r$ |+ z& {/ r( M; _$ E( I
- FD_ZERO(&set); //将你的套节字集合清空5 r/ i& ^+ B8 @
- FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s
5 {9 n5 P! l1 r: v# A4 C - select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,
: e' n* V& |3 {; `6 R9 f V1 J - if(FD_ISSET(s, &set) //检查s是否在这个集合里面,
, p7 _, Z5 J! D - { //select将更新这个集合,把其中不可读的套节字去掉: ?/ g# }$ D F7 y) _
- //只保留符合条件的套节字在这个集合里面
6 _3 T# R0 ]1 ?0 I( j9 E9 O4 C+ y/ L - recv(s,...);, ?0 S7 j. w- }7 I8 g' ^2 d
- }: k @' `8 L, D6 X7 }
- //do something here
) }: U" a" K5 k+ D& w$ x - }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd - (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。5 Q- X. a$ m' m! p* R1 B9 P2 Y
8 r# @/ l4 b$ H* y7 k* v8 F2 f- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)! b$ W! s7 A1 V% d" u5 r r; a
- ; l. P V+ w! j
- (3)若再加入fd=2,fd=1 则set变为 0001,00116 W6 O% [4 P V( h% r
, S0 I! \& M# l- m1 Q; M, s- (4)执行select(6,&set,0,0,0) 阻塞等待
- @* {- I8 d) Q2 s+ C: _$ f - ! R/ X1 _# [3 J+ f& P
- (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.具体过程看代码会好理解 . t) E$ C" _0 m0 p3 g* t
# s7 t4 A; J1 X" [, F
使用select函数的过程一般是:
' G7 _* P1 U; e: U7 V: j. m# w% V 先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1 复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。7 U2 {+ q# \6 a1 `( y2 k+ a7 C
- s6 O# A( a; W% }客户端: - #include <time.h>( w3 q+ C# {9 a5 z. M
- #include <stdio.h>
2 S0 T: K, K5 T% V- v4 C$ P - #include <stdlib.h>
" X% r( |- b4 u Y+ h! G - #include <string.h>7 t$ q' E& _7 n" R
- #include <unistd.h>
$ ]- f7 D/ l' _: W* |% { - #include <arpa/inet.h>) q4 J' t: Z7 w! o6 v5 ^
- #include <netinet/in.h>
4 z+ \8 @! B# S4 o8 s - #include <fcntl.h>& X5 q! M u) n1 h
- #include <sys/stat.h># P$ Y1 t, _/ Z- ]' E
- #include <sys/types.h>
, K1 |, E7 p) {/ @+ `8 K - #include <sys/socket.h>9 L) n8 u# C' B; e9 X$ U) ~
- , q; w: h, r& X2 e
- #define REMOTE_PORT 6666 //服务器端口
1 z) j6 ~; O9 ]3 G) F/ [ - #define REMOTE_ADDR "127.0.0.1" //服务器地址. Q% X; J# |' o- K0 N2 N
- ; _7 @" B! a2 N$ ]
- int main(){" N {% q3 n# [1 e; ~* s k0 c# A
- int sockfd;
8 Y5 g$ Y2 J: z4 v, } G - struct sockaddr_in addr;! d5 k# c, `. g7 g' U
- char msgbuffer[256];
" M9 w) D& V# v - 0 s& g# c% h0 k0 n9 W
- //创建套接字! K# P3 Z% V# Z- R
- sockfd = socket(AF_INET,SOCK_STREAM,0);/ L& y4 n" Q: B
- if(sockfd>=0)$ y1 M, R/ F \# o! C7 W
- printf("open socket: %d\n",sockfd);( r# @3 P9 ?! u2 P- f* \
- 8 I% F) g0 U) Q9 G
- //将服务器的地址和端口存储于套接字结构体中' T* |5 ~( z9 L
- bzero(&addr,sizeof(addr));
) ?( m; R/ t3 p- p - addr.sin_family=AF_INET;
* J: [0 D% j- m& v# y1 \7 w1 c - addr.sin_port=htons(REMOTE_PORT);
- k9 h: W- E9 {; e - addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);4 o; N% w5 {! k b
-
5 j! V& R* R' F, Y8 x3 b$ |* ` - //向服务器发送请求# L+ v* k2 ? O) l1 J- B
- if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
" t0 o3 v: l* v - printf("connect successfully\n");' e8 G8 N4 n7 }4 y
-
1 B7 B6 P' n& k6 V! b - //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
2 {4 D6 [& O0 @: S - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);* V8 }1 r+ q. i( P/ p
- printf("%s\n",msgbuffer);
4 @ d: O9 e; k( R/ B - . e5 m0 D0 v9 ?, F
- while(1){3 D. k: I! L6 l$ l4 R! L3 s. C4 Y" w
- //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息( t; L/ @. T/ j; n
- bzero(msgbuffer,sizeof(msgbuffer));
- j& a8 L1 q' W4 g0 D - read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
: g4 b( U5 f& X# } - if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)4 x. u( f* ~+ D5 f$ r$ A/ r
- perror("ERROR");
9 E* b7 t; }: I* f+ F! Q4 }6 x -
6 g- q( ~ ~$ Q# A1 M& s - bzero(msgbuffer,sizeof(msgbuffer));8 I% q& A# n# L' v( P" }; J l
- recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
6 [% W/ R; ^6 o* ?$ ^ - printf("[receive]:%s\n",msgbuffer);
8 L+ T! @9 c9 T - / I% `" _- U; Z% K: \
- usleep(500000);
" k) K3 j7 }2 @) e - }
$ g$ ?( K( h; G - }
复制代码 , d) c: z! f+ H. ^
" `7 ^4 o1 L+ t& ^: ^( m服务端: - #include <time.h>
/ }( F) ]) X, _1 y1 ^' P& T! J( u. s - #include <stdio.h>
F) M& }7 H) A! { - #include <stdlib.h>% M( L( j, Y9 w1 z* z9 V
- #include <string.h>
3 a* J8 e; D( C" F8 x" } - #include <unistd.h>
1 T3 q3 I" K0 b - #include <arpa/inet.h>3 s! l2 n& E2 B
- #include <netinet/in.h>
/ F2 m$ I8 i, H% H8 Y+ b' |2 W8 s - #include <sys/types.h>
' k6 @7 J8 r5 h0 x0 T4 ^ - #include <sys/socket.h>; L; [4 j' ^6 I+ I, a5 k; }4 |
-
" C% z$ C8 L. F$ L/ t* ? - #define LOCAL_PORT 6666 //本地服务端口% C r i! g' O- s! l
- #define MAX 5 //最大连接数量& Z* V2 B4 |$ I2 `7 _# s
-
5 | F# h; P" h1 x! E - int main(){) G: d7 x" p! e' A
- int sockfd,connfd,fd,is_connected[MAX];
4 J5 z, \2 y8 j5 L6 j8 c O - struct sockaddr_in addr;' N# I$ `. R; I
- int addr_len = sizeof(struct sockaddr_in);
5 Z# u5 d2 G" i# R9 g u* p - char msgbuffer[256];* U& X2 h4 w2 f2 I6 N/ L: m* ?% H
- char msgsend[] = "Welcome To Demon Server";4 f5 G7 [% s1 n9 p6 v' e6 D3 H
- fd_set fds;9 A; y% M% d, J$ H6 H
- 9 N, Z) d2 F( D* m
- //创建套接字2 U0 ^3 O# x g c$ C
- sockfd = socket(AF_INET,SOCK_STREAM,0);
! \" d! ]% \. O6 q# F, [) v: t* x4 l - if(sockfd>=0)3 L3 Q: P, m8 [: @8 Y
- printf("open socket: %d\n",sockfd);4 y- ]: N; W8 g- B6 W" g
- 4 Y% o# o" a* b9 V
- //将本地端口和监听地址信息保存到套接字结构体中
2 \* k1 q( o$ i% x' g# l - bzero(&addr,sizeof(addr));
( x# w0 @+ h4 I+ l% L. n - addr.sin_family=AF_INET;
3 [4 l# c( v6 U \% a# T' _- _4 j3 H' ` - addr.sin_port=htons(LOCAL_PORT);) b, j% M% c+ _6 X$ H8 X
- addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
8 A; I+ @% y$ R5 L; ^ - 2 b1 N. |1 h# S1 G1 F) S
- //将套接字于端口号绑定7 `! h& Q1 W& u8 G
- if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
$ b" m+ @6 ^7 i - printf("bind the port: %d\n",LOCAL_PORT);
+ m1 N0 `/ z9 O9 v/ b - * m! L8 b+ g; Q5 c: Z: |
- //开启端口监听
# G. C- ~+ ]2 K8 R# Q - if(listen(sockfd,3)>=0)
& u" B# p, Z9 ]( k& X0 Z0 f% I! Q - printf("begin listenning...\n");
7 b" a, @* L8 _" E1 u# Z! S7 v - 1 ~' ]- H& l4 @% Z- g0 ?
- //默认所有fd没有被打开7 n C# n% b) c n6 h$ Y! J/ i8 F- a* l
- for(fd=0;fd<MAX;fd++)* S4 E; ~. u4 Y" i$ a
- is_connected[fd]=0;
9 q1 Y! V- A0 Y5 q$ o. O1 |& T2 J -
6 C: @' `( l5 d% U - while(1){
6 }, d W H0 m1 q# c7 r7 O; _ - //将服务端套接字加入集合中
- R' ~# }) |6 P5 ` - FD_ZERO(&fds);6 t& y5 C: q" p4 `0 ^3 o
- FD_SET(sockfd,&fds);/ `+ C1 m& E. w# K% \# M' w) _9 P
- 7 \# W, q, X% \- o. L1 e
- //将活跃的套接字加入集合中
6 t% }$ w6 S) ~7 [' t - for(fd=0;fd<MAX;fd++)
. {& j: |3 D( \0 P - if(is_connected[fd])
8 T7 Q6 M9 n+ o9 K - FD_SET(fd,&fds);; e- z( l/ r; P- G, G6 T3 S
- 1 f; V7 A7 t, W: @3 n: j/ C
- //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为09 \- L) L0 \$ p! r
- if(!select(MAX,&fds,NULL,NULL,NULL))
/ p2 G9 r. N/ j' H - continue;+ C! a: V- d8 P) e+ s2 C
-
% T2 w8 g4 Y/ I. w - //遍历所有套接字判断是否在属于集合中的活跃套接字
, ~* \" r1 _+ D - for(fd=0;fd<MAX;fd++){4 G b- ?. L6 e: W/ y
- if(FD_ISSET(fd,&fds)){
8 U8 |2 M8 \/ G3 r9 z - if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接- ~4 y8 ?7 _/ K8 |: G9 J& X
- connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
1 D/ @- j( F! Z8 [. W) `1 D - write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语* a2 B( t8 a4 I: `; B$ e9 d
- is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用% E: p6 y m$ B W, a
- printf("connected from %s\n",inet_ntoa(addr.sin_addr));* X4 {2 u$ M) L5 m- [1 P
- }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字8 \$ H2 y2 v" @. y9 W7 W
- if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
) y# ]! @2 s3 G - write(fd,msgbuffer,sizeof(msgbuffer));
4 U1 G. J* I9 j: c; W1 T' g9 V - printf("[read]: %s\n",msgbuffer);
) q, p. G8 r; l# f3 w - }else{- U2 q% _7 z) K, K' P" x
- is_connected[fd]=0;
' L1 j5 _: h, p4 R. Y" | - close(fd);
% e* u4 q9 S( z \ O - printf("close connected\n");
3 B5 r# Y) ]' e: C3 ?5 J" c - }8 [# v i% j5 g, S* b1 }
- }8 a) n3 N( S1 M [
- }
. n5 o6 r4 x6 o; y - }
: m9 d* H9 L% F. l' q9 I( o - }
. e5 d2 Y% `# z7 Z, O) O - }
复制代码
% B0 D+ }1 j- L
- `! }- E. M* z/ o0 A+ A- W; t% c* B# k( D2 H8 I
# N; Q, Z' ~5 }+ z% c5 {" x" ?( E: I
0 Q& ?7 b0 d+ d8 S
|