软件世界网 购物 网址 三丰软件 | 小说 美女秀 图库大全 游戏 笑话 | 下载 开发知识库 新闻 开发 图片素材
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
移动开发 架构设计 编程语言 Web前端 互联网
开发杂谈 系统运维 研发管理 数据库 云计算 Android开发资料
  软件世界网 -> 架构设计 -> 网络I/O–阻塞、非阻塞、同步和异步及源码示例 -> 正文阅读

[架构设计]网络I/O–阻塞、非阻塞、同步和异步及源码示例

一. 前言


    在网络编程时,阻塞I/O,非阻塞I/O,同步I/O和异步I/O经常被提及。这篇博文中,我将结合相关材料及源码,尝试对这几种I/O模型进行区别和理解。
    Richard Stevens大神的《UNIX Network Programming Volume 1: The Sockets Networking API, Third Edition》一书在第六章I/O Multiplexing: The select and poll Functions中有对这几种I/O模型的详细描述,人民邮电出版社有中文译本,大家可以参考此书。本篇博客的I/O模型图亦截取于此书。
    为便于描述,后续内容将以网络I/O的读操作(read)为例进行说明。

二. 数据包的接收流程


    首先,先简单描述正常情况下(抛开诸如DMA等其它不经内核的包接收方式),数据包从进入网卡开始到用户进程收到数据的过程。
    为简便,这个过程大致可划分为内核态和用户态过程。
    在内核态,数据帧达到网卡,网卡产生中断给内核,内核调用驱动程序读取网卡缓冲区中的数据,拷贝进入内核缓冲区中,并经协议栈进行层级处理。
    在用户态,用户进程通过系统调用,读取Socket对应内核缓冲区中的数据,将数据拷贝至用户空间。
    总结来说,这样一个读操作包含两个阶段:
    (1)内核等待数据就绪
    (2)将内核读到的数据拷贝至用户空间

三. 相同与不同


    以上说了读操作包含的两个阶段。这两个阶段用户进程的状态和机制,就是区分阻塞I/O和非阻塞I/O,以及同步I/O和异步I/O的关键所在
    先说一下阻塞I/O和非阻塞I/O阻塞和非阻塞侧重点在于用户进程在等待调用结果(即得到反馈)时的状态,尤其是当内核数据未就绪的时候。
      相同点:若数据已经就绪,则阻塞和非阻塞没有区别,读取数据后返回
      不同点:若数据未就绪,阻塞I/O一直等待数据就绪,读取数据后返回;非阻塞I/O则立即返回
    可见,区分阻塞和非阻塞,要在数据未就绪的时候,看二者等待后的状态是否立即返回。
    再说一下同步I/O和异步I/O同步和异步侧重点在于用户进程和待接收数据的消息通信机制,而不论该数据是否已经就绪
      不同点:同步I/O在读取数据时,直到读完数据后才会返回;而异步I/O在发出读数据操作时,直接返回进行其它操作,不论数据是否就绪,且数据的读取不由该读操作负责
      相同点:无
    本文后续部分将按照Richard Stevens书上的说明,来简单描述一下5种I/O模型:阻塞I/O,非阻塞I/OI/O复用,信号驱动I/O,异步I/O

四. 阻塞I/O(Blocking I/O)


    阻塞I/O的模型图如下图所示。
[img]http://img.blog.csdn.net/20160331142315782?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center

    当用户进程调用读操作时,阻塞在内核等待数据就绪状态;若数据就绪,则阻塞在将数据从内核拷贝至用户空间状态。
   Linux系统中所有的Socket默认状态下都是阻塞的。
    以下为示例源码Server.cServer进程阻塞在recvfrom函数处,等待客户端数据达到。一旦数据达到,则读取数据,结束。

#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MAX_MSG_LENGTH 	255 
#define SERV_PORT 		8888 

