文档章节

什么是NIO

kalo
 kalo
发布于 2017/04/06 00:46
字数 3695
阅读 38
收藏 2
点赞 0
评论 0

1. OSI模型及TCP

1.1 OSI模型

I/O是计算机获取、交换信息的主要渠道,它涵盖的含义实际上很广,包含文件IO、网络IO等。在网络中,客户端和服务器通信、应用程序之间的互相通信等都离不开网络间消息报文的传输交换。消息报文在网络间交换的时候,一般需要按照某个网络协议进行通信。实际上,网络通信会涉及到多个网络协议层:应用层、表示层、会话层、传输层、网络层、数据链路层和物理层,被国际标准化组织称之为OSI模型。
在OSI模型中,物理层和数据链路层一般是系统提供的设备驱动程序和网络硬件,网络层则由IPv4或者IPv6协议来处理,这3层加上传输层,是属于操作系统内核层面的。而顶上三层可以统一合并为应用层,可以理解为属于用户进程层面的协议。

在用户进程层面的应用程序互相通信,可以直接调用应用层的通信协议,比如http协议等进行通信,应用程序不需要了解内核层面如何操作;应用程序也可以通过内核提供的套接字API通信。套接字API在操作系统层面实现了对TCP协议的封装。所以,我们想要搞清楚阻塞和非阻塞I/O,就要调用套接字API进行网络I/O通信。那么,就必须先了解TCP协议以及它如何进行3次握手协议进行连接的建立。

1.2 TCP及TCP连接的建立

传输层的通信协议,主要包括TCP、UDP、SCTP等,但是绝大多数的网络应用都使用TCP或UDP。其中,TCP被称为传输控制协议,它提供客户端和服务端之间建立可靠连接功能。不仅如此,TCP还可以保证报文的顺序到达、重复报文丢弃、丢失报文重新发送、报文拥堵控制等。
我们都知道,TCP是通过三次握手协议建立客户端和服务器端的连接,另外操作系统又为我们封装了基于TCP的套接字API。所以,应用程序可以通过如下步骤,利用套接字API实现客户端和服务器端连接的建立:

1、服务端通过调用socket、bind、listen做好接受连接的准备。

2、客户端通过调用connect发起主动连接。客户端在建立连接的时候,会发送一个同步的数据报文,该报文只包括IP数据报等基础信息。

3、服务器必须发送ACK报文确认客户端的SYN,同时也发送一个SYN报文

4、客户端确认服务器端的SYN。

如上图的socket、bind、listen、connect等函数,就是由操作系统内核提供的套接字API。通过以上步骤,我们就通过了3次握手建立了连接。关于TCP连接,还有很多的内容值得探讨,不过我们本篇文章主要是为了说明NIO,所以点到为止,先介绍到这里,后面有时间专门开篇文章探讨关于TCP的其他内容。

2. 网络I/O模型

在网络上,我们经常会看到关于网络I/O模型的讲解,并且给出一些图片,试图用图片来说明I/O模型。但是毕竟这些概念比较抽象,仅凭一张图,是无法彻底表达清楚的。下面将根据我对这些概念的理解,结合一些简单的代码,配合模型图,来描述各个网络I/O模型。在进行各个网络模型探讨之前,我们需要先了解一段客户端发送消息,服务端接收到消息,并把该消息再返回给客户端的代码,后面的内容我们将根据这段代码进行讨论。

服务端代码如下:

#include    "unp.h"  
  
int main(int argc, char **argv)  
{  
    int                 listenfd, connfd;  
    pid_t               childpid;  
    socklen_t           clilen;  
    struct sockaddr_in  cliaddr, servaddr;
  
    // 创建套接字,并获得套接字监听文件描述符
    listenfd = Socket(AF_INET, SOCK_STREAM, 0);  
  
    // 设置监听地址和端口的sockaddr_in结构体
    bzero(&servaddr, sizeof(servaddr));  
    servaddr.sin_family      = AF_INET;  
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
    servaddr.sin_port        = htons(SERV_PORT);  
  
    // 绑定监听文件描述符listenfd和上面初始化好的sockaddr_in结构体
    Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));  

    // 启动监听
    Listen(listenfd, LISTENQ);  
  
    // 服务端一直运行,接受客户端的连接
    for ( ; ; ) {  
        clilen = sizeof(cliaddr);  
        // 接受客户端连接并获取连接文件描述符
        if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {  
            if (errno == EINTR)  
                continue; 
            else  
                err_sys("accept error");  
        }  
        
        // 创建子进程
        if ( (childpid = Fork()) == 0) {
            Close(listenfd); 

            // 在子进程中进行回传接收到的客户端消息
            str_echo(connfd); 
            exit(0);  
        }

        // 关闭连接
        Close(connfd); 
    }  
}  


