目录
一、阻塞 & 非阻塞
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去等待数据和复制数据,复制完后用回调方法通知结果。 所以也这种就是非阻塞的,请求完直接返回了,所以也叫异步非阻塞