实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上 2.启动客户端,与服务端建立TCP连接 3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
$ u% F- ?# [9 h什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
# N6 j3 e' g& R: S "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。 对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。 具体其他高级的定义不是这里的重点。值得说的是: 每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。 应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)& z) v1 [$ c1 p
; l4 D% f6 b! X) }
! y* N5 t' r0 P5 Y; V
如何标识一个SOCKET: 如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个 SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等8 r6 S3 ~, L: C! I8 z9 L
描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。 述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出 当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
1 u; R/ b% W0 |4 S' w( ?9 i5 r) [3 x
8 R7 E8 i/ K/ P u+ {: z2 s3 }服务端实现的流程: 1.服务端开启一个SOCKET(socket函数) 2.使用SOCKET绑定一个端口号(bind函数) 3.在这个端口号上开启监听功能(listen函数) 4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数) 5.接收或者回复消息(read函数 write函数)
8 x# v6 F! D e0 B3 d' ^$ J0 L% s6 u" i
客户端实现流程: 1.打开一个SOCKET 2.向指定的IP 和端口号发起连接(connect函数) 3.接收或者发送消息(send函数 recv函数)
: S! y- p) q' `! W
- ~: }" ]$ o0 h. P) e3 u0 @ v; ~: ~5 }3 J8 W. x) i
如何并发处理: 如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果 直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端 一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到 有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照 单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
3 m( R) E+ o5 U! t9 f' q3 e. W6 Y9 i
6 B4 R8 C( `3 ^如何解决: 下面摘文截取网上的资料,有兴趣者可以看看 系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你 - int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为: - #include <sys/time.h>* S9 \- X8 j1 z3 Y5 g
: Z' J/ K1 m6 B, v- b9 G8 Z- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理 : c+ Q2 k+ j8 z/ X6 ~( T
readset 用来检查可读性的一组文件描述字。
0 ~6 _6 {0 V; Q) M writeset 用来检查可写性的一组文件描述字。
2 J" g6 _6 E$ b$ k5 w7 q exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
- i+ u& j R0 _% e, e timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。6 ?8 ?, `7 Q. u+ s1 j) m9 c
a$ I" h# b8 ^- b C 对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
* B7 \. M, B! H/ w ! B) \. S2 S! m4 o% V' Z$ Y+ r
- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)) m* T6 A7 ~: H9 e* i
- . I# e5 r0 J, v8 R2 O! t, i
- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)5 I: d/ E. Z. h7 r, ?. F
- ( K! @& V* z( q$ _$ O m0 v
- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值: 返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。 否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来, 你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量, 其值通常是1024,这样就能表示<1024的fd。
% u9 R' [1 M# L1 N% r0 @% H; s2 ]$ B- d2 O
* O& X9 x; F* _0 O7 x# w9 G
fd_set结构体: 文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字) 可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作: - FD_ZERO(*fds): 将fds设为空集7 O& k! {7 _( K3 I
- / `# c; r% B0 Z
- FD_CLR(fd,*fds): 从集合fds中删除指定的fd
; a0 u" `2 C. V1 b
5 H9 F" }: _4 I- FD_SET(fd,*fds): 从集合fds中添加指定的fd
5 F/ h8 V/ B3 o
2 f/ M4 _7 k3 @+ Q- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下 - socket s;
+ b2 I+ v! Z5 {1 Y. W% [2 b# Y - .....
- ?+ j: q/ n/ |8 ~# ^, l( ` - fd_set set;
% D7 n4 ^7 e8 w! o4 T+ U - while(1){0 G1 ^+ D: A4 J# ]4 `7 x( N5 U, [
- FD_ZERO(&set); //将你的套节字集合清空: n3 a6 w) z, m; \1 l
- FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s
; T6 P4 i; n7 i+ \2 M - select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,
( s- @5 N& P# Z( v, b1 I V - if(FD_ISSET(s, &set) //检查s是否在这个集合里面, t* D6 w! ?6 Q9 ]
- { //select将更新这个集合,把其中不可读的套节字去掉
( k, s5 N) B/ _ - //只保留符合条件的套节字在这个集合里面
, Y8 c, [( c; ? - recv(s,...);- I2 A- U% N* ]3 m1 r7 e
- }
2 `' D8 d! a7 |% @9 o' U8 i - //do something here
3 H3 d. w" P" a* p6 u3 [ - }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd - (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。: d, N Y* B: \# O, ^# }3 k6 u
6 w+ Y6 ^0 M! W1 y& \- D) ^5 S- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)
5 u% q9 K$ {! ]
! d) V4 M" ]2 C" J- (3)若再加入fd=2,fd=1 则set变为 0001,00110 Q% Y5 O' ?% e3 S) \
- 7 Y8 E! k+ i: c" W3 T# D
- (4)执行select(6,&set,0,0,0) 阻塞等待
: y- p9 K6 V, G A y, y$ a) m - 6 I v4 c0 Z( Y1 |7 M: r+ v
- (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.具体过程看代码会好理解 + v* s/ B2 X. y7 {3 ]5 N1 I
4 w' c, t# b; s, h4 ^使用select函数的过程一般是: ( m0 {6 B- Z; R) v6 w" W
先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set, 接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1 复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。0 a" T" U# ^5 Y
R+ f+ @2 s. Y0 ?! g0 }% c
客户端: - #include <time.h>
, u( J3 b& J5 [ - #include <stdio.h>6 }1 b; m& D1 {" i# k: g
- #include <stdlib.h>
2 T1 S2 N J5 U; Y3 P7 I' u: J( f( u - #include <string.h>9 l* Z2 k4 `7 h1 @* ]9 f1 l
- #include <unistd.h>: g ]3 ?* n* h( I
- #include <arpa/inet.h>
8 Z. E7 K6 L/ T9 {# t; u3 P | - #include <netinet/in.h>
, z0 ~* K) ^6 C* v9 x - #include <fcntl.h>) `4 `& Q$ X5 U$ y- e8 U6 |
- #include <sys/stat.h>: O* C# u4 p' F+ g4 O; E% B: T' e. U' w
- #include <sys/types.h>4 B+ V/ ]# y2 ^+ ^7 h
- #include <sys/socket.h>9 Q8 Z( i8 G5 n! O! r
- 5 M7 b3 v+ }- V* c
- #define REMOTE_PORT 6666 //服务器端口& n" W9 v; P* K5 T5 O
- #define REMOTE_ADDR "127.0.0.1" //服务器地址
, o R3 x' N3 t - % n, q2 \, f' o1 x4 n" J3 F
- int main(){; N. w g- A9 [/ c V
- int sockfd;" f) R, f& n w" H4 }
- struct sockaddr_in addr;( L0 @; Q% z0 S% R6 F! y- I
- char msgbuffer[256];
% e5 h; L8 @: t* D -
; z" f% z' \4 u/ r - //创建套接字* I) D; I+ c# d; E$ P5 U
- sockfd = socket(AF_INET,SOCK_STREAM,0);
$ A! l* w+ D8 o8 _6 L) U0 a - if(sockfd>=0)8 \; D; j! j% Z9 W! B4 ^3 p, \/ X
- printf("open socket: %d\n",sockfd);
7 |& d8 w2 I/ _9 \+ i4 B -
7 |: \# C- \3 q% R% M - //将服务器的地址和端口存储于套接字结构体中
& {- Y/ `% Q5 ^+ V6 v4 h; b# u - bzero(&addr,sizeof(addr));1 }* j$ j6 a) E* L, L( [* |# a) M
- addr.sin_family=AF_INET;
2 c& S) J( z4 u3 O9 |+ {, W - addr.sin_port=htons(REMOTE_PORT);
6 \, A9 k8 J% v% g0 j - addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);7 _3 f8 C1 i+ p5 i3 l T
-
" C. m% Y* | B0 o5 E - //向服务器发送请求3 @: r, Y+ x5 e M4 T
- if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
; C% k# {' {- d7 O - printf("connect successfully\n");
0 T1 i. v, V ? - + ^2 n( \- G' J0 Q; z! e& z5 a. V
- //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)3 q8 F) ~5 |: V5 e* a6 Q! i: C9 y- ]' e
- recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
' { A/ _3 H% X. j( w - printf("%s\n",msgbuffer);. n7 Y" E: q- g' _$ |
-
) f6 W1 h8 h& j; D& a - while(1){
- E8 ]3 h' X3 c( j) {- g# C - //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
* V+ z3 r6 c. ^+ {/ Z' z - bzero(msgbuffer,sizeof(msgbuffer));: A% G( k6 S. @) b8 C* Z) b
- read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
( O+ f( ?( w0 y0 C1 y# _' | - if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)* O! J- I- Q4 r g" W! d; i2 \
- perror("ERROR");2 h2 r# i- H- g o' L7 s5 i5 g6 W
- ) m5 D. r" X9 ?: p4 g* Z s
- bzero(msgbuffer,sizeof(msgbuffer));
( M( d- ]. A8 g ?5 O - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
' v' \3 T1 h' ]" } - printf("[receive]:%s\n",msgbuffer);
+ H0 {' \* b7 ?; @5 W% S: y - 4 h. C; V$ P' G/ @
- usleep(500000);
% G0 E! }- W! V4 N1 s; \ - }
_8 A- l4 S% c" ^/ i: P. q7 C - }
复制代码
5 d2 C0 U5 I4 w+ v T6 b
- D; h5 l& w9 t$ P+ N服务端: - #include <time.h>
2 C9 ?# @; e$ Z0 n x2 Y - #include <stdio.h>0 `9 a2 i. w" j- V, G
- #include <stdlib.h>! N; G s6 k8 t- v+ F) I. H
- #include <string.h>3 A7 c2 i2 v4 P/ s% @
- #include <unistd.h>
1 l0 p! X8 v$ t" l. z6 I- ?! k - #include <arpa/inet.h>
c1 L T0 k. c; o; ]; a - #include <netinet/in.h>
% g5 \7 h9 S' o, `0 T - #include <sys/types.h>
2 v7 H3 a2 y; {7 b - #include <sys/socket.h>
* z: y, M9 [. \/ k3 N0 e. C - 9 X# P2 w2 C" Q0 u5 _9 s. o) f7 w, B
- #define LOCAL_PORT 6666 //本地服务端口* L2 T' c* S1 z4 w# x
- #define MAX 5 //最大连接数量3 N. Y9 S; i2 B/ u5 k) q2 \
-
, }& o( U, @4 N/ r9 }( @ - int main(){
7 y9 n+ [; o1 t! s/ i+ y# u - int sockfd,connfd,fd,is_connected[MAX];( R0 {5 M! {: ?* J: H( x: _ x
- struct sockaddr_in addr;( L& l% Q8 E. X- T$ T
- int addr_len = sizeof(struct sockaddr_in);6 v7 t1 E3 h0 z2 X7 B2 E
- char msgbuffer[256];2 t% w2 K0 X- L: z7 y9 y% l
- char msgsend[] = "Welcome To Demon Server";* t2 H: l+ I: a
- fd_set fds;6 j% E; F8 z W6 _4 `4 Y
-
; B$ x/ K. g9 ~1 w - //创建套接字
$ q$ f. C- s+ _1 k3 |5 {. u" M1 R - sockfd = socket(AF_INET,SOCK_STREAM,0);' P& i1 W8 A0 w( H
- if(sockfd>=0)
) o! a/ q' }3 Y5 ~+ p$ ] - printf("open socket: %d\n",sockfd);
i0 e* ?, {* g9 F- J; A* q - , ]+ f' z2 s! x
- //将本地端口和监听地址信息保存到套接字结构体中2 A2 A4 k: ^+ ^) \' c9 H$ ^- Z' ^
- bzero(&addr,sizeof(addr));
9 j& B8 Z I! ?0 \, ~ - addr.sin_family=AF_INET;% @- g3 `+ r% `# Y" E
- addr.sin_port=htons(LOCAL_PORT);
+ ]) o6 v( V T5 u6 B# E5 [, [ - addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.09 G J" j+ x6 O7 |) k7 O: ~/ d
- 8 t3 C( P. [( w1 v; l% B, F
- //将套接字于端口号绑定
& Q. q+ o L! c0 V8 Z4 P0 [( L: a+ E - if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)# {* F! f$ P8 a. |; L( V
- printf("bind the port: %d\n",LOCAL_PORT);
& t6 `9 Y) ^" Z- g# p/ g) Q2 D -
& b2 N, ?- O: i% |3 _8 [ - //开启端口监听
& M* b6 A9 h8 G- P - if(listen(sockfd,3)>=0)
7 b/ V2 x! F U' h8 E0 L/ Q7 G - printf("begin listenning...\n");
/ z4 i3 v% b+ ~ m0 T$ z1 @, `( o% K7 b -
. s1 R3 Z- ]# S& o3 ]/ U+ Y: R7 d5 A - //默认所有fd没有被打开
2 E' i" L9 Z T- q - for(fd=0;fd<MAX;fd++)3 t4 x5 C6 r; q% W
- is_connected[fd]=0;2 e/ R( x# f8 {/ A0 b6 H
- B* \2 t. W7 q% r- z' t, g% P
- while(1){
! {0 u' w; @/ i - //将服务端套接字加入集合中
# s+ ~- D! V6 R. b; T - FD_ZERO(&fds);
# f, t1 y7 c' y2 q/ F - FD_SET(sockfd,&fds);9 t6 s0 L K1 K
-
; l: y* @! ~; h$ {' O- s - //将活跃的套接字加入集合中1 ?+ l9 p- w+ {! k2 J3 T/ U
- for(fd=0;fd<MAX;fd++)& k- \! l" R3 t! l1 g9 g8 M+ S7 U
- if(is_connected[fd])% _* a' N% [- z3 z) n
- FD_SET(fd,&fds); }" `- a3 l/ c# A9 A
-
: o# e$ u/ z" w+ V! W - //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0+ b( j" H( j6 j/ M/ ^
- if(!select(MAX,&fds,NULL,NULL,NULL))0 b5 t$ K0 {2 I: w' h
- continue;' D6 Y- F' M0 V1 w
- 6 a/ M' s1 N; O3 M! t" L3 V7 @
- //遍历所有套接字判断是否在属于集合中的活跃套接字
! z/ q: g+ }$ p - for(fd=0;fd<MAX;fd++){) l- M5 f: j( v3 N K/ r' ~) N
- if(FD_ISSET(fd,&fds)){
. k: q3 Q( X/ R4 P4 ~+ \ - if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接
/ k& A+ z/ z! b& _1 q$ N - connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
- ]0 j% Z! i: w1 B' d; H - write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语
( _+ m g# z1 ?& ?5 b& e - is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用
( B' H2 L4 p! O1 L' x6 m2 {, | - printf("connected from %s\n",inet_ntoa(addr.sin_addr));3 w1 `+ j1 q b
- }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字 c8 p* B/ e6 b9 f t$ A( W
- if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ ' W- ]: L8 K) L2 ^6 l* ?6 n
- write(fd,msgbuffer,sizeof(msgbuffer));% A! q( p; Y& O$ J+ T; ^
- printf("[read]: %s\n",msgbuffer);9 T& M* e' S1 u$ [8 Q* g8 n2 t
- }else{
a9 x' ~3 K, l" G' Y' M - is_connected[fd]=0;1 u5 _* T1 A$ ]/ X0 p5 u7 \
- close(fd);
) i9 {5 N' ]' W8 \: N( Q. F! | - printf("close connected\n");
4 i4 T; H' K/ m& R8 j - }
9 q6 F' h! a6 C0 m; d - }9 s& L6 t T& R6 J% B/ S. b) C
- }; J* N- |" [* D; b9 v
- }
0 X! V6 n! b. ] - }, K& r0 M% l3 A9 \7 W. Y
- }
复制代码
& p5 C2 F( C, q; _$ x7 D0 V+ s/ o* V2 P" t! D
% o- j: [. Y+ R- c) F, w2 c
. I' ^5 t8 D( |0 f
. o7 s0 r+ j0 V! n9 h5 m' Q; L
. R, d/ l+ L0 g/ i$ J( x# z# c) \ |