|
实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上 2.启动客户端,与服务端建立TCP连接 3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点 ; E; B9 X, n& \7 d: {% ?- p
什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。/ @7 G N/ [: p# J6 }: W! s+ X7 ]
"套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。 对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。 具体其他高级的定义不是这里的重点。值得说的是: 每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。 应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)
% M2 e, f; L" {
. D: s/ o! D: \5 N" b
5 J0 I8 O- O5 y" x1 T9 E如何标识一个SOCKET: 如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个 SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等" A" X. C# z% g. K/ a8 D+ b
描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。 述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出 当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3 & o% Y. I9 b7 t& Y
( e# ]# e& c; j
服务端实现的流程: 1.服务端开启一个SOCKET(socket函数) 2.使用SOCKET绑定一个端口号(bind函数) 3.在这个端口号上开启监听功能(listen函数) 4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数) 5.接收或者回复消息(read函数 write函数) 1 q/ [& O7 L$ n# I
* T0 P" d; V% c: p7 b
客户端实现流程: 1.打开一个SOCKET 2.向指定的IP 和端口号发起连接(connect函数) 3.接收或者发送消息(send函数 recv函数) & H8 R/ U9 }) o' Y" @8 F, Q
! P+ W6 d6 \& M, M9 Q2 z) h( ~
1 F$ K: @, Z+ q7 ?% D$ X
如何并发处理: 如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果 直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端 一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到 有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照 单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
, P: `& P) D: d3 E
2 I% E) N2 ~9 P2 q$ E如何解决: 下面摘文截取网上的资料,有兴趣者可以看看 系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你 - int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为: - #include <sys/time.h>
4 w q# L$ ^! h' l# q4 ?+ e& b0 L( N - + R/ Z- z2 C* ~2 h/ u6 g2 H
- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理 - O/ |2 V. F1 A. E
readset 用来检查可读性的一组文件描述字。
; b; O# l) {- P$ u9 f8 D writeset 用来检查可写性的一组文件描述字。
% I; |' i* N4 ?) k exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
7 N0 x& l$ ?6 e! M+ k% a) O2 R$ ] timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。2 y. T8 k6 g+ z# _. C
# Z% ]+ U) i- g7 ~/ H. J; h! D: t1 K8 n7 W 对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:, i( m% h9 N$ `2 K
+ @9 s) P: i8 y) K* k5 w
- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件) B9 G( m$ i9 N% x# E7 J
- & U: X* Y& {. k n
- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)5 {3 g& P( F% N/ t
- + T$ |5 a S7 G/ Q3 C
- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值: 返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。 否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来, 你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量, 其值通常是1024,这样就能表示<1024的fd。
- g9 P P' S+ L, l4 _/ O* v' L# r' z/ F1 o$ s
8 O0 e# x/ P5 U7 j1 B fd_set结构体: 文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字) 可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作: - FD_ZERO(*fds): 将fds设为空集
9 j9 l* ^9 R1 r+ n! z$ a -
6 G" M* ?0 u; }* x9 G! m - FD_CLR(fd,*fds): 从集合fds中删除指定的fd
9 n0 ^: n/ |- f0 G5 T+ `; A& S - 6 G2 d, [ _' Q- ]/ n
- FD_SET(fd,*fds): 从集合fds中添加指定的fd
% U( f e( p$ _( { - " u' i8 n7 T3 D- }9 R
- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下 - socket s;* t6 p9 M- A7 h4 P5 A9 f* o4 C! N
- .....+ F8 c) d+ O7 @9 s- y
- fd_set set;
4 b4 ]% p, l( L - while(1){
$ q( Y+ y, Q! |$ Y# @* Q - FD_ZERO(&set); //将你的套节字集合清空5 F. x; F9 V* O" ~1 e5 Z$ _# b. \3 K
- FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s
, D2 ]1 E. g! ~/ J3 G - select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,# e/ Y9 l3 q* e+ G" G: [
- if(FD_ISSET(s, &set) //检查s是否在这个集合里面,$ ~: Z; {' P' ]0 F! ?" R
- { //select将更新这个集合,把其中不可读的套节字去掉7 ` S7 k3 t7 P5 D& |
- //只保留符合条件的套节字在这个集合里面
% \; T3 P! I" d8 ]* N" |1 |4 R - recv(s,...);
) |$ ]: r, k: n7 S1 I - }( N# v8 G3 @2 ?8 M
- //do something here
; l5 d W9 l; _: A0 U+ T- J. p - }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd - (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。8 Y( {, `2 n- y) A
. u& u3 P5 y+ ~) K& n( C' Y- h- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)
' z- D6 |8 r# L/ ` - 6 S& B A2 a$ F3 E$ A
- (3)若再加入fd=2,fd=1 则set变为 0001,0011
: s" a5 E5 W4 a
1 ^; O, E, z9 d- j. T: j5 J' o) R% a- (4)执行select(6,&set,0,0,0) 阻塞等待2 A+ e2 i# d. n/ [* N8 [* X0 a
% ^) K1 q! S I( ]/ L: i" h- (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 x" J9 B, N* n- X
! w( g0 }' q7 {+ G
使用select函数的过程一般是:
/ W6 Y7 C2 b5 k, w! I3 U 先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1 复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
0 m1 }. a. ~" G - v e3 n5 o. c7 P( r
客户端: - #include <time.h>
# A4 r# k9 U: S/ z" E - #include <stdio.h>1 x( c7 X6 Q# _. X8 w' y8 H
- #include <stdlib.h>$ d( D9 f1 \- ?: u* d
- #include <string.h>
1 I( b! h( A3 ~) W, q' B( W - #include <unistd.h>
. h# e1 E8 h" B; t- p - #include <arpa/inet.h>
$ ^4 f3 }) y- i% Y - #include <netinet/in.h>0 t: q1 s9 J- Q+ F6 e# U
- #include <fcntl.h>. }3 e+ H6 N+ H/ _7 P
- #include <sys/stat.h>
7 ?: g8 X1 b4 } - #include <sys/types.h>& J* l" g, n5 a, l
- #include <sys/socket.h>, K& U) d' b1 v! o: g: v3 p
- " n9 A9 {- {" I, C! M. s
- #define REMOTE_PORT 6666 //服务器端口
" ]% ]0 b4 y, |6 I* t - #define REMOTE_ADDR "127.0.0.1" //服务器地址, m d6 T- d# |3 a
- 4 ^+ L0 ]% `6 W
- int main(){2 L. t6 q. k9 K3 {
- int sockfd;
& i! V- m) p/ l {. B6 q b0 b - struct sockaddr_in addr;% K( n7 X. C* t% c
- char msgbuffer[256];* D# E0 }/ H( e ?, c- a: S3 f( r0 G- W
- 4 g- B, E+ ~" c, i' d! b
- //创建套接字( ~; X& X! ^8 I
- sockfd = socket(AF_INET,SOCK_STREAM,0);
, g# D5 C; g( O- }' s2 ` - if(sockfd>=0). {: g8 m8 h2 m5 g) A2 P+ e: }
- printf("open socket: %d\n",sockfd);
: \% ]% M9 r1 D! T7 Q" I -
- l% I# }# R3 M4 u; F" |1 e - //将服务器的地址和端口存储于套接字结构体中+ T% s& q" q, X4 \, k- @
- bzero(&addr,sizeof(addr));
, U5 n4 a' H9 g* e" \2 d - addr.sin_family=AF_INET;2 l B2 A* ?. ~; o/ o
- addr.sin_port=htons(REMOTE_PORT);0 d$ V) X; d% E" m1 F
- addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
% Q. \# H Q- E/ Z) x% ? -
7 Q; x, N+ N: ^* x% q - //向服务器发送请求
1 W! Z: X3 {1 G# \ - if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
. G# a, J% t& i - printf("connect successfully\n");
E5 \* ?( I2 ` z3 u K: ] - 6 d( i- M$ k; h+ r+ t
- //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
5 ^, s% [. g- Q! E2 s - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);' [9 V5 j/ H. c4 n3 }& P
- printf("%s\n",msgbuffer);7 M) Q z( f- [: r. ~/ W" u
- - w* s) C9 S1 J1 K& G* D
- while(1){
8 k# ?1 ^# @6 Q1 R6 S/ P - //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
- s3 o+ B" F, _% o, B- a - bzero(msgbuffer,sizeof(msgbuffer));
, m W2 k# Y! h7 F - read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));" N1 b9 V: W. |1 U S
- if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)9 C. U- v" Z/ R" X
- perror("ERROR");: {$ H) M1 f0 X4 o" @
- 9 A8 @' ?% t( A* U* ^) n
- bzero(msgbuffer,sizeof(msgbuffer));, h* W2 K! z1 I9 v" V* Y/ d a
- recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
0 R4 R( O% O+ z - printf("[receive]:%s\n",msgbuffer);
0 {, v6 |' i o9 Q) n -
9 K9 b7 ^$ y: I. Z1 G - usleep(500000);
6 b' u) m+ n: D8 ]* e$ ^- N7 N$ ~4 x* o - }
: D4 m0 a8 F" w6 q- X - }
复制代码 9 X1 v* O: d- E5 r) I
# ]% ^; @0 L. _1 d9 W5 P
服务端: - #include <time.h># T' W, n* V$ B7 z1 C
- #include <stdio.h>
& N' c9 w8 v8 g6 a" g/ W - #include <stdlib.h>
1 b/ p& O. W; l+ G1 m - #include <string.h>
5 ^! I" T5 q# U" p# G- D - #include <unistd.h>
, |! x$ A6 M/ `& q9 h8 X8 G$ w' | @) | - #include <arpa/inet.h>
+ u4 Q6 o' S4 O7 O# t8 h - #include <netinet/in.h>
) s4 ~8 Y) Y& @+ U - #include <sys/types.h>
+ M) j, h V8 k E) m - #include <sys/socket.h>
r' l/ r1 b* R( X0 S: E- O) V7 V - 7 x0 {' X1 k# J# I
- #define LOCAL_PORT 6666 //本地服务端口' k5 k, x8 h }7 k- S: Z+ P
- #define MAX 5 //最大连接数量; O9 h: ` l/ J- W4 C6 l' ~" }
-
4 i$ E0 C2 \ j - int main(){3 {4 l6 Z% a. T. Z( v9 v- Y# K( b
- int sockfd,connfd,fd,is_connected[MAX];6 l1 w' t6 D( Z" P" n7 o: [ B
- struct sockaddr_in addr;5 R! R% {% ]% E/ d/ {3 M8 z1 o
- int addr_len = sizeof(struct sockaddr_in); K! w8 Y1 w% u* b# ^5 F
- char msgbuffer[256];% c. l' @8 W# F7 m0 N
- char msgsend[] = "Welcome To Demon Server";. R, x+ j. o# h/ F
- fd_set fds;
& U @7 x. E8 @4 K - % J2 k% @+ u7 u+ O
- //创建套接字: D3 b) d% T/ y3 \3 U7 U( x8 k
- sockfd = socket(AF_INET,SOCK_STREAM,0);- Y) N6 t$ S/ H) `1 @/ r
- if(sockfd>=0)
( H/ _% t6 h" ?6 ?0 i* I - printf("open socket: %d\n",sockfd);
9 n+ a# K+ o5 h3 n2 e - - V6 `2 ^& q2 G
- //将本地端口和监听地址信息保存到套接字结构体中7 t/ V* i2 @* S( [
- bzero(&addr,sizeof(addr));
2 h7 _* n( U7 d C. T2 _ - addr.sin_family=AF_INET;* ?$ j/ v* P# h4 z0 W/ {; W
- addr.sin_port=htons(LOCAL_PORT);
& d7 j6 m+ Q6 T% U" l, L0 I - addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0 X: Q# X6 Z$ H8 W ]8 x" h1 l, ~
-
. z1 r, W3 r. a# o0 \. `( v - //将套接字于端口号绑定5 R8 {; d2 |6 t* w* D: Y
- if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0). W5 b# Y% E! v b1 [
- printf("bind the port: %d\n",LOCAL_PORT);1 U8 f! d+ ^8 J, Y" N
-
6 g8 Y" F2 \8 L' H6 w7 }6 m1 r - //开启端口监听$ b. ~$ u3 {7 H; z; t3 }+ L
- if(listen(sockfd,3)>=0)3 d8 p F0 T5 N7 m+ z. I6 n
- printf("begin listenning...\n");
$ Q; S0 ]* U- \6 b+ S8 ?" | - # ^1 ^+ ~) @" ~' w4 a$ s! t9 q& y+ \
- //默认所有fd没有被打开
8 W% E% L ~. N9 e2 j: @! G* _3 Q" ] - for(fd=0;fd<MAX;fd++)
7 }. @1 J0 l' g8 l5 M - is_connected[fd]=0;
6 {- W4 K$ B4 ?3 H% V - % Z' K1 [4 t+ V- D+ R3 j$ b: I! ]6 a
- while(1){
4 ?- `, I6 k4 Y+ U& D% N, ] - //将服务端套接字加入集合中( h7 v& [" l# ~) Q% u
- FD_ZERO(&fds);
$ M* m3 o7 R) X; R - FD_SET(sockfd,&fds);5 w4 Y; i- s0 p
- 9 o- K D$ R2 v" ?2 R. k
- //将活跃的套接字加入集合中
# G% j) @# C. D9 ~; B3 s - for(fd=0;fd<MAX;fd++)
2 l& a" R F* }; n, D& k - if(is_connected[fd]), P& e) q+ r! G5 U: P
- FD_SET(fd,&fds);
- a# m; N6 [# e$ ] -
. E* y# m/ @" x6 a - //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
5 ^ m/ I% K- t! J) e - if(!select(MAX,&fds,NULL,NULL,NULL))
8 _& V; [! A. G9 C7 n0 ~0 J& m7 M - continue;
. d/ S |# d7 b& v$ L - 0 e6 ~! q# Q5 u: }
- //遍历所有套接字判断是否在属于集合中的活跃套接字
) b2 m9 t6 ~7 v8 i1 E% @; T1 W - for(fd=0;fd<MAX;fd++){
7 h+ r8 V6 K# @: ] - if(FD_ISSET(fd,&fds)){& X& o+ z5 ?+ T% {6 X
- if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接" q) B7 g9 F4 c! h) Z& {
- connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
, R& |9 g$ G% z8 N2 O" B" u - write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语
2 n! O+ M2 q; s& S$ a - is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用
& R+ o! L$ v: Z1 |5 Z - printf("connected from %s\n",inet_ntoa(addr.sin_addr));6 ~& e) f( g" a+ \: t+ ?
- }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字! d6 k) |- R$ b- f" A' A
- if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ $ \; V; U' n$ X3 D X E
- write(fd,msgbuffer,sizeof(msgbuffer));4 ]; d* a+ Y9 D- f
- printf("[read]: %s\n",msgbuffer);3 p1 v' y( m6 U- P& `: c) y
- }else{, k8 p* I: W" v
- is_connected[fd]=0;
" p% z' Z9 \% e/ N/ t - close(fd);
/ @- a' s# J# F+ p; b - printf("close connected\n");
3 d. ]! q$ p8 J - }; H) b0 h9 l- i. I1 U
- }
* {# h% v. g: \$ S8 _) m - }0 S. s$ j9 |1 J4 i
- }
" {" L: @4 B( s: l - }
9 l9 P7 U2 v0 D - }
复制代码 # P6 w: ~4 ]7 c% _
; Y( v3 `# I4 ~& s; j1 u+ a0 _+ G( L5 O0 b
- n \5 b \! E0 m
, |1 h- J0 _) y. D- {' T& r; X
# n2 M! y/ v _7 @ |