深入理解io模型
一,深入理解io模型
IO模型就是说用什么样的通道进行数据的发送和接收,Java共支持3种网络编程IO模式:BIO,NIO,AIO。接下来分别对这三种模型进行一个简单的讨论。
1,BIO
就是一个阻塞io,在服务端进行这个监听客户端的时候会进行一个阻塞操作,在读数据的时候,如果客户端没有收到数据,也会进行一个阻塞操作。如果出现阻塞,那么其他客户端可以连接这个服务端,但是服务端不会接收客户端的请求。
就是说在同一时刻,客户端只能响应一个客户端,底层为一个单线程版本。
ServerSocket serverSocket= new ServerSocket(9000);
while(true){
//监听等待连接
serverSocket.accept();
}
优化
引入线程池,再进行响应的时候以多线程的方式来处理多个请求,现在就可以响应多个客户端。
多线程缺点
1,如图线程数据量太大,会导致响应效率慢;
2,并且会有线程切换,会消耗大量资源;
3,如果连接成功之后,这个客户端不发数据,会导致这个请求一直处于这个阻塞状态,其他的线程就会用不了。
应用场景
适用于连接数目比较小且固定架构,对服务器资源要求比较高,但是程序简单易理解
2,NIO
None BlockQueue IO;就是一个同步非阻塞,并且可以控制阻塞和不阻塞问题。不管是否有客户端,服务端都会一直处于一个轮询的状态,当有客户端过来访问就建立一个连接。解决了bio的阻塞问题。因此这个就添加了一个非阻塞的一个配置,来解决BIO的阻塞问题
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket.bind(new InetSocketAddress(9000));
//如果设置为true,那么和bio一样,flase就是一个非阻塞
ssc.configureBlocking(false);
while(true){
//监听客户端
SocketChannel sc = ssc.accept();
//设置这个客户端为一个非阻塞
ssc.configureBlocking(false);
//将客户端加入到list集合里面
List.add(sc);
//遍历list,遍历所有的客户端,读取数据。
}
弊端
如果有十万个客户端,但是只有1000个客户端经常连接,因此会对资源会造成很大的浪费,并且需要的时间会比较长一点。
解决方案,引用一个多路复用器。就是引入一个选择器,会对需要操作的客户端往这个selector增加一个Event事件,然后后面回去对这个事件进行一个处理,比如说有连接事件,读取事件,打印事件等首先就是解决了这个bio的阻塞问题,
public static void main(String[] args) throws IOException {
// 创建NIO ServerSocketChannel
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(9000));
// 设置ServerSocketChannel为非阻塞
serverSocket.configureBlocking(false);
// 打开Selector处理Channel,即创建epoll
Selector selector = Selector.open();
// 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
SelectionKey selectionKey = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务启动成功");
while (true) {
// 阻塞等待需要处理的事件发生
selector.select();
// 获取selector中注册的全部事件的 SelectionKey 实例
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 遍历SelectionKey对事件进行处理
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 如果是OP_ACCEPT事件,则进行连接获取和事件注册
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
// 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
SelectionKey selKey = socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端连接成功");
} else if (key.isReadable()) { // 如果是OP_READ事件,则进行读取和打印
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(128);
int len = socketChannel.read(byteBuffer);
// 如果有数据,把数据打印出来
if (len > 0) {
System.out.println("接收到消息:" + new String(byteBuffer.array()));
} else if (len == -1) { // 如果客户端断开连接,关闭Socket
System.out.println("客户端断开连接");
socketChannel.close();
}
}
//从事件集合里删除本次处理的key,防止下次select重复处理
iterator.remove();
}
}
}
早期在jdk1.5之前会通过这个select + poll的方式,主要是通过轮询所有的客户端的方式实现;后面使用这个epoll模型来优化这个nio,会通过感知的方式来判断当前客户端里面是否有携带事件的方式来实现。
selector底层
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
public static SelectorProvider provider() {
provider = sun.nio.ch.DefaultSelectorProvider.create();
}
在这个DefaultSelectorProvider的默认类里面,不同环境有不同环境的实现,有windows版本,还有mac,linux版本等。因此这个就解释了这个跨平台操作,不同平台有不同平台的环境
public class DefaultSelectorProvider {
private DefaultSelectorProvider() {
}
public static SelectorProvider create() {
return new WindowsSelectorProvider();
}
}
如下图,如果是在这个linux的环境下,就会返回一个epoll的选择器。在创建这个epoll的时候,会通过一个native的本地方法epollCreate来构建,最终会调用底层的c语言实现。
因此这个服务端响应是通过操作操作系统底层,通过一个硬中断实现。
在jdk1.5之前,这个select和poll会有一定的连接数量的限制,但是这个epoll没有限制。其主要对比如下
epoll的主要函数有
epoll_create:创建一个epoll
epoll_ctl: 设置要监听的socket,当epoll监听某个socket发生了事件,将其fd放入就绪队列(rdllList),
epoll_wait:就绪队列有事件则响应,没事件则阻塞
NIO应用场景: NIO方式适用于连接数目多且连接比较短(轻操作) 的架构, 比如聊天服务器, 弹幕系统, 服务器间通讯
总结(总点)
首先,客户端和服务端会建立连接,会通过一个open方法来实现,然后会调用一个epoll_create方法来创建一个selector多路复用器的一个实例,这个selector的底层就是一个epoll的一个实例,这个实例里面就是有一个空的rdlist的一个事件的就绪列表,在客户端有读写事件的时候里面就会存入数据。这个事件响应是通过这个操作系统内核去响应的,建立连接时感知到有事件之后,会通过中断程序,通过回调的方式来把这些事件存放到这个socketChannel通过里面,selector调用这个epoll_ctl方法,如果通道里面带有事件的时候,会将这个客户端注册到这个rdlist里面,selector在调用这个epollwait的时候,会判断一下通道里面有没有事件,有事件的话就会将这个selector和这个事件做一个绑定,就会去判断一下这个就绪列表里面有没有数据,没有数据则阻塞,有的话则遍历读取。
3,AIO
和这个nio差不多,不过它是使用的异步非阻塞原理。由操作系统完成后回调通知服务端程序启动线程去处理, 一般适用于连接数较多且连接时间较长的应用
实现原理:提供了异步通道的方式实现,没有向NIO一样使用这个多路复用器。不需要手动创建独立的线程去处理I/O读写操作,客户端的 I/O 请求都是由操作系统先完成后通知服务器启动 JDK 提供的线程池负责回调和数据读写(当真正读取到/写入完数据的时候,会通知我们注册的回调实现类
在实际开发中,一般用的比较少。
4,redis多路复用底层
Redis就是典型的基于epoll的NIO线程模型(Nginx也是),epoll实例收集所有事件(连接与读写事件),由一个服务端线程连续处理所有事件命令。