实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上 2.启动客户端,与服务端建立TCP连接 3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
* J2 n- h0 R5 p+ F: a什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
/ Q+ E" P! |' N3 o$ U) t "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。 对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。 具体其他高级的定义不是这里的重点。值得说的是: 每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。 应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反). `$ y9 h: S V- T0 B0 z
0 M" S4 A9 }2 X, K( v# r Y# o0 d$ g
: z. m' j% v2 \
如何标识一个SOCKET: 如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个 SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等( C7 q% v: I; o
描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。 述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出 当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3 ! ^7 a# [3 R3 @9 _! b& \+ u4 F
M9 c, N5 |, A8 ~
服务端实现的流程: 1.服务端开启一个SOCKET(socket函数) 2.使用SOCKET绑定一个端口号(bind函数) 3.在这个端口号上开启监听功能(listen函数) 4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数) 5.接收或者回复消息(read函数 write函数)
. o- E% F3 q/ l9 s H2 r- j, T
( u4 E" }6 a& m8 M客户端实现流程: 1.打开一个SOCKET 2.向指定的IP 和端口号发起连接(connect函数) 3.接收或者发送消息(send函数 recv函数) * _2 w* M; J( Q- K O
: y0 f1 r7 z" K! d* X% F- S
0 u: [3 y( {7 r5 \# M8 o5 _如何并发处理: 如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果 直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端 一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到 有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照 单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
' _9 [8 R( O2 e, l6 d5 y! d8 W) z
9 U4 `. Q2 P9 l5 G, }如何解决: 下面摘文截取网上的资料,有兴趣者可以看看 系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你 - int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为: - #include <sys/time.h>
% ^, X8 ]: \! R% }) } - 2 x$ p8 j/ o" P, ], T, C5 M3 {6 \4 Y
- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
, P2 X1 ?, r: S# f3 A' ]& R readset 用来检查可读性的一组文件描述字。
e: y d" r' H& x7 K
writeset 用来检查可写性的一组文件描述字。 + {9 C. G @+ G' N. _. \( Z& h
exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
5 z5 K3 |8 c; w- W& d i timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。$ q0 o4 d, `$ a" n' w
) l2 X) N$ V4 n! O6 q+ c
对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
; T+ C/ ~2 Z/ z1 k
3 J) C& P$ o x& d7 j- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
, w9 t F3 z j4 @# T! ]
9 z b- z0 i9 o! K1 {3 ]- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)5 t7 `/ `8 S: s+ S
8 s9 p$ J/ l. N/ _9 A$ ?9 p* T* f% q- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值: 返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。 否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来, 你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量, 其值通常是1024,这样就能表示<1024的fd。
/ p8 |! K9 q p. r' W0 ~) V! a& Q6 D( B: b9 V
# C! O: \1 S" H! ]
fd_set结构体: 文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字) 可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作: - FD_ZERO(*fds): 将fds设为空集' W& J- B2 ?& j; L2 t1 d
-
* x0 o% X7 l' a, d i5 `: p - FD_CLR(fd,*fds): 从集合fds中删除指定的fd
% A2 E! e% W& a5 ^
F( m$ M. H/ F) V8 V: i# s- FD_SET(fd,*fds): 从集合fds中添加指定的fd, R. Y4 C& p3 B6 W
- 6 K5 V/ Z6 x9 Y/ a) d s! x1 h0 o
- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下 - socket s;- n' i' h, F6 \" x2 s, q9 T1 _
- .....2 {) d+ h! ?1 G: v
- fd_set set;
0 g1 u$ t1 _" C9 y# H9 O - while(1){& x4 t- Z2 r# ?, C; [
- FD_ZERO(&set); //将你的套节字集合清空- c# E9 E8 [/ }; ]# J- Z! D! y
- FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s/ _- D1 D+ O# |, ~6 k' e2 A
- select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,- X1 Z; u& x/ W g' Y$ e
- if(FD_ISSET(s, &set) //检查s是否在这个集合里面,
1 v% G i: N6 ?2 y( o7 ? - { //select将更新这个集合,把其中不可读的套节字去掉
2 o& f! f% M2 M. m/ r) X - //只保留符合条件的套节字在这个集合里面
" k; `6 j% Q& j9 ` - recv(s,...);. Y7 l) [$ u, h& ]
- }# A: n4 q7 P$ y; y1 W. j7 `
- //do something here7 _8 J9 y5 L& E8 h8 K
- }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd - (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。' K6 b/ O6 u y" `& `
- $ ~3 P" }, e }+ E) R7 h# _: {" d
- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)
' j4 e2 K# F* i6 D. k3 m: R
# o, M/ h& J0 x! V+ e' X! e! ?- (3)若再加入fd=2,fd=1 则set变为 0001,0011
6 F5 ?- ^: \* o/ @6 ^7 j5 w
6 f: ?! @' H5 p- (4)执行select(6,&set,0,0,0) 阻塞等待" D8 {& c. n5 e. h2 z8 L, {
- # }8 _' a0 K- B
- (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.具体过程看代码会好理解
7 G- W! y7 E5 r
9 Z$ J4 x4 h ~$ ?# J* z0 a使用select函数的过程一般是: * R5 \! v) V o
先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set, 接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1 复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
- z" ~9 M" v; |
9 E7 t; ?; D: j1 o K客户端: - #include <time.h>
% B, ~( X4 _1 f8 \6 ]+ X: F# A% n - #include <stdio.h>* C8 j& s# w) [
- #include <stdlib.h>
5 E' L m; k( c2 N - #include <string.h>% F9 T6 R# b2 q8 F) f$ c
- #include <unistd.h>& \) e6 F7 Q. j7 n, Y$ p6 E
- #include <arpa/inet.h>
* G h7 Y! c* D; ]7 M! p - #include <netinet/in.h>' p6 C" d3 S& K& R& \1 k4 z
- #include <fcntl.h>
$ g D2 i% _4 n# H( f* ` - #include <sys/stat.h>
7 k6 K( s8 D, z3 B6 d# C% `3 F - #include <sys/types.h>0 ?4 U( M. H- X
- #include <sys/socket.h>
: H/ P& |0 f; e: b - % g- X) @7 I8 o/ P0 Q8 r
- #define REMOTE_PORT 6666 //服务器端口" L+ u$ G" t! M( U; x
- #define REMOTE_ADDR "127.0.0.1" //服务器地址. w [8 n! U4 r1 E. p
-
- d) p9 E6 @) J% \ - int main(){
5 t$ c+ y0 b3 S0 O - int sockfd;
3 s& v$ P9 Y, s" \ - struct sockaddr_in addr;
4 F! A! P0 N$ P* m - char msgbuffer[256];. @5 W( o/ X1 ], h. z
- 2 _1 g0 p2 y, L3 G7 N" U) t7 K
- //创建套接字9 ~) R8 V, h: q" ~' W: l
- sockfd = socket(AF_INET,SOCK_STREAM,0);3 L3 T" o; U- K2 z+ S- x1 I+ ^
- if(sockfd>=0)
2 Z. F9 r% |* x - printf("open socket: %d\n",sockfd);
) X: L9 z. x/ m+ T - " k" G9 T$ e# m4 S5 Y
- //将服务器的地址和端口存储于套接字结构体中
, k: S! [" C% X4 m2 P1 I9 f - bzero(&addr,sizeof(addr)); G1 C3 S! q' B$ P% H B0 P
- addr.sin_family=AF_INET;
f1 _; K" I/ s8 x - addr.sin_port=htons(REMOTE_PORT);
0 r; ]9 z- a7 }1 Y @' X1 ~) C d - addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);" I; I! x) V0 A5 R
-
( X0 s) v$ V0 g3 I% L8 o8 [ - //向服务器发送请求! E$ o0 S- a8 S; r% k
- if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)+ f% q& M0 `2 I% f
- printf("connect successfully\n");
. U! e v1 @8 o8 q8 D0 Z! m8 y - * p% A! o% `' R3 x: v
- //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
3 d# T0 W E( m# a/ v$ ~! w - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
4 Q, q3 p& r) v - printf("%s\n",msgbuffer);
5 [7 p* I( Y6 E -
P% `7 t! M( e - while(1){- \/ C0 H1 D+ D8 W$ C
- //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
+ ]" h" U Q: K - bzero(msgbuffer,sizeof(msgbuffer));) y( M. f- ^: b. t, C; K
- read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
J$ n1 t7 C1 c3 G - if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
6 P7 i9 M' F5 s( i f - perror("ERROR");
& K4 `) T/ i4 w, \ Y6 T% ~ -
2 O3 m- E. R z9 B! q7 W6 H- C - bzero(msgbuffer,sizeof(msgbuffer));
7 h9 u+ B: ~/ L; F( R - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
) J# ^; z7 [% q _ - printf("[receive]:%s\n",msgbuffer);7 ]$ x# d: e! \# v2 R
- , c) b8 ~) _5 ~. U1 W0 D
- usleep(500000);
" n5 @% n: O4 _: `' u7 a4 h - }) `7 m B5 R7 w6 Z. Z$ U8 _
- }
复制代码
, S% k; A6 U$ q& M7 A* m# w& Q, D8 F4 T0 i9 E7 \0 U
服务端: - #include <time.h>' a8 y: n: T3 b( @0 W" M
- #include <stdio.h>
! H6 q5 P: Y8 t! `/ o- B5 P - #include <stdlib.h>
: d! k8 H+ Q6 i2 x" N; K1 u: L3 | - #include <string.h>1 W, {9 F8 w: Y3 {; }+ c
- #include <unistd.h>3 ]$ Z: o# P9 k& ?. b- @$ r0 C
- #include <arpa/inet.h>4 r" z+ c/ r: I% q9 ^" G1 i
- #include <netinet/in.h>
7 n" Z. i4 p; O) Z0 R$ t+ G - #include <sys/types.h>
" G9 S& \% J8 C% v; ]0 Q - #include <sys/socket.h>
) p* S F# [. A9 A2 p - / m- p# S7 Y2 H$ Q7 o) R
- #define LOCAL_PORT 6666 //本地服务端口
- n! n( r& g; d/ L: R$ l) @( j - #define MAX 5 //最大连接数量
9 A# y9 v% E5 z, o9 c -
5 k u; d/ }; y/ X - int main(){+ k# C( [$ V& [
- int sockfd,connfd,fd,is_connected[MAX];" S2 C1 t. Y" p1 ~% B- \: Z
- struct sockaddr_in addr;
/ M- P8 }) D( Q U - int addr_len = sizeof(struct sockaddr_in);9 [) L6 p6 ^) u* c3 B& Z
- char msgbuffer[256];
% D8 G4 Z: s$ h" Y+ F* } - char msgsend[] = "Welcome To Demon Server";; y9 C2 L8 e' P, y
- fd_set fds;
2 R" o) o8 _9 U* H3 c# v -
( L/ _! _+ s4 { [; @* D - //创建套接字
1 r( _8 J+ A2 p4 A5 p) f; z - sockfd = socket(AF_INET,SOCK_STREAM,0);2 K' c) g) i4 Z8 Q, ?) H0 N
- if(sockfd>=0)
% [2 T9 d$ ~0 X8 e/ n: |, ]# T5 ? - printf("open socket: %d\n",sockfd);
& g( D& w* N O- [, V* z. z - - ]" G9 ~5 F6 u4 q8 I' o; `; U! i6 W$ b
- //将本地端口和监听地址信息保存到套接字结构体中
2 {8 @+ a l9 Y4 l8 e - bzero(&addr,sizeof(addr));) _2 B. |1 S- O
- addr.sin_family=AF_INET;) w5 Z, C8 T) C P9 d& V) T
- addr.sin_port=htons(LOCAL_PORT);
( Z% p; \8 ], k# Y) M* d+ f* ` - addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.01 D( T- r: K9 ?
-
+ J) r9 P9 s5 u+ B. {, C. r - //将套接字于端口号绑定
; r. U- J/ Q7 U) }4 j$ T - if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
) S6 ?- S; N9 g& O! u1 U - printf("bind the port: %d\n",LOCAL_PORT);
7 Y6 h! r- Y) P" U: O. z -
" Y- I) B+ i# q( t - //开启端口监听; L/ i3 A+ C0 o3 n
- if(listen(sockfd,3)>=0)' h1 m9 Y( T8 `
- printf("begin listenning...\n");
7 V i! V! E" d1 G4 F& O* q -
7 w& z0 {; ^ ?, b - //默认所有fd没有被打开( q2 l4 e* X- D3 b5 |
- for(fd=0;fd<MAX;fd++)
. e0 e8 O; }8 B& Y+ N - is_connected[fd]=0;0 [! \& C% U/ l6 t! f
- ' m" P3 v0 z2 q3 J5 C; o
- while(1){
4 Y' [+ K/ x9 m- J- N$ F - //将服务端套接字加入集合中( _1 a# p' H a# _- J& ~
- FD_ZERO(&fds);
3 Y |& T; p. u2 M - FD_SET(sockfd,&fds);) V ]4 Y+ y- \( h6 D$ L. L8 B
- 3 P# m" T3 K( W: x$ l6 }+ @
- //将活跃的套接字加入集合中
! T' E& {/ D8 o& j7 J; E - for(fd=0;fd<MAX;fd++)0 \+ M; k" O! a- c+ [
- if(is_connected[fd])6 }. }4 Z4 l3 k4 J
- FD_SET(fd,&fds);
& \/ n# O7 q! i4 I. ` - & Z7 n; O. P' K+ C; V6 f% W3 u( Y
- //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0* J- E% x1 g$ N, Z
- if(!select(MAX,&fds,NULL,NULL,NULL))/ N8 ^- E1 }( Z8 t. x' l9 x
- continue;
2 d9 @5 ~4 x: f$ L2 U: ] - 9 d/ e p9 M4 }8 O
- //遍历所有套接字判断是否在属于集合中的活跃套接字
7 Z1 f+ X$ @ g - for(fd=0;fd<MAX;fd++){' y9 L# w3 |$ d, s4 ^* y
- if(FD_ISSET(fd,&fds)){
* q) \5 _+ ^+ f0 R9 l - if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接
+ M; {% \+ H* A1 s - connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);8 [- b" |. [" R% D3 z
- write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语5 f+ l3 \/ H# R4 M. G! ^$ G
- is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用
3 K# G: o M3 L* G: j. t - printf("connected from %s\n",inet_ntoa(addr.sin_addr));* l, c; `$ f+ E% Q* T _ v
- }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字" ], a6 Y" `0 o% ~. r/ Y
- if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ : H# `1 `+ N% |" H
- write(fd,msgbuffer,sizeof(msgbuffer));
) m- _* Q; t+ d# d% [" p - printf("[read]: %s\n",msgbuffer);, a% s' D+ X0 D; l0 y. _: Q& m
- }else{
- X4 q4 g. V4 Z. q% B - is_connected[fd]=0;
5 T9 d$ x; q4 t: T- r6 W8 u - close(fd);% ~% L' ?' Y7 z! R+ m( W* L+ a
- printf("close connected\n");( y8 C+ p T: i/ D' Q; }- S# N( Y
- }2 A3 t& z( x1 Y" J4 e+ E
- }. N6 p0 c1 J0 h, V) ?& z
- }( B M ]) f9 c
- }7 c4 L* d* I* ~8 W
- }
9 N( [$ q7 R! G3 d l" ? - }
复制代码 ! z! U1 a4 C: f; a7 y& T
x6 t7 M$ ?! J7 Y* v5 y: F
! V% T9 i- E9 |5 u0 Y' _! X: u9 ?" ]; O+ j$ T, `0 Z, @# S9 G, {
, O& a- ?# P+ e0 s$ v
{% T' l, I! p3 V# m" D |