|
实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上 2.启动客户端,与服务端建立TCP连接 3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点 . D) A) n9 E- o A. F; `
什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
4 ~0 [( z2 f6 e- e& e7 f( x "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。 对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。 具体其他高级的定义不是这里的重点。值得说的是: 每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。 应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)
( R T! e [: N4 b
5 Y% b5 M7 |, f/ h: T
5 ~5 k& \8 K7 `8 c如何标识一个SOCKET: 如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个 SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
/ N3 @# S4 t+ ~, w 描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。 述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出 当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3 & G6 g3 Q, S( E
9 U, w! R9 l" ^6 Y3 J服务端实现的流程: 1.服务端开启一个SOCKET(socket函数) 2.使用SOCKET绑定一个端口号(bind函数) 3.在这个端口号上开启监听功能(listen函数) 4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数) 5.接收或者回复消息(read函数 write函数)
: `& k: z& K2 X, N4 g4 `
2 H8 y% v3 }% Q' X7 r! E5 T客户端实现流程: 1.打开一个SOCKET 2.向指定的IP 和端口号发起连接(connect函数) 3.接收或者发送消息(send函数 recv函数)
0 y# k! I, E5 ]9 U& m2 K7 \: S& E% ~+ b5 G. H
- V, `5 _2 R Y, p- t& W
如何并发处理: 如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果 直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端 一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到 有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照 单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
2 f# m% |* V" ^- U
- Y3 d( {- k# r5 ^ i. D0 X# @4 t1 N0 _如何解决: 下面摘文截取网上的资料,有兴趣者可以看看 系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你 - int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为: - #include <sys/time.h>8 t, F; g1 J8 v+ Q
- 8 z6 b+ ^8 U) \4 E9 z
- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
3 }( Q: S( s$ v: Z9 o- d readset 用来检查可读性的一组文件描述字。
7 F5 U0 ?+ l8 z7 F S4 P, r- A9 o* M writeset 用来检查可写性的一组文件描述字。
7 e* T) F5 B5 j
exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误) 2 {* P! e' b, C/ {0 [
timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。8 C& Z3 _1 _* [( V6 l' L
6 }( k; X# y: o3 X 对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
) j6 V" y8 Y" N" ~5 L- O9 y # m: `8 p M4 M3 \4 R3 T
- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
0 h2 q) B# Q% N/ B4 J) P& w
6 [5 X8 ]/ O4 }( _* f. @% \- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)$ `1 k" d9 m- _( [: m. Z+ M
+ a t* h7 I" [3 |5 i- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值: 返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。 否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来, 你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量, 其值通常是1024,这样就能表示<1024的fd。0 d- W* }3 [5 P
3 Y$ S8 }" i1 t2 O
7 h8 H+ U6 C9 y/ g1 z# h# i fd_set结构体: 文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字) 可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作: - FD_ZERO(*fds): 将fds设为空集0 b% `6 ~# j9 n( ^% f
-
B/ q' [. v( G. h, y" Y - FD_CLR(fd,*fds): 从集合fds中删除指定的fd
/ X" L7 f3 s$ l) U7 v* S/ p
0 V( i" c7 O7 @- FD_SET(fd,*fds): 从集合fds中添加指定的fd
5 r" I& u7 i# C* ~$ O9 ]2 k4 J, Q: A - j8 q) E7 r/ r* {2 o) o& A: T
- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下 - socket s;+ a' E1 s0 @- T0 l! c8 t2 C
- .....8 M. X/ \5 _8 E. l# [7 a9 [. h4 h
- fd_set set;1 H0 ?7 z( }/ e" a; j
- while(1){( ?& Y" R1 d. ~3 a
- FD_ZERO(&set); //将你的套节字集合清空* ^; w6 l$ ?3 U" g1 @: }, @
- FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s5 ?; e7 q9 N. B6 X) `& Q
- select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,
* w/ \( y- B8 f1 w) [ - if(FD_ISSET(s, &set) //检查s是否在这个集合里面,
' p& X/ ?0 M% S7 N - { //select将更新这个集合,把其中不可读的套节字去掉
1 _1 f) {2 b( ^, u# |; ~ - //只保留符合条件的套节字在这个集合里面
, a8 N! c) b- ~9 Y! W$ ]( X8 d - recv(s,...);6 O' J: v0 T( t$ M6 g# h
- }* [/ W" J6 l0 j' P9 p
- //do something here
D0 c; H# ?' n. r3 a0 U) p - }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd - (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。# \& ~- g4 J3 v" j; i6 s
- 9 {; B9 a5 u+ |5 f. f
- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)# H4 U, X2 Y8 h6 S3 g# `
- 1 ^6 C- W; h/ T5 B/ {
- (3)若再加入fd=2,fd=1 则set变为 0001,0011
, }) M0 q3 y) o6 c" s* ?6 k - X* X( |5 M- B9 |
- (4)执行select(6,&set,0,0,0) 阻塞等待9 X# o% }1 z7 z, E! ?3 G
- . B. m* W8 w: s9 z5 R
- (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.具体过程看代码会好理解 : S8 F( ]' D+ K. ]* l
9 z( w2 J5 \& I+ R& p: z+ Z
使用select函数的过程一般是: ; Y0 j- X3 @- J1 F
先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set, 接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1 复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。! n G. d- v6 b2 a: [9 O
, B4 m# h4 T# ]: Q% Z, Z
客户端: - #include <time.h>& C2 q1 Y0 L, J6 k
- #include <stdio.h>! e2 u' L/ W2 S& _$ T( L! v
- #include <stdlib.h>8 A- p( p1 P# I, d7 Y& m% w
- #include <string.h>, N3 Z3 X; D0 o2 c: a7 k
- #include <unistd.h>
2 }3 R# K1 r S/ } X" x - #include <arpa/inet.h>- k* y, i% `4 o" A- C; S, w$ f
- #include <netinet/in.h>0 v* [' M' K) |- Z% e
- #include <fcntl.h>5 V7 v$ ]) [! a7 z1 ~3 o
- #include <sys/stat.h>
5 R% g0 ~. t. o6 L - #include <sys/types.h>1 Q2 ?1 L. y' ]0 V2 `2 w
- #include <sys/socket.h>
* W4 T# E) h/ H1 ]9 s - 1 A8 `& |" a2 q0 p8 b& A
- #define REMOTE_PORT 6666 //服务器端口
9 W( E8 R8 f: H7 z% R6 y$ @5 ?0 m2 u - #define REMOTE_ADDR "127.0.0.1" //服务器地址
, S6 v. s" Y+ @* Z( V; O9 j - , M3 P, f8 q) I
- int main(){
4 \1 L1 G# ~; r* M2 e% D( P - int sockfd; Y- T5 z1 E* `8 W' B* T
- struct sockaddr_in addr;
7 `; ]& O% @6 |% Y! M( e - char msgbuffer[256];+ v1 Y! z- F- K
-
& E# }6 z. |: M+ F2 V4 c0 z+ Z - //创建套接字! |- E& M% K$ P' }# q$ |
- sockfd = socket(AF_INET,SOCK_STREAM,0);
8 A' \, q3 G0 g X - if(sockfd>=0)
+ G5 z$ e* z8 r" S9 p m m0 I - printf("open socket: %d\n",sockfd);
( i: z5 J V" N4 X# N9 d1 [ L - ( }# g1 q8 }# h$ H
- //将服务器的地址和端口存储于套接字结构体中) N( k& H( n `1 l( c
- bzero(&addr,sizeof(addr));
0 M, c) j: o5 M2 Q! z - addr.sin_family=AF_INET;
2 }4 e4 }6 p3 L - addr.sin_port=htons(REMOTE_PORT);- J1 S+ P5 i+ ~2 ]3 s
- addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);3 z3 d+ d/ F' Q7 T
-
* ]* b) \/ y. F j - //向服务器发送请求# k2 t$ ?. B# a+ J. y
- if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)- h# D3 O9 X1 ?- z* E2 q" I
- printf("connect successfully\n");1 Y9 ]2 F3 P* L: ~$ I4 t2 H
-
4 N! m- K) _- z* ]/ M - //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
3 r" F- |/ w6 T5 N - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
" Z8 ~$ S- u+ k& l0 @1 W- Y7 U% r - printf("%s\n",msgbuffer);
) v7 M2 l& \) S9 F; \5 b: G. S - % s* s; N# q' t2 k% E+ x/ b1 S. }: E
- while(1){) C0 e1 ?3 w6 Z7 y% r6 C7 i
- //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息( ]8 E8 w. x4 D$ ?
- bzero(msgbuffer,sizeof(msgbuffer));% F4 c! k& Y& _# N" P P
- read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
; K' G, j; ?( |2 W - if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
; f2 A4 ^, P7 q4 _( @% r - perror("ERROR");# `" Y" r6 t/ }0 ]* A3 o
- , G* }& X$ [2 ]! ?! s9 N
- bzero(msgbuffer,sizeof(msgbuffer));9 W; p' x: H% s7 q" C# z4 w; V
- recv(sockfd,msgbuffer,sizeof(msgbuffer),0);9 Z& p1 K8 `1 G
- printf("[receive]:%s\n",msgbuffer);8 x0 W, E! p& G7 P! @5 b! |" z
- 0 `1 L9 |( Q) W& A# A* O
- usleep(500000);
; i- c E8 P, g' e% F: R ?2 U# s - }; Z& \5 Q' F, v9 x
- }
复制代码 / y5 }+ s5 O+ P6 w7 M2 D7 o. O) h
( J1 i3 Q5 {6 C" {) G; t
服务端: - #include <time.h>
% F( |; ~/ z( _$ t - #include <stdio.h>
" \; ]' U! S, j - #include <stdlib.h>
7 w. N% Q9 ]# Q% b: ` - #include <string.h>3 w% s, Y; N0 k* \
- #include <unistd.h>
' V1 ]" A" w: d( P$ W - #include <arpa/inet.h>& n6 U: @: ^' ~' s! t
- #include <netinet/in.h>
" k4 t& `4 z0 k0 E6 {1 } - #include <sys/types.h>" @" N. C$ I' Z5 d( n. R
- #include <sys/socket.h>
6 _1 X8 X4 y+ e9 [ - , g1 _& K/ N& x. B; V
- #define LOCAL_PORT 6666 //本地服务端口/ t$ `( g4 \: V- [2 B. y$ |# E
- #define MAX 5 //最大连接数量
, b' T. V. G9 N7 s$ e1 | - ) G9 A# X) j* G; a
- int main(){4 Z; f2 B2 }! h8 z: o1 k+ c
- int sockfd,connfd,fd,is_connected[MAX];
) M3 A! `/ R8 }0 Y - struct sockaddr_in addr;
& O& D* O$ `& t/ C. Q - int addr_len = sizeof(struct sockaddr_in);7 B1 u" I* k' I6 t9 e
- char msgbuffer[256];0 k# W5 [9 o e$ i6 i+ G
- char msgsend[] = "Welcome To Demon Server";; x7 ]0 X" s# P4 _* c0 n9 _' I3 ^
- fd_set fds;; ^ E& l0 r4 z6 x) g* W8 A$ T# h
- : f. f6 d) {- D: r
- //创建套接字
! [+ A$ E* i4 p! ^ - sockfd = socket(AF_INET,SOCK_STREAM,0);7 Q. A6 C G- M& A8 I! G* o! K( `
- if(sockfd>=0)
! V1 t: j6 j& m1 e( t" R1 p - printf("open socket: %d\n",sockfd);
$ d; ^+ }' x# ?1 [( Z' n4 r w, e' |" z -
" S5 G9 ?" s( E5 L1 v( |! G! O& R3 ^ - //将本地端口和监听地址信息保存到套接字结构体中
- e" X0 d/ `' b! k( v( {# u - bzero(&addr,sizeof(addr));1 a1 N, ^% S) W; Z+ i
- addr.sin_family=AF_INET;
. Q4 v( m1 F1 U! P - addr.sin_port=htons(LOCAL_PORT);5 G C, U( X9 A+ E; [
- addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0* j- K/ |! e0 Y; m) U c
-
1 R. I4 @* E1 j - //将套接字于端口号绑定
, a8 H6 }. K0 l* }# T, E$ G - if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
6 v" L- f6 c; ^3 Y6 v( c# L) h5 K - printf("bind the port: %d\n",LOCAL_PORT);+ `$ f+ I% y$ O: w2 I) u
- h3 e- b% E1 n/ i' ~( C
- //开启端口监听
* o8 _9 ^7 q7 r - if(listen(sockfd,3)>=0)
) g8 B. U8 R ~/ h - printf("begin listenning...\n");8 ] n4 l/ n v, a% O( r# ]
- ; W) Q8 r/ h ]. \+ C
- //默认所有fd没有被打开3 \: t' I* j7 S% J
- for(fd=0;fd<MAX;fd++)
) U# t# r* a0 k2 y - is_connected[fd]=0;8 t; @$ C3 _- @6 H8 H7 ^
-
, N& p6 t8 g5 p4 s& j0 @ - while(1){
" l5 `, b" N% y5 C$ L# I3 W1 w) r/ p - //将服务端套接字加入集合中0 ^8 x* z4 p0 j, q4 t& |$ H _; I
- FD_ZERO(&fds);
( ?3 {& n7 q7 R" A+ \! w# ^ - FD_SET(sockfd,&fds);* ]7 O8 q4 @0 B% c: a0 @
- 7 a- F& n$ a+ {5 v
- //将活跃的套接字加入集合中( H2 ]5 Z9 R. N$ h; Q$ `
- for(fd=0;fd<MAX;fd++)" \7 I1 O; J' x( q) f$ y) Z3 v
- if(is_connected[fd])
" Q- M# C' r0 G, E8 \' U( Z - FD_SET(fd,&fds);) r3 }# B o) O! ]; @
- 7 N5 I( J, r% b+ h+ p& W
- //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
6 z4 O% M2 q, v1 J! b( p" O5 E - if(!select(MAX,&fds,NULL,NULL,NULL))/ `9 b6 N/ T3 X3 z
- continue;
" s7 F$ `2 G1 k8 e - 8 d% ?' t7 R# s5 V% \" E
- //遍历所有套接字判断是否在属于集合中的活跃套接字
( X& T5 J% }; t* m3 D$ c - for(fd=0;fd<MAX;fd++){
: V' L' }# Y4 `" H; \& A) v) L - if(FD_ISSET(fd,&fds)){
/ b% L* B3 B! p- I - if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接
) y& N9 v; J$ {% v - connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
! A) U. G( _; t; m3 e3 g6 K% F' m - write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语& p. k- e/ M& L3 V8 k) X9 G; |% ~
- is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用! `# X+ t: w+ o$ M! N+ q4 ^- O8 c* [
- printf("connected from %s\n",inet_ntoa(addr.sin_addr));
; d; v. S+ z- R2 P - }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
; y9 _4 y( H5 l - if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
0 T2 x4 \) P, [& y( M. r) `# L - write(fd,msgbuffer,sizeof(msgbuffer));) O7 b# D7 }' r8 ~5 h+ g
- printf("[read]: %s\n",msgbuffer);9 A6 r+ M6 \( z- a- T
- }else{& b1 L: c, v2 b) n5 \! a( A2 g! h
- is_connected[fd]=0;/ g; t' u, @# ?/ g+ I
- close(fd);! Y1 s7 v8 B8 I! S8 X' d
- printf("close connected\n");
: H# A. m V. l5 e2 B" C5 f8 |- K - }
0 |7 _+ i0 p+ G o8 w - }! d- S2 L/ c+ b- t
- }
+ D+ D4 [" ~ _7 U6 I - }+ y1 n5 q9 w& A3 r# t
- }- w( g9 a: e5 T. s; [
- }
复制代码
' W w+ a6 u5 N% g/ P/ F& f8 {% n; `) Q+ h: f2 l
# }" k3 X( ` B2 i- ^. Z: k8 d; S( t# n) E5 O
8 M8 p( [ U5 ?# j1 o
0 E4 G) z. a8 v( }! F
|