NIO-Selector 网络编程

目录

一、阻塞 & 非阻塞

1、阻塞

2、非阻塞

二、selector

1、连接和读取

2、处理客户端断开

3、处理消息的边界

4、ByteBuffer大小分配

三、多线程优化

四、NIO vs BIO

1、stream vs channnel

2、IO模型

阻塞IO

非阻塞IO

多路复用

异步IO模型


一、阻塞 & 非阻塞

1、阻塞

服务器端的代码

然后创建客户端,直接连服务器端的代码就可以了

运行发现服务端线程执行到accept这个方法后就停止运行了,这个方法是阻塞的,要等连接建立完成之后才能继续运行;建立完连接后走到read方法又会阻塞等待读入数据,读到数据之后才能恢复运行。

当有多个客户端的时候,服务端在执行到一个客户端的堵塞方法的时候会一直等待,其他客户端就不会被处理。 

2、非阻塞

我们可以把ServerSocoketChannel和SocketChannel改成非阻塞模式,这样如果没有连接直接返回null,没有读取到数据就返回0,不会去堵塞等待了。

但是这样也会有缺点,因为线程是一直循环检查有没有链接和数据的,如果一直没有连接和数据来,线程依然会不断循环,太过繁忙了,所以这种非阻塞模式在开发中也不常用。

二、selector

为了解决上面非堵塞的不断循环问题,我们就引入selector,接下里看看selector的使用

1、连接和读取

用的时候我们要先调用静态open方法创建Selector对象,然后来管理这些channel,channel调用register方法注册到selector,返回值是个SelectionKey对象, SelectionKey就是将来事件发生后,通过它可以知道事件是哪个channel的事件,然后调用interstOps设置对哪个事件感兴趣

channel事件有4中类型:

  • accept-有连接请求的时候触发
  • connect-客户端连接建立后触发
  • read-数据可读时
  • write-可写事件

然后监听的时候用select()方法,没有时间发生的时候线程堵塞,有事件才执行,然后遍历集合调用selectedKey方法处理事件。

如果我们遍历出来不对事件处理,那么下次遍历的时候还会有,也就是说如果有未处理事件,那么是不会阻塞的,如果不想处理的话,我们可以用cannel()方法来取消处理

socket处理的时候要想不去不断循环读取,那么也要交给selector管理,所以这个socketChannel也要调用register方法注册到selector上,还有感兴趣关注的事件

这里要把监听到的事件类型分开处理

这个代码在运行的时候缺空指针了,为什么呢?

selector的集合和selectedKeys集合中每次监听事件中的内容执行完是不会自己删除的,所以我们第一次执行完第二次遍历到有元素执行,建立连接的时候却没有需要建立的返回null,这个时候就抛出空指针了,我们每次执行完一个事件就得手动从容器删除

2、处理客户端断开

我们客户端和服务器连接之后,如果客户端退出会抛出异常,如果我们直接try catch去抓的话会循环,因为客户端关闭会引发read事假,这个事假一直不处理就会一直执行,所以我们必须在catch里面去把key调用cancel()方法取消处理事件

但是如果是正常断开呢,还是会循环,因为正常断开执行不到catch语句不会cancel,就会一直循环处理close这个read事件,所以我们要用read的返回值来区分是不是close指令,是就取消处理掉

3、处理消息的边界

看看下面这种边界问题:

我们用分割符的方法来解决边界的问题,遇到分隔符就说明读取结束了,我们就分割成一个消息

如果我们实际的内容超过了byteBuffer,一次没有读取完不会报错,第二个会把剩下的部分发过来会当成一个完整的消息处理,所以当空间不够的时候,我们需要扩容,而且我们不能把byteBuffer做为局部变量,要当共享的第一个和第二次共用一个byteBuffer。扩容我们是重新搞个更大的buffer2,然后第一次复制过去,第二次的直接读过去

1、我们先修正ByteBuffer不能为局部变量,但是我们不能直接放最外层,因为如果放最外层就变成多个socketChannel公用一个ByteBuffer,这样就乱了,应该每个channel都有自己的byteBuffer,这里就要用到attachment附件,我们可以在注册的时候把buffer当成附件(Object)一起注册为SelectionKey里面。后面要用的时候直接用key调用attachment方法就能获取到buffer

 2、扩容的时候就是比较如果压缩后的长度和限制最大的长度相等,说明这个消息超过了要扩容,压缩就是每次读取完一个分割符后都要进行的,这个压缩方法就是把因为读取的去除

4、ByteBuffer大小分配

每个channel都需要记录可能被分割的消息,因为byteBuffer不能被多个channel共同使用,因此需要每个channel维护一个独立的byteBuffer

