之前分析了ByteBuffer、Channel相关的基本知识,现在对于NIO的基石已经有了基本的了解。不过NIO最突出的特性还是其基于select编程模型的网络编程体验。

NIO网络编程通常有两种使用方式,同步阻塞式编程(与BIO网络编程相同)和同步非阻塞(select模型)。下面就从NIO的编程模型来了解NIO的基本原理。

一、NIO阻塞式网络编程

NIO可以像传统的Socket编程一样,进行阻塞同步的Socket编程,样例代码如下:

服务端

客户端

socket通信基本流程如下

结合socket通信的基本流程和样例代码进行初步分析

ServerSocketChannel

结合样例代码,可以看到NIO中的xxxSocketChannel对象其实就是对应着系统层面的socket,服务端通过下面方法创建了一个服务端的server—socket套接字用来监听本地端口,从而能够响应客户端的connect事件,从而建立服务端与客户端的tcp连接

默认情况下的provider应该是下面这个类:

在Linux环境中实际创建的SelectorProvider为EPollSelectorProvider

默认情况下EPollSelectorProvider的实例是通过反射进行创建的

openServerSocketChannel方法的具体实现在EpollSelectorProvider的父类SelectorProviderImpl中提供具体实现:

openServerSocketChannel方法生成的ServerSocketChannel实例的实际类型为ServerSocketChannelImpl,

fd是通过Net类创建的socket所对应的文件描述符

参数true表明这是一个stream类型的套接字,即tcp套接字

socket0是一个native方法

如果当前支持ipv6则创建ipv6的套接字,如果不支持则创建ipv4的套接字,SOCKET_STRAM表明是tcp套接字,SOCK_DGRAM表明是UDP套接字

创建socket套接字是使用的c语言的socket库函数,其内部通过socket系统调用创建了一个socket套接字对应的文件描述符,

socket系统调用​blog.csdn.net

通过上述源码可以了解到ServerSocketChannel是一个实际类型为ServerSocketChannelImpl的实例,该实例的fd属性关联了一个socket文件描述符

服务端的套接字创建后,需要先绑定到本地的具体地址,然后监听本地端口,即socket通信模型中的bind和listen两个步骤:

在NIO中,bind和listen在同一个方法中完成,backlog参数是服务端半连接队列的大小

服务端开始listen本地端口之后,可以调用accept方法,同步阻塞的接收客户端的连接请求

accept也是个系统调用,所以要依靠native方法来实现

native调用了accept系统调用,当有新的客户端与服务端成功建立tcp连接后,则会为这个tcp连接创建一个socket文件描述符

configureBlocking方法给新建立的客户端与服务端之间的socket对应的文件描述符设置了阻塞标志,该动作是通过fcntl系统调用来完成的:

fcntl​blog.csdn.net

SocketChannel:

Java中将已建立的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之Buffer​zhuanlan.zhihu.com

二、NIO同步非阻塞式网络编程

NIO同步阻塞的网络编程模型和传统BIO网络编程模型并没有什么太大不同,唯一的优点就是通过操作堆外内存进行数据传输效率高一些,但是这并没有显著的改善BIO的痛点,即BIO是阻塞的,所以NIO支持了同步非阻塞的多路复用网络编程模型。

NIO服务端

NIO客户端

从上面的demo来看,NIO非阻塞式编程主要有以下几个特点:

  • 无论是ServerSocketChannel还是SocketChannel,都需要显示的设置为非阻塞模式
  • 开发者通过注册事件的方式来表明当前SocketChannel/ServerSocketChannel下一次要进行什么动作
  • SocketChannel/ServerSocketChannel是否已经可以执行开发者注册的事件所对应的操作,交给Selector来管理,对开发者是透明的
  • 每个Channel在同一个Selector上同一时间只能存在一个事件,如果想在同一个Channel上执行多个操作可以有两种办法,创建多个Selector或者是每次处理完一个事件后注册另外一个事件

下面来通过源码稍微深入的了解一下Selector是如何工作的

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系统调用完成的

epoll​blog.csdn.net

register 注册事件

在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需要将该事件保存到自己的数组中

Selector.select 取出就绪事件

当注册完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是怎么做的。

线程中断异常处理——主动唤醒epoll

回顾一下前面的内容,EPollSelectorImpl实例中通过IOUtil.makePipe创建了一个管道,并且把管道的输入端(读端)、输出端(写端)的文件描述符传递给了epoll的包装类EPollArrayWrapper,并且在EPollArrayWrapper中给管道的端文件描述符注册了可读事件监听:

而管道的输出端,也就是写端在interrupt方法中被调用

该方法的主要作用就是向管道写入一个字节的数据