str_echo(int sockfd)  
{  
    ssize_t  n;  
    char  buf[MAXLINE];
    again:  
    // 从连接文件描述符sockfd中读取客户端发送的消息
    while ( (n = read(sockfd, buf, MAXLINE)) > 0)  
        // 将读取到的消息再写入到套接字连接中返回
        Writen(sockfd, buf, n);
        if (n < 0 && errno == EINTR)  
            goto again;  
        else if (n < 0)
            err_sys("str_echo: read error");  
}

服务端主要是建立并监听套接字服务,接收客户端的消息然后返回。

如下是客户端代码:

int main(int argc, char **argv)  
{  
    // 套接字连接文件描述符和套接字地址结构体
    int                 sockfd;  
    struct sockaddr_in  servaddr;   
    
    // 创建套接字
    sockfd = Socket(AF_INET, SOCK_STREAM, 0);  
  
    // 给套接字结构体赋值
    bzero(&servaddr, sizeof(servaddr));  
    servaddr.sin_family = AF_INET;  
    servaddr.sin_port = htons(SERV_PORT);  
    Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);  
  
    // 进行连接
    Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));  
  
    // 向服务器发送消息
    str_cli(stdin, sockfd);
  
    exit(0);  
}


void  str_cli(FILE *fp, int sockfd)  
{  
    char  sendline[MAXLINE], recvline[MAXLINE];  
  
    // 从控制台读取输入字符串
    while (Fgets(sendline, MAXLINE, fp) != NULL) {  
  
        // 将读取到的字符串发送给服务器
        Writen(sockfd, sendline, strlen(sendline));  
  
        // 读取从服务器返回的消息
        if (Readline(sockfd, recvline, MAXLINE) == 0)  
            err_quit("str_cli: server terminated prematurely");  
        
        // 将得到的服务器消息打印到控制台
        Fputs(recvline, stdout);  
    }  
}

2.1 阻塞I/O

经过上面的讨论,我们已经知道,在应用程序层面执行通信的时候,是需要调用操作系统内核的。如果我们的应用程序发送系统调用命令之后,就一直阻塞等待,那么这种情况就是阻塞I/O。结合前面的实例代码while (Fgets(sendline, MAXLINE, fp) != NULL),客户端将阻塞于Fgets函数的调用,套接字上即便发生什么事件,客户端也不能做及时的处理。所以,上面的客户端代码就是典型的阻塞I/O。阻塞I/O可以使用下图进行描述:

同时需注意,在图的右边部分,内核中,其实分两个阶段,一个是无数据报准备好------ > 数据报准备好;另一个是复制数据报文 ----- > 复制完成。因为上面示例代码的客户端程序阻塞于Fgets调用,所以,在内核中的这两个阶段,客户端应用程序是无感知的,客户端程序对这两个事件并不能做出及时处理。

同样的,对于上面的示例代码,因为在while循环中执行了Fgets,造成了从套接字连接中读取服务端的数据,阻塞于从控制台读取输入字符串的Fgets。而实际上,它们是可以独立开来,互相不受影响的。下面,我们就使用select函数修改示例代码,让其不再阻塞于Fgets。

2.2 阻塞I/O之select阻塞

上面我们知道str_cli函数阻塞于Fgets,那么,如何进行改造呢?首先,我们需要认识一个函数:select。该函数函数允许进程将其关心的描述符注册给操作系统内核,如果这些描述符有什么事件发生,则由操作系统将其标出。用户进程通过轮询FD_ISSET函数,判断是否有关注的事件发生。

#include    "unp.h"  
  
void str_cli1(FILE *fp, int sockfd)  
{  
    // 定义相关变量并初始化fd_set
    int maxfdp1;
    fd_set rset;
    char sendline[MAXLINE], recvline[MAXLINE];  
    FD_ZERO(&rset);

    for ( ; ; ) {
        FD_SET(fileno(fp), &rset);
        FD_SET(sockfd, &rset);
        maxfdp1 = max(fileno(fp), sockfd) + 1;

        // 将关注的套接字连接描述符sockfd 和 输入文件描述符fileno(fp)注册给系统内核
        Select(maxfdp1, &rset, NULL, NULL, NULL);

        // 判断套接字连接描述符sockfd是否有读事件发生
        if (FD_ISSET(sockfd, &rset)) {
            if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli");

            // 套接字有读事件发生,则输出到控制台
            Fputs(recvline, stdout);
        }

        // 判断输入文件描述符fileno(fp)是否有读事件发生
        if (FD_ISSET(fileno(fp), &rset)) {
            if (Fgets(sendline, MAXLINE, fp) == null) return;

            // 能从控制台读到数据,则发送给服务器
            Writen(sockfd, sendline, strlen(sendline));
        }
    }
}  