ByteBuffer不能太大,太大的话很多连接就需要很大内存了,因此需要设计大小可变的byteBuffer。一种思路是首先分配一个小buffer,比如4k,如果不够再扩容为8k,用新的替换掉旧的,优点是消息连续容易处理,缺点是数据拷贝耗性能。另一个思路是多个数组组成buffer,一个数组不够,把多出来的内容写入新的数组,与前面的区别是消息存储不连续解析复杂,优点是避免了拷贝引起的性能损耗。

三、多线程优化

现在的处理都是单线程的,没有充分利用多核cpu,

我们可以向下图这样优化,分为boss好worker,boss负责建立连接,而worker负责读写操作,如果连接了,可以直接去worker读,worker里面有一个selector,线程数可以很多很多,不可能一直建立很多worker,所以一个worker可以管理多个channel,多个thread,相当于把任务分担了。

问题: 

执行我们现在是worker-0不断循环执行select方法和boss的register可能会有问题,因为他们是同一个selector管理,又是不同线程的,如果上面执行的selector执行select方法会阻塞,所以下面register就不能执行。

我们可以把上面的worker.registery方法移动到sc.register上一行,这样就有可能会先执行到register,再执行worker.registery()里面的select方法,register不会阻塞直接执行完这样就不会有问题了,因为他们是两个线程,所以可能是先执行的register,执行完又会停留到select方法上阻塞,这个时候如果来了个新的客户端,所以肯定阻塞又出现了那个问题

解决:

我们要想一个办法,参考netty的解决办法,让他们在同一个线程内执行,这样就可以管理执行顺序了。

我们在搞个队列,然后注册的方法里像队列中添加一个注册的任务

 

这样我们就能保证是worker-0在执行任务的时候,就会去注册了,就能保证在worker-0来执行这两个事件了,实现两个线程的事件用一个线程执行,这样就可以控制顺序。最后要注意,添加完事件之后要唤醒selector因为那边的selector是一直阻塞状态的

回顾下整体流程:

我们先接收连接,调用worker进行register去初始化,如果是第一次就会把selector线程都创建好,如果是第二次,都会向队列加入任务,这个worker-0的run进来是select就是阻塞住如果没有的话,然后队列加入完任务就会唤醒这个阻塞的worker执行任务,继续他执行完又select没任务阻塞了。

四、NIO vs BIO

1、stream vs channnel

  • stream不会自动缓冲数据,channel会利用系统提供的发送缓冲区,接收缓冲区(更底层)
  • stream仅支持阻塞API,channel同时支持阻塞、非阻塞API,网络channel可配合selector实现多路复用
  • 二者均为全双工,即读写可以同时进行

2、IO模型

当调用channel的read方法或stream的read后,会由用户态切换至操作系统内核态来完成真正的数据读取,而读取又分为两个阶段:等待数据阶段复制阶段

阻塞IO

当调用read切换内核态后,如果没有数据过来,就会等待数据,等数据到了之后处理好了,复制数据完成之后才切换回用户态,这个整个过程用户线程是阻塞的

非阻塞IO

用户线程会一直循环切换内核态看看有没有数据,没有就立即返回,然后继续转化,用户线程始终在运行,这就算非阻塞IO,但是正在有数据复制数据的时候还是阻塞的(这种切换太频繁了,可能会影响系统性能)

多路复用

多路复用一上来不是调用read,而是用select等待事件(这个是阻塞的),如果有内核态会告诉他,这个时候再切换过去复制数据(复制的时候还是需要阻塞) 

他们都是阻塞,那比阻塞的优势在哪里呢?看看下面的图就知道了,阻塞IO的时候,如果当read在阻塞等待的时候,其他还有线程要执行别的操作,他必须等待这个read阻塞完成全部完成才执行;而多路复用selector可以检测多个事件,什么事件都可以触发唤醒他运行,也就是他阻塞的时候等的是一批事件不是一个事件。

异步IO模型

  • 同步:线程自己去获取结果(一个线程)
  • 异步:线程自己不去获取结果,而是由其他线程送结果(至少两个线程)

我们先分析一下上面三种是同步还是异步

  • 阻塞IO就是同步的,是用户线程自己发起read,也是自己阻塞等待接受accept的,所以是同步的,所以也叫同步阻塞IO
  • 非阻塞IO也是同步的,他也是线程自己发送自己接受,只不过他是不断循环去发送,同步非阻塞
  • 多路复用也是同步的,他也是自己发送select之类的等待阻塞,有事件了唤醒自己去执行,还是自己发自己阻塞自己做,也是同步的

异步阻塞是怎么样的呢?

