首页 > 代码库 > 网络IO模型
网络IO模型
网络IO模型
切记:IO特性不是由接口决定,而是由描述符(fd)的属性决定
本文内容目录:
一:网络IO模型的分类,各个模型的定义和特点
二:每个模型的原理和比较
三:每个模型的编程步骤、编程实例,以及注意细节。
一:网络IO模型的分类,各个模型的定义和特点
在网络IO模型中,有五种模型:
* blocking IO 阻塞IO
* nonblocking IO 非阻塞IO
* IO multiplexing IO多路复用
* signal driven IO 信号驱动IO
* asynchronous IO 异步IO
二:每个模型的原理
对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:
1)等待数据准备 (Waiting for the data to be ready)
2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
(1)、阻塞IO(blocking IO)
阻塞模型
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来
我们注意到,大部分的socket接口都是阻塞型的。所谓阻塞型接口是指系统调用(一般是IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。
实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用send()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。
(2)、非阻塞IO(non-blocking IO)
非阻塞模型
从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。
非阻塞的接口相比于阻塞型接口的显著差异在于,在被调用之后立即返回。使用如下的函数可以将某句柄fd设为非阻塞状态。
(3)、多路复用IO(IO multiplexing)
多路复用IO的模型
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(多说一句:所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。因此select()与非阻塞IO类似。
多路复用模型的一个执行周期
实际上,Linux内核从2.6开始,也引入了支持异步响应的IO操作,如aio_read, aio_write,这就是异步IO。
(4)、信号驱动IO (ignal driven IO)
首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
(5)、 异步IO (asynchronous IO )
调用aio_read函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。这个操作和信号驱动的区别就是:异步模式等操作完毕后才通知用户程序而信号驱动模式在数据到来时就通知用户程序。
五种IO模型的比较:
前四种模型的区别是第一阶段,第二阶段基本相同,都是将数据从内核拷贝到调用者的缓冲区。而异步I/O的两个阶段都不同于前四个模型。
以上部分内容的参考文献:
http://blog.csdn.net/blueboy2000/article/details/4485874
http://blog.csdn.net/zhoudaxia/article/details/8974779
三:每个模型的编程步骤、编程实例,以及注意细节。
头文件:head.h(下面几种IO的公用头文件)
<span style="font-size:14px;">#ifndef _HEAD_H_ #define _HEAD_H_ #include <stdio.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <strings.h> #include <unistd.h> #include <fcntl.h> #include<errno.h> #include<error.h> #define BUFF_SIZE 128 #define SER_IP "127.0.0.1" #define SER_PORT 50001 #define ECHO_PORT 7 #endif </span>
<span style="font-size:14px;">#include "head.h" int main(int argc, const char *argv[]) { int sockfd,n ; char buf[BUFF_SIZE]; struct sockaddr_in peer_addr; socklen_t addrlen = sizeof(peer_addr); if(-1 == (sockfd = socket(AF_INET,SOCK_DGRAM,0))) { perror("socket"); return -1; } //填充服务端地址结构体 struct sockaddr_in ser_addr; bzero(&ser_addr,sizeof(ser_addr)); ser_addr.sin_family = AF_INET; ser_addr.sin_port = htons(SER_PORT); ser_addr.sin_addr.s_addr = inet_addr(SER_IP); //必须绑定 if(-1 == bind(sockfd, (struct sockaddr *)&ser_addr,sizeof(ser_addr)) ) { perror("bind"); return -1; } //1. 获取文件状态标志位 int flag = fcntl(0,F_GETFL); printf("set before,stdin flag = %d\n ",flag); int flag1=fcntl(3,F_GETFL); printf("set before,socket flag1=%d\n",flag1); //2.添加非阻塞特性,首先获取器描述符的属性,然后进行或运算,这样不会覆盖以前的描述符属性 fcntl(0,F_SETFL, flag | O_NONBLOCK);//将标准输入IO(stdin)设置为非阻塞状态 , fcntl(3,F_SETFL,flag1 | O_NONBLOCK);//将套接字设置为非阻塞状态, 也可以写成fcntl(sockfd,F_SETFL,flag1 | O_NONBLOCK); //为什么是3呢? 因为0是标准输入 stdin 1是标准输出stdout 2是errno //这些在默认的情况下,系统是将其打开的,套接字自然就是3了,如果继续打开一个IO,其值是4,以此类推 flag = fcntl(0,F_GETFL); flag1=fcntl(3,F_GETFL); printf("set after,stdin flag = %#o \n ",flag); printf("set after,socket flag1=%#o \n",flag1); int num; //非阻塞:接收客户端数据并且还要读取当前终端的输入 //实现终端输入和接受数据并发执行 while(1) { if(NULL!=fgets(buf,BUFF_SIZE,stdin)) { printf("buf = %s\n",buf); } if( -1 == (n = recvfrom(sockfd,buf,BUFF_SIZE,0,(struct sockaddr *)&peer_addr,&addrlen))) { if(errno!=EAGAIN) { perror("recvfrom"); return -1; } } if(n>0) printf("peer_addr IP : %s port: %d \n",inet_ntoa(peer_addr.sin_addr),ntohs(peer_addr.sin_port)); } return 0; } </span>
#include "head.h" int sockfd,n ; struct sockaddr_in peer_addr; socklen_t addrlen = sizeof(peer_addr); void handler(int signum) { char buf[BUFF_SIZE]; bzero(buf,sizeof(buf)); if( -1 == (n = recvfrom(sockfd,buf,BUFF_SIZE,0,(struct sockaddr *)&peer_addr,&addrlen))) { perror("recvfrom "); return ; } printf("------%s-------\n",buf); } int main(int argc, const char *argv[]) { char buf[BUFF_SIZE]; if(-1 == (sockfd = socket(AF_INET,SOCK_DGRAM,0))) { perror("socket"); return -1; } //填充服务端地址结构体 struct sockaddr_in ser_addr; bzero(&ser_addr,sizeof(ser_addr)); ser_addr.sin_family = AF_INET; ser_addr.sin_port = htons(SER_PORT); ser_addr.sin_addr.s_addr = inet_addr(SER_IP); //必须绑定 (在tcp或udp中,服务器端必须调用bind,进行和IP地址的绑定) if(-1 == bind(sockfd, (struct sockaddr *)&ser_addr,sizeof(ser_addr)) ) { perror("bind"); return -1; } int flag= fcntl(sockfd,F_GETFL); fcntl(sockfd,F_SETFL,flag|O_ASYNC); //设置套接字允许异步接收 fcntl(sockfd,F_SETOWN,getpid()); //将该信号和本进程绑定,即只允许该进程来相应对应的信号 /* 在 fcntl(sockfd,F_SETOWN,getpid()); 中指定sockfd的原因? 假如在本程序中,有两个套接字,到底由哪个进行和本进程一起与外部程序进行通信呢?所以要指定sockfd */ signal(SIGIO,handler);//注册信号处理函数 while(1) { if(NULL!=fgets(buf,BUFF_SIZE,stdin)) { printf("buf = %s\n",buf); } } return 0; }
多路复用IO的server.c
#include "head.h" /* 多路复用IO,类似与火车站 其中火车站的多个进站口就是检测表 有动车可以就绪,有普通列车可以就绪 站长在站口进行监听,查看,然后让已经到达的火车视为就绪状态(就绪出发) 不出站的火车永远都是就绪状态,因此要判断哪个火车到达后,就视为就绪,然后及时让其出站。 在这里,检测表就是火车站的多个站口。各种火车类型就是不同的描述符类型 列车长得监听和查看就是select 火车的进站就是描述符就绪 火车出站就是对应的描述符数据被取走 */ int main(int argc, const char *argv[]) { int sockfd,n ; char buf[BUFF_SIZE]; struct sockaddr_in peer_addr; socklen_t addrlen = sizeof(peer_addr); if(-1 == (sockfd = socket(AF_INET,SOCK_DGRAM,0))) { perror("socket"); return -1; } //填充服务端地址结构体 struct sockaddr_in ser_addr; bzero(&ser_addr,sizeof(ser_addr)); ser_addr.sin_family = AF_INET; ser_addr.sin_port = htons(SER_PORT); ser_addr.sin_addr.s_addr = inet_addr(SER_IP); //必须绑定 if(-1 == bind(sockfd, (struct sockaddr *)&ser_addr,sizeof(ser_addr)) ) { perror("bind"); return -1; } /* *多路复用服务器的创建步骤: *1建立检测表 *2将需要监听的描述符加入该表 *3监听该表 *4判断该表中的就绪描述符,进行对应的读数据 * * */ // 建立检测表 fd_set readfds; bzero(&readfds,sizeof(readfds)); //将描述符加入该表 FD_SET(0,&readfds); FD_SET(sockfd,&readfds);//此时检测表的相应位被置1 //sleep(10); 可以在此加入睡眠,让多个描述符同时输入就绪状态,查看t会发生变化,相应就绪的会被置为1 int t=select(sockfd+1,&readfds,NULL,NULL,NULL); //select之后,只有就绪的相应为设置为1,其他没有就绪的描述符新、相应位为0了 printf("t=%d------------------\n",t); char rbuf[BUFF_SIZE]; while(1) { bzero(rbuf,sizeof(rbuf)); printf("stdin FD_ISSET()===%d\n",FD_ISSET(0,&readfds)); printf("sockfd FD_ISSET()===%d\n",FD_ISSET(sockfd,&readfds)); if(FD_ISSET(0,&readfds)) fgets(rbuf,sizeof(rbuf),stdin); } return 0; }
未经允许,禁止转载