int main(int argc, char **argv)
{
	int 		serv_sock_fd;
	struct 		sockaddr_in serv_addr, cli_addr;
	char 		msg[MAX_MSG_LENGTH] = {0};
	int 		sock_len;
	time_t		start_time, end_time;

	serv_sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family 		= AF_INET;
	serv_addr.sin_addr.s_addr 	= htonl(INADDR_ANY);
	serv_addr.sin_port			= htons(SERV_PORT);

	if(bind(serv_sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0){
		printf("Bind error!\n");
		exit(-1);
	}
	
	sock_len = sizeof(struct sockaddr);

	start_time = time(NULL);
	printf("%s", ctime(&start_time));
	printf("\tBlocking, waiting for client message\n");
	if (-1 != recvfrom(serv_sock_fd, msg, MAX_MSG_LENGTH, 0, (struct sockaddr *)&cli_addr, &sock_len)){
		end_time = time(NULL);
		printf("%s", ctime(&end_time));	
		printf("\tReceive client message\n");		
	}
	
	close(serv_sock_fd);

	return 0; 
}

    以下为示例源码Client.c。客户端发送UDP报文给服务器端。

#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>

#define SERV_IP		"127.0.0.1" 
#define SERV_PORT 	8888

char msg[] = "hello";

int main(int argc, char **argv)
{
	int cli_sock_fd;
	struct sockaddr_in serv_addr;
	int 		sock_len;

	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = inet_addr(SERV_IP);
	
	cli_sock_fd = socket(AF_INET, SOCK_DGRAM, 0);

	sock_len = sizeof(struct sockaddr);
	sendto(cli_sock_fd, msg, strlen(msg),0, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

	return 0;
}

    编译指令

#gcc -g Server.c -o Server
#gcc -g Client.c -o Client

    先运行服务器端,再运行客户端。服务器端输出结果如下:
Thu Mar 31 14:28:33 2016
        Blocking, waiting for client message
Thu Mar 31 14:28:47 2016
        Receive client message

五. 非阻塞I/O(Non-Blocking I/O)


    可以将创建的Socket标志位设置为non-blocking,将其转变为非阻塞I/O。但非阻塞I/O的数据未就绪时,对其操作会返回错误提示(常用EWOULDBLOCK/EAGAIN)。依据错误提示,判断Socket是否出错,或者需要进行下一次的读取。
[img]http://img.blog.csdn.net/20160331151247254?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center

    下面给出Server.c代码,Client.c代码和以上一致,无需修改。

#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#define MAX_MSG_LENGTH 	255 
#define SERV_PORT 		8888 

int main(int argc, char **argv)
{
	int 		serv_sock_fd;
	struct 		sockaddr_in serv_addr, cli_addr;
	char 		msg[MAX_MSG_LENGTH] = {0};
	int 		sock_len;
	time_t		start_time, end_time;
	int 		sock_flags;

	serv_sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
	sock_flags = fcntl(serv_sock_fd, F_GETFL, 0);
	fcntl(serv_sock_fd, F_SETFL, sock_flags|O_NONBLOCK);
		
	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family 		= AF_INET;
	serv_addr.sin_addr.s_addr 	= htonl(INADDR_ANY);
	serv_addr.sin_port			= htons(SERV_PORT);

	if(bind(serv_sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0){
		printf("Bind error!\n");
		exit(-1);
	}
	
	sock_len = sizeof(struct sockaddr);

	for(;;){
		start_time = time(NULL);
		printf("%s", ctime(&start_time));
		printf("\tNon Blocking, waiting for client message\n");
		if (-1 != recvfrom(serv_sock_fd, msg, MAX_MSG_LENGTH, 0, (struct sockaddr *)&cli_addr, &sock_len)){
			end_time = time(NULL);
			printf("%s", ctime(&end_time));	
			printf("\tReceive client message\n");
			break;
		}else if (errno == EAGAIN){
			sleep(1);	
		}else{
			end_time = time(NULL);
			printf("%s", ctime(&end_time));	
			printf("\tSocket Error\n");
			break;
		}		
	}

	close(serv_sock_fd);
	return 0; 
}
    先运行服务器端,再运行客户端。服务器端输出结果如下:

Thu Mar 31 14:51:29 2016
        Non Blocking, waiting for client message
Thu Mar 31 14:51:30 2016
        Non Blocking, waiting for client message
Thu Mar 31 14:51:31 2016
        Non Blocking, waiting for client message
Thu Mar 31 14:51:32 2016
        Non Blocking, waiting for client message
Thu Mar 31 14:51:33 2016
        Non Blocking, waiting for client message
Thu Mar 31 14:51:34 2016
        Non Blocking, waiting for client message
Thu Mar 31 14:51:35 2016
        Non Blocking, waiting for client message
Thu Mar 31 14:51:35 2016
        Receive client message

    从运行结果来看,服务器端每次调用recvfrom时,由于没有数据达到,直接返回。直到有数据到达时,读取数据并退出。

六. I/O复用(I/O Multiplexing)


    I/O多路复用模型,就是将多路I/O阻塞在同一个地方,等到某个或者某些I/O有事件到达时,就通知进程进行相应Socket的数据读写操作。
    I/O复用的好处,就是将原先阻塞在各自I/O读写系统调用的地方,统一迁移到由某个系统调用函数来管理。而数据就绪时,由该系统调用函数负责通知。这样,对I/O的操作就不会阻塞在各自的等待数据就绪上面。
 [img]http://img.blog.csdn.net/20160331151306364?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center
    Linux下的selectepoll函数就可以实现I/O复用功能。
    此处例子就不给出了。

七. 信号驱动I/O(Signal-driven I/O)


    信号驱动I/O需要Socket打开信号驱动I/O模式,并且通过sigaction系统调用注册SIGIO信号处理函数。当数据准备就绪时,内核通过发送SIGIO信号通知用户进程,用户进程通过信号处理函数读取数据。通过信号通知这种方式,用户进程不会因为数据未就绪而被阻塞在I/O上。
    让Socket可以工作于信号驱动I/O模式,一般需要完成以下三个步骤:
    (1).注册SIGIO信号处理程序 
    (2).设置
Socket所有者 
    (3).置位
SocketO_ASYNC标志,允许套接字信号驱动I/O。 

[img]http://img.blog.csdn.net/20160331154222720?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center
    服务器端源码Server.c如下。
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>

#define MAX_MSG_LENGTH 	255 
#define SERV_PORT 		8888 

int 		serv_sock_fd;
	
void handle_sig_io(int sig)
{
    int cli_sock_fd, sock_len;
    struct sockaddr_in cli_addr;
    char msg[MAX_MSG_LENGTH];
	time_t		now_time;
 
	if (-1 != recvfrom(serv_sock_fd, msg, MAX_MSG_LENGTH, 0, (struct sockaddr *)&cli_addr, &sock_len)){
		now_time = time(NULL);
		printf("%s", ctime(&now_time));
		printf("\tReceive client message\n");		
	}
}
	
int main(int argc, char **argv)
{
	struct 		sockaddr_in serv_addr, cli_addr;
	char 		msg[MAX_MSG_LENGTH] = {0};
	int 		sock_len;
	time_t		start_time, end_time;
	int 		sock_flags;
	struct 		sigaction sig_io_action;

	serv_sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
	sock_flags = fcntl(serv_sock_fd, F_GETFL, 0);
	fcntl(serv_sock_fd, F_SETFL, sock_flags|O_NONBLOCK);
		
	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family 		= AF_INET;
	serv_addr.sin_addr.s_addr 	= htonl(INADDR_ANY);
	serv_addr.sin_port			= htons(SERV_PORT);

    memset(&sig_io_action, 0, sizeof(sig_io_action));
    sig_io_action.sa_flags = 0;
    sig_io_action.sa_handler = handle_sig_io;
    sigaction(SIGIO, &sig_io_action, NULL);
 
    fcntl(serv_sock_fd, F_SETOWN, getpid());
    sock_flags = fcntl(serv_sock_fd, F_GETFL, 0);
    sock_flags |= O_ASYNC | O_NONBLOCK;
    fcntl(serv_sock_fd, F_SETFL, sock_flags);
 	
	if(bind(serv_sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0){
		printf("Bind error!\n");
		exit(-1);
	}
	
	while(1){
		sleep(1);
		start_time = time(NULL);
		printf("%s", ctime(&start_time));
		printf("Main Thread, doing jobs!\n");
	}
	
	close(serv_sock_fd);
	return 0; 
}

    先运行服务器端程序,再运行客户端程序。服务器端程序输出如下:

Thu Mar 31 15:44:02 2016
        Main Thread, doing jobs!
Thu Mar 31 15:44:03 2016
        Main Thread, doing jobs!
Thu Mar 31 15:44:04 2016
        Main Thread, doing jobs!
Thu Mar 31 15:44:05 2016
        Main Thread, doing jobs!
Thu Mar 31 15:44:06 2016
        Main Thread, doing jobs!
Thu Mar 31 15:44:07 2016
        Receive client message
Thu Mar 31 15:44:07 2016
        Main Thread, doing jobs!
Thu Mar 31 15:44:08 2016
        Main Thread, doing jobs!
Thu Mar 31 15:44:09 2016
        Main Thread, doing jobs!
    可以看到,服务器端不会阻塞在I/O等待上面,而是执行其它操作。当客户端数据到达时,由信号处理函数负责处理。

八. 异步I/O(Asynchronous I/O)


    异步I/O是指,当用户进程发起I/O操作时,内核立即给用户进程返回,用户进程不受到任何阻拦,并且可以去完成其它操作。而用户进程所发起的I/O操作,由内核负责进行数据的准备和内核态到用户空间的数据拷贝。当这一切工作都完成时,内核向用户进程发送一个信号,告知读操作已经完成。
    可见,以上提到的阻塞I/O,非阻塞I/O和信号I/O,都归属于同步I/O的范畴,而在执行读操作时,都属于阻塞操作的范畴。而异步I/O,则是真正的非阻塞,因为它不会对用户进程产生任何的阻塞。

[img]http://img.blog.csdn.net/20160331161140263?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center

九. 总结


    最后,还是借用Richard Stevens书中的一幅图,来把阻塞I/O,非阻塞I/O,同步I/O,异步I/O进行一下总结。
[img]http://img.blog.csdn.net/20160331161341123?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center
......显示全文...
    点击查看全文


上一篇文章      下一篇文章      查看所有文章
2016-04-01 16:50:07  
架构设计 最新文章
Opengl教程之读取obj并绘制在picturecontro
读《企业应用架构模式》第五章并发
StepbyStepintoSpring(IOC)
设计模式(2)用例图之一
使用实体组件系统(ECS)开发”吃方块”游戏实
编程学习之简单工厂模式与策略模式
Invalidprojectdescription.
基于Redis实现分布式消息队列(2)
《开源框架那点事儿15》:借船下海还是造船
原型模式——浅复制和深复制
360图书馆 软件开发资料 文字转语音 购物精选 软件下载 美食菜谱 新闻资讯 电影视频 小游戏 Chinese Culture 股票 租车
生肖星座 三丰软件 视频 开发 短信 中国文化 网文精选 搜图网 美图 阅读网 多播 租车 短信 看图 日历 万年历 2018年1日历
2018-1-16 21:22:36
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  软件世界网 --