他是这样的,客户端线程read后,直接返回一个回调函数和参数,然后会异步启动线程2去等待数据和复制数据,复制完后用回调方法通知结果。 所以也这种就是非阻塞的,请求完直接返回了,所以也叫异步非阻塞

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/8103.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

虚拟现实(VR)在医疗保健中的5种应用

医疗保健中的VR虚拟现实 虚拟现实的由来已久,18世纪,法国的医生使用布制的分娩模拟器向助产师和外科医生教授医学技术。在20世纪60年代初,医生一边对心肺复苏学员口述心肺复苏的技巧,一边使用一家塑料玩具厂家制造的塑料娃娃现场…

K8s 为什么要弃用 Docker

K8s 为什么要弃用 Docker 最近在学习容器技术的过程中,看到有关于Kubernetes“弃用 Docker”的事情,担心现在学 Docker 是否还有价值,是否现在就应该切换到 containerd 或者是其他 runtime。 随着深入了解,这些疑虑的确是有些道理…

Unreal 5 实现丧尸伤害和死亡

这一篇主要是实现玩家攻击丧尸可以造成伤害和自身血量为零时,丧尸可以死亡。丧尸也可以对玩家造成伤害,有攻击范围的判定。 这一篇的功能实现有四个功能: 丧尸被攻击掉血丧尸死亡处理玩家被攻击掉血玩家死亡处理 丧尸被攻击掉血 子弹的修改…

【C++】异常

文章目录 1. C语言传统的处理错误的方式2. C异常概念用法异常的抛出和匹配原则规则1情况2抛出异常位置最近的验证对象类型匹配的验证 规则3规则4 异常的重新抛出异常规范 1. C语言传统的处理错误的方式 错误处理机制: 1.终止程序 如:assert 断言终止 &a…

TX Text Control .NET for WPF 31.SP3 Crack

.NET WPF 应用程序的文档处理 将文档编辑、创建和 PDF 生成添加到您的 WPF 应用程序中。 视窗用户界面 功能齐全的文档编辑器 TX Text Control 是一款免版税、完全可编程的丰富编辑控件,它在专为 Visual Studio 设计的可重用组件中为开发人员提供了广泛的文字处理功…

[每日一水] Latex Tikz foreach 循环嵌套

\documentclass{article} \usepackage{tikz} \begin{document} \begin{tikzpicture} \foreach[count\i] \x in {20,18,...,2}\foreach[count\j] \y in {20,18,...,2}\node at (\i,\j) {\x \y}; \end{tikzpicture} \end{document}\node at (\i,\j) {\x \y}; 就是写文本的意思

Day.1 LeetCode刷题练习(最长公共前缀 C/C++两种解法)

题目: 例子: 分析题目: 主要目的:求出各个字符串的公共前缀 思路(本人解法): 用所给实例来看,不难看出我们可以直接以竖着对应来查看是否是公共前缀 , 这样就有了一…

【Python爬虫与数据分析】文件读写

目录 一、概述 二、txt文件读写 三、csv文件读写 四、Json文件读写 一、概述 读写模式: r:读数据w:覆盖写入a:追加写入rb:以二进制形式读出wb:以二进制形式写入ab:以二进制形式追加r、w、…

TypeScript 总结

文章目录 TypeScript 总结概述运行ts文件方式一方式二 基础声明变量类型数组元组联合类型取值限制 枚举类型any & unknownvoid & undefined类型适配 面向对象函数普通函数箭头函数可选参数默认参数 对象创建对象对象的类型限制 类和接口泛型简单使用多个泛型默认泛型类…

C语言经典案例-2

今天继续给大家分享C语言学习的经典练手案例 记录在校学习第三天,继续加油 今日案例来源:csdn社区每日一练 1.商品优惠计算机 商品优惠计算器 使用if语句编程实现输入购货金额,输出实际付款金额。购货折扣率如下: 购货金额≤500元…

obs录制功能源码分析

录制按钮 界面文件: 主界面:OBSBasic.ui 中开始录制按钮的objectName 是 recordButton 槽函数: void OBSBasic::on_recordButton_clicked() {//1 输出模式是否被激活if (outputHandler->RecordingActive()) {bool confirm config_get_bool(GetGlo…

LiDAR SLAM 闭环——BoW3D: Bag of Words for Real-time Loop Closing in 3D LiDAR SLAM

先说总结 现算法已经开源,代码链接: GitHub - YungeCui/BoW3D: [RA-L] BoW3D: Bag of Words for Real-Time Loop Closing in 3D LiDAR SLAM. 背景 SLAM(同时定位与地图构建)是一种让机器人在构建环境地图的同时,定位…