实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上 2.启动客户端,与服务端建立TCP连接 3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点 % g0 K. z, @' R( f
什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
! {1 o; z) v8 ^, _2 l "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。 对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。 具体其他高级的定义不是这里的重点。值得说的是: 每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。 应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)
! ^( a1 j4 x$ C' r6 Z! r( o1 s 3 n1 T$ r- F% Q% }# K4 ]
; d/ y3 n5 p( ?' |) {) s如何标识一个SOCKET: 如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个 SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等2 f: \( }) A9 t/ H' Z" y& g3 |
描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。 述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出 当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3 $ l: Z8 _' M0 F% C$ Q8 M1 V; _# |
) ~: T! L4 w3 b ] N, n服务端实现的流程: 1.服务端开启一个SOCKET(socket函数) 2.使用SOCKET绑定一个端口号(bind函数) 3.在这个端口号上开启监听功能(listen函数) 4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数) 5.接收或者回复消息(read函数 write函数) ; T; j9 s. T) O6 k/ E( |! i
5 X# U6 V; q& M( A) a1 t2 j( J6 Y
客户端实现流程: 1.打开一个SOCKET 2.向指定的IP 和端口号发起连接(connect函数) 3.接收或者发送消息(send函数 recv函数)
: C2 S1 d4 R1 B1 r9 _* H$ |0 R4 s
$ H! p" m9 s0 q" s
如何并发处理: 如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果 直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端 一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到 有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照 单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。 + \+ \1 A9 q2 J- g, V
# z* n5 B, f3 ~. U2 B% Z+ K
如何解决: 下面摘文截取网上的资料,有兴趣者可以看看 系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你 - int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为: - #include <sys/time.h>
4 k0 I$ Y+ P! }( | - , X, o3 x0 S) E9 e- w$ S- q# P
- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
; u4 z: \9 `& N* O7 K ]( U readset 用来检查可读性的一组文件描述字。
/ P" I5 l2 f! |, H: f- \- C writeset 用来检查可写性的一组文件描述字。
7 I3 \: A/ e* z
exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
- K$ R! V. A$ _, Z9 h4 N. L# j" S3 h timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
" a f- _9 q z; v" J
: G8 K/ s- T0 O+ f% Q5 j 对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:6 J' _, }+ S9 m: w* m" f
% @# d' q" c y" [" `, l- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
& w4 C1 J! x. i9 W! u6 ` - 3 R _! H+ P0 ]& E- L7 s* U
- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
) C: F& k- J4 l4 D' Y0 R1 k - & j6 I- j4 L: Z# ~8 p- _3 |
- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值: 返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。 否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来, 你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量, 其值通常是1024,这样就能表示<1024的fd。
5 C; d7 k6 d* I5 `* I. V1 g z! p5 G, R! `% W2 S6 n
& `3 z9 c& Q7 B) @& n. k2 s" c
fd_set结构体: 文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字) 可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作: - FD_ZERO(*fds): 将fds设为空集
3 X1 x8 X c4 D9 s* s; {0 @ [+ ? -
: b6 n9 m8 `$ f4 T( K - FD_CLR(fd,*fds): 从集合fds中删除指定的fd. o& R* q8 f3 U; m6 [
. Z. \+ N( I, c5 W$ X6 O- FD_SET(fd,*fds): 从集合fds中添加指定的fd9 d- P' H; Y& S8 ~7 _, e6 r
- ( o6 U5 Y/ ~6 a' a
- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下 - socket s;
9 R7 {$ E, M9 ]. q: a$ [+ e } - .....
1 W1 X9 Y, _+ n* d: |! X( y/ B - fd_set set;
9 T7 X3 D" s) i8 h# K - while(1){
8 F: I0 _5 h: }) n. {, N6 R* `- U - FD_ZERO(&set); //将你的套节字集合清空
8 ?3 y# ~; b _ - FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s
3 J3 [2 ~' T( s( a( M( H; x* Z - select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,4 t$ F4 X# z7 d4 W5 _1 h
- if(FD_ISSET(s, &set) //检查s是否在这个集合里面,/ j8 c) q: v8 K) {1 a5 B
- { //select将更新这个集合,把其中不可读的套节字去掉8 h+ D6 w1 X; P( w* l+ ]: m% R
- //只保留符合条件的套节字在这个集合里面
* }8 g3 U0 X$ |4 {5 g7 O1 r0 i5 v - recv(s,...);8 A, k2 @5 k, s1 A
- }
9 u8 ]# C: x5 U - //do something here6 j5 K) p2 I& y4 _+ ^
- }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd - (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。
" Q8 b/ \7 j" c* h+ O
' }8 ^5 q1 `- ^) f" F$ ~( P( w- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)
( m' S* P( x' ]. z( K5 p
" u" q, u4 _0 ^% W" }- (3)若再加入fd=2,fd=1 则set变为 0001,0011" o/ r+ l* k0 u1 [) J% K
- A, W7 s: M% c, {8 N
- (4)执行select(6,&set,0,0,0) 阻塞等待9 G- J4 E! K; \
- 9 i! s n# R# B" O; P
- (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.具体过程看代码会好理解
) {! K" n7 o. j6 [; ~3 c& {- g: \& e2 c7 |# y' K& ?
使用select函数的过程一般是: 3 j" n" e9 i* o4 }& L7 x v" H
先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set, 接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1 复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
G) k( [; R; Y j, n u z 6 P8 q" R E/ j8 @2 \
客户端: - #include <time.h>8 E8 b( e2 x* [
- #include <stdio.h>; q: P' K! N3 W \' l
- #include <stdlib.h>
/ i- T0 `3 w- a9 z6 |) ] - #include <string.h>6 Q; ] Z, Y/ E; h1 `( c! H9 |
- #include <unistd.h>
. |& N' t# S2 ~' |0 c. j7 k+ W - #include <arpa/inet.h>7 u7 t$ \2 P. J1 X" b
- #include <netinet/in.h>
% d/ I/ l0 c0 `/ g - #include <fcntl.h>2 E' F! M2 c( N# G6 t
- #include <sys/stat.h>
) {) L- c* t' Y( B, o( N: N9 ^ - #include <sys/types.h>4 p+ y# g8 H L& Q8 r
- #include <sys/socket.h>
x( p6 e* f+ j7 {7 u. c - 2 n# I" I% A/ f6 Y: c1 B) d4 ]
- #define REMOTE_PORT 6666 //服务器端口/ W( @: e0 U+ L/ ]
- #define REMOTE_ADDR "127.0.0.1" //服务器地址
1 f# V3 L+ _2 p6 |+ ~ n - : m; R9 s1 h5 K5 w0 }3 @
- int main(){
6 I6 Q4 F7 f: S$ H - int sockfd;
; K& G1 Q5 t& W9 M; w2 L" Q - struct sockaddr_in addr;) T+ @+ p( y; ~# W5 l- I
- char msgbuffer[256];
! U( g9 p: U0 c' |/ M5 S9 D - 6 D _0 j+ V7 L! V9 A- A
- //创建套接字
8 v* o' `8 X8 B' J8 d5 l, i" O# X - sockfd = socket(AF_INET,SOCK_STREAM,0);
( e- R& q3 D3 }9 ]% {& ]. v - if(sockfd>=0)
! f' @9 E( x4 X+ `% T# k0 t; o - printf("open socket: %d\n",sockfd);
3 f9 c3 i6 [ O6 U/ U! R! \ - ; R- S8 o; {" z0 j4 U
- //将服务器的地址和端口存储于套接字结构体中
/ M% j/ M2 Z5 u& K" J4 M( I - bzero(&addr,sizeof(addr));+ E1 }; y; ^3 K% T5 L2 U) `/ j0 N- Y
- addr.sin_family=AF_INET;9 A/ \ m+ o! l- K' Y$ C2 q
- addr.sin_port=htons(REMOTE_PORT);
$ R; c, b) x7 F9 T$ c' u/ l - addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
. Z1 q8 }3 D d2 g - 2 T2 r: l" e8 o! X, Z
- //向服务器发送请求
! n8 [8 r/ |8 ~! L' a - if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)4 T+ J5 @! }/ E8 v3 n
- printf("connect successfully\n");
' n( P" H8 Q9 @8 P" l -
, E3 t( l& F8 A. G - //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
. x* B( `& I B- g - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);! J: y( N$ `; x2 m( X: ^5 `
- printf("%s\n",msgbuffer);
( v0 y' G! m2 g) v -
0 Q, O4 V" J+ I/ j! c8 D2 V - while(1){; W' _1 W8 b9 S% Z
- //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息8 J, v8 r& v2 ^. U$ t+ r7 q
- bzero(msgbuffer,sizeof(msgbuffer));
2 D5 P8 A) U/ _7 }$ O' A# _+ }& P - read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
7 }" ]/ ]% N8 w' r2 ^( J - if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
: X5 |3 Z, o: e/ @+ V - perror("ERROR"); T' }1 l/ p4 Y& R9 D0 h5 X j9 I
- # r0 o/ R; q! N# i# v% }2 i
- bzero(msgbuffer,sizeof(msgbuffer));8 r9 w( `8 W& }( Q, O8 }$ E0 [: X0 A
- recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
- k$ Z6 |( c% W! t - printf("[receive]:%s\n",msgbuffer);
* P. H4 }: T: ]7 X - - H# f% I8 b! v1 g9 C
- usleep(500000);* X0 ^. X( x5 T# P3 A5 n5 C/ D
- }1 K) q. |# ^ `& j2 ^, ?1 p
- }
复制代码 & ?) Y/ d4 a: q9 q1 t8 X
8 V8 c y" Y( Z! t) c9 b, E7 C6 J
服务端: - #include <time.h>
6 L, i: M9 b1 J# {: W* u8 e - #include <stdio.h>" \1 w+ l7 e2 i9 s
- #include <stdlib.h>! {% q" B3 B$ {; Z
- #include <string.h>
; c: u3 j) A# @ I/ ?! K7 O - #include <unistd.h>. d) @% G) n9 d! \% I+ _% a( P
- #include <arpa/inet.h>
9 Z( V, P. J8 H) ~3 \5 C - #include <netinet/in.h>
$ k" V; G) f& S( q5 u Z - #include <sys/types.h>
! I, d$ c: _$ V - #include <sys/socket.h>% @# m) _% c* o, g0 I* |# V& S
- 7 d- m* b# k' o1 r7 P! I Q
- #define LOCAL_PORT 6666 //本地服务端口9 q3 Z( H, U# S, C3 \3 j) w
- #define MAX 5 //最大连接数量
6 F# S# N" p5 x2 ^ -
! {8 M; b" Q& l# m( i8 T( } - int main(){/ S8 r8 J" C! E) q/ i- [
- int sockfd,connfd,fd,is_connected[MAX];/ |( f1 C8 @3 G) a3 _2 ]% d% K
- struct sockaddr_in addr;" ^# q5 l3 @8 u+ s! K& a
- int addr_len = sizeof(struct sockaddr_in);
2 L3 c) D k1 ?+ T: W5 l& z# k - char msgbuffer[256];
6 h0 G: \" u5 L, ` - char msgsend[] = "Welcome To Demon Server";
' B- s: y7 r: g' e6 p$ ? - fd_set fds;; x0 b) z! i% u3 y U C
- 2 H E' M1 \! J0 x: `# m5 b& F
- //创建套接字
5 K& O; ^; ?) A7 M8 j& c - sockfd = socket(AF_INET,SOCK_STREAM,0);
% }: d6 v7 f* Q+ w. N, t2 k3 o - if(sockfd>=0)* o3 e0 Z/ m6 H1 S
- printf("open socket: %d\n",sockfd);
; r& \ l; H5 s6 v6 c9 \ - & v6 H/ ]* ?: L. R& D/ r
- //将本地端口和监听地址信息保存到套接字结构体中
6 }" h$ ~" ^ q# ]& f; ^# h - bzero(&addr,sizeof(addr));0 L( ?; v- `& U5 V1 V
- addr.sin_family=AF_INET;
2 K7 T4 g) x/ I2 X1 X - addr.sin_port=htons(LOCAL_PORT);
1 h7 X" E# a( O7 X, {& |8 \ - addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
' v3 l* V8 F, |) G - 1 i1 y9 W4 @- G- F
- //将套接字于端口号绑定
/ `* h2 B' s) o7 `- O- n - if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0); t2 N4 Q( I0 n
- printf("bind the port: %d\n",LOCAL_PORT);& u8 E8 n% o3 t, O
-
! c4 n& h; z9 k: l7 F - //开启端口监听! u- e3 _: L+ S1 r% [+ R, N0 z% s: g8 s
- if(listen(sockfd,3)>=0)
9 t* E! l+ u1 M$ x- ~2 I8 i1 f - printf("begin listenning...\n");
7 S, \/ u: C. o: w# ?: B - \8 |6 F2 ]0 ?5 ~7 s; k2 o
- //默认所有fd没有被打开8 Q( |/ S$ A: {2 D+ q1 W
- for(fd=0;fd<MAX;fd++)( Z7 p1 F6 I- m
- is_connected[fd]=0;
) ?; \& E _0 `( F) p7 G -
; u4 c" C* m8 h9 p% Z! x% z; i% y - while(1){$ \* }+ s" W, R7 f; l5 M0 m) x9 ^3 I
- //将服务端套接字加入集合中& @6 f* ?4 f5 H0 o' N
- FD_ZERO(&fds);
2 k* o, p- M% R& W: ^ - FD_SET(sockfd,&fds);( @' V, R4 |. X4 V3 N$ {$ ]# r+ t
-
( P- C8 T- g. e' ]& T9 L5 S - //将活跃的套接字加入集合中/ Z5 z3 [1 R/ K# C7 p3 m
- for(fd=0;fd<MAX;fd++). t- {# f; l0 `: s5 ?
- if(is_connected[fd])
' _! r8 q) t# C2 b - FD_SET(fd,&fds);9 C7 p/ \' \( @/ r7 ^
- ' U- t; p! }1 x
- //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
! G" C" E5 ?- b! P" C8 S+ B- m - if(!select(MAX,&fds,NULL,NULL,NULL))
; L& {, c6 g- Q) ~3 {( C, H: t - continue;2 n" ~- b; z% R) B! l
-
. r! n( w1 b$ _1 {1 u- G7 }5 M9 Q$ W - //遍历所有套接字判断是否在属于集合中的活跃套接字
. C. q% e7 [# M f6 j3 j - for(fd=0;fd<MAX;fd++){
3 o" {3 f- U1 O% i4 d' F1 ? - if(FD_ISSET(fd,&fds)){0 h2 |( U" z* r
- if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接
/ A+ R! k$ f* D2 J9 W! H$ ~ - connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
; z, u9 p S: m& z9 T - write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语
: J0 }' s5 {% V6 w- ?2 E - is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用
# K) i& S5 f& t* L7 a - printf("connected from %s\n",inet_ntoa(addr.sin_addr));
1 X+ L$ S* g9 e - }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
5 F M$ h4 y5 X* @2 E - if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ 0 T8 { {9 W1 z/ I1 g! ~, o
- write(fd,msgbuffer,sizeof(msgbuffer));5 r6 e$ `" \0 D1 M4 ^% H* I) K% F. J3 b
- printf("[read]: %s\n",msgbuffer);
% D4 Z6 ~' m$ U# S - }else{
/ Z# U& p' B1 z( z7 ~* X# x" T - is_connected[fd]=0;
1 w: q+ O8 H6 d9 r - close(fd);1 m! a9 |' l! s% Y+ s6 } ^
- printf("close connected\n");- N/ e% R" J+ G8 E t: T% e
- }
* r: B, u8 |# D2 F) `5 s - }) f9 F7 u1 D: N% |5 [3 L5 @0 F* b
- } l7 }& s" V' f
- }! r6 _; N4 z7 c2 x8 w2 u
- }
( N6 X% j3 x$ J - }
复制代码 1 n% X9 N- q- J$ x |7 b- e: t
. Q# [+ g/ B/ |
( I2 ^) g4 R# D3 s, y @4 j& ?+ V' }- f& M% j
# g* `$ G3 Y, ]+ b' y4 V% G- d
$ `- Z. c. v. t6 n0 j4 a |