之前分析了ByteBuffer、Channel相关的基本知识,现在对于NIO的基石已经有了基本的了解。不过NIO最突出的特性还是其基于select编程模型的网络编程体验。
NIO网络编程通常有两种使用方式,同步阻塞式编程(与BIO网络编程相同)和同步非阻塞(select模型)。下面就从NIO的编程模型来了解NIO的基本原理。
NIO可以像传统的Socket编程一样,进行阻塞同步的Socket编程,样例代码如下:
socket编程步骤?服务端:
客户端:
socket编程例子,socket通信基本流程如下
结合socket通信的基本流程和样例代码进行初步分析
结合样例代码,可以看到NIO中的xxxSocketChannel对象其实就是对应着系统层面的socket,服务端通过下面方法创建了一个服务端的server—socket套接字用来监听本地端口,从而能够响应客户端的connect事件,从而建立服务端与客户端的tcp连接
socket编程 c语言、
默认情况下的provider应该是下面这个类:
在Linux环境中实际创建的SelectorProvider为EPollSelectorProvider
socket网络编程教程、
默认情况下EPollSelectorProvider的实例是通过反射进行创建的
openServerSocketChannel方法的具体实现在EpollSelectorProvider的父类SelectorProviderImpl中提供具体实现:
网络编程socket,
openServerSocketChannel方法生成的ServerSocketChannel实例的实际类型为ServerSocketChannelImpl,
fd是通过Net类创建的socket所对应的文件描述符
socket套接字编程。
参数true表明这是一个stream类型的套接字,即tcp套接字
socket0是一个native方法
socket代码,
如果当前支持ipv6则创建ipv6的套接字,如果不支持则创建ipv4的套接字,SOCKET_STRAM表明是tcp套接字,SOCK_DGRAM表明是UDP套接字
创建socket套接字是使用的c语言的socket库函数,其内部通过socket系统调用创建了一个socket套接字对应的文件描述符,
socket系统调用blog.csdn.netc语言socket编程udp,通过上述源码可以了解到ServerSocketChannel是一个实际类型为ServerSocketChannelImpl的实例,该实例的fd属性关联了一个socket文件描述符
服务端的套接字创建后,需要先绑定到本地的具体地址,然后监听本地端口,即socket通信模型中的bind和listen两个步骤:
在NIO中,bind和listen在同一个方法中完成,backlog参数是服务端半连接队列的大小
c++ socket编程。
服务端开始listen本地端口之后,可以调用accept方法,同步阻塞的接收客户端的连接请求
socket网络编程基础。accept也是个系统调用,所以要依靠native方法来实现
native调用了accept系统调用,当有新的客户端与服务端成功建立tcp连接后,则会为这个tcp连接创建一个socket文件描述符
configureBlocking方法给新建立的客户端与服务端之间的socket对应的文件描述符设置了阻塞标志,该动作是通过fcntl系统调用来完成的:
fcntlblog.csdn.netJava中将已建立的tcp连接封装成SocketChannel对象,服务端的SocketChannel是通过监听本地绑定的端口然后通过accept操作建立的,而客户端的SocketChannel是通过主动与服务端进行connect建立的:
SocketChannel的open方法,可以向服务端发起一个连接建立请求:
和ServerSocketChannel一样,客户端的SocketChannel也是通过SelectorProvider创建的
与ServerSocketChannel不同的是,客户端socket对应的文件描述符是通过Net.socket方法创建的
其底层同样是通过socket0这个native方法实现的,不过与ServerSocketChannel创建不同的是其reuse参数为false
当reuse为true时,会通过setsockopt给socket设置SO_REUSEADDR这个参数,表示可以进行端口复用。通常客户端是不允许进行端口复用的,而服务端一般来说可能有好几张网卡,所以可以考虑给每一个网卡上的IP地址都绑定相同的监听端口来启动多个服务端进程。
当客户端的SocketChannel实例创建完成后,通过调用connect向服务端发送连接建立请求:
Net.connect通过调用connect0这个native方法完成连接建立请求的发送
connect0的底层实现为connect系统调用
connect系统调用blog.csdn.net当连接建立完成后,就可以通过socket套接字进行数据的读写的。由于NIO中直接面向的数据对象为Buffer,所以一般开发中要借助ByteBuffer来完成数据的读写功能,至于Buffer的基本概念,可以看前面的源码分析:
landexiang:NIO之Bufferzhuanlan.zhihu.comNIO同步阻塞的网络编程模型和传统BIO网络编程模型并没有什么太大不同,唯一的优点就是通过操作堆外内存进行数据传输效率高一些,但是这并没有显著的改善BIO的痛点,即BIO是阻塞的,所以NIO支持了同步非阻塞的多路复用网络编程模型。
从上面的demo来看,NIO非阻塞式编程主要有以下几个特点:
下面来通过源码稍微深入的了解一下Selector是如何工作的
Selector是通过open方法创建的
从前面的分析可知,默认的provider是EPollSelectorProvider,所以Selector的实际类型为EPollSelectorImpl
EPollSelectorImpl实例中首先通过pipe系统调用创建了一个管道
linux 管道blog.csdn.net管道创建后,fd数组中保存了读端和写端的文件描述符,并且都被置位非阻塞的模式
EPollArrayWrapper是epoll的文件描述符的包装类
其内部epfd属性保存的是通过epoll_create系统调用创建的epoll的文件描述符
操作系统层面的epoll创建成功之后,将之前创建的管道的输入端和输出端对应的文件描述符分别保存起来,然后通过epollCtl函数,将管道的输出端对应的文件描述符添加到epoll中,注册事件类型为EPOLLIN,即有数据到达时epoll会认为其处于就绪状态
epollCtl底层是通过epoll_ctl系统调用完成的
epollblog.csdn.net在Selector创建完成之后,需要将Channel注册到Selector上,该方法由ServerSocketChannel和SocketChannel的公共抽象父类SelectableChannel提供
其内部调用了抽象子类AbstractSelectableChannel的register方法,从源码来看,如果不讲Channel设置为非阻塞的模式,在注册事件时会直接抛异常
findKey方法会先遍历当前Channel中所有的SelectionKey,找出已经在当前Selector上已经注册过的事件 如果没找到则说明是第一次注册
如果不是第一次注册,说明是重复注册,则需要对之前的事件进行覆盖,SelectionKey.interestOps方法提供修改epoll监听事件的功能
以SocketChannelImpl和EpollSelectorImpl为例,在SocketChannelImpl的translateAndSetInterestOps方法中,将SelectKey添加到了EpollSelectorImpl中
pollWrapper前面已经介绍过,是epoll的java层面的包装类,
setInterest方法首先保存了SocketChannel持有的文件描述符
通过注释可以得知eventsLow和eventsHigh是两个等待变更epoll监听事件的缓冲区,即在Channel已经注册过事件的Selector重新注册事件时,事件的并不会实时的更新到操作系统层面的epoll中,而是先将变更事件在内存中缓存起来
上述是重新注册事件的情况,下面来看下首次注册事件的流程。首次注册的任务是交给Selector的register方法完成的
源码中告诉我们 SelectionKey中保存了Channel和Selector的引用,以便于在处理事件时进行引用
而至于interestOps方法,前面在介绍重新注册时已经分析过,该事件并没有同步的注册到操作系统层面的epoll中
最后在首次注册完事件后,Channel需要将该事件保存到自己的数组中
当注册完Channel对应的事件后,调用Selector的select方法即可进行阻塞式的等待,当Selector中任何一个Channel的任何一个事件到达时,阻塞解除。select方法的具体工作是交给子类重写的doSelect方法来完成的
以Linux操作系统环境下的EPollSelectorImpl为例
processDeregisterQueue是对已经取消的SelectionKey的相关数据清理工作
清理完已经无效的SelectionKey之后,通过poll(timeout)方法触发epoll的select动作,不过在select之前首先要将上一次select到这次select之间新添加或者新修改的文件描述符的注册事件生效到同步到操作系统层面的epoll上去
同步注册事件的动作依然依赖于epoll_ctl系统调用
epollwait是等待就绪事件到达的主要动作,从源码看其底层有两种方式,有限等待和无限等待,默认情况下不显示指定过期时间就是无线等待,即下面的epoll_wait,而不是iepoll,epoll_wait同样是一个系统调用
当epoll_wait系统调用完成后,如果有就绪的事件到达,这些事件和对应的文件描述符已经被放置到内存中一块连续的地址空间中去了,然后需要比对当前Selecotor中所有的SelectionKey,将所有就绪的SelectionKey取出来放到selectedKeys集合中:
updated是当前Selector中所有的事件数量,在进行一次select之后,所有的就绪事件都被保存在了selectedKeys集合中
所以我们可以通过Selector.selectedKeys()方法取出所有的就绪事件进行相应
上面就是select的基本过程,无非就是给socket对应的文件描述符注册事件,然后用epoll去监听是否有事件到达。不过我们需要注意一个问题,epoll可以是无限阻塞的,假如我们的线程由于其他原因被中断了,我们如何去显示的唤醒底层的epoll呢?否则他将一直阻塞下去。这里就要来看下Java是怎么做的。
回顾一下前面的内容,EPollSelectorImpl实例中通过IOUtil.makePipe创建了一个管道,并且把管道的输入端(读端)、输出端(写端)的文件描述符传递给了epoll的包装类EPollArrayWrapper,并且在EPollArrayWrapper中给管道的端文件描述符注册了可读事件监听:
而管道的输出端,也就是写端在interrupt方法中被调用
该方法的主要作用就是向管道写入一个字节的数据
由于管道的输入端(读端)已经在epoll中注册了EPOLLIN事件,假如当前epoll处于阻塞状态,则会监听到这个读事件从而唤醒线程
版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。
工作时间:8:00-18:00
客服电话
电子邮件
admin@qq.com
扫码二维码
获取最新动态