实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上 2.启动客户端,与服务端建立TCP连接 3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
) W- u; ^: U/ N什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。. h/ I5 x7 F' i& {, Q, v4 ?4 t
"套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。 对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。 具体其他高级的定义不是这里的重点。值得说的是: 每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。 应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)6 T& h) O' ^$ o6 M
6 M4 ?1 c7 U, |+ H& [# }( `
6 v& W% [0 N8 O2 D' r如何标识一个SOCKET: 如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个 SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
+ A# h; d: l: j* }0 z* i7 Y 描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。 述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出 当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
$ D% ~4 W, v/ O. J
0 G2 J0 ~0 g3 i服务端实现的流程: 1.服务端开启一个SOCKET(socket函数) 2.使用SOCKET绑定一个端口号(bind函数) 3.在这个端口号上开启监听功能(listen函数) 4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数) 5.接收或者回复消息(read函数 write函数)
) w' Z# Z! O+ i! H/ U* ^6 v3 V" N# i$ G* u% V4 d
客户端实现流程: 1.打开一个SOCKET 2.向指定的IP 和端口号发起连接(connect函数) 3.接收或者发送消息(send函数 recv函数) . w- O& F3 B5 d/ I* N4 J9 H" ]/ `
6 X1 o9 n4 I# S* a7 S2 [
3 r' C! S R$ M9 q$ l% E如何并发处理: 如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果 直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端 一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到 有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照 单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。 0 g+ q3 a/ \3 ]& E
' }% ~7 Y6 n* M; m3 _& W' {
如何解决: 下面摘文截取网上的资料,有兴趣者可以看看 系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你 - int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为: - #include <sys/time.h>8 W( h' c5 a2 _1 k! A3 ]4 s: m
- " {' {; U. u7 a6 w7 r
- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理 . \! w% z: E: S+ E6 g
readset 用来检查可读性的一组文件描述字。 1 V* l, U- M5 y; V- C8 |1 m, f
writeset 用来检查可写性的一组文件描述字。 % [% v, W5 o* u& R& D
exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误) 8 f8 N. J( Y/ Q; K- V
timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。& P9 [, B. R6 k/ J
8 Y! f4 h$ ?; Q; [ 对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
% V. ?( G% e4 [5 ]/ w* ^" |+ r: u ( j1 p3 `7 M9 T( G6 ?2 t
- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)9 S# F5 W$ t6 ~* N0 A# f
- 8 B9 l3 i3 W8 M1 |
- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回), U9 o4 c) V: g
. f# d$ @% j7 o- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值: 返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。 否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来, 你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量, 其值通常是1024,这样就能表示<1024的fd。
- C# L6 |- d+ L3 O- U+ {5 |5 L' @6 l J- l; h6 M
% j, \6 ^8 e" v7 M4 ]4 |3 w4 g) Z* R fd_set结构体: 文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字) 可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作: - FD_ZERO(*fds): 将fds设为空集/ f$ s8 p/ F* d! w! @0 w
- $ J1 N3 G; j' Q9 E, `" v( e- p! F
- FD_CLR(fd,*fds): 从集合fds中删除指定的fd! m& ~& o; O5 p( |
3 p! M0 d" Y b3 M2 ?2 o- FD_SET(fd,*fds): 从集合fds中添加指定的fd
, q( ]+ P* A+ b- O& Y6 {7 T
* H n2 {% ~6 k i- }) b0 F0 z( q- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下 - socket s;- I+ F% z/ x# `7 ~" s
- .....% @$ h: x. ~6 Z6 u
- fd_set set;
k5 g: V7 C% ]2 C5 t/ G5 d - while(1){
4 {/ `) G8 ]% e3 Z - FD_ZERO(&set); //将你的套节字集合清空* `. v( R: x, j6 `5 J
- FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s7 I- w4 X- V$ J6 `2 {! i
- select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,2 I* h- W3 G, B) @+ e: u
- if(FD_ISSET(s, &set) //检查s是否在这个集合里面,
( g F* S: s# i7 z% y: ]: M - { //select将更新这个集合,把其中不可读的套节字去掉
- x6 Y- z5 x7 f4 }0 i2 }! l - //只保留符合条件的套节字在这个集合里面; s" ?$ E1 L0 }) D, e
- recv(s,...);6 v9 i& j! u0 H$ p
- }
7 e& v% }, _. R9 `2 u) @ - //do something here
6 n/ D2 ?/ i- n' L8 M4 { - }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd - (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。
( @9 c2 q1 Y. k! L9 P# t& O
; o. D: e$ Z4 r; ?7 }: ]* x- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)
9 y2 I) ~3 q, {* i0 [ - ! P# t& D1 B6 q" q
- (3)若再加入fd=2,fd=1 则set变为 0001,00114 V" o4 C3 v9 E1 x
- , v$ p( b& G/ f: Y1 Z7 S
- (4)执行select(6,&set,0,0,0) 阻塞等待 |8 j9 P, ]% f @
! Z- ^' S4 X; H% _. Y* G/ B3 _8 N- (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.具体过程看代码会好理解
8 u, q \8 w5 r, Z3 \. j# {8 n+ a6 ?/ p% f6 O) |6 W* l% t
使用select函数的过程一般是:
; ?2 ?9 v$ S" \ 先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1 复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。, z1 F) f. r) s" l. \+ D- \1 f0 W
; M; ~: y/ C+ L, X
客户端: - #include <time.h>
5 l7 l9 T! f7 r) t/ v - #include <stdio.h>
$ u( _. x6 |/ Z6 A+ n - #include <stdlib.h>0 \# I/ N$ z+ i" X ^. q
- #include <string.h>
& B! o, m$ W& N% b2 X - #include <unistd.h>
- ?3 W; [1 H" T - #include <arpa/inet.h>; b5 t, c0 U1 n. |1 S7 s4 v0 f
- #include <netinet/in.h>7 u- \ s: S9 w5 Q" t+ O
- #include <fcntl.h>% m, M) R$ O- f" q/ y$ Z
- #include <sys/stat.h>' _% ^% I% [# x T+ N3 S
- #include <sys/types.h>
6 i: l# i1 J' B9 }3 J; w% E$ y - #include <sys/socket.h>
8 Z: `7 }5 K, s7 Y. n# n -
6 a; `! F8 m/ B @4 \' H - #define REMOTE_PORT 6666 //服务器端口8 L& h8 q3 S" G
- #define REMOTE_ADDR "127.0.0.1" //服务器地址1 ?1 v; s$ \4 W
- 1 P8 a% k! r- i- {9 v4 v1 f
- int main(){/ i9 U; H3 w# r9 Y# J) ~
- int sockfd;
h D( K5 N0 Z+ y; v# v; M: Q0 ~ - struct sockaddr_in addr;4 p/ d/ L' K% H
- char msgbuffer[256];) U8 q! F! i3 [8 {
-
4 ]& ?; d& s ]7 P Z - //创建套接字
9 J: m1 d. V6 D+ u Q' w - sockfd = socket(AF_INET,SOCK_STREAM,0);
- h+ R- D4 C1 W - if(sockfd>=0), q2 O1 X1 h8 z* T- b6 ?# `5 o
- printf("open socket: %d\n",sockfd);4 `( b( V8 J, O! U I' c
- ' ?! }+ {+ B& J0 [
- //将服务器的地址和端口存储于套接字结构体中
& |& M- e; G1 k1 l. r* W& ` - bzero(&addr,sizeof(addr));
R. Z5 @- ^8 @- r( I2 B# h - addr.sin_family=AF_INET;0 M8 k7 z* C u& a; }
- addr.sin_port=htons(REMOTE_PORT);7 y" C2 I$ } j
- addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
1 |% r0 F8 F: F% }6 ]- }5 } - 5 C, G& a/ M3 ^0 v$ \- w5 Z
- //向服务器发送请求
+ d% _& M* r1 S - if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
9 { Y2 m4 u4 V4 M2 f. a - printf("connect successfully\n");
% s( U; R1 O, F6 [# k& h8 L -
+ d, R$ L) b7 }- x8 P+ F - //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
5 M8 U- ^7 O7 R9 h - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);# U8 n4 s' U4 r! ]1 ]0 `* W8 K/ O
- printf("%s\n",msgbuffer);* L# k/ y$ K; ?4 C) V+ p$ |
- # z \& O- y1 Z$ L: E0 t( f, j
- while(1){9 @( Y( E$ D7 }; r5 G6 K5 P
- //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息; n$ w- l# i, f8 H; v
- bzero(msgbuffer,sizeof(msgbuffer));
+ |" ~6 a1 }) z! O - read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));$ v' [! |) H" S/ T) k
- if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)5 a$ W3 j; w. @9 t. u' S
- perror("ERROR");9 d+ L9 }; b2 E1 R) a5 Q! W
- , v! {3 w( t& I5 S' Y
- bzero(msgbuffer,sizeof(msgbuffer));( a: |* ?% W0 }6 e
- recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
0 y7 c+ z; p2 u. h5 Z - printf("[receive]:%s\n",msgbuffer);9 B1 a, g; c7 V8 C6 d1 N5 V* L
-
7 R# V$ `- X9 H* Y. E% l" x - usleep(500000);
( u/ R. x1 q5 s8 I- d - }0 t; B2 J$ U, t9 I- U
- }
复制代码
; x- V; U+ f9 B6 O
3 B1 y% {6 B$ E服务端: - #include <time.h>: f7 V) d% i/ s; p8 M
- #include <stdio.h>, i+ Q# t9 A$ K/ Z& O: F; J) I5 R( ]
- #include <stdlib.h>2 a3 q0 T& V. ^0 G& r
- #include <string.h>) ]7 V4 {4 I7 y6 p$ M
- #include <unistd.h>
( A5 a/ q' _4 L - #include <arpa/inet.h>" z$ G( U" t: D9 J8 h: x3 s' r3 k
- #include <netinet/in.h>5 l: \" u: A- ?& f% s% ?
- #include <sys/types.h>
; V4 d7 [) x& W7 V1 C% b - #include <sys/socket.h>
. }2 N9 X! _0 `$ x1 q5 F4 N -
) `5 R) L: b9 T: V+ `5 W - #define LOCAL_PORT 6666 //本地服务端口
) Y& j* u0 \7 |( h1 i, e, s* i - #define MAX 5 //最大连接数量
! R# K4 b6 ~$ }4 B8 K) ? - ) q1 r" z# C6 t' X
- int main(){. n0 g r/ }9 J8 o( f2 O# B
- int sockfd,connfd,fd,is_connected[MAX];+ @; k6 e$ J& B7 G$ T$ f# a: _8 }5 n3 J
- struct sockaddr_in addr;3 A# r! P% I: A; {3 ~' j
- int addr_len = sizeof(struct sockaddr_in);
3 H: ^! A( X G p - char msgbuffer[256];/ Y, Z, J5 N! k$ \& U1 ^
- char msgsend[] = "Welcome To Demon Server";* B8 Z: e ~6 T3 f; j/ o0 b/ B
- fd_set fds;
4 W' y, j0 V' @: h$ v- @0 f - , Z. }0 i$ T; i; n+ r. {0 J
- //创建套接字
+ A4 k. z: U+ I# G - sockfd = socket(AF_INET,SOCK_STREAM,0);% I' G5 v {5 ?1 }' D
- if(sockfd>=0)
6 R% Z* y; J& q - printf("open socket: %d\n",sockfd);
. d+ Z! e, L7 r: q3 c. C1 F - ; M, i6 R, w& b
- //将本地端口和监听地址信息保存到套接字结构体中
. e1 R: A, Q7 s' c- ~2 r - bzero(&addr,sizeof(addr));- H8 g0 p. S4 _0 T4 g2 `+ H9 Q% @, U
- addr.sin_family=AF_INET;& ?) f+ b! v9 t4 M" Y N' a5 c
- addr.sin_port=htons(LOCAL_PORT);
! v. k/ Y5 F1 i, L# Y4 { - addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
: ^5 j- D1 x X8 \ Z - + d! c% {5 F# a1 B
- //将套接字于端口号绑定
% ~6 N- k4 `+ I/ B - if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
# G2 k- G a9 s7 |+ V2 m9 \9 m! Z - printf("bind the port: %d\n",LOCAL_PORT);) X D( ^7 h S# @" |4 Q% L
-
) O* O) \+ |' d - //开启端口监听
4 ?% W7 ?2 W& r2 `* U, E4 P' N) v - if(listen(sockfd,3)>=0)
/ C$ ~, z9 |& Y7 _' a- r S6 ~ - printf("begin listenning...\n");, L- Y3 b7 v2 z* Y; N: t1 X W* z
- ! V& W- Y. `5 R- c. J# c0 d
- //默认所有fd没有被打开
2 A2 H. D P7 s' d/ x' j1 n2 h | - for(fd=0;fd<MAX;fd++)
) O4 U& {9 R0 V, Q# J" q4 d+ D - is_connected[fd]=0;4 q/ q5 }- m: f& C5 J0 o$ |
-
5 u" |' J8 Q( r8 ~; J - while(1){% {2 d5 ]( l* j0 \/ a
- //将服务端套接字加入集合中
( E0 m; C1 r! O+ n - FD_ZERO(&fds);: J+ p. U( X" [, [. u L1 E
- FD_SET(sockfd,&fds);
) l/ }$ ~! j1 P0 i - h4 g. g3 `0 j
- //将活跃的套接字加入集合中; |/ n! T" t5 r; f1 b3 O8 D# c" n9 l2 e
- for(fd=0;fd<MAX;fd++)4 Q- U' L2 B$ s0 v
- if(is_connected[fd])
* E. i+ E, U2 {' z% Q0 k3 f0 Q/ @9 }) o5 z - FD_SET(fd,&fds);
. f0 Z4 _' d* y [; Q$ ? -
9 d; D) R$ j( d( J# Y - //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为04 N6 j" n. C: \$ h6 I
- if(!select(MAX,&fds,NULL,NULL,NULL))
$ C% @* l. Y9 ~. J! y9 `$ _% @6 h+ a - continue;
# d4 G% M* _, S' ^ -
* V3 R+ M5 m6 u3 [3 Q - //遍历所有套接字判断是否在属于集合中的活跃套接字
; l% g/ F7 _3 A: j' i - for(fd=0;fd<MAX;fd++){4 Z6 N2 n) b- @" m. \7 a- F7 x' [
- if(FD_ISSET(fd,&fds)){
9 ^$ S) R7 X7 G - if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接% {, _( ~0 V7 ~3 z o% [ i- O0 G
- connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
; v- K1 z, \8 L& R' ^7 U3 f - write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语# @! L: V& s4 W# \
- is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用
1 ~- P' u" [6 a! m* X: N - printf("connected from %s\n",inet_ntoa(addr.sin_addr));6 I* d: u/ A$ z) n+ k; l& ~7 t, P
- }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
6 {8 P5 q: Y7 E; r, L' i - if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ 4 U" g( P% b" Z3 J3 b c+ K
- write(fd,msgbuffer,sizeof(msgbuffer));
3 O G5 |( v5 F7 w" ^) U1 U3 q - printf("[read]: %s\n",msgbuffer);
' L5 E( f0 X- `& c! J - }else{' y0 v h+ Q+ ?8 c
- is_connected[fd]=0;+ ?* x5 s( Y! \0 @/ h
- close(fd);8 |- d: _3 N) ?. Y- ]- i. B2 S
- printf("close connected\n");. {2 G/ g- e! J' \& O7 M
- }
* R; t4 U' P' Z - }
- c. J) X6 C7 A ? - }5 v. ]8 S s8 p2 I3 O
- } M( ~/ ]; y0 Z
- }
1 ~* g$ n: B! ~3 {* { - }
复制代码 # G" o' h1 r0 [- \4 ]/ o
$ d0 |0 y# e9 w% u" a, x( n- z; U6 x# l: g |5 ~! P' ?- S2 s" J
, Z F4 M8 j, x) M3 z% `7 v
8 e. T" y& K" d( g7 ^) {* ~0 w: k2 u; b; }+ V1 R7 a
|