cncml手绘网
标题: 编写一个简单的TCP服务端和客户端 [打印本页]
作者: admin 时间: 2020-5-9 01:53
标题: 编写一个简单的TCP服务端和客户端
实验环境是linux系统,效果如下:
1.启动服务端程序,监听在6666端口上
2.启动客户端,与服务端建立TCP连接
3.建立完TCP连接,在客户端上向服务端发送消息
4.断开连接
实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点
/ c) ]; Q; k: E9 l( ~什么是SOCKET(插口):
这里不用 "套接字" 而是用 "插口" 是因为在《TCP/IP协议卷二》中,翻译时也是用 "插口" 来表示socket的。
1 f# ~; v, @+ X3 t! u
"套接字" 这词不知道又是哪个教授级人物造出来的,听起来总是很怪,虽然可以避免语义上的歧义,但不明显。
对插口通俗的理解就是:它是一个可以用来输入或者输出的网络端,另一端也具有同样相对应的操作。
具体其他高级的定义不是这里的重点。值得说的是:
每个插口都可以标识某个程序通信的一端,通过系统调用使得程序与网络设备之间的交流连接起来。
应用程序 -> 系统调用 -> 插口层 -> 协议层 -> 接口层 ->发送(接收的话与之相反)3 q& N" O9 z U* L( ?, x1 Q8 J
" S7 \$ u6 O1 A: x& J
; I$ w3 q0 n3 E" g ?如何标识一个SOCKET:
如上定义所述,可以通过地址,协议,端口三要素来确定一个通信端,而在linux C程序中使用 标识符 来标识一个
SOCKET,Unix系统对设备的读写操作等同于对描述符的读写操作,标识符可以用于:插口 管道 目录 设备 文件等等
& ?2 X" J- A" k) p ?
描述符是个正整数,事实上他是检查表表项中的一个下标,用于指向打开文件表的结构。
述符前三个标识符0 1 2 分别系统保留:标准输入(键盘),标准输出(屏幕),标准错误输出
当我们使用新的描述符来创建socket时,他一般从最小未使用的数字开始分配,也就是3
7 j6 G$ n5 ?8 T% J9 b/ K
# ]7 \# m. Y; n
服务端实现的流程:
1.服务端开启一个SOCKET(socket函数)
2.使用SOCKET绑定一个端口号(bind函数)
3.在这个端口号上开启监听功能(listen函数)
4.当有对端发送连接请求,向其发送ack+syn建立连接(accept函数)
5.接收或者回复消息(read函数 write函数)
/ i* D$ e6 Z3 P* _8 k1 |; W% `- q; x2 u0 {4 Y1 @1 I }
客户端实现流程:
1.打开一个SOCKET
2.向指定的IP 和端口号发起连接(connect函数)
3.接收或者发送消息(send函数 recv函数)
4 C L) k5 P7 Y8 }3 o _6 J$ @3 d2 @: y# l( P
4 @. p7 e: ?: `' G1 F: C$ ^1 Z' {
如何并发处理:
如果按照以上流程实现其实并不难,但是有个缺陷,因为C语言是按顺序单一流程运行,也就是说如果
直接在程序当中使用accept函数(建立连接)的话,那么程序会阻塞在accept这里,这是因为如果客户端
一直没有发送connect连接,那么accept就无法得知客户端的IP和端口,也就只能一直等待(阻塞)直到
有请求触发继续执行为止,这样就导致如果同时多个客户向服务端发送请求连接,那么服务端只能按照
单一线程去处理第一个客户端,无法开启多个线程同时处理多个用户的请求。
$ x; E$ k% W6 E3 t+ t4 T& ^, U, {! L* |4 u
如何解决:
下面摘文截取网上的资料,有兴趣者可以看看
系统提供select函数来实现多路复用输入/输出模型,该函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你
- int select(int nfds, fd_set *readfds, fd_set *writefds, exceptfds, const struct timeval* timeout);
复制代码所在的头文件为:
- #include <sys/time.h>/ v" g. N6 N) ?. v+ k
. ], ]1 @5 C8 z- #include <unistd.h>
复制代码 功能:测试指定的fd是否可读,可写 或者 是否有异常条件待处理
5 i5 t6 ^- L8 |! ~, A
readset 用来检查可读性的一组文件描述字。
6 k6 z) i0 b1 w: m( r+ m
writeset 用来检查可写性的一组文件描述字。
+ ~9 c% N. ?4 x& \- m8 b
exceptset用来检查是否有异常条件出现的文件描述字。(注:不包括错误)
4 O1 m E+ ^7 ], R7 [% Q timeout 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。. N# n! P4 k1 `& ^7 f
5 c9 E9 [5 U, U/ Q 对于select函数的功能简单的说就是对文件fd做一个测试。测试结果有三种可能:
) O1 n m9 p9 i$ u/ J4 X! C9 w% f
: i( W( a" s6 C) Z: l. Q {- 1.timeout=NULL (阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)/ j D" ^5 `4 {; R1 P# Y
/ Q0 r- t' X: B/ Z' Q- 2.timeout所指向的结构设为非零时间 (等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回) w& y) n( J) H' i7 \: B
$ P7 O9 X& g, S6 ^: x [. x- 3.timeout所指向的结构,时间设为0 (非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)
复制代码 返回值:
返回对应位仍然为1的fd的总数。注意啦:只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
否则为0哦。举个例子,比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞,如果数据一直不来,
你的线程就要阻塞很久.这样显然不好。所以采用select来查看套节字是否可读(也就是是否有数据读了) 。
现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,
其值通常是1024,这样就能表示<1024的fd。" k0 D( }9 ^& Y3 S9 g" }
* {4 \- r1 w+ J j2 g/ ~- Z
& U* I$ x% [& b/ N6 s
fd_set结构体:
文件描述符集合,用于存放多个fd(文件描述符,这里就是套接字)
可以存放服务端的fd,有客户端的fd。下面是对这个文件描述符集合的操作:
- FD_ZERO(*fds): 将fds设为空集- \; [( c/ c4 `" B6 J
- " i9 {6 O" d5 g0 F: c, v& b$ e0 o
- FD_CLR(fd,*fds): 从集合fds中删除指定的fd2 ?+ a& t. x8 w& P$ o5 F% W
- " s; Q3 b3 N, O' r; N
- FD_SET(fd,*fds): 从集合fds中添加指定的fd
: }- g6 B% a' ?/ j3 @& L - 6 a D9 z$ X2 k' m3 s
- FD_ISSET(fd,*fds): 判断fd是否属于fds的集合
复制代码步骤如下
- socket s;
q# H( e; \" |" F9 X! W - .....
9 F9 \' M% G0 F; E* u - fd_set set;/ [/ z* H( k6 B' z
- while(1){
4 s v) \9 o4 P& D; I - FD_ZERO(&set); //将你的套节字集合清空# w3 ]- D' y0 A
- FD_SET(s, &set); //加入你感兴趣的套节字到集合,这里是一个读数据的套节字s: U/ {3 Y" @1 S2 h ~4 a1 z
- select(0,&set,NULL,NULL,NULL); //检查套节字是否可读,4 d3 i% v+ E6 {7 h
- if(FD_ISSET(s, &set) //检查s是否在这个集合里面,
- q. p8 r5 n( R; V& u) b, Y, l) Y4 c2 P - { //select将更新这个集合,把其中不可读的套节字去掉
6 E/ W% I8 t, V) m# A8 a8 A% B0 f - //只保留符合条件的套节字在这个集合里面$ e7 e8 B9 Y4 S& \( x
- recv(s,...);
( y( N d( z6 {' L6 N) I! @# H. p - }
' N7 G) ~# B# I - //do something here/ U% f3 _' X3 v |- G( e: b
- }
复制代码假设fd_set长度为1字节,fd_set中的每一位可以对应一个文件描述符,那么1字节最大可以对应8个fd
- (1)执行fd_set set; FD_ZERO(&set); 则set用位为0000,0000。4 d/ m0 j& ?, y: w$ q
2 Y5 S: u+ |5 y- (2)若fd=5,执行FD_SET(fd,&set); 后set变为 0001,0000(第5位置为1)( g1 p- u- V" r& S( [( }
- - J) Q$ F" d' \ E
- (3)若再加入fd=2,fd=1 则set变为 0001,0011- E3 [ ~: }% E9 E- z; x u
1 x- C7 K+ \% s2 m- (4)执行select(6,&set,0,0,0) 阻塞等待: ?; j2 E8 B* _' ]3 \" i3 a y4 r3 M
- 5 t/ _4 ?' [3 Y N; h0 N3 J
- (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.具体过程看代码会好理解
6 s& t1 d& I1 X9 ]: {9 L F* H1 I* @* a/ A' U- g% i
使用select函数的过程一般是:
2 \/ t4 }. W$ m. N4 ?# [$ S: L 先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,
接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1
复制粘贴的摘文排版起来真的是痛苦,我已经尽力排版了。。。% H, u( C9 ?9 h5 M
. T8 t1 ?9 t. ^) V$ ~/ f客户端:
- #include <time.h>& D3 Z" v" h& S' w' D
- #include <stdio.h>
: j1 m5 o( ~! B: q$ h9 o. s: @% [7 F: m - #include <stdlib.h>
4 X& P( t2 ]8 u1 b$ A/ I - #include <string.h>1 i \8 u& U/ U' d& d, Y
- #include <unistd.h>) @* V' m0 ~. ?% ^- O) _
- #include <arpa/inet.h>
! K2 Y2 k" E6 G% R# B; z6 b/ \8 x - #include <netinet/in.h>( M2 g+ P0 O) m
- #include <fcntl.h>8 i _& a7 h. y2 q+ t
- #include <sys/stat.h>
! F! b0 p- |+ J/ E$ P - #include <sys/types.h>
- G3 Z% g% h: a' ]* s - #include <sys/socket.h>
5 P; H' o. f% p1 _& J3 n5 }. F - 2 W1 t0 X4 |4 r8 b# T3 ~9 ?& N
- #define REMOTE_PORT 6666 //服务器端口2 R' o3 M$ o2 A& a6 N7 X
- #define REMOTE_ADDR "127.0.0.1" //服务器地址
/ W/ y( D D) P' L0 E( O: b - ! v8 s0 K2 ^, _. ^, E8 f
- int main(){
_' |/ ?7 T/ P - int sockfd;
3 S7 p: i: Y1 [9 U1 A X4 _' X - struct sockaddr_in addr;! e$ f$ N, h- R
- char msgbuffer[256];. |$ i$ m+ d9 C( M6 y/ v
-
* E, Q4 ~2 E- n! k1 {* i1 }+ T - //创建套接字
5 o1 x& M3 R: v9 T - sockfd = socket(AF_INET,SOCK_STREAM,0);/ Q* w9 R, q2 W3 d" N7 o& T
- if(sockfd>=0)0 b( J$ W0 Y& K6 ]. e. w, y3 S
- printf("open socket: %d\n",sockfd);
- ?: R, S, M7 S5 n -
/ w9 }9 e: r! C0 R" T- r( b W - //将服务器的地址和端口存储于套接字结构体中% X! Y8 [ e( O6 p
- bzero(&addr,sizeof(addr));
; N7 C+ J. Y: H8 c( z" T% f E% @ - addr.sin_family=AF_INET;
# \) V3 n6 e4 z& g0 e - addr.sin_port=htons(REMOTE_PORT);
. B4 V$ c5 v$ W+ s - addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
+ K7 O5 q( H9 O* a0 f -
9 }3 u% p- i/ [ O1 Z- ?8 L - //向服务器发送请求! D+ D/ e& g' U# G+ I8 c$ P4 i5 q
- if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)0 A3 Z; R5 N0 D. e0 o- y' m0 E
- printf("connect successfully\n");+ o/ A$ R4 P" P" u& F% R- R
- 5 i( ^- Y" O* d6 y( a' X6 x5 `
- //接收服务器返回的消息(注意这里程序会被阻塞,也就是说只有服务器回复信息,才会继续往下执行)# C+ O o' O% d- ]. f- q
- recv(sockfd,msgbuffer,sizeof(msgbuffer),0);6 ?9 }5 |, v$ Z* k$ i
- printf("%s\n",msgbuffer);: l+ o S# K* }$ w' R, R- `
-
- M: ]1 R/ P+ R, E7 C' ` - while(1){) _4 m& {+ A5 _7 S ~$ H
- //将键盘输入的消息发送给服务器,并且从服务器中取得回复消息
. S$ L' O% u/ V4 W - bzero(msgbuffer,sizeof(msgbuffer));
6 T$ ^ g8 c5 Z7 n - read(STDIN_FILENO,msgbuffer,sizeof(msgbuffer));
) T5 V1 Z6 Y/ |& y7 y' I; _ - if(send(sockfd,msgbuffer,sizeof(msgbuffer),0)<0)9 g8 R/ N0 b7 o
- perror("ERROR");/ h }4 `* D; U# M$ q. a6 m; k
-
: q4 p, }# Y+ w; x! \7 | - bzero(msgbuffer,sizeof(msgbuffer));
% |2 L4 }2 ~5 I' ~; U - recv(sockfd,msgbuffer,sizeof(msgbuffer),0);
+ D3 L! ]0 } k7 z3 M" R+ i - printf("[receive]:%s\n",msgbuffer);
5 {* J5 C% y. }; C) U3 Q# y -
& \: y% h6 i J; B9 ^ - usleep(500000);
. |/ ^4 [' Q# q3 r( g$ F2 ^ - }- D7 z; _0 ?6 U' z: Z) J U: d
- }
复制代码
1 L& n: ?! v% H! z1 N3 m. q4 n5 T4 i$ @0 A! h
服务端:
- #include <time.h>
- i) n U; \3 Q, j0 Z - #include <stdio.h>4 G% }3 B3 w+ ~& C( p
- #include <stdlib.h>
$ z. n; K1 U: q, _$ p' u - #include <string.h>6 ~$ W% `) X6 W, t8 {
- #include <unistd.h>7 W* e0 T' O1 a% a; d
- #include <arpa/inet.h> B0 P: y: Z: W8 X8 n! F2 p# Y
- #include <netinet/in.h>
( N/ o3 O9 u0 ?6 U0 y5 c0 D - #include <sys/types.h>
1 K9 X( @- C8 L - #include <sys/socket.h>
7 d4 d; g( I6 z# h2 k& q -
/ M* O! e, f7 W9 k* [ - #define LOCAL_PORT 6666 //本地服务端口, W7 ?2 I2 _; J3 }& y' \: g
- #define MAX 5 //最大连接数量 o0 _2 K( U C* U9 I8 w. b2 U
- 2 B3 {1 ~: ~8 D0 b1 j/ }
- int main(){
; M# {" b$ \6 B h o. l# ^ - int sockfd,connfd,fd,is_connected[MAX];
4 X* j5 B& O3 r# q2 t# N$ f/ `) g - struct sockaddr_in addr;' C0 z$ F: Z# B0 y
- int addr_len = sizeof(struct sockaddr_in);
* N: ?5 Q1 m1 f - char msgbuffer[256];) I3 h Y5 v6 K+ S" F6 z% @
- char msgsend[] = "Welcome To Demon Server";5 P) h! y, \9 C. |
- fd_set fds;
, N1 i/ A- c/ D* F# ?" ^- _ -
% S+ Q! n# O' [9 ~, J0 Y - //创建套接字
0 T1 M% g" ]$ O) B - sockfd = socket(AF_INET,SOCK_STREAM,0);
3 [% r b- F; }5 \5 \+ s0 \ - if(sockfd>=0)
9 s2 m( J0 z9 c - printf("open socket: %d\n",sockfd);8 }$ o8 S. a. C: a: ^" r+ c
- ; x9 w! {3 y2 j0 e. T. {# J) d. }
- //将本地端口和监听地址信息保存到套接字结构体中7 y. }- y- P- u
- bzero(&addr,sizeof(addr));9 s" ^0 W2 Y6 G S0 M2 j# R
- addr.sin_family=AF_INET;7 M5 o1 L5 B) I1 L
- addr.sin_port=htons(LOCAL_PORT);
1 Y& e: Y U8 T" L - addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示任意地址0.0.0.0 0.0.0.0. D9 f! l* `! m1 ]* M
- ; p! I g5 j5 @' I# e0 _/ y
- //将套接字于端口号绑定' T: D' C4 x5 P( u9 {$ e
- if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))>=0)
, t( l0 @9 A/ s/ j0 s( W - printf("bind the port: %d\n",LOCAL_PORT);4 z! q1 k4 T& |2 X
- 6 x3 W3 K3 }6 W" P: u8 s8 }6 z
- //开启端口监听
- S7 S( a" v# c) P- T* c2 w - if(listen(sockfd,3)>=0)
6 ^$ s x# ]7 ^& B, l - printf("begin listenning...\n");( j1 M; ?" m) B6 c- E3 W6 V
- ( x( Y$ r* |/ {/ G0 l" W
- //默认所有fd没有被打开
6 S# h1 c r! j# U# R" g% U7 E$ c - for(fd=0;fd<MAX;fd++)( p/ t3 ~3 e$ G! D
- is_connected[fd]=0;$ {$ Y6 L( _) r4 h1 b
- * Q/ e, L2 `) _: }
- while(1){. \- d! ]5 F0 c4 v
- //将服务端套接字加入集合中
F) q4 \8 L# R - FD_ZERO(&fds);
7 r T G1 t$ K$ y+ C - FD_SET(sockfd,&fds);
: @5 [) N8 k: I% d( M2 K# E - 7 ]/ F9 u. C2 E) t+ [
- //将活跃的套接字加入集合中, A" N. y8 Y8 R7 G4 t
- for(fd=0;fd<MAX;fd++)
7 ~+ k+ w/ y3 G- E9 B% Z - if(is_connected[fd])
1 o/ C; ]& ^- u - FD_SET(fd,&fds);
# X0 F- t2 [; }& n3 w2 F& B1 I9 h( J - ( N( |3 Z0 l' d6 t- b! E" R' f
- //监视集合中的可读信号,如果某个套接字有信号则继续执行,此时集合中只有存在信号的套接字会被置为1,其他置为0
. J6 ]" s0 K( n5 L - if(!select(MAX,&fds,NULL,NULL,NULL)); u4 Z2 q- ?$ E
- continue;, Z0 s6 A* {0 e* d) B3 L
-
) v: I; b) E; P5 V n$ o9 | - //遍历所有套接字判断是否在属于集合中的活跃套接字
* y" G \. Y5 b. I+ O( Q - for(fd=0;fd<MAX;fd++){
5 }$ J, w y4 s/ [. n+ L - if(FD_ISSET(fd,&fds)){5 \0 X3 n) Q8 j7 E2 ^% g- Y) B1 g
- if(fd==sockfd){ //如果套接字是服务端,那么与客户端accept建立连接
/ j1 O- ~) z& N4 B7 b' X4 `* j# Q - connfd = accept(sockfd,(struct sockaddr*)&addr,&addr_len);, {- Y7 \' M: h7 e
- write(connfd,msgsend,sizeof(msgsend)); //向其输出欢迎语2 u7 H3 r8 g$ c7 b) M) N) \
- is_connected[connfd]=1; //对客户端的fd对应下标将其设为活跃状态,方便下次调用
* ?4 |+ {! H5 H# v - printf("connected from %s\n",inet_ntoa(addr.sin_addr));8 u$ H. h: y# B6 R; t5 ]- R4 n
- }else{ //如果套接字是客户端,读取其信息并返回,如果读取不到信息,冻结其套接字3 O! K% S) b& ?
- if(read(fd,msgbuffer,sizeof(msgbuffer))>0){
: A) O2 R U/ |1 w4 @ @, N5 I4 I - write(fd,msgbuffer,sizeof(msgbuffer));
) A9 |1 S- ~8 j' u3 z W0 ^ - printf("[read]: %s\n",msgbuffer);1 {. s5 y' m T( n
- }else{5 j& d1 V/ ?2 F% G" i
- is_connected[fd]=0;
m, w' X& n9 q- G2 O9 Q& m* _ - close(fd);
2 U0 y- y+ S# M8 l) ~* g - printf("close connected\n");
! J( ?3 [7 E$ {) A# u. { - }
& d6 [" E5 K, Y; T- V - }8 b Q- u$ H8 G4 v. s( j& B5 `. r
- }
' R. E& M- r. l7 V; S9 [ - }! C7 N! q& L' \) {0 o
- }- ^0 S& b( j" t E
- }
复制代码
3 v* w9 r2 i9 H
% ?( H6 ]$ T2 E. u& @* n- {& ]$ _" p+ ~- t: w/ U( C
1 U& @, t7 m0 r7 t) K
& i! I1 r6 V7 J$ ~- s+ l% ?
( {0 }: s7 a% e( U
| 欢迎光临 cncml手绘网 (http://bbs.cncml.com/) |
Powered by Discuz! X3.2 |