经过以上代码的改造,我们已经将str_cli程序修改为阻塞于select调用,或是等待标准输入可读,或是等待套接字可读。但是,上面的代码仍然是阻塞的。举例来说,如果在标准输入有一行文本可读,我们调用Fgets读入,再调用Writen写入socket发送到服务器。但是如果套接字发送缓冲区已经满了,Writen函数将会阻塞。类似,如果从套接字有一行文本可读,一旦标准输出比网络还慢,进程一样会阻塞于Fputs调用。
解决这个问题的办法,就是使用非阻塞IO,我们下小节探讨,现在我们先注意以下select函数。需要知道的是,select函数也可以用在服务器端。在服务器端将已经连接的套接字连接描述符集合注册给服务器内核,由内核标志出来这些套接字连接符的事件,然后服务器端代码进行轮询,即可实现IO复用。见图:

2.3 非阻塞I/O

上面几节,我们通过示例代码分别讨论了阻塞I/O,非阻塞I/O,I/O多路复用,那么怎么样才能做到非阻塞I/O呢?要想搞清楚这个问题,首先我们得明白,我们前面的阻塞IO,到对阻塞到了哪里。上面两个例子,一个是阻塞于从控制台读取数据的Fgets函数,一个是阻塞于select函数。那么,怎么样让他们不阻塞呢?先看一张非阻塞IO的图示:

图片来源于《Unix网络编程》,由图片可知,所谓的非阻塞IO,其实就是用户进程在和内核进行交互的时候,实时得到内核的反馈消息,而不是一直等待内核返回最终的结果。因此,我们可以把系统调用的各个阶段,划分为更细的调用过程,在每个更细分的调用过程中,同步得到系统的反馈。举例来说,我们在读取标准输入字符串,并发送给服务器这两个过程,就可以不断细分,划分为如下过程

以上是表示从标准输入发送到套接字的数据的缓冲区。其中tooptr指针指向将要写入到套接字的下一个字节,toiptr表示从标准输入读入的数据可以存放的下一个字节。

以上表示从套接字到标准输出的数据的缓冲区

原本呢,我们只需要调用

Fputs(recvline, stdout);

Writen(sockfd, sendline, strlen(sendline));

即可完成的操作,现在我们通过更细的划分,不仅精确到读写缓冲区,同时利用select函数,将STDIN_FILENO和STDOUT_FILENO注册到内核,轮询其事件的发生。

如果toiptr < &to[MAXLINE],说明是从stdin读取数据,开启内核对STDIN_FILENO读写事件的检测;

如果friptr < &fr[MAXLINE],说明是从socket读取数据,开启内核对sockfd读写事件的检测;

如果tooptr != toiptr,说明在缓冲区还存在写入套接字的数据,开启内核对sockfd读写事件的检测;

如果froptr != friptr,说明缓冲区还存在输出到标准输出的数据,开启内核对STDOUT_FILENO读写事件的检测;

通过以上轮询内核中STDIN_FILENO、STDOUT_FILENO、sockfd的读写事件,再通过代码来判断,就可以避免select阻塞中遇到的问题。

