|
实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上 2.启动客户端,与服务端建立TCP连接 3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点 " m0 L6 F0 [8 r2 @# z
什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
# @' `* x+ s" }# w) x- ]# s( }4 Y "套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。 对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。 具体其他高级的定义不是这里的重点。值得说的是: 每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。 应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)
* `' b6 C) @: M& C! { ' O5 ?, M6 K. Y/ S
/ S$ o( K) v; D }/ T8 B如何标识一个SOCKET: 如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个 SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
! g d7 p! p0 K! d/ I0 f$ n 描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。 述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出 当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
) E8 Y; \7 E! p0 r' M
" L6 v. `& s, o; \服务端实现的流程: 1.服务端开启一个SOCKET(socket函数) 2.使用SOCKET绑定一个端口号(bind函数) 3.在这个端口号上开启监听功能(listen函数) 4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数) 5.接收或者回复消息(read函数 write函数)
8 k% `- Z! b% h0 W, Q) r1 k; w1 C0 L9 r& i$ l+ ]
客户端实现流程: 1.打开一个SOCKET 2.向指定的IP 和端口号发起连接(connect函数) 3.接收或者发送消息(send函数 recv函数) ) A8 W1 q+ L7 W
. x% V' y# A1 c# u& q3 @% k% V4 V2 {" Y
如何并发处理: 如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果 直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端 一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到 有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照 单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。 " ]$ n1 J; r( s# ^, h- G7 z5 }3 X
5 t+ b& e" j c# V1 l& h如何解决: 下面摘文截取网上的资料,有兴趣者可以看看 系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你 - int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为: - #include <sys/time.h>' h: I8 Q! Z2 S% S( \* l. c2 f
- : L/ z2 U6 U4 p2 d2 |* R* I' t
- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理 7 c; w# A! d% O: j2 Y
readset 用来检查可读性的一组文件描述字。 0 u. s' Z, x/ M$ z5 b
writeset 用来检查可写性的一组文件描述字。
- b5 A2 U$ K- ?% P exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
. B2 {$ O* |$ C4 J7 X) O
timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。. b! @6 X( z, Q) @: \# n
: [. ^8 H! S ?1 N& j9 o5 x 对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:# W* S2 {0 x3 @
! _) ^7 c( a v, x( r$ n g
- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
. q; a7 q+ L4 `7 F
5 K3 u) e; {& b* D0 c- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
0 l D1 o0 L. l/ i6 z" R
! ]: w$ } [1 }% n4 l% f- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值: 返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。 否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来, 你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量, 其值通常是1024,这样就能表示<1024的fd。
& P0 `4 [) Y! k0 ?1 y, O' x$ R
& ]' K( o' j5 Z1 J/ c! p6 J: W4 f
5 C/ G; I7 X. l7 E fd_set结构体: 文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字) 可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作: - FD_ZERO(*fds): 将fds设为空集- b/ r0 [% H' ^5 F, L
- : |- X) s/ a. n# L% q, ~
- FD_CLR(fd,*fds): 从集合fds中删除指定的fd
/ V. \8 h. N& s" Z8 D7 f V3 n& A- p
; R. I/ L9 y% Q: x' ?: F7 @7 B- FD_SET(fd,*fds): 从集合fds中添加指定的fd7 q7 x: e) n5 x
- + A- L1 Y: `8 s: c2 |
- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下 - socket s;
" k+ i/ P Z# W. a5 ] J8 r9 c - .....1 ?9 V+ f# M7 H `2 m8 o
- fd_set set;( d B+ q# _# p0 i* g
- while(1){/ O" l3 u5 o! _0 y
- FD_ZERO(&set); //将你的套节字集合清空
/ K$ `! e3 f7 g - FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s9 B/ J" C6 G& F/ ~. Q" I
- select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,
! a% j, D$ r2 g( @1 t - if(FD_ISSET(s, &set) //检查s是否在这个集合里面,
; B a7 ? L- e+ k - { //select将更新这个集合,把其中不可读的套节字去掉
+ Z* k3 E B: q2 O - //只保留符合条件的套节字在这个集合里面
3 h: M% i/ d# @. d - recv(s,...);
. d* O1 d" |9 m/ [3 u5 h R- V - }
6 z* y5 k/ W3 U' O - //do something here
3 T; Z5 C; S. f. g7 G - }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd - (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。
- G( L: G+ w) E - - ~- R9 n9 U) v2 k
- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)& u) ]1 A0 Q, j N% H
; O% X- r: Q. _$ I$ B2 O- (3)若再加入fd=2,fd=1 则set变为 0001,0011& v( t- {. F" a( @" V
" v) E0 L4 d; D) c. U! w4 W- (4)执行select(6,&set,0,0,0) 阻塞等待4 t/ }, N* [- E' E5 p0 Y( L
- + o6 d, b- l9 q; ]1 p# T
- (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.具体过程看代码会好理解
0 J+ a- V8 g' `& s- z) ?% Y" x! V) f: _0 i' |3 b
使用select函数的过程一般是: / H! {1 @! e- _9 k
先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set, 接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1 复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
5 Z0 ~! e) r( V5 u; {, ~: h ( ~0 a4 W& m' J/ t) y, \
客户端: - #include <time.h>) L* O0 h [' j
- #include <stdio.h>
1 p ?( v& P, o7 Q5 K - #include <stdlib.h>
7 Z7 s3 g: I) _+ Q) d) K, m - #include <string.h>
' D( c5 F# c* z- ^8 k) i0 |; a; J( ` - #include <unistd.h>" h. |$ Q1 u8 }8 c7 m$ b$ S
- #include <arpa/inet.h>" h. e% l% s s% V0 R2 q1 M! p; ^3 b- E
- #include <netinet/in.h>
4 @* t) u# f+ [ w, Q8 [ - #include <fcntl.h>
0 M; e( {. S. Q - #include <sys/stat.h>
1 S% _1 N4 L; k' B/ c - #include <sys/types.h>+ f% O4 [# b9 \4 R2 k# Q* Z% U
- #include <sys/socket.h> n D }" Q8 u! ]
- : M! `' Q j+ p
- #define REMOTE_PORT 6666 //服务器端口
2 J) ^0 x, w# O7 V( }: R1 R+ D - #define REMOTE_ADDR "127.0.0.1" //服务器地址
. M6 u! E# F6 `- w8 V -
! ?+ i1 m: W: _0 s* J- W; W- X - int main(){8 i8 z; e( R6 p7 A E. N4 W; @
- int sockfd;
# f0 F7 E2 L) l! C - struct sockaddr_in addr;
7 C8 m {& e( e/ r8 u" n' _% } - char msgbuffer[256];0 Z2 r4 [9 e4 Y
- ( X' S7 d# I: `) i
- //创建套接字% c" Z( y. w. ^
- sockfd = socket(AF_INET,SOCK_STREAM,0);- u) x7 ^+ N% X: f! v: _/ b8 @; l
- if(sockfd>=0)# m( ]! z5 I/ S. Y: l
- printf("open socket: %d\n",sockfd);
+ a% Q) e$ z! j$ c - . M2 y# H& {: Q
- //将服务器的地址和端口存储于套接字结构体中
- F( B4 y9 @% Y# d - bzero(&addr,sizeof(addr));
$ v" T+ E2 p3 I2 R$ F$ b- b - addr.sin_family=AF_INET;7 x( n$ R \% W6 P5 |' s/ n
- addr.sin_port=htons(REMOTE_PORT);: D, c" N5 ~+ I$ Q
- addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);3 T6 M1 t* |+ j( ^! ?5 T$ _1 c
-
2 [( e# I/ V, M9 ^7 h - //向服务器发送请求
% `5 J% {0 W2 h' F9 J - if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)8 g$ `: P2 P0 v6 O
- printf("connect successfully\n");
5 F) ]9 E4 i1 Y- z - l2 \7 h* p' n0 L* |. {
- //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)+ m& v" w7 F; b* ?1 B
- recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
: K `4 X4 z0 S) T6 n; s - printf("%s\n",msgbuffer);
# x( S0 n* s3 x3 | -
/ A1 h6 {0 k7 I2 s6 C+ R - while(1){
3 h" {: H% t$ _/ ?# m4 I - //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息, i% V' x: r- t4 E
- bzero(msgbuffer,sizeof(msgbuffer));+ E1 m5 ]0 h3 J# ^& w5 Y# g) h! B
- read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
7 D2 }( z: B/ d/ ~$ s' G- ^+ C - if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
# K9 ^$ S! R4 E7 T0 O - perror("ERROR");
: L8 h, N4 b' m" ^ \ - 1 O7 t/ E5 h9 s; y
- bzero(msgbuffer,sizeof(msgbuffer));
- ]+ f6 J0 P% r9 Q! } - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);" V, x) j5 T3 S4 `4 L" R
- printf("[receive]:%s\n",msgbuffer); b$ W1 v: \8 d, G! f( z
- 9 r9 g) u, n( \' @) q# b' }9 }
- usleep(500000);
9 n) g% _( v2 S) p; X3 _4 Z0 s - }+ W5 l3 E1 [% v) c; F; Y1 z" z+ H. r
- }
复制代码
. M0 [( G' e2 p0 u" L4 d/ {' n% L* t) n' T) T' ~) T* U
服务端: - #include <time.h>
- l0 ]* @9 l5 W! E" E, c% e+ L2 } - #include <stdio.h>0 w; D; G/ @( h: B: [8 f4 P
- #include <stdlib.h>( d, Y4 M& d/ R5 G9 X
- #include <string.h>" b# @9 x' R6 C* l; b- [8 F7 J
- #include <unistd.h>
! [6 c2 A0 k/ z - #include <arpa/inet.h>- D# e* W# L m" m0 d) W) }% y% i
- #include <netinet/in.h># A, _9 \( H! J( n8 x- u
- #include <sys/types.h>3 K1 p+ {7 m" T
- #include <sys/socket.h>1 T: s: O( e R9 L2 C
-
/ Y( |, V9 s x - #define LOCAL_PORT 6666 //本地服务端口6 n* K/ n5 f* V. ]' P+ Y% ]3 B
- #define MAX 5 //最大连接数量6 v! t6 C5 Y4 E- \" y
-
4 i- T& G2 n( \4 C9 F - int main(){/ P( t& d0 B$ h) S
- int sockfd,connfd,fd,is_connected[MAX];% }) I, D+ Y* D! v u* g J0 p1 r3 {
- struct sockaddr_in addr;% l* m1 r& [+ P) y6 s6 t
- int addr_len = sizeof(struct sockaddr_in);+ f9 i) r( g3 L; ?: _& i
- char msgbuffer[256];
4 ]3 {$ @9 Y! d( n' h) Z0 f8 e/ r - char msgsend[] = "Welcome To Demon Server";
* P: z* [6 g# ]* l - fd_set fds;
7 ?) W$ i+ K6 `, T -
# w M" X$ b+ i# W - //创建套接字
' ~, d' h& ? }$ f6 G9 r: j - sockfd = socket(AF_INET,SOCK_STREAM,0);, [' M) }# ?. ^' I9 J
- if(sockfd>=0)" N5 M2 u( N1 k$ j& u
- printf("open socket: %d\n",sockfd);
! M3 {9 c: I1 I6 { -
1 z; z! x5 _. F5 K - //将本地端口和监听地址信息保存到套接字结构体中# `. Z5 {" w. m# C5 [
- bzero(&addr,sizeof(addr));4 R! C: g0 u* H! T9 Z- p( [
- addr.sin_family=AF_INET;6 Y% L' r8 i' B; P% v" S
- addr.sin_port=htons(LOCAL_PORT);! ~3 b! Q1 Q7 m3 D/ s/ [
- addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0 B" a9 I* @" a1 D7 i0 g
-
' d, z% X- N' c: F; K2 [ e7 a - //将套接字于端口号绑定
5 @/ Q% _3 t! ~ - if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)( i6 ^0 z2 p0 i
- printf("bind the port: %d\n",LOCAL_PORT);- j1 V1 {7 j, ]
- 3 `7 k4 l$ Z7 D) n/ i2 U3 B9 U
- //开启端口监听7 J! K. E; K t) @6 H. f) H
- if(listen(sockfd,3)>=0); B/ J2 b: z; w5 T+ K | N
- printf("begin listenning...\n"); C& s& a# Q' p4 M9 p; H) d _) w
-
# N( X- D7 @2 A; x$ [: t- K0 u+ N - //默认所有fd没有被打开
' ?* B% @9 q4 n4 b9 L - for(fd=0;fd<MAX;fd++)4 c- |* Y( u# P
- is_connected[fd]=0;
; J6 T6 ?" O. B ^. e& T -
7 S4 s8 M5 Y' B, Y - while(1){
: b4 ?7 d4 f V. K% h - //将服务端套接字加入集合中
5 W9 [* U/ w% `) @ - FD_ZERO(&fds);0 d6 C- _0 h+ L* D
- FD_SET(sockfd,&fds);
6 v% u; {7 z, a) F0 S& h/ S - ; R# N) g, E8 v, P) w
- //将活跃的套接字加入集合中
* V( |4 S& T$ b, A! m - for(fd=0;fd<MAX;fd++)7 @' F- \5 M: A/ E. A
- if(is_connected[fd])( J; n& f5 X+ H# q
- FD_SET(fd,&fds);3 X$ N" O3 M! T2 F) Z
- . H' }4 `; q5 [6 P
- //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
! G) c) L* `, }4 Q& u. U - if(!select(MAX,&fds,NULL,NULL,NULL))( s H3 K8 X5 z" T4 l
- continue;
) [* p3 C R8 U6 @9 U9 G -
~0 x- \5 v! ^* f+ }3 f0 _* M - //遍历所有套接字判断是否在属于集合中的活跃套接字
+ W: J! h H1 z, r - for(fd=0;fd<MAX;fd++){
# B( _& o3 F `6 z& l - if(FD_ISSET(fd,&fds)){
& Y! N6 x+ e5 L9 H: [% ~3 f ` - if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接( T( Y# c" I9 |4 S; Y
- connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);/ |( l: G- E6 H7 V9 J$ M
- write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语
- Y$ D/ b6 B2 i( z; s" G3 z) n: t - is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用: A! v% P! D: Y6 E8 X: A7 ], C- F
- printf("connected from %s\n",inet_ntoa(addr.sin_addr));/ _ u$ _: p* R! D' _2 P+ }
- }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
8 y; G" N( Y% h' U& z - if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ ) Y$ v0 T- ?6 [* R
- write(fd,msgbuffer,sizeof(msgbuffer));
D% f! G3 ]$ `7 x; Y3 t0 v - printf("[read]: %s\n",msgbuffer);6 v; V7 x% L1 @
- }else{
; t! i" O0 s' c5 D- H - is_connected[fd]=0;
2 Y$ E8 V [# T' j6 ~ - close(fd); B; X% X( k2 z m
- printf("close connected\n");
) ~1 \; X6 G6 S+ v - }, i6 g9 n8 h$ \) x: J) b, _& z
- }
0 _$ Y9 @. P* ]' ?' z9 C3 v3 a - }7 s# Y- C! a# O2 k( b+ B
- }; h( Y5 ]+ B( Q$ u
- }( N) P# d' G p5 [% ]1 h
- }
复制代码
S0 E6 B/ v, e$ X0 E. t& }5 P! Y, ~5 H
2 {4 f) u: V/ }5 j1 r0 L, \5 Y" O6 E6 a
4 n a ^7 K( _3 h m i
3 @1 \( U/ O9 F, A% [ I7 l! _
|