cncml手绘网
标题: 编写一个简单的TCP服务端和客户端 [打印本页]
作者: admin 时间: 2020-5-9 01:53
标题: 编写一个简单的TCP服务端和客户端
实验环境是linux系统,效果如下:
1.启动服务端程序,监听在6666端口上
2.启动客户端,与服务端建立TCP连接
3.建立完TCP连接,在客户端上向服务端发送消息
4.断开连接
实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
8 |1 t- E& D8 Q( a: P' l什么是SOCKET(插口):
这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
% ~! O0 K% W7 i! m G1 r: Z2 K; R
"套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
具体其他高级的定义不是这里的重点。值得说的是:
每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)
2 w8 Y4 Q" ~# ~8 C! H+ c& m
: K- L4 v0 ?2 [( a
; W* I @& U; |$ T! g如何标识一个SOCKET:
如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
4 k' O: s" ]: `3 B# I: c! y: o
描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
1 u* S' {$ p2 ]9 M3 F! K! s) `, v& x9 [& x
服务端实现的流程:
1.服务端开启一个SOCKET(socket函数)
2.使用SOCKET绑定一个端口号(bind函数)
3.在这个端口号上开启监听功能(listen函数)
4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
5.接收或者回复消息(read函数 write函数)
/ h' w8 i! j# m6 Q3 Q- I: U& ~1 K0 I8 P9 R0 Q) d8 r7 b
客户端实现流程:
1.打开一个SOCKET
2.向指定的IP 和端口号发起连接(connect函数)
3.接收或者发送消息(send函数 recv函数)
* U9 z: K9 H, ]. u) V$ K, C5 q N ]3 O7 c8 a) E
- @. F0 T3 n- b- Q2 u2 C如何并发处理:
如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
; c- t- x7 x, b. E) Q+ x- b% g
4 D# N8 j# e) z: S3 k: @
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
- int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为:
- #include <sys/time.h>
# n+ ]% A _6 m; Y2 f
3 q. ~ B) `+ ?- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
- z/ ]# F. N( Z" A+ h
readset 用来检查可读性的一组文件描述字。
! V7 ]4 Z8 j5 |. X# }0 _" v* K writeset 用来检查可写性的一组文件描述字。
' a* l' n' _$ V7 c0 j exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
+ T- R7 ~5 }" G' q0 V% [ timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。8 s* q* O' p( T% W! N2 E9 l
: F, I% R. M* g E* z9 r* c
对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:0 C; B' ?7 b- y/ [8 }
; l- N. z# S5 r$ z1 A% s* J
- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)9 v( [6 Q* N3 e$ V
6 w5 } g6 `9 v, K, \4 _- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)
+ _+ c% |+ H7 S% h
* w+ x @8 q- U& m1 \- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值:
返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
其值通常是1024,这样就能表示<1024的fd。! f: t( n x% q* o* v. S
. t& b/ K& f6 ~0 x+ K9 n A) y
& D; @. i3 S# `$ F8 q5 S; V
fd_set结构体:
文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
- FD_ZERO(*fds): 将fds设为空集8 p5 {( ^( L* ?% u
- 2 d: W$ Z. \0 i& Q
- FD_CLR(fd,*fds): 从集合fds中删除指定的fd) B& c0 L) |+ T) S. X+ p* x
0 s1 y& ?3 i! k3 `9 u- FD_SET(fd,*fds): 从集合fds中添加指定的fd& h! E* l( g2 b2 M# u- }
5 |# A' u' D8 X8 b! N- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下
- socket s;6 a0 E+ @ o; c& j. Y
- .....
' B! m B. A5 F& V- \# x! v' N - fd_set set;
3 G; ]( o% ]/ \( | - while(1){
4 h( F- N3 S/ m - FD_ZERO(&set); //将你的套节字集合清空 z* R) L. K& k/ ]/ j% x
- FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s
, U* x% z0 L0 n7 v# K, } - select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,
1 K( J+ i: Q* v% |& m' F* Y - if(FD_ISSET(s, &set) //检查s是否在这个集合里面,/ \( U: c. b1 ^1 b' ^) P
- { //select将更新这个集合,把其中不可读的套节字去掉
0 ?* ~1 o6 g; g' F - //只保留符合条件的套节字在这个集合里面1 u, ~+ I2 K2 G5 c& w* h
- recv(s,...);
$ A$ ?) q' Z6 {" [+ l% A. T% d - }
b! m% m* { @! W* i) } - //do something here
- m0 c9 V$ ~, Z5 x# k& `8 ^6 d - }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
- (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。
4 m: Z7 Q2 p( z- _8 L% A& m
n. f2 B( G9 G8 U8 V! b- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)
# k+ S; z9 v$ X* d0 R
2 }+ \0 M2 B; u6 X# R- (3)若再加入fd=2,fd=1 则set变为 0001,0011
( E! z% m) E6 I$ y/ t ~ - % O1 n) o1 }) B1 N
- (4)执行select(6,&set,0,0,0) 阻塞等待
" x; L$ y/ F8 `$ j: L - + z- K5 D' j9 ^6 V3 [2 G
- (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.具体过程看代码会好理解
5 Y& v" o. q0 H5 y1 w
6 ^: U- B% L; y3 D9 K使用select函数的过程一般是:
! x9 ]- b \! l 先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。
6 ?/ |: s6 x! s. t3 o1 T1 U* ]
( @/ _# X: n, _9 b1 Q) t/ u) j客户端:
- #include <time.h>5 ]! L0 m/ Y' l" i+ F# ^
- #include <stdio.h>! X8 P) I( E; M9 m; m, Y- }
- #include <stdlib.h>
5 E/ q' k* J% n( B( }; \ - #include <string.h>: t3 D p' S( d3 ]! ?, V1 }) q9 B$ ?
- #include <unistd.h>: c5 B" `8 W* |
- #include <arpa/inet.h>
+ ]0 m$ |: K0 {+ _ - #include <netinet/in.h># o: L! _1 ?9 `. D
- #include <fcntl.h>
, j0 ?3 c8 X4 s9 B/ z+ G - #include <sys/stat.h>
# L5 f0 a/ }9 h& B3 U9 X1 x& E - #include <sys/types.h>
7 R2 ~# |( [( p# l. x* J' M - #include <sys/socket.h>. W) k N. e4 a0 [- O8 i
-
: j: `- V& C2 [2 L i# v - #define REMOTE_PORT 6666 //服务器端口
% m/ i$ ]6 J i7 \ - #define REMOTE_ADDR "127.0.0.1" //服务器地址) Y5 N! m4 ~7 Z8 s$ b9 \/ g* i$ o, g
- 9 ^. F2 Z2 h2 h& |: J" B3 C
- int main(){
7 z1 o0 M" C4 t0 W& H% v! P - int sockfd;
/ X5 X9 ]+ t( L4 d$ T - struct sockaddr_in addr;: ~( @) ]$ a) @ j7 t9 s
- char msgbuffer[256];+ x2 o& H7 x3 Y6 b
-
" X T' @ [& Q4 M' h: z" S0 U b - //创建套接字( s$ O, ], A1 y3 D
- sockfd = socket(AF_INET,SOCK_STREAM,0);9 E$ v e/ D1 X' Q9 o8 i' r
- if(sockfd>=0)
/ Y$ P% d) w( u6 G8 L( |+ F - printf("open socket: %d\n",sockfd);
2 H7 Y. K; t) w& l# [ -
1 `; o' J# a1 W7 R - //将服务器的地址和端口存储于套接字结构体中
# ~0 g) ?+ U- X/ q- t2 \ s - bzero(&addr,sizeof(addr));: B1 r/ x9 F3 ~" o
- addr.sin_family=AF_INET;
9 ]8 y d$ W7 C9 k: w" s - addr.sin_port=htons(REMOTE_PORT);
) c+ ~" e3 P$ S6 a& f - addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
9 h5 l; |4 G n8 Q! T$ h# A -
) ~, p/ S0 X" ]- P - //向服务器发送请求
; G. ~* U% R& n: f1 ~4 m - if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)* }4 i9 D9 \; ^' S
- printf("connect successfully\n");
) U/ E4 w) X/ j o0 a - 9 T' u e9 ?: B$ @8 `& h. `
- //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)
4 ^+ ]7 K( e6 S' D4 M0 j. { - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
; J. N" d9 F$ s1 Z0 b, V - printf("%s\n",msgbuffer);. C, }9 @5 x0 S+ ]4 T1 X9 f- H/ b
-
. f1 S) P3 [ q; G0 K5 _ - while(1){
8 p6 a0 D7 q- ^0 X+ n5 |: C - //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息* w7 `: {4 H u
- bzero(msgbuffer,sizeof(msgbuffer));
1 ]; p) y7 j) z2 | - read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));- u% j, _$ f7 s+ h$ k! _* U
- if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)
1 Q. x, P4 }( u. t2 b - perror("ERROR");& ?8 F! j* W. C+ g+ q2 w
-
5 A- c9 t: R: h$ s9 J& [. L$ t - bzero(msgbuffer,sizeof(msgbuffer));
* [5 N) u$ K, B6 n - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);3 ?" t9 t$ l! g( {% L( A6 V) {
- printf("[receive]:%s\n",msgbuffer);
# N( d; J, y4 ?( x0 \ -
( _% r9 x. K: x9 x - usleep(500000);
0 F5 \7 z+ `8 t- L# d - }
4 t* ~5 r) i0 y - }
复制代码 * M8 j& Q3 d3 I5 F7 t3 B
" M" m4 `6 V) w1 V; E4 p
服务端:
- #include <time.h>* \9 s& e& U6 {
- #include <stdio.h>
R' G$ q* w" r - #include <stdlib.h>
" x: F2 z. T% e - #include <string.h>
; c" b1 |1 V% p& m - #include <unistd.h>5 k5 v! \3 _0 L' B! O6 [6 [
- #include <arpa/inet.h>. K- F6 g4 H' h2 [
- #include <netinet/in.h>8 h# X6 D9 b, K) o6 j) S. O
- #include <sys/types.h>
9 ^) {. j4 G( z( E' p - #include <sys/socket.h>
: U: |" y( ]. u -
* q9 A* h w' |. k: ?& ` - #define LOCAL_PORT 6666 //本地服务端口" c$ }5 S$ w, K6 Y# \7 q
- #define MAX 5 //最大连接数量
0 {4 v3 N: ? `; ^2 U2 J- z/ J5 O- F -
( V4 ?# E+ T* L - int main(){( Y/ ~2 p* s- i- W1 u/ C
- int sockfd,connfd,fd,is_connected[MAX];4 q6 r! G1 J0 g' W4 x& S5 m5 X, n# g+ ]
- struct sockaddr_in addr;1 B9 K: B8 g* t
- int addr_len = sizeof(struct sockaddr_in);
2 j0 u, Z. G, i2 R% }1 O - char msgbuffer[256];5 c2 R$ t) H3 P! s$ @" v
- char msgsend[] = "Welcome To Demon Server";7 w; H, [# w/ W5 u
- fd_set fds;; w/ z: |! j% N3 ^0 V# v1 q# e
- ]! }1 a' ?: @. E
- //创建套接字& }3 }- a- j0 T: d* Y
- sockfd = socket(AF_INET,SOCK_STREAM,0);
1 z( ? L. |* s: F% S1 M5 } - if(sockfd>=0)
+ ?) g8 u6 ]% T - printf("open socket: %d\n",sockfd);' i4 n6 {5 H9 g9 l# d! G( u
-
% S* B% s6 O8 o3 ^! Z8 w8 d - //将本地端口和监听地址信息保存到套接字结构体中
$ D- ~& g( W, U - bzero(&addr,sizeof(addr));
7 l, `1 E* |8 M, R - addr.sin_family=AF_INET;& j: f) c" ^1 k, |/ y$ a3 I' E
- addr.sin_port=htons(LOCAL_PORT); n$ [9 L, \# `6 h: H" v) {/ e( `) e
- addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0
& ~3 W% Y6 \9 B, Q: b) Y; ] - # d; x+ b d. P4 T% |- Y) h, U9 f
- //将套接字于端口号绑定1 [. I: t9 ~+ S; ^/ t5 L& o5 s% Z
- if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)0 [7 Q9 v7 [. V2 ^
- printf("bind the port: %d\n",LOCAL_PORT);' _4 r& Z3 S( M2 Q. s* b. ^
-
/ x- d, U# ^% } - //开启端口监听+ i1 G; [- V0 u: p1 J
- if(listen(sockfd,3)>=0)
% G @. ]5 |& G! [' O - printf("begin listenning...\n");. N5 U$ e) Z: {& w
- 1 W/ ^5 j4 h O6 D; D: `. {
- //默认所有fd没有被打开
. l0 Q# ^1 a( G, C; X- z! G - for(fd=0;fd<MAX;fd++)
; T, @2 L" w; V: `8 u& n3 ^0 m. | - is_connected[fd]=0;
5 V3 a+ b7 e3 t& A4 H4 Q- p' b - + j( Q; V" ^+ o6 q) z
- while(1){) |) k& t0 P- [, p% v C
- //将服务端套接字加入集合中4 q- Y) ]$ T; ]$ n: z v
- FD_ZERO(&fds);
6 e9 { a5 W9 @5 |3 e% W - FD_SET(sockfd,&fds);1 B" u+ ]) w8 s- |* r$ {
-
4 P8 l! v. N! h9 F2 S6 K E; r - //将活跃的套接字加入集合中; x' ~% M. F/ l+ p4 S
- for(fd=0;fd<MAX;fd++); f+ Q1 i, T" ~6 F' o a: r, q
- if(is_connected[fd])
0 C# n* p/ S$ I ? - FD_SET(fd,&fds);2 G3 p( }" n: U' |# O# l
- $ ^/ ~9 f- Q7 V( j5 C( }) s+ ^8 O
- //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
7 b2 o4 V1 z {0 f- f0 ^# S) d - if(!select(MAX,&fds,NULL,NULL,NULL))
( J t& m& M% u& }/ c) ] - continue;
. W4 I4 U: T9 q6 E, F( [5 P - . c2 J* T( o! @; [! a6 m) X, L8 b
- //遍历所有套接字判断是否在属于集合中的活跃套接字
. z2 T2 V! d. b* n1 ~* e7 l - for(fd=0;fd<MAX;fd++){
* V9 {; r0 w6 f. r+ M# J9 j0 W - if(FD_ISSET(fd,&fds)){* S( F/ l3 _4 ]1 d
- if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接6 K/ Q- ~/ K- A- X
- connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);
- g+ H. N! X- K7 K' G B* z - write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语
2 C" \# ^6 l/ \& u) b& m! M - is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用& s! g$ H. z2 `" W
- printf("connected from %s\n",inet_ntoa(addr.sin_addr));
! M# b K# ^0 d - }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字; h/ A1 x, T; f
- if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
6 F# T j2 z! k/ A; t7 B' T. m - write(fd,msgbuffer,sizeof(msgbuffer));$ S1 L; }* ~: y6 D3 x
- printf("[read]: %s\n",msgbuffer);
! k/ h0 z& B( M' Z - }else{
: w& o3 Z B+ U) G6 a! @' g - is_connected[fd]=0;
6 Q& n) G& K2 G/ _ h - close(fd);
9 k) d4 x. T( B1 L B2 Y* p) }1 e - printf("close connected\n");
* p3 [+ D! R8 I; A4 r) ^ i9 g - }
2 T3 C; ~* p5 ]/ Y1 ] - }! \3 u; q8 L4 i# H6 ^( c
- }
3 N1 i8 L: R3 c- R - }
7 s8 D& V7 k) y8 p; M+ v/ y# ?# z - }2 K9 b1 v- z) Y) L( C: I2 R6 o
- }
复制代码
: |2 B/ c; W' D% o9 P! X# I9 t- ^# @& `" W3 G
$ ^) q+ d, v. Y% P, i
" [9 X8 J; }9 {8 a+ [7 c" `. r2 g7 q; ^4 C* R$ @' ^
& T0 R4 G* _& c0 X
欢迎光临 cncml手绘网 (http://bbs.cncml.com/) |
Powered by Discuz! X3.2 |