void str_cli(FILE *fp, int sockfd)
{
	int			maxfdp1, val, stdineof;
	ssize_t		n, nwritten;
	fd_set		rset, wset;
	char		to[MAXLINE], fr[MAXLINE];
	char		*toiptr, *tooptr, *friptr, *froptr;

	val = Fcntl(sockfd, F_GETFL, 0);
	Fcntl(sockfd, F_SETFL, val | O_NONBLOCK);

	val = Fcntl(STDIN_FILENO, F_GETFL, 0);
	Fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK);

	val = Fcntl(STDOUT_FILENO, F_GETFL, 0);
	Fcntl(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK);

	toiptr = tooptr = to;	/* initialize buffer pointers */
	friptr = froptr = fr;
	stdineof = 0;

	maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
	for ( ; ; ) {
		FD_ZERO(&rset);
		FD_ZERO(&wset);
		if (stdineof == 0 && toiptr < &to[MAXLINE])
			FD_SET(STDIN_FILENO, &rset);	/* read from stdin */
		if (friptr < &fr[MAXLINE])
			FD_SET(sockfd, &rset);			/* read from socket */
		if (tooptr != toiptr)
			FD_SET(sockfd, &wset);			/* data to write to socket */
		if (froptr != friptr)
			FD_SET(STDOUT_FILENO, &wset);	/* data to write to stdout */

		Select(maxfdp1, &rset, &wset, NULL, NULL);

		if (FD_ISSET(STDIN_FILENO, &rset)) {
			if ( (n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0) {
				if (errno != EWOULDBLOCK)
					err_sys("read error on stdin");

			} else if (n == 0) {
				stdineof = 1;			/* all done with stdin */
				if (tooptr == toiptr)
					Shutdown(sockfd, SHUT_WR);/* send FIN */

			} else {
				toiptr += n;			/* # just read */
				FD_SET(sockfd, &wset);	/* try and write to socket below */
			}
		}

		if (FD_ISSET(sockfd, &rset)) {
			if ( (n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0) {
				if (errno != EWOULDBLOCK)
					err_sys("read error on socket");

			} else if (n == 0) {
				if (stdineof)
					return;		/* normal termination */
				else
					err_quit("str_cli: server terminated prematurely");

			} else {
				friptr += n;		/* # just read */
				FD_SET(STDOUT_FILENO, &wset);	/* try and write below */
			}
		}
		if (FD_ISSET(STDOUT_FILENO, &wset) && ( (n = friptr - froptr) > 0)) {
			if ( (nwritten = write(STDOUT_FILENO, froptr, n)) < 0) {
				if (errno != EWOULDBLOCK)
					err_sys("write error to stdout");

			} else {
				froptr += nwritten;		/* # just written */
				if (froptr == friptr)
					froptr = friptr = fr;	/* back to beginning of buffer */
			}
		}

		if (FD_ISSET(sockfd, &wset) && ( (n = toiptr - tooptr) > 0)) {
			if ( (nwritten = write(sockfd, tooptr, n)) < 0) {
				if (errno != EWOULDBLOCK)
					err_sys("write error to socket");

			} else {
				tooptr += nwritten;	/* # just written */
				if (tooptr == toiptr) {
					toiptr = tooptr = to;	/* back to beginning of buffer */
					if (stdineof)
						Shutdown(sockfd, SHUT_WR);	/* send FIN */
				}
			}
		}
	}
}

 

3. 总结

以上仅仅是标准输入发送给服务器、接收服务器消息输出到标准输出的例子,其实服务器端到connect、accept函数等都有非阻塞实现,关于套接字和TCP的网络编程还涉及到非常多非常多的内容,想要完全讨论清楚,需要大量的时间,所以还是后面有机会再针对其中的某个点,做详细的探讨,这次先到这里。

 

注:文中代码和图片均来自《Unix网络编程》

© 著作权归作者所有

共有 人打赏支持
kalo
粉丝 12
博文 5
码字总数 11716
作品 0
闵行
程序员
MINA项目1.1.7做的项目 更新到MINA 2.0.2,很多东西报错?

MINA 1和mina 2 到底有什么区别呀,哪里也找不到介绍区别的,有没有大家总结的资料呀,给小弟参考下。 我们原来的项目是用MINA 1 做的,现在要升级到mina2,但引进去后,报了好多错误,好多东...

erwei1983
2010/12/24
1K
5
Linux IO模型与Java NIO

概述 看Java NIO一篇文章的时候又看到了“异步非阻塞”这个概念,一直处于似懂非懂的状态,想解释下到底什么是异步 什么是非阻塞,感觉抓不住重点。决定仔细研究一下。 本文试图研究以下问题...

yingtju
06/29
0
0
【Java】BufferedReader与NIO读取文件性能测试

说你CSV读入效率太差,是指你用的是行读方式,行读是效率比较慢的一种读法。 请问还有什么高效的读取大文件的方法吗? 我对 BufferedReader 与 NIO 读取文件效果做了一个简单的测试 测试结果...

linapex
2014/01/24
0
3
14. Java NIO vs IO

当学习Java的NIO和IO时,有个问题会跳入脑海当中:什么时候该用IO,什么时候用NIO? 下面的章节中笔者会试着分享一些线索,包括两者之间的区别,使用场景以及他们是如何影响代码设计的。 NI...

逝去的回忆
2016/11/19
7
0
NIO和IO的区别

(NIO翻译单独拎出来) 当我们开始学习IO和NIO的时候,有个问题:我应该在什么时候使用NIO和IO。在本文中我会尽量简明扼要说明NIO和IO的区别,它们的使用环境,以及它们是怎样影响编程。 NI...

marjey
2016/11/04
14
0
java网络编程3 -- NIO一些简单说明

什么是NIO NIO是相对有BIO而言的,就是非阻塞性IO 。 什么叫非阻塞性 ?我举一个简单的例子: 比如,你客户端发送了一个字符串: nio test 。 考虑到网络底层的传输情况的复杂性,有可能,前...

爱无痕
2016/12/18
4
0
memcached 的invalidateNamespace失效问题

使用invalidateNamespace方法令指定命名空间中的数据失效,但是会报这个错误。 net.rubyeye.xmemcached.exception.MemcachedClientException: cannot increment or decrement non-numeric v......

IT_你好
2014/01/28
627
0
tomcat报错问题

SEVERE [http-nio-5050-exec-10] SEVERE [http-nio-5050-exec-1] 我的服务器时候tomcat-7.0.30 + jdk1.7 能问一下,[http-nio-5050-exec-1]代表什么?详细解释一下,对这些东西了解不多!网上...

love_fang
2016/04/05
120
2
java.io.IOException: Connection reset by peer

java.io.IOException: Connection reset by peer at sun.nio.ch.FileDispatcherImpl.read0(Native Method) ~[na:1.7.0_80] at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39) ......

