|
实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上 2.启动客户端,与服务端建立TCP连接 3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
/ C* I8 Y. [# H' ]: C! v什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
' S9 D ]( k' y p "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。 对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。 具体其他高级的定义不是这里的重点。值得说的是: 每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。 应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)
; C7 d. ~5 V4 h' m8 D, S; P
+ ]! Y# b7 }3 ]/ X! ^
/ l1 S8 M- f4 a7 L如何标识一个SOCKET: 如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个 SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
" q* S* R9 H5 \# ?0 t4 [& E 描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。 述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出 当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3 * H3 Q7 y+ r* }/ ?/ J
- c' T# ^$ l/ G/ `8 Z9 H8 p
服务端实现的流程: 1.服务端开启一个SOCKET(socket函数) 2.使用SOCKET绑定一个端口号(bind函数) 3.在这个端口号上开启监听功能(listen函数) 4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数) 5.接收或者回复消息(read函数 write函数)
' r, }9 e m! X( T% e9 ~; k& K" k
: D% c* w7 ^0 \& k, b. r& ^客户端实现流程: 1.打开一个SOCKET 2.向指定的IP 和端口号发起连接(connect函数) 3.接收或者发送消息(send函数 recv函数) ; H7 D; `+ V' f$ n
, W2 `' u% b! V" d
5 T- N( `) I- {6 D; @) b
如何并发处理: 如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果 直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端 一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到 有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照 单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。 " u8 ^" |7 d$ L8 `( Y+ z
9 a; n& v" [. @. P6 E% A如何解决: 下面摘文截取网上的资料,有兴趣者可以看看 系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你 - int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为: - #include <sys/time.h>; c8 _* }- y7 _+ l+ k
$ Z3 c1 X- z, v- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理 - E- w' j- U* E2 S6 a
readset 用来检查可读性的一组文件描述字。 $ @6 k* ?' t1 m0 S/ J' V; z3 H3 ~
writeset 用来检查可写性的一组文件描述字。
. X1 u! Y+ i$ W" D, x; i: H$ H exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
4 T" r6 t, t u, t timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。+ q+ ]0 q. O" V4 h' n {) Z8 A. H
4 R6 w4 j# k5 m. _( H 对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:; ^0 P6 x# P$ L6 E! v# v
/ y' q0 ?; `+ O& H5 \- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)% M: j% f% {* V9 l% S
2 @3 D, F; ^' y* u/ I- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
5 Y7 A, b# X# y4 c4 n. r
% b$ [( U, j' B- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值: 返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。 否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来, 你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量, 其值通常是1024,这样就能表示<1024的fd。 |2 x3 M, _/ V3 h9 B0 ?6 R
5 B) H5 n' @2 g " B$ ? }$ |% }2 A' B1 T u
fd_set结构体: 文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字) 可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作: - FD_ZERO(*fds): 将fds设为空集2 P* l! [9 G) F( S
-
% l4 O# u0 }, m - FD_CLR(fd,*fds): 从集合fds中删除指定的fd
0 A# {1 o. Z& }8 B! `4 D' C6 ? - / {! R8 Q) f5 H- F: C1 r8 E
- FD_SET(fd,*fds): 从集合fds中添加指定的fd9 H# ~# T' N2 C$ i0 R0 P
- & `( {3 v9 F/ l3 o- t5 m, r
- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下 - socket s;
; @# r+ P6 C! Q. e& ~ - .....2 z* ?+ @4 O! `( K. X
- fd_set set;
: A/ u/ w o; v- H6 P1 M5 h2 `0 p - while(1){! U. {4 T: E: `+ n8 ]# }7 w
- FD_ZERO(&set); //将你的套节字集合清空% w2 G0 u+ [, F% _; n) b* n
- FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s5 O9 b# m' E9 z1 o9 p+ o, J
- select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,- j2 S& w3 L- L; }( [8 T+ ?
- if(FD_ISSET(s, &set) //检查s是否在这个集合里面,& \; y9 v x; H* O8 J) ~8 A
- { //select将更新这个集合,把其中不可读的套节字去掉
# A+ `8 B- H" h( h$ S7 n3 r1 E/ P - //只保留符合条件的套节字在这个集合里面3 U! o( Q1 t5 ?6 a4 a8 |; E3 _- J( q: g
- recv(s,...);0 M ]( ]/ _3 o, Q, A( Y& _: A
- }3 ~5 f" S2 V; U* G
- //do something here
* ~7 S3 h; W; `& u - }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd - (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。; I5 ~5 I3 W+ V( _* i, {! O
r% Z% }/ u+ S2 n/ \9 u- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)
$ T6 _6 t: A; S7 l3 E0 O - 3 q/ K. I% c1 t. r" K' G/ D
- (3)若再加入fd=2,fd=1 则set变为 0001,0011+ Q: T/ B; P8 w" K7 U; G
" G0 S3 P) S+ E+ e* t8 |# x% W4 w0 B- (4)执行select(6,&set,0,0,0) 阻塞等待
% Z! g- k2 }9 H - 5 P! L, X- S- J0 A/ a& Y& U
- (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% E5 ~$ |$ c( U" \
/ G) r, R4 Z; M+ h# C使用select函数的过程一般是: ! v* w( q) w: W1 m1 I/ u
先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set, 接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1 复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
. P. b2 E" b0 h1 d) I5 _) @ 6 H& A8 B$ Q7 Z7 H5 y( u6 d
客户端: - #include <time.h>
/ \* l/ {9 e# _3 g8 F - #include <stdio.h>
6 d& \: d+ D" v+ M# ~ - #include <stdlib.h>
$ N/ ]1 H5 r0 T - #include <string.h>3 x9 w7 W8 F) L3 `+ D
- #include <unistd.h>% Y$ n7 |, x: c( ]3 _- Q
- #include <arpa/inet.h>' K) Z& |! ]3 X% o6 N
- #include <netinet/in.h>; R! p4 N# _' p, O
- #include <fcntl.h>' `3 O, A8 L' D9 C
- #include <sys/stat.h>
7 V+ |+ @- v, G2 Q - #include <sys/types.h> _- H' @8 w( V
- #include <sys/socket.h>% F9 g5 ^. X* W
-
) i) F' R7 G) S6 V+ t* @8 q - #define REMOTE_PORT 6666 //服务器端口
( P( c. t( {* o# l! V1 u6 a1 O - #define REMOTE_ADDR "127.0.0.1" //服务器地址% F$ t/ a' I" d+ h5 P
- + E- r$ G) G/ ]: w" g( M; _
- int main(){8 [4 }+ }" z; O) N l) i3 N, v4 i8 ~: L
- int sockfd;
+ n1 S4 j3 A- o: I6 N, b - struct sockaddr_in addr;0 ]9 g* m& y$ V* ^3 ~% D
- char msgbuffer[256];
5 i1 T, i4 a" Y -
+ @. O& h# A; i1 V- Z2 d4 h - //创建套接字3 W0 `0 R# l. R3 x
- sockfd = socket(AF_INET,SOCK_STREAM,0);
! v2 g2 ]0 r: w! I; i" I0 Q a - if(sockfd>=0)/ g8 M# _ J6 N d6 x
- printf("open socket: %d\n",sockfd);
3 n( s5 w# h2 t - 7 h8 g# b8 H% T% f
- //将服务器的地址和端口存储于套接字结构体中" C0 D. D5 I1 L( _
- bzero(&addr,sizeof(addr));3 h) i# S G2 {
- addr.sin_family=AF_INET;/ y3 P) j' ?( b7 M
- addr.sin_port=htons(REMOTE_PORT);* p+ K* o6 m0 z5 i# V
- addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
q' w* r. `- t2 `8 \2 ?9 u5 I& t - T$ Q5 y) o. V! h" }4 r
- //向服务器发送请求
- ^" E% v1 [/ ~' D( d - if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
( I9 b) y% L9 ^0 o6 | - printf("connect successfully\n");" }8 R/ n. |5 D5 @" ?
- 6 {6 J# U8 i* Z J9 k5 t1 c
- //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
; \0 ~0 p8 p, A/ f+ X6 X Y- C, w) a - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
/ O8 h' c8 {' X5 u/ n# a - printf("%s\n",msgbuffer);* D: I: P+ f& m* ]: Q \9 ~7 [
- , i- h. o" d" b% E
- while(1){
5 @$ L( [6 } r$ t/ ^ - //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
0 U4 b, w8 l: K3 b8 z - bzero(msgbuffer,sizeof(msgbuffer));
! W I1 @8 |0 i$ `( I# r4 ~ - read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
- d1 Y0 B/ x% a7 G; i - if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
- e8 S. P# K5 \, B - perror("ERROR");
) W2 W$ m8 x; W( s) w; a% r/ _ -
) i8 `4 n, S7 C- N - bzero(msgbuffer,sizeof(msgbuffer));
# x2 D9 }( B6 u5 ? - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);" X$ o8 h7 b& f9 [# H
- printf("[receive]:%s\n",msgbuffer);
8 p6 b4 z& M3 r0 J& A. { - 3 I7 Z. G/ p, k& ]
- usleep(500000);
( n5 w4 P7 N2 x+ r& P# j1 Q - }9 f# R7 E1 ^( F3 r. L7 `% Z+ P7 V
- }
复制代码 : O* X$ ~+ d9 \7 b! i/ Q* l# I
0 {" V0 l2 s$ ^! {1 ~9 x
服务端: - #include <time.h>, H% e% ?1 j4 f! o \- A& |
- #include <stdio.h>6 U4 c. \6 }) D7 y; J: [
- #include <stdlib.h>! z- P3 M5 m% @* g- S% T
- #include <string.h>
$ u& U9 w; w# M - #include <unistd.h>8 F3 v& z3 c6 J2 d8 y: h7 s, {2 e
- #include <arpa/inet.h>, H+ K8 r4 x/ d! |1 f/ k. x7 I
- #include <netinet/in.h>
/ H& L% v$ n9 a3 ^ - #include <sys/types.h>
- p% T' {) K" g3 @* ` - #include <sys/socket.h>
/ @+ G r: C0 u4 D; \* P -
7 O' }* @- N0 J$ k - #define LOCAL_PORT 6666 //本地服务端口# c5 U' |3 z( @& Y
- #define MAX 5 //最大连接数量2 o2 p1 E* f5 X; V6 ~
- : g; {& t; }0 ^# }
- int main(){2 b* B) ^3 E9 m
- int sockfd,connfd,fd,is_connected[MAX];
! ~6 F) I+ R* ?8 r* P - struct sockaddr_in addr;
& \, v- T& A: e( H - int addr_len = sizeof(struct sockaddr_in);. X9 q4 i/ _* `7 ^
- char msgbuffer[256];6 d, ~4 F0 x9 Y8 p* j- S* a7 _- D
- char msgsend[] = "Welcome To Demon Server";
; k0 @& D" J7 w5 M! o - fd_set fds;
; L3 j/ r& D" T) p - 3 _. V) J T' {9 K& L* a
- //创建套接字# u/ _6 k5 ^5 [, K+ ^! h8 c* M
- sockfd = socket(AF_INET,SOCK_STREAM,0);
* E8 W6 p) X' w) H0 o7 W - if(sockfd>=0); b& ^) U6 j9 u& k" y& t* V
- printf("open socket: %d\n",sockfd);
" w" I/ ~0 }' | - 3 c- v7 E# T' q3 n
- //将本地端口和监听地址信息保存到套接字结构体中
/ t1 \5 b }5 ]* O ~* j, h - bzero(&addr,sizeof(addr));
; F5 S2 L2 `* R( s+ l" o - addr.sin_family=AF_INET;
: @' M" r; U1 w9 T) d8 V1 Y - addr.sin_port=htons(LOCAL_PORT);7 z+ Y1 L3 [9 T) q" C3 F0 A
- addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
2 o$ t3 C' u; ]4 `& c( l0 w - : \% }. R' E* \4 W9 z
- //将套接字于端口号绑定- ]4 f5 E) G# d! `& G b
- if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
5 c6 v |$ u0 ^! |& o+ O5 ^ - printf("bind the port: %d\n",LOCAL_PORT);0 V- `* \7 S" q
- 7 H$ w1 p9 X: d; x
- //开启端口监听 Q# i/ [( B0 X) I7 i
- if(listen(sockfd,3)>=0)- G* I) j: W5 m5 D
- printf("begin listenning...\n");9 ]; ^ e! y% F/ Z. j& u
-
- B! s8 x$ E9 n. ^ - //默认所有fd没有被打开# W% }0 N4 C& W/ c- Z! ~; E4 `
- for(fd=0;fd<MAX;fd++)
0 g6 Y2 |3 C7 `- `* p+ t - is_connected[fd]=0;
- M4 n2 h. B5 v' J5 @) J) | -
. J# S) G( C0 g9 m* }# s* v, b* M6 O - while(1){5 A7 d: J# |- |9 [2 b4 w
- //将服务端套接字加入集合中
, x8 _! ~4 A; R - FD_ZERO(&fds);! ]6 S1 n" T5 I" B' ]2 V
- FD_SET(sockfd,&fds);& L9 y3 L. K0 l0 j# ?6 c$ D
-
5 [. M# f7 V' z1 O* S! m - //将活跃的套接字加入集合中
% X0 a0 J& @1 U% Z9 J( @! U" G - for(fd=0;fd<MAX;fd++)
, R: H# a& G1 S, r4 t - if(is_connected[fd])
2 g! K5 [+ d! ~4 [3 X& c - FD_SET(fd,&fds);
3 P$ u: `# D# D/ n T -
# z0 G% E$ W, q1 r2 j0 y* [ - //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
, Q2 e9 l H: b7 O4 V - if(!select(MAX,&fds,NULL,NULL,NULL))7 g% h8 s# j% M: m S& v& |
- continue;
- g9 D+ C" n/ d& N: ~' I - ( n8 o" f* M. e' c0 }) {
- //遍历所有套接字判断是否在属于集合中的活跃套接字6 T0 R$ W* ?! i9 _
- for(fd=0;fd<MAX;fd++){6 d# z3 P7 m4 n C+ W+ {6 e% h/ `
- if(FD_ISSET(fd,&fds)){3 h* [4 o! g7 j; H4 v+ _
- if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接
/ Z% `6 l z0 n4 \( H - connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);" k2 d. F" S$ N3 N3 B
- write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语' a% V- T& a3 I( X& u
- is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用
& `+ v: [; t8 Y4 [! } - printf("connected from %s\n",inet_ntoa(addr.sin_addr));
) h8 J! r! W t, B0 G2 c. x$ P+ t - }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字6 I. L2 j+ s. L( Y9 f% l
- if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ & \$ I# ^8 P' _+ {9 t' C
- write(fd,msgbuffer,sizeof(msgbuffer));
# h) J/ o4 x3 v8 e z. a( R - printf("[read]: %s\n",msgbuffer);2 ~2 k' v1 u0 J/ a
- }else{; l+ ~ Z7 F+ }( [" @* D, }! f. _
- is_connected[fd]=0;
3 d/ `; P! i5 }: X9 E& n9 l# |. i: `9 W - close(fd);
, L6 ]. N' z1 _3 T2 B2 r4 U* u - printf("close connected\n");
) T8 J" `3 U% W1 B - }
; n- b2 ~. n% o& E) J" c* G. f: Q - }
( s0 p0 U& @( o' K, I - }
7 P$ h+ n! S, r1 { - }9 `) O: Q5 g+ I7 { o
- }; `2 N/ d% t7 X' F! u9 @. d
- }
复制代码
* Z! a( i* j* H! ?
6 q. R$ W. g: i/ Z, b9 i8 X/ D. Q) @7 |3 S
6 _/ o* |: ]3 |$ w6 N+ |2 L( m! y
1 U' E4 F0 `7 ]! g$ ~- _1 s8 w; e
|