cncml手绘网
标题: 编写一个简单的TCP服务端和客户端 [打印本页]
作者: admin 时间: 2020-5-9 01:53
标题: 编写一个简单的TCP服务端和客户端
实验环境是linux系统,效果如下:
1.启动服务端程序,监听在6666端口上
2.启动客户端,与服务端建立TCP连接
3.建立完TCP连接,在客户端上向服务端发送消息
4.断开连接
实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
3 r- q; y# y3 _# m- \
什么是SOCKET(插口):
这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
9 P8 H6 H/ v0 Q2 r1 D0 M N
"套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
具体其他高级的定义不是这里的重点。值得说的是:
每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)
( G+ x6 D% h Z9 K9 v8 ?4 y
7 ?. {- n$ c( f
9 a/ B* k" W4 R& Y( C+ D) B; I如何标识一个SOCKET:
如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
6 x. z# `3 M( f, _# F
描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
/ M$ k! K2 p* n! i3 g6 p6 B/ n/ _' c: Z6 q
服务端实现的流程:
1.服务端开启一个SOCKET(socket函数)
2.使用SOCKET绑定一个端口号(bind函数)
3.在这个端口号上开启监听功能(listen函数)
4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
5.接收或者回复消息(read函数 write函数)
# w! i# Y2 g$ t* t. O/ v) p" ]+ Y) u) x, ^5 b9 m
客户端实现流程:
1.打开一个SOCKET
2.向指定的IP 和端口号发起连接(connect函数)
3.接收或者发送消息(send函数 recv函数)
, c c) u0 W' H
0 X7 n, k0 D7 Z4 Q' u$ {7 F# y! y1 h! H+ ^ _
如何并发处理:
如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
2 W8 K0 B& W6 z' m* S$ c5 j; B8 ?. X0 A. J
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
- int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为:
- #include <sys/time.h>+ K$ D9 I2 i* T" q7 ~' Q
2 V. v! `+ W7 U- b, w- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
& R! s7 K" g; H
readset 用来检查可读性的一组文件描述字。
: B1 A1 s: g9 O. o I& F
writeset 用来检查可写性的一组文件描述字。
/ w# L. x. N F* Y exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
2 K; h4 W% y+ ^ timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
* }" `" D' N }3 t
l& [/ J# P: Z% |0 g d 对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:, _% y( C% }" A' `2 h: b7 H( E. ?
: k' a. a q; l/ J) L
- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件), V K! J* [, o; c% m, z9 Q" l
$ H* ]& W7 F3 _( L- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)! |+ a ~: K+ ?/ X. r a
- 9 w0 V- W" D, K2 e- U3 E6 P
- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值:
返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
其值通常是1024,这样就能表示<1024的fd。 ~1 V: B: n/ i; i
5 w6 d' j% U$ S9 b* d. p6 R
% C) M2 I2 _% }+ x1 d% q
fd_set结构体:
文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
- FD_ZERO(*fds): 将fds设为空集8 s' @0 e. |. ^" O" `) X/ ]
-
1 M% R* g! C0 b, {' h) t* g( J - FD_CLR(fd,*fds): 从集合fds中删除指定的fd
" t' `4 }9 }4 |" x) M& r - : o, Q8 t5 M( l; s* p( [2 T
- FD_SET(fd,*fds): 从集合fds中添加指定的fd
( O4 A ^$ S" n/ Y+ u0 r4 ] - 3 J8 @) `, o7 u
- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下
- socket s;
, E) }, M' _% U6 L4 @& t - .....
& S4 g" B7 a( |) g5 s& H - fd_set set;& V, b5 G" I0 F9 l% w5 \
- while(1){& k0 p8 n. Z* o. m1 m
- FD_ZERO(&set); //将你的套节字集合清空
! }6 i! R" D# {$ N# A6 Z6 n - FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s3 c3 P/ C2 y5 L0 ]. c( {2 `
- select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,( p4 T5 B! s3 v, @( y
- if(FD_ISSET(s, &set) //检查s是否在这个集合里面,% T. `7 G4 C+ g& U, o. c( ]
- { //select将更新这个集合,把其中不可读的套节字去掉+ T% u* k6 [9 K3 y/ U7 H1 D, `
- //只保留符合条件的套节字在这个集合里面
" n. q3 s4 ^! {+ x4 o$ I - recv(s,...);' r1 D+ q1 J: V w" O9 T
- }% J% o3 p- {+ h* T, k2 T4 X
- //do something here8 i- a9 x- {' C: |5 J4 G, S7 N* t
- }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
- (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。/ O& c. ]% _1 P/ R
4 `+ {' g* J# W- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1), t8 ^3 J8 u( g
- / s! B" {6 c! ~5 w
- (3)若再加入fd=2,fd=1 则set变为 0001,0011
# c0 p& c5 f6 ^" s
6 F! B6 I6 v- p- o+ u- (4)执行select(6,&set,0,0,0) 阻塞等待
& L7 j7 u v- \# n) e$ u - ; g. Y( i: i' D
- (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 t$ U( W$ I; ]; j& s6 e. ]3 M, v1 m, R: r. H/ A
使用select函数的过程一般是:
/ J) b/ c% W- e8 U' ~; N
先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。( I3 h$ ?) ~/ F* c: [9 R8 H
6 ?5 ]7 h, |# \! J0 @( ^& ?
客户端:
- #include <time.h>! w# _: ~ [) k% c L& o
- #include <stdio.h>9 l% Q0 g/ `# V% }
- #include <stdlib.h>
' o9 R4 C, O3 N* q0 W) Y. f - #include <string.h>( h. ^: |. d% L# X( a
- #include <unistd.h>% k }8 H: q7 b) u
- #include <arpa/inet.h>
' S; B! ~3 a2 u" N3 n - #include <netinet/in.h>( Q e) i( Y" @# v$ ^5 p0 F# o
- #include <fcntl.h>0 a: }7 W5 n V9 c; M9 g: C+ P
- #include <sys/stat.h>" `7 i8 i+ e: J5 Z1 O0 v
- #include <sys/types.h>
' ~/ S9 F! F* x1 a. h$ a - #include <sys/socket.h>
/ q7 Y1 |; D' P+ [; a+ ^1 |1 F - 5 G" e! ~: ^, h! v
- #define REMOTE_PORT 6666 //服务器端口3 X, Q* h0 T4 w1 _* w* V. e
- #define REMOTE_ADDR "127.0.0.1" //服务器地址* @8 d% x) V2 s* }9 h
- 6 K @( R- l" t& x8 D
- int main(){
) u) |/ m+ s5 F5 m$ ?2 P - int sockfd; O- T0 \+ R# g4 o% n
- struct sockaddr_in addr;8 t2 l3 q+ p- m# v
- char msgbuffer[256];
' v# R& e+ O) k/ O -
6 k( {1 ~$ ~. B2 s- y) P2 O+ K - //创建套接字
! C& w9 D+ y8 r/ M3 S6 w; U' S% k - sockfd = socket(AF_INET,SOCK_STREAM,0);
. C! K! e( P( q. X, g - if(sockfd>=0); [" [! h# k( w m' s- u4 C9 A. {. y
- printf("open socket: %d\n",sockfd);
1 w5 U* l! }& @& l - + u) Y: _4 e% z& y" i. m* u
- //将服务器的地址和端口存储于套接字结构体中6 W' b( a, q- X: ~- P
- bzero(&addr,sizeof(addr));
6 t' _- J4 v4 J' z) O! F - addr.sin_family=AF_INET;9 E( Y9 }, ^( ~: B# P4 H1 A
- addr.sin_port=htons(REMOTE_PORT);) B, [% Y" W" J# T: T* N
- addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
1 N$ o" D$ h! a8 G; l1 N. Q -
) ^5 ]- ^0 ~4 c/ V# u - //向服务器发送请求
: V: O$ d+ P! {* h2 Q - if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0): W6 e% h$ K6 w# e$ y9 w" W
- printf("connect successfully\n");
1 y3 r& `/ t% w) m$ [ -
C6 i9 F/ u% v0 R3 T, [$ @6 | - //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行); L3 r( S* B, E! R/ w2 a
- recv(sockfd,msgbuffer,sizeof(msgbuffer),0);! w4 G) S8 p1 f$ c4 E9 c* z% M
- printf("%s\n",msgbuffer);
7 S* b' X" j) ] -
! O4 F* v/ P& J! u' C, q! \ - while(1){* [4 f4 i& h/ a1 L7 ^
- //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
: n% i3 @- L8 s: j0 b - bzero(msgbuffer,sizeof(msgbuffer));
# |3 e3 V# g/ O1 A - read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
, U/ W3 o" b6 K# c- H - if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
t$ e9 F; U% J6 v3 [) m8 F - perror("ERROR");
, W/ Q G$ N3 H" z" y - 9 U/ n0 G; |2 M+ {' M
- bzero(msgbuffer,sizeof(msgbuffer));
& o; D" |" R: ^( y7 R( p% _ - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);% K1 A1 z9 j$ k" Q8 o5 [! P4 N6 k
- printf("[receive]:%s\n",msgbuffer);
2 S: c. t/ x4 U' w -
7 t% Z% u9 y; ^# o/ X2 t# e - usleep(500000);' i4 g; I* M4 F6 ?' X+ F9 [
- }+ s9 x( ]8 [4 M
- }
复制代码 9 G6 [6 ^: T2 ~8 R Y. `
6 K) U P5 X! F, q0 D8 ^服务端:
- #include <time.h>
, M4 x' ^6 M) j- ?7 d" t, a - #include <stdio.h>
8 g# K& D$ {! G; B8 a - #include <stdlib.h>
" ~# u* {- d$ X - #include <string.h>
' B( m# [* w, W, k3 c' ?5 N' w$ e - #include <unistd.h>6 |4 K, S7 @0 D1 |3 ~* K9 k6 P
- #include <arpa/inet.h>
' o+ ^9 q" A1 y1 G - #include <netinet/in.h>
/ d) t: h+ b- C( E: l& L% f - #include <sys/types.h>/ X9 y) S8 S2 j: \* j6 k9 O
- #include <sys/socket.h>
- O( e* n. r$ o! U. M - , K7 l/ R: V$ T
- #define LOCAL_PORT 6666 //本地服务端口8 y3 I B( g @9 M( y$ Z
- #define MAX 5 //最大连接数量
0 F% |' r) @" u: ^ -
u- Q. M( u5 w5 }3 ~ - int main(){
2 {$ e6 s% b3 [+ |% ~ - int sockfd,connfd,fd,is_connected[MAX];
# j6 W9 \6 y4 t2 L( H - struct sockaddr_in addr;
/ o) o- N* v- G& c% c - int addr_len = sizeof(struct sockaddr_in);8 p8 `8 G7 E: R+ }' E. R. R R
- char msgbuffer[256];& s/ U5 R ?: E) j! @
- char msgsend[] = "Welcome To Demon Server";
2 z. y5 p5 L6 Q! S( S( ?" _ - fd_set fds;' H( c, U" P' @ ]( M" E' M* s+ ]0 i
-
; s& t+ B* @, i- {- T6 P$ m - //创建套接字& T( X: _) ?$ I
- sockfd = socket(AF_INET,SOCK_STREAM,0);
" j1 O" O: `& O& d - if(sockfd>=0)# p2 F' M5 O" Z
- printf("open socket: %d\n",sockfd);
0 l( |) a3 L }5 R: e% p( @3 M -
2 h" D+ {: |9 `8 K4 [, ~% Q6 M - //将本地端口和监听地址信息保存到套接字结构体中0 _; Y" L3 S; _7 B
- bzero(&addr,sizeof(addr));
+ S3 ]# N2 |$ v4 P# S6 j: w1 X' } - addr.sin_family=AF_INET;
3 \% K& t; _; y$ {) Y2 j - addr.sin_port=htons(LOCAL_PORT);4 ?3 | {5 H. Q9 L4 `
- addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0) @* k- l# l" {3 B/ q/ t
-
0 y9 B$ q/ M* `. W: a - //将套接字于端口号绑定, g$ w2 a i: ?
- if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
7 Y# J n" m; x - printf("bind the port: %d\n",LOCAL_PORT);, v9 i- V; Q% {' S
-
# p% W2 [1 [; D5 [0 u4 S8 R - //开启端口监听6 G: l; o5 w7 Q8 N! T
- if(listen(sockfd,3)>=0)* d! o; ~: W3 R
- printf("begin listenning...\n");2 A" H8 V+ g* p* n
-
2 T4 g- B Y8 L+ y - //默认所有fd没有被打开
# J! S# V* _7 r; D- E9 H - for(fd=0;fd<MAX;fd++)8 c9 e. I7 K, T/ I/ g
- is_connected[fd]=0;/ |7 z+ W2 N0 I/ `, x3 V
-
( }. v$ Y: F7 _0 G6 e - while(1){
2 S$ G( Q3 v( }$ H/ a/ p1 ?) A; Z - //将服务端套接字加入集合中
4 `3 `) L! S3 ]4 ~( N - FD_ZERO(&fds);, u5 ?/ [& H) B
- FD_SET(sockfd,&fds);8 N' |5 j$ Q% ^% R
-
% m0 o7 `7 D9 i& c6 @8 V4 Q8 Z - //将活跃的套接字加入集合中
# e8 m" d+ }1 S7 c3 V% }: @ - for(fd=0;fd<MAX;fd++) v: q4 }7 t# d0 J
- if(is_connected[fd])
( F. m4 C% {9 Z - FD_SET(fd,&fds);
6 L- R& d& e$ p7 c -
$ k! p2 T# a8 ~0 z - //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
5 D1 ~* c/ _6 Z+ R- K- x - if(!select(MAX,&fds,NULL,NULL,NULL))5 V" e* i: k- V* {! c
- continue;$ R( R& s+ e s h8 z/ X
- ; S- I0 s. L6 @8 {) v2 O
- //遍历所有套接字判断是否在属于集合中的活跃套接字8 d/ A* a; I# D0 q0 a4 Q
- for(fd=0;fd<MAX;fd++){
2 E# N6 t# `# }7 G" M! d - if(FD_ISSET(fd,&fds)){) f: l6 i9 Z! c
- if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接
2 ?. }3 f* W) q" [+ ^" w) g - connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);1 d6 s/ U5 j/ C. V
- write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语
) a% j2 ~/ Y/ W( x# } - is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用3 `1 K$ y9 ~) H$ Y
- printf("connected from %s\n",inet_ntoa(addr.sin_addr));
" T& `- i4 M3 N% y+ U' u - }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字
/ D5 s) s4 a7 p! W8 z - if(read(fd,msgbuffer,sizeof(msgbuffer))>0){ 7 h9 x# s/ t7 `! Y) m
- write(fd,msgbuffer,sizeof(msgbuffer));
2 A$ E: g+ R8 R- B - printf("[read]: %s\n",msgbuffer);
3 {7 {, X7 l* R/ j* _+ u" T: C - }else{' z* N/ g. S( T+ {. q6 @5 }
- is_connected[fd]=0;
% e; g, l+ W' d - close(fd);
2 ?5 C0 h2 d' C+ m2 } - printf("close connected\n");/ [/ g: {5 U0 l, Y0 F+ b
- }
4 n1 V$ s* @9 ~ - }
2 W) g6 B, N6 o- H% d - }
% O9 j) `$ ^8 r u, o7 U - }
, \1 m- ~. E2 Q$ N- V3 z2 ^ - }
7 p7 N2 K1 B `( W) G3 X - }
复制代码 3 t) U5 m, Y, e) b2 l, O! `
' E) l5 r8 m8 l+ N
- c( ^+ G2 d: w
6 o" m1 Y1 L3 p& T% x
( `4 A2 [3 I+ o W) Y6 X) X- K' V. l& @, [! i& e) c' p4 t: X
| 欢迎光临 cncml手绘网 (http://www.cncml.com/) |
Powered by Discuz! X3.2 |