三十回头
2016/07/16
740
0
新手对于java NIO疑问

看了之后感觉nio的优势是服务端,避免线程多,单个线程经常空闲,造成服务器资源的浪费,但是,客户端的话,好像没啥了。 我现在有种需求是 我这边客户端访问多个服务端读取数据并发到别的地...

s33ker
2014/08/01
739
5

没有更多内容

加载失败,请刷新页面

加载更多

下一页

win10 上安装解压版mysql

1.效果 2. 下载MySQL 压缩版 下载地址: https://downloads.mysql.com/archives/community/ 3. 配置 3.1 将下载的文件解压到合适的位置 我最终将myql文件 放在:D:\develop\mysql 最终放的位...

Lucky_Me
8分钟前
0
0
linux服务器修改mtu值优化cpu

一、jumbo frames 相关 1、什么是jumbo frames Jumbo frames 是指比标准Ethernet Frames长的frame,即比1518/1522 bit大的frames,Jumbo frame的大小是每个设备厂商规定的,不属于IEEE标准;...

问题终结者
22分钟前
0
0
expect脚本同步文件expect脚本指定host和要同步的文件 构建文件分发系统批量远程执行命令

expect脚本同步文件 在一台机器上把文件同步到多台机器上 自动同步文件 #!/usr/bin/expectset passwd "123456"spawn rsync -av root@192.168.133.132:/tmp/12.txt /tmp/expect {"yes...

lyy549745
23分钟前
0
0
36.rsync下 日志 screen

10.32/10.33 rsync通过服务同步 10.34 linux系统日志 10.35 screen工具 10.32/10.33 rsync通过服务同步: rsync还可以通过服务的方式同步。那需要开启一个服务,他的架构是cs架构,客户端服务...

王鑫linux
31分钟前
0
0
matplotlib 保存图片时的参数

简单绘图 import matplotlib.pyplot as pltplt.plot(range(10)) 保存为csv格式,放大后依然很清晰 plt.savefig('t1.svg') 普通保存放大后会有点模糊文件大小20多k plt.savefig('t5.p...

阿豪boy
36分钟前
0
0
java 8 复合Lambda 表达式

comparator 比较器复合 //排序Comparator.comparing(Apple::getWeight);List<Apple> list = Stream.of(new Apple(1, "a"), new Apple(2, "b"), new Apple(3, "c")) .collect(......

Canaan_
昨天
0
0
nginx负载均衡

一、nginx 负载均衡 拓扑图: 主机信息: 1、负载均衡器1(lb1):192.168.10.205 RHEL7.5 2、负载均衡器2(lb2):192.168.10.206 RHEL7.5 3、web服务器1(web01):192.168.10.207 Centos...

人在艹木中
昨天
0
0
做了一个小网站

做了一个小网站 www.kanxs123.com

叶落花开
昨天
0
0
继社会佩奇之后,又尝试了可爱的蓝胖子,有趣 Python

#哆啦A梦# !/usr/bin/env python3# -*- coding: utf-8 -*-# @Author: dong dong# @Env: python 3.6from turtle import *# 无轨迹跳跃def my_goto(x, y): penup(...

Py爱好
昨天
0
0
shell及python脚本方式登录服务器

一、问题 在工作过程中,经常会遇见需要登录服务器,并且因为安全的原因,需要使用交互的方式登录,而且shell、python在工作中也经常用到,并且可以提供交互的功能。都是利用了expect、spawn...

yangjianzhou
昨天
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部