|
实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上 2.启动客户端,与服务端建立TCP连接 3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点 # Q8 s: o/ [+ L# i+ T2 B
什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。: f& ~7 t4 e l2 c4 M: y7 i; F
"套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。 对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。 具体其他高级的定义不是这里的重点。值得说的是: 每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。 应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)
e/ B, E& g# Z ; T8 o: q* P0 p, Z) ~
5 Z0 L- `7 Z5 v7 [" }+ v如何标识一个SOCKET: 如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个 SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
; L% z) E* W/ J( j- G 描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。 述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出 当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
5 m4 N/ o/ h5 w+ I; q- O. d* I
2 L4 e$ I1 w; h# P: p i) h" ~: e服务端实现的流程: 1.服务端开启一个SOCKET(socket函数) 2.使用SOCKET绑定一个端口号(bind函数) 3.在这个端口号上开启监听功能(listen函数) 4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数) 5.接收或者回复消息(read函数 write函数) - m' H( g7 W. f7 o; c
' P3 q, z+ @* i$ y: ^
客户端实现流程: 1.打开一个SOCKET 2.向指定的IP 和端口号发起连接(connect函数) 3.接收或者发送消息(send函数 recv函数) 5 C) V0 \! d N& o2 Y
; t1 c& p5 Z7 T7 b, J, X# y
7 y, M F2 w! Z2 K2 B- ^, l; w如何并发处理: 如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果 直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端 一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到 有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照 单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
) X# {: v1 d' _! Z5 j: |/ s* L8 {' _& y- x& j1 \
如何解决: 下面摘文截取网上的资料,有兴趣者可以看看 系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你 - int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为: - #include <sys/time.h>
0 Y7 i: U6 Q4 D6 c - ' X+ `8 ]5 J6 f( _$ S+ K" F
- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理 8 p, w5 S3 A" K; Z. T2 q' R1 k* \
readset 用来检查可读性的一组文件描述字。
- J- b/ I- A$ O, E writeset 用来检查可写性的一组文件描述字。
% p) I4 y! P5 X1 c0 ]
exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
# E7 \) \" a5 i timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。8 {3 e/ U$ I4 e. q6 M: P- l
) a6 r4 R6 q3 Z) j
对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
! I i) W+ Y: _! @: R & V N% U9 o0 @7 L. _" l
- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)7 o9 u/ j$ Q8 N1 M# V, w1 U
4 i! ?) g, _+ k- l7 g; [) M% `$ E7 K- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
0 o' T5 Q1 v- N u G2 }1 H - 4 A4 t- s9 r/ l' G
- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值: 返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。 否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来, 你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量, 其值通常是1024,这样就能表示<1024的fd。
" X! A' ^, i6 K) p7 \9 f
) N9 \% C! V& m' w! y 4 L& ]* L: ~* `# K+ @8 O6 ?
fd_set结构体: 文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字) 可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作: - FD_ZERO(*fds): 将fds设为空集; b' P5 W6 x' Q' ^" i: G
-
; V v a# o _! T3 E, O0 L - FD_CLR(fd,*fds): 从集合fds中删除指定的fd1 S1 {8 x! ]( D( m; I
- & F( ^ g+ Z# V6 ]& q# L2 n/ D
- FD_SET(fd,*fds): 从集合fds中添加指定的fd/ O! H5 I; c1 I* R" K& b
* _. L% Y7 Y+ a1 q( Z- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下 - socket s;
. D5 S( j" S+ m$ `" E/ N& \ - .....5 ~4 D9 ?3 \8 c3 c2 p5 e
- fd_set set;3 H; U T7 {/ c. L
- while(1){
8 ~% G' C4 j6 _5 V - FD_ZERO(&set); //将你的套节字集合清空9 A$ n% Q7 L1 B+ I) S h4 ]
- FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s4 U4 w5 D1 B/ s7 _7 T( N
- select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,
" x+ i ]& X6 h D# o k+ q" `9 u - if(FD_ISSET(s, &set) //检查s是否在这个集合里面,
# P0 f) r5 {5 v1 m - { //select将更新这个集合,把其中不可读的套节字去掉
( t# b9 h& X% U6 _ - //只保留符合条件的套节字在这个集合里面
: X+ H6 Z+ |& s# v$ O2 ? - recv(s,...);" R. x8 u& H( ~ G
- }0 `: r6 y( ~. ^8 s: e
- //do something here/ z- {- i6 ~- M5 E# l4 P, K
- }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd - (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。
# K# s! j4 A# k! a3 t* f2 n" l9 U3 Y - , ` c9 D+ ^! y5 f
- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)* X( f$ r7 ^1 G, L5 z+ A* F; @
- & M$ P+ [) W& b9 y+ v! C
- (3)若再加入fd=2,fd=1 则set变为 0001,0011
! }3 s0 s, y$ I8 K' ^1 a8 [2 e
' f$ a3 n& i' y- (4)执行select(6,&set,0,0,0) 阻塞等待5 x( [ r2 R4 d1 Z9 p4 K; k+ u
- 4 W0 X( o1 a( m
- (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.具体过程看代码会好理解
" q) `1 o! {2 _4 P' P m5 ]% ? ~0 ]% [! C6 \7 A k$ ?& F
使用select函数的过程一般是:
3 S( L7 i8 l8 P: \) O 先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1 复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。/ u, L1 T. L3 F3 R! R: P$ E T9 l
( R6 f7 W' s! d" J
客户端: - #include <time.h>
' k3 s4 }5 _3 x A3 P7 j - #include <stdio.h>
5 `9 ~2 ?. z* N: q* @/ { - #include <stdlib.h># f! g$ H& A" x2 L% Q0 f
- #include <string.h>7 A9 ^4 \0 }4 x7 Q$ [7 z2 B2 ?
- #include <unistd.h>! E: j3 a9 S: Y1 D" W) k
- #include <arpa/inet.h>* _6 [* b) u# Y7 V! G2 V% W+ H
- #include <netinet/in.h>/ }0 x: w3 o# a$ u4 i3 X
- #include <fcntl.h>
: |. |4 w6 ?# ?3 ~3 N3 a - #include <sys/stat.h>$ K( N; a+ S& o4 C& J4 a D
- #include <sys/types.h>3 ]7 r: B- \8 f. V
- #include <sys/socket.h>
% q; y0 C _' p -
; P" V s' j" _) r - #define REMOTE_PORT 6666 //服务器端口+ Q* O" h/ u7 ]0 p8 P9 `" R
- #define REMOTE_ADDR "127.0.0.1" //服务器地址; b% V$ b$ V, g% r+ A# v% P
- 9 v4 T1 q' m! X
- int main(){4 ^9 t* u( W1 R
- int sockfd; O j& [6 i1 ~4 j6 l: _
- struct sockaddr_in addr;
5 G( \0 m$ K- K, S4 | [& ] - char msgbuffer[256];2 } a" e" [- Z/ P& @# U
- 4 b/ k! E ]/ {8 ^2 }1 t* b
- //创建套接字
+ O# E# b3 t& N - sockfd = socket(AF_INET,SOCK_STREAM,0);
+ d* p0 I( f, a p: X9 ~ - if(sockfd>=0)" l p; w2 Q$ ?& n3 |- m- X8 n
- printf("open socket: %d\n",sockfd);
9 m& l: r) G$ m( T - 1 I2 ~- j1 _1 b2 n
- //将服务器的地址和端口存储于套接字结构体中2 ?- E `6 k% N: m( c* u Y/ W
- bzero(&addr,sizeof(addr));
( t# p+ p. L1 q9 K% f - addr.sin_family=AF_INET;, T$ C) {0 c/ w" j6 Y
- addr.sin_port=htons(REMOTE_PORT);. J* L& N+ M7 t
- addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);+ H5 L" ^$ J( [' Z7 g" F, M$ K
- " ^" p5 l8 n1 R; W v
- //向服务器发送请求8 c* k9 P4 n H2 c. k, {% n
- if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)0 f7 w! B* K% r
- printf("connect successfully\n");6 P9 n/ Q3 M/ M
- , f+ i S, [1 G: d. V
- //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
, A: j, ?1 l/ T - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);0 z1 y9 q1 q' L( p4 W3 v- X1 J6 j
- printf("%s\n",msgbuffer);/ O* R, j; V5 X$ @! L5 A
-
+ e+ ? n' S& c8 [ - while(1){
5 L9 R& X# Z) L' n0 z7 f - //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息& Q" b6 N' E/ L1 ~
- bzero(msgbuffer,sizeof(msgbuffer));
; Y- ^4 O$ A: C" a' o! T - read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));+ h. e" }# t6 P; O4 \
- if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)# q, |# }! C: C6 a X" j1 o
- perror("ERROR");, X3 V+ p* E9 X# l
- ' b; M, j7 Z3 R# R# H
- bzero(msgbuffer,sizeof(msgbuffer));4 v: _& H s3 j! H) s. G T
- recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
2 d/ z+ V# `) f0 M/ }! c - printf("[receive]:%s\n",msgbuffer);
5 X6 C' ^- T4 I O3 S -
, U2 e7 @- Q6 S( c1 N: Y4 a/ A - usleep(500000);/ J7 E3 Q0 {7 D7 r/ I
- }* H u% N3 i: T, E
- }
复制代码 9 _* | W) b) a3 l% |' }
- c+ Z8 M. i5 r* @ ~: c服务端: - #include <time.h>
4 }, [2 w! {1 n1 o" ? W - #include <stdio.h>) a" P: M+ J# z* M$ |$ L
- #include <stdlib.h>! a- n; g/ J9 Q4 s
- #include <string.h>
2 |: O# S" _# j- A9 X+ _/ Z, } - #include <unistd.h>
: f5 @$ S3 P' P, k0 o - #include <arpa/inet.h>7 n' |( T3 R, H1 l8 k9 V/ R4 J
- #include <netinet/in.h>
: e" S x( r3 r8 H# Z& G8 Q7 {% T - #include <sys/types.h>: x3 ?) B" w9 M$ j4 t% F
- #include <sys/socket.h>
" ^8 a, H* q0 ] - - f& u5 b1 \3 H m" _: A( r/ f
- #define LOCAL_PORT 6666 //本地服务端口
2 ]+ C5 E9 G+ @( J9 t - #define MAX 5 //最大连接数量% T% t" X4 y1 |4 T
- ( Z& k0 K3 J5 z' ?& O
- int main(){" O! s" }+ X4 N3 V) j- D) X& W* z
- int sockfd,connfd,fd,is_connected[MAX];( O) m9 k& k/ P$ y
- struct sockaddr_in addr;
* g0 D& Y6 M1 U7 e7 |9 E" u - int addr_len = sizeof(struct sockaddr_in);5 s/ ?' ^% ]0 X& y, O5 m: }3 L M5 N6 h
- char msgbuffer[256];
! C, n: X7 j, q: {+ f - char msgsend[] = "Welcome To Demon Server";8 ^0 ~" I W* N; M; [% Q$ B; o+ \
- fd_set fds;
, \$ J, i8 {8 U z, y% g+ b4 G( ? -
$ U6 I" v; i. D }# c E4 Y - //创建套接字, b" w- y- C* i+ }4 n9 n0 a3 `. V
- sockfd = socket(AF_INET,SOCK_STREAM,0);
. E0 g' j. q" l" Z U+ k - if(sockfd>=0)$ r2 A* }9 | o, ^& F$ Z% C
- printf("open socket: %d\n",sockfd);; f7 Y$ n" ^, r4 V! |/ r3 ?& ?0 ?
-
! s- }/ H; j1 P+ Q - //将本地端口和监听地址信息保存到套接字结构体中
. u$ V& ^ D7 N' _8 ^ w7 ~ - bzero(&addr,sizeof(addr));
6 A# ~, ^: V+ v' B* G$ I* l - addr.sin_family=AF_INET;: m3 `. q/ q4 |/ n* B. ?3 u. \
- addr.sin_port=htons(LOCAL_PORT);* q0 N. @+ h: O6 ?" Y
- addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
" N& u- ?3 l4 `4 X! E/ q2 X -
$ j7 B* c* q1 Y' r - //将套接字于端口号绑定
3 A& I5 N" U2 a7 C/ {- g3 x - if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
% K! T C; s4 C" k4 Z - printf("bind the port: %d\n",LOCAL_PORT);
* r7 K5 |8 v2 R -
: G, @. ]0 U& O- f" @5 F: x+ N - //开启端口监听
$ Q' {$ t. p+ e: `4 F+ y - if(listen(sockfd,3)>=0)3 }" y6 n3 ~, v- V: m/ D/ |0 {
- printf("begin listenning...\n");/ h/ u+ Z1 p5 I+ G, J
-
% o7 I/ k) g& V% s6 k6 }8 n - //默认所有fd没有被打开
( `( d3 b$ U5 y4 v - for(fd=0;fd<MAX;fd++)
' Z6 v4 S$ ]% {; z* ~8 h - is_connected[fd]=0;1 N) c V, i ?9 N( M
- ( o, d# s8 K8 R7 ~# v
- while(1){
( N0 r* K( h9 ]; Q; ]) N' R" u5 U - //将服务端套接字加入集合中
, R$ I7 I: g9 ^ `5 A - FD_ZERO(&fds);
. Z! u. T* f C4 y; S - FD_SET(sockfd,&fds);. f( q, {, F5 o5 X5 `
-
# l0 ~& @9 m0 p% S: c - //将活跃的套接字加入集合中( G* S R& j+ a1 b
- for(fd=0;fd<MAX;fd++). x5 }1 z4 G% N+ G0 }- a. j
- if(is_connected[fd]). {" J5 q) w2 w8 e
- FD_SET(fd,&fds);" C. N" F7 s9 z9 _8 A
-
* h: u" ?8 f" p& q3 h - //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0& q' F% L# k2 g! X- @7 Y) |
- if(!select(MAX,&fds,NULL,NULL,NULL))
H7 s* _# x4 N* h z5 k - continue;" m! p1 ~. E6 x2 c
- ) h Y- t; q* I8 a* q
- //遍历所有套接字判断是否在属于集合中的活跃套接字. v2 U* b: b2 I' y+ t7 w5 a6 U
- for(fd=0;fd<MAX;fd++){6 ~% h) k% y, O/ L% q
- if(FD_ISSET(fd,&fds)){
* C& O) [9 Q$ g$ h8 I - if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接
U N$ z% q: p2 ]8 \ - connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
9 k+ O: d9 v- {$ R - write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语
% f0 |' l) a4 G- T1 ~. |- Z - is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用
) m& J# @1 l' c/ I - printf("connected from %s\n",inet_ntoa(addr.sin_addr));( o9 O; G/ m8 w+ c* }5 h
- }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字' k& k( o6 E" }, E
- if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
" d( O% Q7 E0 u; } - write(fd,msgbuffer,sizeof(msgbuffer));
8 |; t& t; \* M - printf("[read]: %s\n",msgbuffer);, m2 z) Q% s8 U6 h8 a
- }else{# H) ?3 G5 c; Q7 T. K* y4 i ^# J9 e
- is_connected[fd]=0;- G* j& G: _+ t2 p4 o! j
- close(fd);7 A+ m r; v4 _& ]
- printf("close connected\n");
1 o& }- J; ]1 M2 k2 S8 f7 Z - }) | W/ f" K1 i. p. m
- }
' \9 s) S& q! v& J; ^ - }
! r( l2 d1 V9 A! N5 K# m - }
1 I. T: \' o: Z4 \: Y - }
( I) H2 k5 T$ n% o4 c5 @ - }
复制代码 0 N% w: }1 x; o8 f
% C( @2 b8 ?' x" ?; z9 \6 X
4 E: I& z/ S9 r6 c0 Z# L$ r c2 j( ?4 }: V# B4 F6 j
1 M$ n! b: u. g; S0 e/ h9 _; E' x
1 j+ S) d* x, ?1 ^5 ?' G8 {: I0 } |