实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上 2.启动客户端,与服务端建立TCP连接 3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
5 v/ m# i1 t) H; C什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。) W1 ]# m% L# [! U
"套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。 对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。 具体其他高级的定义不是这里的重点。值得说的是: 每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。 应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)/ |+ |+ W- e' n Y
& B6 j [2 m$ t4 I
, r9 {4 j: H" Z5 [如何标识一个SOCKET: 如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个 SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等) e( A, A* o4 U7 q+ F9 f9 I
描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。 述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出 当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3 ( {) o% S/ U U4 r, v4 t0 u
& [+ E& e2 Z) c$ e& b9 @: n8 v
服务端实现的流程: 1.服务端开启一个SOCKET(socket函数) 2.使用SOCKET绑定一个端口号(bind函数) 3.在这个端口号上开启监听功能(listen函数) 4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数) 5.接收或者回复消息(read函数 write函数) . y3 |, O6 C% _- _
; J6 O0 F' {+ r1 q. \9 J客户端实现流程: 1.打开一个SOCKET 2.向指定的IP 和端口号发起连接(connect函数) 3.接收或者发送消息(send函数 recv函数) ) ]! X, y }: l. [0 g" k) V w5 q
& @* v. e3 m3 @/ u+ }
, {5 M* N" _2 Q* C4 [如何并发处理: 如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果 直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端 一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到 有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照 单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。 , w l2 W) M# \4 P8 t5 O/ r
9 b! h8 s }6 A/ v, k
如何解决: 下面摘文截取网上的资料,有兴趣者可以看看 系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你 - int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为: - #include <sys/time.h>
' |% I4 Z$ f, e8 W; o& s4 e8 j+ y
# S# M$ h# H5 m3 a0 j- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
+ v( p. J6 K. q# m- V' j readset 用来检查可读性的一组文件描述字。
9 {* e# Z9 u. V$ k! T" d
writeset 用来检查可写性的一组文件描述字。
7 D* U& d- x; N( J; t& q6 k/ a/ u exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
7 L7 f5 m2 f( z* C5 }) _4 W
timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
% ^5 e, \& q' i/ W; h! {4 I1 @; b6 G9 S) g% S. I( O7 g, ?
对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
% p# `3 U w& C, M4 i * w! D6 x" Q7 q" {: |4 X) n
- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)
% x( ]7 m0 y) G- X: E* h5 {
% F& k* u! |9 O2 I1 H- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
" y& D6 Q. _6 E' i! P) F( ?
I& l# E v% n! E" c- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值: 返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。 否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来, 你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量, 其值通常是1024,这样就能表示<1024的fd。
: ?- d7 [ S$ J+ w' x$ @" d1 l
3 j0 ]: O, `6 U. v1 _3 O- I0 V
1 I/ y& t8 y' e3 k } fd_set结构体: 文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字) 可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作: - FD_ZERO(*fds): 将fds设为空集* q$ c W; O5 `% C9 G- M* H
-
" l( I9 d2 A/ |6 ^& q4 ]8 X% N - FD_CLR(fd,*fds): 从集合fds中删除指定的fd
' R) S' N1 _3 k7 D% W" o+ O8 P# I$ G - 0 F% M V' r, t5 s( ~! N: }
- FD_SET(fd,*fds): 从集合fds中添加指定的fd$ j/ A% a- J6 J- Y$ P
% Y- {- G! {6 a, f. w6 h- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下 - socket s;
# t ^4 O! F' h$ V% ]8 Y# D - .....; M4 g! z$ q% g. [( K0 l
- fd_set set;' w* Z/ A" q# B
- while(1){
9 [. o' i8 x4 e* D/ e$ Q% X - FD_ZERO(&set); //将你的套节字集合清空/ {9 ]- D @4 j) h
- FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s+ M0 Y' {4 h6 R! ?- F
- select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,
0 d1 j' l3 \3 g; P* V - if(FD_ISSET(s, &set) //检查s是否在这个集合里面,9 t! a' I% k& ^5 s
- { //select将更新这个集合,把其中不可读的套节字去掉
) K s) i7 ]( c. C/ f9 W6 u - //只保留符合条件的套节字在这个集合里面
0 e8 ^! ?; v5 g8 m5 g& N1 L - recv(s,...);9 k' n6 G( L* ]9 f
- }
9 P# H( ?# m2 b6 K- {7 m - //do something here9 E9 w/ t8 m8 D+ d. |8 Q/ ?8 Y' i
- }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd - (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。
" g% @7 H1 |5 b. P; |
" S7 C3 r# y0 Z0 r- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)" o y& F9 S; B! f1 K) s) \6 U' ]
- 5 N3 m0 \. i8 r, P
- (3)若再加入fd=2,fd=1 则set变为 0001,0011
# P7 e! K+ r. y9 x/ M - * |5 k2 f& L4 n2 y/ |
- (4)执行select(6,&set,0,0,0) 阻塞等待
" v+ d" N6 R# ^' T2 _ - 7 K- b3 ]( |! M' ^" t; Q
- (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.具体过程看代码会好理解 ' C. Z8 X" J( N( e/ J
; i9 G, a; o; ?9 s% ~% t
使用select函数的过程一般是:
, i! A! \' E8 i6 b3 c 先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1 复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。* @9 v* n1 Y- c, ?. }
. J/ q4 x9 b% b3 N1 L客户端: - #include <time.h>
4 Z8 w, y- Z* W' D* {; R, ` f0 N9 r - #include <stdio.h>
! _0 ^/ P6 n7 Y# g - #include <stdlib.h>
, D% E/ I4 t! j7 V& p4 I, @7 u - #include <string.h>
t0 E0 J1 e! C; w( C - #include <unistd.h>
. S" i5 Y6 V _5 l* Q* N1 } - #include <arpa/inet.h>
3 L( K9 s; p6 t! S+ c - #include <netinet/in.h>+ O$ q9 Y. ^* p8 J G `
- #include <fcntl.h>: e5 G7 `1 Y5 } q/ U# t$ {+ S
- #include <sys/stat.h>
8 e. u8 _% ]! v0 m; o - #include <sys/types.h>
8 Q( j3 L" H# i; A$ a& U% B5 c3 x - #include <sys/socket.h>9 h& F' n' {, [9 q
- / A: o0 Z. D9 O& K! }* U5 A6 Z
- #define REMOTE_PORT 6666 //服务器端口+ H9 t r: F+ _. R" q4 D
- #define REMOTE_ADDR "127.0.0.1" //服务器地址6 `8 A: v) }+ G/ ~
- / z- m% p% M' K1 x+ w0 d, [
- int main(){
* f- h& s( K) S& r - int sockfd;
" \" M$ x# H& x0 n6 n' o - struct sockaddr_in addr;
& d( E% ?, T' L+ i - char msgbuffer[256];% l3 H/ a( B% T0 C0 }5 W5 ^! w
-
: K ?; x2 i: R k1 C - //创建套接字+ [5 p7 t0 l" X# R3 L
- sockfd = socket(AF_INET,SOCK_STREAM,0);
) m: D7 C! U9 v - if(sockfd>=0)
1 J: K% L. z+ b* M8 g1 D8 n - printf("open socket: %d\n",sockfd);
$ W1 {* Z7 H! j7 i9 P -
5 Q! q4 H! F8 g: Y$ }6 E - //将服务器的地址和端口存储于套接字结构体中
- D& o# u. H8 C4 y% H - bzero(&addr,sizeof(addr));
: H! A7 _: @" b' C: P& [2 t - addr.sin_family=AF_INET;
" i6 H+ J! E2 y& h" q5 J - addr.sin_port=htons(REMOTE_PORT);) ~8 U* m4 M- I0 y2 m8 Q4 |6 p
- addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);4 g$ r2 H" q8 V
-
9 a0 I# T! c( E - //向服务器发送请求
5 J7 ^# w5 h# ~* }0 y& I - if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
* ?0 n& l' R4 }8 l0 _ - printf("connect successfully\n");( \* u+ N7 m9 E: H- J) D
- 2 t2 [& H4 m- x- |3 t3 H5 L
- //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行), T' E3 r: g' c, [
- recv(sockfd,msgbuffer,sizeof(msgbuffer),0);2 R, O7 w+ S1 z, x6 w
- printf("%s\n",msgbuffer);
' d- X U% m& t0 J9 L+ T - 4 O) t) H1 i& q( ?% n- y0 h% x
- while(1){
& e- g B0 L, U: o6 p - //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
^7 g X, ^* z N! y3 s+ L - bzero(msgbuffer,sizeof(msgbuffer));
2 T: l1 X1 M' H( Z& [* f - read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));5 ^1 \ l+ G( P; ~3 b+ j1 i
- if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
4 E. V7 I$ s' X) w4 V# d& d - perror("ERROR");
! {/ ?$ ~0 R" L0 {# l8 g5 {& E - ; g6 E3 c( j" d) m6 Q
- bzero(msgbuffer,sizeof(msgbuffer));$ ?* b5 v, E& k$ d9 J( x
- recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
& o- }+ Z9 O- d6 Q. {5 O7 j# d - printf("[receive]:%s\n",msgbuffer);
7 K+ I9 s. u) M+ T1 S. g - . J; ?5 T0 }7 Z7 W& t1 {
- usleep(500000);
* E: j3 R2 O* q2 w6 G; J - }* ~, F g" v2 @# Z* ]) K
- }
复制代码 & c# j& p3 A6 @5 K" h5 {2 X
5 d" Y' i* h" d# H; D6 ?( Q服务端: - #include <time.h>
$ Y( t X: Q$ W: p( T0 e1 y# A. |) T - #include <stdio.h>
3 K* v+ q( V/ x ^+ ]& Y( | - #include <stdlib.h>5 j$ ~. Y" i/ @, Q
- #include <string.h>
9 `: H$ c5 E' G7 S, Q5 Z8 T' ]! Z - #include <unistd.h>7 d" U1 u$ {0 Y- `- U% Y: g$ b3 Q
- #include <arpa/inet.h>, L% k @; N/ t$ B' m
- #include <netinet/in.h>/ c8 L" n: Y# o; n# [
- #include <sys/types.h> j$ L4 a% `( p' x! v/ V$ L
- #include <sys/socket.h>3 O# V7 f) D' |# j' G
- 3 I' g: J0 V# n$ d) ? ^
- #define LOCAL_PORT 6666 //本地服务端口2 S6 {9 q9 M P6 ^" Y: ~% F
- #define MAX 5 //最大连接数量
9 o' r- M; H" } I - ( c0 s( S! v V1 O3 B8 e% H
- int main(){: R8 W. N2 }$ \9 M
- int sockfd,connfd,fd,is_connected[MAX];& F0 L7 f9 A, k, W$ j; w8 V8 J
- struct sockaddr_in addr; h* O n( [3 D0 ?8 N6 Q
- int addr_len = sizeof(struct sockaddr_in);) z9 i, ^4 D0 W- f8 w c
- char msgbuffer[256];
. M6 |4 r9 R) Q% I5 H - char msgsend[] = "Welcome To Demon Server";
% i& k8 }$ w7 S* M - fd_set fds;6 P* w1 p2 b1 p
- * o) L4 L& J | k
- //创建套接字! a9 F: d K1 I B/ l. i, q( A
- sockfd = socket(AF_INET,SOCK_STREAM,0);
* Q0 n1 E# `; t" d - if(sockfd>=0)
; q4 p6 v6 k) C. a7 j - printf("open socket: %d\n",sockfd);( `# [5 a4 [; C7 |6 J0 l( j3 k
-
3 v' W3 l3 S0 d7 @$ R - //将本地端口和监听地址信息保存到套接字结构体中7 [( L( p4 B0 W: E3 d/ @% p" b
- bzero(&addr,sizeof(addr));
g# R/ t* A- N - addr.sin_family=AF_INET;
0 \9 u9 C. y) b - addr.sin_port=htons(LOCAL_PORT);6 \6 ~" R. N q1 `' m
- addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
/ i' _3 e; u% l/ n8 N: w7 B0 s, M - , Q% ~6 g- _( `6 \
- //将套接字于端口号绑定! u# N0 v$ t3 U
- if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
1 {5 M+ l! g1 F" u, Z' ^+ } - printf("bind the port: %d\n",LOCAL_PORT);8 L2 w. B8 |& n. S8 q$ p* _
- ; p( `& M P! e' s0 H1 k
- //开启端口监听% b/ Z4 I; O5 E/ L# | }5 C ?
- if(listen(sockfd,3)>=0)
5 Q& O9 K! M6 t* B& r+ ^0 |! c - printf("begin listenning...\n");
/ R3 U3 J- |4 O8 |$ O* a -
; ~ e0 H z. C) f3 J6 o, s - //默认所有fd没有被打开# J" e( V0 _- @
- for(fd=0;fd<MAX;fd++)
/ ]7 }4 v5 i( n* ^3 e$ ` - is_connected[fd]=0;
3 v# i3 c9 {8 Z& o$ K - ! v# Q7 p; B0 \% n/ F
- while(1){
" T# p, K9 Z0 _) h - //将服务端套接字加入集合中3 O4 g2 d6 b& l. {% d; X; x' n$ C
- FD_ZERO(&fds);8 g. i. `9 B+ n }
- FD_SET(sockfd,&fds);
6 O" B& C2 r. G& ]! k0 x - $ a. ^* d& p: B
- //将活跃的套接字加入集合中
% O F- o: L3 u, f% s: { - for(fd=0;fd<MAX;fd++)
$ }2 \; T3 N- P; n - if(is_connected[fd])
! n' I9 b) \" W7 W' D7 V - FD_SET(fd,&fds);' z3 g, C6 g7 ]3 r9 r6 P4 C `3 \5 v
- , n5 M* e4 \ e% c! r9 Y4 ~/ {8 r
- //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
2 R6 B1 b, O- E3 W, j; L) i - if(!select(MAX,&fds,NULL,NULL,NULL))
, {* r: f+ \" j - continue;) u$ `# O( {* T! L
-
" t. A% R# F! d* } - //遍历所有套接字判断是否在属于集合中的活跃套接字. j2 O# B, L( t9 p2 }* P
- for(fd=0;fd<MAX;fd++){
' P. l6 F2 @* S6 g) |2 `9 i/ e) _ - if(FD_ISSET(fd,&fds)){8 a, Y( _/ b$ y/ F/ N; Z' G
- if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接
3 k. D2 D0 O4 p( C* g2 Q - connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);5 d1 f: z1 x, a# v
- write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语+ N+ ?. b- F! K1 ]! x
- is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用' c9 u0 W& r. @4 V1 A4 S
- printf("connected from %s\n",inet_ntoa(addr.sin_addr));) ~1 V! R3 F. g* o F
- }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
8 r+ o$ t# ?1 c5 S$ X - if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
2 C4 F6 Y$ Z1 Q3 j) D* k' Z - write(fd,msgbuffer,sizeof(msgbuffer));# m# j9 c! Q; Q! g9 p) y
- printf("[read]: %s\n",msgbuffer);! U! Z2 c& O5 y7 V b; A9 l$ b6 K, e) O
- }else{
' b$ s# N7 d% O+ z - is_connected[fd]=0;9 N$ J8 v) @ c. F
- close(fd);$ D' P- j/ w! G5 |5 O3 M! A
- printf("close connected\n");* B g% k# l' X4 X e. h) z
- }
6 z+ ^( @ w+ `5 l - }
$ @! v0 V1 r+ ]' X - }
. P. t/ {* e3 O+ M, u! C - }, ~7 w1 Z( Z! v' i: J
- }' t4 b5 P/ f8 o+ s
- }
复制代码 - F6 k! U) a& n( t% Q
5 t* |- A: {$ H- X* o! M- z
( @* ?7 O6 ~. M7 [- d3 f/ K* |+ O) x
3 S( Y9 W& _" ]! A4 E/ |
) A; y# t8 n4 Y A% ]) A/ i/ `& g# I' |: [
|