由于管道的输入端(读端)已经在epoll中注册了EPOLLIN事件,假如当前epoll处于阻塞状态,则会监听到这个读事件从而唤醒线程

socket编程实例_NIO之网络编程源码阅读相关推荐

  1. Tars-Java网络编程源码分析

    作者:vivo 互联网服务器团队- Jin Kai 本文从Java NIO网络编程的基础知识讲到了Tars框架使用NIO进行网络编程的源码分析. 一.Tars框架基本介绍 Tars是腾讯开源的支持多语 ...

  2. UNIX网络编程源码

    下载源码 http://www.unpbook.com/ 解压 tar zxvf unpv13e.tar.gz unpv13e/ cd ./unpv13e ./configure一.编译lib文件 c ...

  3. Socket编程实例

    2019独角兽企业重金招聘Python工程师标准>>> Java具有非常强大的网络应用开发能力.学习网络编程,估计都是从编写一个socket通信开始,从中慢慢掌握网络编程的一点点知识 ...

  4. C语言 socket编程实例

    C语言 socket编程实例 一. 面向连接的流式套接字 C/S 例子 二. 非阻塞的多人聊天服务器端例子 三. 简单的 IPv6 UDP socket编程 四.使用wireshark抓包分析tcp协 ...

  5. android c++ socket编程,C++ 中 socket编程实例详解

    C++ 中 socket编程实例详解 sockets(套接字)编程有三种,流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW):基于TCP的soc ...

  6. matlab dll 通达信,【通达信】通达信插件(dll)编程源码及讲解

    [通达信]通达信插件(dll)编程源码及讲解 2018-11-29 通达信DLL插件编程示例(源码).rar (829.39 KB) 通达信提供了DLL插件接口和DLL指标接口标准(见"插件 ...

  7. UNIX环境高级编程源码

    UNIX环境高级编程源码 一.下载源码 利用wget命令下载源码,源码地址http://apuebook.com/src.3e.tar.gz 可以在服务器的根目录下创建文件夹,mkdir 1.mkdi ...

  8. 优秀网站源码、编程源码下载网站大集中

    原文:https://blog.lmlyz.online 优秀网站源码.编程源码下载网站大集中 1.51源码:http://www.51aspx.com/ 2.源码之家:http://www.code ...

  9. winform停车场收费管理系统VS开发sqlserver数据库CS结构c#编程源码程序

    一.源码特点        winform停车场收费管理系统是一套完善的桌面程序,设计管理系统,系统具有完整的代码数据库,系统主要采用B/S模式开发. 应用技术:winform c#+sqlserve ...

最新文章

  1. 浮层java_通过遮罩层实现浮层DIV登录的js代码
  2. Windows 技巧篇-电脑蓝光过滤,颜色校准调节蓝光,电脑源头过滤蓝光,保护眼睛,护眼软件原理
  3. simple-android-flux,深入浅出Flux
  4. BAPI:BAPI_CONTRACT_CREATE(内部合同创建)
  5. iframe如何发送请求_插件分享 | 如何半天玩转一个“ES未授权利用”插件
  6. html元素data属性设置变量,HTML5 自定义属性 data-* 和 jQuery.data 详解
  7. 【前端 · 面试 】HTTP 总结(七)—— HTTP 缓存概述
  8. (转)区块链:为什么说finchain是下一代金融应用平台
  9. mysql按基本时间单元_超星尔雅MYSQL数据库基础加讲义单元测试答案
  10. 光模块测试CEI-28G-VSR 浅析1---背景基础介绍
  11. [转]验证码识别技术
  12. CSS3弹性布局、响应式布局、PS
  13. mac chrome更新错误12
  14. android 转码工具下载,m3u8视频转码工具
  15. 华为云文件服务器配置,华为云 文件服务器配置
  16. 【性能定位】使用jstack定位线程堆栈信息
  17. 最大子矩阵(C语言)
  18. 什么是json数据?json数据应该怎么设计
  19. 【FME】模板模块化组织思路
  20. 冲刺!这篇 1658 页的《Java 面试突击核心讲》学明白保底年薪 30w

热门文章

  1. eclipse添加源码的另外一种方法
  2. OpenCV中Kinect的使用(3)
  3. 云中漫步 - 3:2013-4-27 微软云体验营北京站
  4. .NET中的内存管理,GC机制,内存释放过程
  5. 大家觉得创业需要些什么
  6. MS SQL 2008认证考试大纲
  7. 一种开发AJAX的方法,ajax.dll
  8. 首个面向手绘草图的深度自监督表示学习
  9. 阿里巴巴开源轻量级深度神经网络推理引擎MNN
  10. ECCV18 | UC伯克利提出基于自适应相似场的语义分割