|
实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上 2.启动客户端,与服务端建立TCP连接 3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点 $ X2 @& ~2 K! y, ?: R+ n3 u
什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。* a9 K2 j) O0 i
"套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。 对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。 具体其他高级的定义不是这里的重点。值得说的是: 每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。 应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)# }% i( f/ f- Q
$ Z* ]! u% x/ d" `/ i
- D% Q3 Y( a* g% _* o H
如何标识一个SOCKET: 如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个 SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等. D: l# [ t5 q7 [4 l1 H" C
描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。 述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出 当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3 , p. h0 O! M' K9 ~+ i" D1 U
_8 e4 U7 d( J8 \服务端实现的流程: 1.服务端开启一个SOCKET(socket函数) 2.使用SOCKET绑定一个端口号(bind函数) 3.在这个端口号上开启监听功能(listen函数) 4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数) 5.接收或者回复消息(read函数 write函数)
" I n$ S7 |" J: F1 s9 p, y/ T' k8 x( J' G+ C
客户端实现流程: 1.打开一个SOCKET 2.向指定的IP 和端口号发起连接(connect函数) 3.接收或者发送消息(send函数 recv函数) 6 K% h4 a5 @+ H7 @2 Q0 ~
& n9 X" L: ^. r9 T
7 t) m/ ~2 L- [7 F+ x8 y% x* ]
如何并发处理: 如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果 直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端 一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到 有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照 单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
9 w" f9 k! S8 R4 r& z9 `9 s6 R( p, B1 r- ?: U! e: B2 w
如何解决: 下面摘文截取网上的资料,有兴趣者可以看看 系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你 - int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为: - #include <sys/time.h>
& t. b, X0 D' X6 ^6 u
8 Z( R, M! b& ~" x6 ~$ C- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理 ! P2 C( F6 o0 w \" g5 p4 N% r
readset 用来检查可读性的一组文件描述字。
( z% @% M* b4 v) d4 ` writeset 用来检查可写性的一组文件描述字。
& S9 H" w9 C5 _8 l% O7 M6 Z
exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
; J& D- E+ N- X* q timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。( o" m, x0 p. a2 N& M) }5 r
4 C, X4 y# i* K. p* M 对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:7 `0 V' ~. v) K/ Q6 N4 k$ q
- G; T6 p/ h) g; N5 j5 ?7 k4 F
- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)" F4 c& w! W! ]! s6 `" c& i
4 N+ S/ \! f# z- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)+ J+ L, E- H2 g1 E; }
! O7 X+ B: ?, C- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值: 返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。 否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来, 你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量, 其值通常是1024,这样就能表示<1024的fd。
9 P9 k8 O& ]. [7 U3 t
* p# |: ~. `, h1 d
6 o3 `6 a. }" n* p fd_set结构体: 文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字) 可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作: - FD_ZERO(*fds): 将fds设为空集
; d5 m2 M5 M$ X. r/ x* b - , v# u) ^/ Q7 L7 R
- FD_CLR(fd,*fds): 从集合fds中删除指定的fd( |" z& E, J6 S" s X
- 9 w8 R* a: ?( v1 x
- FD_SET(fd,*fds): 从集合fds中添加指定的fd$ c& }$ v5 B8 A5 J% Z% o
- 6 \5 N7 H! t- R' g
- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下 - socket s;
3 m( k' S4 s1 F - .....8 W9 S, h8 H, D# w$ w1 U* J4 @
- fd_set set;
9 x9 V( X8 f, M! j; L- E - while(1){
# z8 h% w. |; F8 v* n& l - FD_ZERO(&set); //将你的套节字集合清空
1 p7 S! }8 `. ` b8 e) P3 I - FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s D& g: [) s1 r$ f
- select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,' j* J( H" i" ]! i; D: k+ T8 H
- if(FD_ISSET(s, &set) //检查s是否在这个集合里面,
: H# H; K2 ]( H( \6 n4 k - { //select将更新这个集合,把其中不可读的套节字去掉4 {( t" M/ t/ a6 z! [" U3 A0 [( y- a
- //只保留符合条件的套节字在这个集合里面/ K0 K H& k2 e% E" `9 o& d+ Q
- recv(s,...);9 _0 j# B; [% ?6 @- G* c
- }
- S) a4 d+ _1 }, C% z+ f Z2 \0 q - //do something here4 Y. d" S: m* N
- }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd - (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。
3 `& j+ v; } U) D/ S) C - ; m, ?% {9 I1 Y8 ~- x
- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)
" p8 V+ @, H9 m/ N4 F% I- i - , b; `9 p# G8 w; C3 {
- (3)若再加入fd=2,fd=1 则set变为 0001,0011
! n w+ d, f9 D" z: w% Z$ { - 4 b( w* E8 I/ x+ k: x. P5 |) Q
- (4)执行select(6,&set,0,0,0) 阻塞等待
" L# I) L& q& t - 8 _8 m. F3 s, P" S6 o; }
- (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.具体过程看代码会好理解 3 V' ~/ m& b! P9 l, V5 ^# \
( u5 H, D7 B, i# H$ B+ k- _
使用select函数的过程一般是: - ^1 `* Z6 f& Y9 @
先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set, 接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1 复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
* X1 t5 t9 C1 m6 [ 0 o v! N8 m' c6 n$ |2 f& I9 g3 ?
客户端: - #include <time.h>
7 q x# t# d7 s! P - #include <stdio.h>5 a% c9 w' d8 @! f$ Y3 D
- #include <stdlib.h>
7 z4 Z% ^1 w0 P0 G: n% @0 l: x6 ` - #include <string.h>8 J8 r3 c( [) C7 Y n
- #include <unistd.h>
' Z" J$ R; N6 `3 O* u2 m* W" I - #include <arpa/inet.h>
1 j& A& u4 V& K - #include <netinet/in.h>
& s( ]) n" T% D. Z* ~6 n8 U# r W" M - #include <fcntl.h>
* K( a, O7 k P! L- B - #include <sys/stat.h>9 K; S4 G( m. f9 o: ?& H2 k2 ?) N
- #include <sys/types.h>
2 R6 O, n0 Y+ T+ a9 W - #include <sys/socket.h>
. \6 c' Z, H' u- V5 O -
" d8 k8 o% `/ f3 N# F- y% m- V - #define REMOTE_PORT 6666 //服务器端口- ^! M/ h* S2 b0 K0 A
- #define REMOTE_ADDR "127.0.0.1" //服务器地址
7 B! ^. Z/ l1 x - / U! V: K7 _3 h: x7 @6 z/ X
- int main(){
e W- f1 h& a6 V* i% H3 K9 A8 z - int sockfd;! K4 Z2 A" h* l6 k( C" J
- struct sockaddr_in addr;( @5 r7 y. z1 ^% P
- char msgbuffer[256];( T6 y+ i V% K/ C: p' J! A' s1 }
- 0 @! R! Y# [9 l/ ^. S
- //创建套接字& o# L9 L" Q3 V1 I, I
- sockfd = socket(AF_INET,SOCK_STREAM,0);; I7 a' j7 Q* q- V
- if(sockfd>=0)& v ?7 ?% A/ T9 v( v% J
- printf("open socket: %d\n",sockfd);; G f* s' P6 P; V6 a
- # W% H! j. F$ d% H) g
- //将服务器的地址和端口存储于套接字结构体中* S, t3 N, Y% `! r c% [
- bzero(&addr,sizeof(addr));2 m7 o5 d1 A8 z- [4 b7 D
- addr.sin_family=AF_INET;
8 g& M& [$ \% k - addr.sin_port=htons(REMOTE_PORT);3 i0 L1 y% V& f5 G5 T7 [. a$ [
- addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);; w% q( n7 q* C' |$ d3 b. C
- $ m' E9 ~# K6 l
- //向服务器发送请求3 L# H, E! i/ v
- if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
1 ]% b2 c6 n$ Y, b$ G; [% s) C. d/ D9 k - printf("connect successfully\n");1 o3 Y% H# G7 W" C0 l
-
; g" A: ?9 `, G# N; t3 N1 L - //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)9 s8 M2 g8 B. o9 H' H0 }
- recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
6 S r. l Y. s: _% d" O" ] - printf("%s\n",msgbuffer);' p$ v$ T- d) r2 g- o# G. {
-
, R+ }) w- C/ ~9 D$ x: F1 B2 H - while(1){
8 J( m* l$ o6 z - //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息. j& G' J1 r k/ a. O
- bzero(msgbuffer,sizeof(msgbuffer));5 Q2 z& f- X' U; @* U& {" C/ z
- read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
! M0 Q" | P7 b% v% k - if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)/ a, h j7 }( S$ K# c% R, Q+ p$ O) T4 N/ x
- perror("ERROR");
$ S" q Y4 ]: h. c" t$ z5 T# N) C -
/ _' x7 t& e# r3 C8 A& N - bzero(msgbuffer,sizeof(msgbuffer));
: }) `! ?4 s8 }8 @' F! E - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
. \( d# B9 d p( v - printf("[receive]:%s\n",msgbuffer);, I) G- R& c; e u- Z
-
7 e# |) q5 e) ]5 U2 y, T: O# t- A - usleep(500000);/ b" E# W3 C, \% ]
- }% s# \4 t. `9 T K& D9 F7 y6 s# @
- }
复制代码 9 ~3 `3 o3 y% [# F
! t# i) s: g/ L2 l
服务端: - #include <time.h>
# B+ g) a6 l) q! z6 `# j - #include <stdio.h>0 u# m/ o3 ?$ f1 k) E- `
- #include <stdlib.h>) [2 c, W9 J. z$ U4 Q
- #include <string.h>
2 S3 {$ x5 e" c" O - #include <unistd.h>
8 w' d9 R* [/ s1 Z: b5 m - #include <arpa/inet.h>
. Y* Y4 i3 z7 w - #include <netinet/in.h> e% a' G' @" }$ e% }
- #include <sys/types.h>
$ c6 h; }) ?! m/ _1 k - #include <sys/socket.h>
& s2 }1 u a6 d6 J j) B -
. A# x5 Q1 |7 T- I( F - #define LOCAL_PORT 6666 //本地服务端口: F' U# Z5 S5 }! {4 v* H
- #define MAX 5 //最大连接数量& ]; z7 i; z9 H2 F6 H
-
1 {! L: V4 o% d& c/ v. S$ @: j& b - int main(){/ O B4 k6 |$ b6 z2 J' X
- int sockfd,connfd,fd,is_connected[MAX];
{; w0 _6 p6 M - struct sockaddr_in addr;
6 W, S; T+ S8 ^5 d6 H% o, b9 A( n4 X - int addr_len = sizeof(struct sockaddr_in);$ l9 {9 V5 o( B5 C
- char msgbuffer[256]; A$ v" k; c# i# G4 T
- char msgsend[] = "Welcome To Demon Server";+ U. l+ x( M* D( ^6 H
- fd_set fds;
2 o$ y2 p) |) n- N. B/ L4 e3 P3 _ -
+ O" ^: E! E* l+ O& P - //创建套接字$ I! Y+ _; M9 R
- sockfd = socket(AF_INET,SOCK_STREAM,0);9 [( d# e8 v! `. ^
- if(sockfd>=0)1 x& l( t1 N, _/ F
- printf("open socket: %d\n",sockfd);
; i8 V) B1 g. t# D, Z- S7 ? - ! I9 V' h! _) w9 `6 \' f. N( u
- //将本地端口和监听地址信息保存到套接字结构体中; `- O2 l& J0 a8 F3 Y
- bzero(&addr,sizeof(addr));
1 t$ y d6 W/ x } L - addr.sin_family=AF_INET;
1 v; ]/ p7 n* y" q/ @) } - addr.sin_port=htons(LOCAL_PORT);
; g4 k4 }: ^, P1 R - addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
3 ]( x# Y) P, |! J - 8 L, J# P9 p1 Z# O' B# w& u/ |7 M( p
- //将套接字于端口号绑定
9 E, ^& d5 `( v. }- h$ _5 i - if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
, P. ?1 a/ |9 I - printf("bind the port: %d\n",LOCAL_PORT);
8 d: O& i8 @* n1 H -
, }0 g0 C6 J2 |6 ~2 f7 y/ @ - //开启端口监听
) K. B Z, i0 j - if(listen(sockfd,3)>=0)( \: I! R6 W. r2 B* H5 Z
- printf("begin listenning...\n");
/ H3 ]2 p0 ]/ ]6 k& ~* E - ! u% K+ e7 s, r# \' x( |
- //默认所有fd没有被打开/ c0 }9 I0 @: ~9 y, h" ]6 \
- for(fd=0;fd<MAX;fd++)
. n4 a* a& h. `! v - is_connected[fd]=0;4 A: m k, Y+ t y4 M+ b/ Y" L( _2 a
- : z' q8 T: \5 `$ T
- while(1){# u5 }; ^) U* J; e4 { { e& z' @
- //将服务端套接字加入集合中/ ], T! Q1 G# S4 T2 O) I4 e" I2 F3 g
- FD_ZERO(&fds);
# z t" n: G: V" l# O" M. J' m - FD_SET(sockfd,&fds);
% U: L; k) j& t% j -
9 y9 }) y9 X" d% n) W$ E- \" H5 } - //将活跃的套接字加入集合中
! |: X4 x) U8 C - for(fd=0;fd<MAX;fd++)" Y6 M$ k& C& j* N3 h# h+ {) ?
- if(is_connected[fd]) v" \( ]: I9 p8 i, f
- FD_SET(fd,&fds);. {! Y1 P4 b5 {# u0 [/ N
- 6 F( E% q" a9 l, G5 m/ U
- //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0; d4 S. p( W) `# A5 n0 e
- if(!select(MAX,&fds,NULL,NULL,NULL))% F3 m) |7 k0 Y9 U" y" O
- continue; b+ l0 X6 J1 i& e7 K- ] \
- * Z. N0 C) U: m$ a9 N
- //遍历所有套接字判断是否在属于集合中的活跃套接字$ _2 P4 b* B0 e6 e& n. \
- for(fd=0;fd<MAX;fd++){- a0 g8 X! | @+ K
- if(FD_ISSET(fd,&fds)){" N% W2 `! v6 V+ B' ?4 |5 i; Y
- if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接
0 m6 r2 M1 Q& {/ ^; _ - connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
& ~5 g% v& v# ` - write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语# o- i- {2 S$ {: j1 G; U1 d
- is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用
0 ?$ j% T- s$ U- w - printf("connected from %s\n",inet_ntoa(addr.sin_addr)); C$ J4 V$ @; B$ q
- }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字' |$ d3 E7 x d
- if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
. w0 `# |% \" r3 O) y ^+ M - write(fd,msgbuffer,sizeof(msgbuffer));
4 O0 P4 K! n) K$ Z - printf("[read]: %s\n",msgbuffer);' R1 Q9 V, g2 I4 R
- }else{' m2 }% t, u+ Z- D" B+ n/ ]
- is_connected[fd]=0;
2 g& R" V3 z' E* e9 w! p - close(fd);. c+ j/ R7 x- `8 U) s+ ?3 L z
- printf("close connected\n");2 U ~) w/ B# D4 B6 o' a7 c
- }
5 E! v+ U& K0 j - }
# ^, O& P" T) o. o. P- ^ - }
9 l" c: E- ^9 `. d8 Y3 I4 F! | - }
/ x- S: z L* Z% ` - }! }5 j/ v2 H' q- c; _1 M: S9 g
- }
复制代码 3 l* L4 A! o, k9 | }, ~3 ^3 z
: P6 [* l( J/ G: T
6 P7 E2 k" W" Q
3 Z L* M# x6 u6 Z) |# p
1 i( S3 N& N2 g& Z5 c5 A4 ]( o2 ^+ K- w' G. @& N0 J. e
|