|
实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上 2.启动客户端,与服务端建立TCP连接 3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
! J! f* g3 H7 ]0 m什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
0 K Y& e \0 _" k "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。 对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。 具体其他高级的定义不是这里的重点。值得说的是: 每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。 应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)
( z, n$ Z/ e: Y" d+ S" ~ * C8 V, H" Q, @/ p
0 g! F; l# B6 O! [3 F
如何标识一个SOCKET: 如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个 SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等1 O6 Q5 T" `% T+ H
描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。 述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出 当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3 - ~4 n2 a6 T/ u# v9 T: M M
5 S5 p& @) ?7 G8 J服务端实现的流程: 1.服务端开启一个SOCKET(socket函数) 2.使用SOCKET绑定一个端口号(bind函数) 3.在这个端口号上开启监听功能(listen函数) 4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数) 5.接收或者回复消息(read函数 write函数) % M) A: C6 d# ?/ l0 Y) ^: M
?/ w( q7 L! K客户端实现流程: 1.打开一个SOCKET 2.向指定的IP 和端口号发起连接(connect函数) 3.接收或者发送消息(send函数 recv函数) , m: I; w* X6 w( j
% @! @7 `3 E* I# ]! m: \! S8 ?4 T/ T* ]" H# ?4 I! i! r
如何并发处理: 如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果 直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端 一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到 有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照 单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
% e3 `: M7 h! O& |; d, _ W) t- _
4 e/ V; {, N: d, X- L如何解决: 下面摘文截取网上的资料,有兴趣者可以看看 系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你 - int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为: - #include <sys/time.h>
2 J/ @$ f0 L; T - 5 B8 m9 G6 @% Q/ r
- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理 - M$ w; \8 t2 K: K6 ^$ W9 O
readset 用来检查可读性的一组文件描述字。 * L; y; k- V4 f4 ^
writeset 用来检查可写性的一组文件描述字。 . B$ J# d E+ ~3 V$ I
exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误) 1 u) h7 H! P, M: C z8 ^8 U( \
timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。7 u* l" C2 T* A' h0 J# V
; G" i& `9 k6 C h6 d- k
对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:+ _) n( h* f m" m6 |/ w0 O6 W
9 O$ _! @ E; T. l) Y! E k; b- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
! L' J5 U4 C) _/ n: R) h+ ?
( x8 O7 e/ `& o+ G- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)( G: Q3 _. E1 P Z
- 4 c, q5 f! n0 m ^, ]$ C$ `
- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值: 返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。 否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来, 你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量, 其值通常是1024,这样就能表示<1024的fd。) q6 p; c$ K5 v5 `
7 k% N) J% A' t+ Z N* C6 Q
3 ^2 I# |8 U. S- c- n5 v$ C) ? fd_set结构体: 文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字) 可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作: - FD_ZERO(*fds): 将fds设为空集
8 ]# R( a3 k) I# U+ d: v - $ F. ]) ~ b( b& y* [# {6 ^3 S
- FD_CLR(fd,*fds): 从集合fds中删除指定的fd
) v* g$ ]4 l ]3 n$ P1 c - + U# ~% }; l7 s, P+ t) f
- FD_SET(fd,*fds): 从集合fds中添加指定的fd! u8 z8 \! u( N6 u) o8 `" O! h
- 0 x) |8 B0 X! P( X! D
- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下 - socket s;
' M. [+ w* H( u+ l2 y - .....
0 {3 o; G8 y# A q9 v+ a& v& w; x& B - fd_set set;! l8 v4 x* n! G* g" X
- while(1){8 F/ }, x& Q }! }0 E7 E. e1 a5 t
- FD_ZERO(&set); //将你的套节字集合清空
9 D9 ]0 O9 h+ h+ s: L! l - FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s! s. m/ M }: `% T
- select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,
+ O* [8 o. Q z; S - if(FD_ISSET(s, &set) //检查s是否在这个集合里面,
) l2 @& z$ o1 P: C - { //select将更新这个集合,把其中不可读的套节字去掉
2 Q J+ m* a8 ^) t* @+ s - //只保留符合条件的套节字在这个集合里面; @& @3 ]& v5 W. }8 i2 x
- recv(s,...);
8 r1 c* V4 I+ Y0 U7 u: g8 L1 w - }' s% J4 l1 z) ?" {0 Q" z: B2 J
- //do something here
, U1 n4 y8 p. f' Y* }1 _ - }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd - (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。. p1 w" f& i$ x% F
- # |7 q8 @/ ]8 H
- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1); n! w* @: H1 o* `% O
- 0 m& v' ]$ h& e& e+ O+ {9 d
- (3)若再加入fd=2,fd=1 则set变为 0001,0011
- S: w5 b5 Q* |3 w
5 g9 v% g& X9 }! ?9 H. x- (4)执行select(6,&set,0,0,0) 阻塞等待* U1 |+ V o" j4 e0 \9 @1 T3 ~
" c2 Y; o4 M) x- (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 P( ^: b4 Z9 e: `) s: P
1 L3 r2 [. G8 e6 c使用select函数的过程一般是:
+ h \/ y; w* Q( p4 W+ b/ W 先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1 复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。$ [, B* ~* {+ I) T8 |
0 r1 Z; Z: ]5 B( \
客户端: - #include <time.h>( x0 c F/ ] Z) O O0 z
- #include <stdio.h>
; j! b5 j' J, V- \" s - #include <stdlib.h>
, E1 p- A5 C+ r& G+ W - #include <string.h>. @3 C+ d) q- ?! `
- #include <unistd.h>' x4 p8 t0 V+ J# x7 \# O
- #include <arpa/inet.h>& ?! P5 h( v5 [: y! S
- #include <netinet/in.h>
# c' w2 ]* W9 E0 J; O - #include <fcntl.h>
; N8 Q* P5 p: V - #include <sys/stat.h>7 K; q5 c" f' I: A& o+ k
- #include <sys/types.h>
+ t0 ?) o3 n7 v% v3 L' |2 _( j - #include <sys/socket.h>
$ o" Y% G( F) m -
. `7 E& D( T5 h# [ - #define REMOTE_PORT 6666 //服务器端口) A2 a' `- I/ b- } Z: O1 D9 j
- #define REMOTE_ADDR "127.0.0.1" //服务器地址& j: f$ v$ L$ L1 `. a1 R
-
# a; _9 g& [+ ?( P - int main(){
4 s$ K. Y2 R _% T, ?1 |8 }1 w0 W' h& i - int sockfd;$ a' g: l# q( u& _5 g+ ?4 ~/ M9 R
- struct sockaddr_in addr;
9 B6 Q$ F) [( ^ q8 O - char msgbuffer[256];
) ?0 a; B2 V Z. b- P! F: u8 H - 6 n* a! l% X# R
- //创建套接字
. x+ _6 M, N/ k: k& k2 a" | - sockfd = socket(AF_INET,SOCK_STREAM,0);; N5 M. m- @( X
- if(sockfd>=0)
9 v3 h( R9 ?% k2 M - printf("open socket: %d\n",sockfd);
3 \( Z: k/ \# l: h3 Y) Z: X5 j - 0 r, g/ ^( ]4 @8 O7 r8 [' v1 f
- //将服务器的地址和端口存储于套接字结构体中( y: p/ J% N+ k5 T* t$ z; @
- bzero(&addr,sizeof(addr));, x. d, L7 A4 s" c1 G
- addr.sin_family=AF_INET;
& M6 D/ `7 n9 i: H - addr.sin_port=htons(REMOTE_PORT);% k6 {: p+ Q! v" C: O
- addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);1 h! ]! S, p. T/ @& Q
-
' i$ W9 c3 E6 e3 F, R - //向服务器发送请求
+ S5 N7 N2 |# V3 ?4 k9 J$ ~! m# W - if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)% U& U4 a9 z- w% q. ~
- printf("connect successfully\n");8 H G& Q4 k8 O& i+ _
- C7 F( D- r6 v1 L
- //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行) D: J4 S8 L: U0 L! O% X! J1 J
- recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
; l3 o+ b5 i$ @. g - printf("%s\n",msgbuffer);
) ?7 n7 Y, O5 E6 J1 C5 B' ?4 t - % k- G9 `4 p, S3 |6 j+ R
- while(1){
q6 S1 a- ^0 ]. K+ C8 |. X - //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
) L- l7 f/ p7 c- _+ v5 |; c/ w( q - bzero(msgbuffer,sizeof(msgbuffer));8 ^, B& [. x* E( E; F0 f" r
- read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
1 j( ?1 T5 G; }* `; b' G3 d - if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0), w8 {8 Z( N3 a8 ~1 B$ w
- perror("ERROR");3 s8 k! W$ y9 s5 I. Q! [8 R
- - x$ _( j7 e" j; @; ~
- bzero(msgbuffer,sizeof(msgbuffer));
1 b0 s. n* ~7 D7 s/ } - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);: ]: W( q6 _9 @, Y6 j5 Q) r& `
- printf("[receive]:%s\n",msgbuffer);
# |! J5 {( h. u! @+ p5 H& M -
8 H* k( ^3 A5 v; F' [- g - usleep(500000);
- p. \. H, ?7 a: Q. n. @ - }
$ N, s4 a: R. M, F, ]3 u - }
复制代码 ! l& j( l$ E4 G" [7 W7 ]7 a
9 y1 j$ ~ }" K0 c
服务端: - #include <time.h>
/ L5 ~# E7 {4 ^$ {7 d - #include <stdio.h>5 X+ @6 [ v8 m. `
- #include <stdlib.h>
+ W% Y/ [6 H0 y, k7 X& f - #include <string.h>
2 n: b6 E$ ~5 e4 Y% M4 U3 ` - #include <unistd.h>
" s" K ~0 M) i: I - #include <arpa/inet.h>
" g! i+ e/ m; |4 L8 Z y" [ - #include <netinet/in.h>, A/ D$ }* \ J- ^5 o
- #include <sys/types.h>9 M M- ?* s) \0 i! i) ~
- #include <sys/socket.h>
7 K# H. r8 o: Q2 p - 4 U: m& `- p5 O% ?2 L: J
- #define LOCAL_PORT 6666 //本地服务端口
' _* ? r2 G# g - #define MAX 5 //最大连接数量6 R+ y L6 i, s: s
-
, o0 |; U$ _$ U9 i3 L5 n - int main(){
: m f4 Q2 N8 s6 P; N - int sockfd,connfd,fd,is_connected[MAX];1 y; E8 @ |2 U$ ^5 c1 X7 P9 e
- struct sockaddr_in addr;
# m0 L9 P+ B( X1 T% Q. W - int addr_len = sizeof(struct sockaddr_in);$ |7 D8 t/ i I$ v9 B" Q
- char msgbuffer[256];
" {. v3 A: H* t - char msgsend[] = "Welcome To Demon Server";
9 B; r+ ?3 t [. Z2 Q' j8 y3 l. | - fd_set fds;
" m" S/ l8 ]: S; o# |- G7 L- L - 4 m$ ]' C: ?% P g% p
- //创建套接字
- v" o" l" D! B1 G: I; s - sockfd = socket(AF_INET,SOCK_STREAM,0);
/ d- b% k1 M6 z% n1 c - if(sockfd>=0)
7 W; S5 \8 T1 N# [& ` - printf("open socket: %d\n",sockfd);
: r5 q% h" O, f* z) [ - 1 n+ v; |, i0 o5 B
- //将本地端口和监听地址信息保存到套接字结构体中4 F, d! H4 J, M1 U; g( Q" L5 L- S, j
- bzero(&addr,sizeof(addr));
8 _& v& l) f# E) y7 \ - addr.sin_family=AF_INET;
. Y& a1 L; Z6 Q6 Z! W2 a6 G+ C* F3 K - addr.sin_port=htons(LOCAL_PORT);; h' f4 ^% _1 t
- addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
% D9 `* `& G3 s# T3 W0 c - $ n, n- L6 g0 Y" q/ @
- //将套接字于端口号绑定! Q7 z& w2 |0 q5 l
- if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
; l# \; J8 J( S3 E+ a1 _! K( ] - printf("bind the port: %d\n",LOCAL_PORT);
5 f1 o! M* q4 r" _3 k& N' F5 \ -
, _ t, P9 L7 g - //开启端口监听4 z# k6 S. L4 m4 Q. C% j* S" }
- if(listen(sockfd,3)>=0)
& a' \% V& |, @1 v; c8 U: ` - printf("begin listenning...\n");; e' B l6 t4 l0 k. J5 ?& y" r
-
6 y8 g0 }. K( f$ h. c. `8 N - //默认所有fd没有被打开
: w2 j6 }3 n' w! l, t - for(fd=0;fd<MAX;fd++)
. H" j1 ]) J! f: M: m3 K - is_connected[fd]=0;
# i: [2 x, I# s6 x2 m- S - ! m- C5 G1 w5 s4 ^
- while(1){5 G# J: M1 d. S" e9 B" D
- //将服务端套接字加入集合中
, |' M; u: x4 x5 ^" u5 M% G# M% X ^ - FD_ZERO(&fds);! n v3 K- F6 ?( R6 C
- FD_SET(sockfd,&fds);
* g3 l9 z( W: l" m+ ^( i# r( i! o, A) b - . k+ Z& e3 K( S; x, g% M
- //将活跃的套接字加入集合中
. ] y2 @9 l( e! y) O. g. K - for(fd=0;fd<MAX;fd++)
" A0 J* i5 |+ Q6 B. b5 e2 b% O( X4 W - if(is_connected[fd])5 B* R/ g% [( C, m( q) E
- FD_SET(fd,&fds);# w) }& n6 p+ K' Z( x) ?
- ' i. K7 ^* V# k; G/ o, a% |
- //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
& I( r9 O/ p- Z2 b9 `1 S* C: `% e - if(!select(MAX,&fds,NULL,NULL,NULL))
; ~1 g- ]% d5 t7 [ - continue;
$ m$ }7 e& B7 l* J - ( L" A, S9 H4 G: l& `
- //遍历所有套接字判断是否在属于集合中的活跃套接字
6 E2 ^2 F* v; F! y - for(fd=0;fd<MAX;fd++){. J L5 o4 l. ~
- if(FD_ISSET(fd,&fds)){( K0 Y& r; u4 Z; l1 K& p8 ^
- if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接3 J- z( i% V& F! o+ b* }4 c
- connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
; ]! R, g/ q9 r. j$ n; J; ? - write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语- W b! j& K8 J9 m# ^3 g
- is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用, T' ~" q0 C3 x V6 E
- printf("connected from %s\n",inet_ntoa(addr.sin_addr));5 d6 u! M/ n5 ]- w/ ]$ L% j S J
- }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
' l8 r6 [5 t b2 x$ b6 t- y - if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
! `: Y+ j7 r; Z* |2 D8 z - write(fd,msgbuffer,sizeof(msgbuffer));
, a- N6 l1 U! ~- w - printf("[read]: %s\n",msgbuffer);
3 L: B8 v/ c/ R5 r1 j - }else{
& S" D+ M) u/ c% B$ J/ d - is_connected[fd]=0;
" T1 l0 g! t. X# ~$ n5 C, X! v. h) m/ { - close(fd);" }9 a; I3 B" ^1 j2 b9 `- ~" k6 q# ~/ F
- printf("close connected\n");; q8 l; _- G ^) x5 Z: O9 h* T4 ]
- }
2 w8 x( {2 }4 I0 Q6 w* f - }
) P3 F( }2 g# S+ B9 \+ E( \. E/ I - }
' B; W# f! G! v5 Y2 g+ g4 g6 A# b - }1 X6 [7 O7 l5 F2 |! z. W
- }# y8 d2 T& F a5 n$ K y$ W2 G8 f
- }
复制代码
9 {$ L* y. ^7 s! q7 x! W- q3 g3 t. V3 J1 A) G6 Y
$ j% Z4 G w# V" f: e( b: n: a! x+ Q
0 o v4 d" W* R$ Q
- b5 k( z6 t3 r+ W) Z$ \! V4 W! \. T/ F: c
H4 i; [7 _9 X- ` |