一、同步阻塞IO(BIO)
BIO全称(blocking I/O):同步阻塞。
当客户端有连接请求时,服务端就会需要启动一个线程对客户端的连接进行数据读写,如果客户端不进行读写了,那么这个线程也会等着,这样就会造成阻塞。
1、tcp代码
服务端:
//1、创建server端socket
ServerSocket serverSocket = new ServerSocket();
//2、绑定9000端口
serverSocket.bind(new InetSocketAddress(9000));
//3、监听端口(没有客户端连接时会阻塞)
Socket socket = serverSocket.accept();
//4、读客户端发来的消息(没有消息时会阻塞)
byte[] bytes = new byte[1024];
socket.getInputStream().read(bytes);
System.out.println("收到客户端消息:"+new String(bytes,StandardCharsets.UTF_8));
//5、响应客户端消息
socket.getOutputStream().write("我是服务端".getBytes(StandardCharsets.UTF_8));
//6、关闭socket
socket.close();
客户端
//1、创建socket
Socket socket = new Socket();
//2、连接服务端socket
socket.connect(new InetSocketAddress(9000));
//3、发送消息
socket.getOutputStream().write("我是客户端".getBytes(StandardCharsets.UTF_8));
//4、接收消息
byte[] bytes = new byte[1024];
socket.getInputStream().read(bytes);
System.out.println("收到服务端消息:"+new String(bytes,StandardCharsets.UTF_8));
//5、关闭socket
socket.close();
2、udp代码
服务端
//1、创建socket对象,指定监听的端口号
DatagramSocket socket = new DatagramSocket(9999);
byte[] buf = new byte[1024];
//2、创建packet,接收数据,指定字节数组buf接收数据,最多接收buf.length长度的数据
DatagramPacket packet = new DatagramPacket(buf, 0, buf.length);
//3、接收数据,数据存放在packet对象的buf数组中(socket中的receive方法会阻塞线程,等待客户端发送数据)
socket.receive(packet);
System.out.println("服务器接收到的数据为" + new String(buf,0, packet.getLength()));
//4、关闭socket
socket.close();
客户端
//1、创建socket负责发送数据
DatagramSocket socket = new DatagramSocket();
byte[] buf = "hello world".getBytes();
//2、打包好数据报,指定要发送到的ip和端口号
DatagramPacket packet = new DatagramPacket(buf, 0,buf.length, InetAddress.getByName("localhost"),9999);
//3、发送数据给服务器端
socket.send(packet);
//4、关闭socket
socket.close();
缺点:
- 每一个客户端建立连接后都需要创建独立的线程与客户端进行数据的读写,业务处理
- 当并发数较大时,会创建大量的流程来处理连接,系统资源会出现很大的开销
- 连接建立后,如果服务该客户端的线程没有数据可读时,线程则会阻塞在Read操作上,等待有数据后才读取,造成线程资源的浪费
二、同步非阻塞IO(NIO)
当用户进程发出read操作时,如果kernel中的数据还没有准备好,,那么他并不会block用户进程,而是立马返回一个 error,从用户角度来讲,他发起一个read操作后,并不需要等待,而是马上等到一个结果,用户进程判断是一个error时,他就知道数据换没有准备好,于是它再次发送read操作,一旦kermel中的数据准备好了,并且再次收到了用户进程的read,那么他此时就会将数据拷贝到用户内存,然后返回。
默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库。
Java NIO 系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输,Buffer 负责存储
1、通道Channel
java.nio.channels.Channel
包下,实现类为:
FileChannel
SocketChannel
ServerSocketChannel
DatagramChannel
2、缓冲区Buffer
(1)缓冲区类型
常用ByteBuffer
(2)缓冲区分配位置
堆内区域
通过 allocate()
方法分配缓冲区,将缓冲区建立在 JVM 的内存之中。
堆外区域
通过 allocateDirect()
方法分配缓冲区,将缓冲区建立在物理内存之中。
代码
//定义一个list保存建立连接的socketChannel
List<SocketChannel> channelList = new LinkedList<>();
//打开一个socket通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9000));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
while (true) {
//接收连接
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
System.out.println("连接成功");
socketChannel.configureBlocking(false);
//添加到channelList集合中
channelList.add(socketChannel);
}
//遍历集合进行数据读取
Iterator<SocketChannel> iterator = channelList.iterator();
while (iterator.hasNext()){
//非阻塞模式,read方法不会阻塞
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = iterator.next().read(byteBuffer);
if(len>0){
System.out.println("收到消息"+new String(byteBuffer.array()));
}else {
iterator.remove();
System.out.println("客户端断开连接");
}
}
}
缺点:
一直轮询耗费大量CPU。
三、IO多路复用
也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。这里复用的是指复用一个或几个线程,用一个或一组线程处理多个IO操作,减少系统开销小,不必创建和维护过多的进程/线程;
代码
//打开一个socket通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9000));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//创建epoll(打开selector处理channel)
Selector selector = Selector.open();
//把ServerSocketChannel注册到selector上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//阻塞等待需要处理的事件发生
selector.select();
//获取selector中注册的全部事件的 Selectionkey 实例
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//如果是Accept连接事件
if (selectionKey.isAcceptable()) {
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel)selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel1.accept();
socketChannel.configureBlocking(false);
//这里只注册了读事件,如果需要给客户端发数据可以注册写事件
socketChannel.register(selector,SelectionKey.OP_READ);
System.out.println("连接客户端");
} else if (selectionKey.isReadable()) {//如果是读事件,则进行读取
SocketChannel socketChannel1 = (SocketChannel)selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = socketChannel1.read(byteBuffer);
if(len>0){
System.out.println("收到消息"+new String(byteBuffer.array()));
}else {
iterator.remove();
System.out.println("客户端断开连接");
}
}
}
}
缺点:
当大量连接有读写事件时(一直无法执行到selector.select()),例如:有10万连接,有9万有读写事件,业务处理非常耗时,这会导致无法建立新的连接。
底层原理
IO多路复用使用两个系统调用(select/poll/epoll和recvfrom),blocking IO只调用了recvfrom;select/poll/epoll 核心是可以同时处理多个connection,而不是更快,所以连接数不高的话,性能不一定比多线程+阻塞IO好,多路复用模型中,每一个socket,设置为non-blocking,阻塞是被select这个函数block,而不是被socket阻塞的。
(1)select机制
客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和exceptfds(异常)。select会阻塞住监视3类文件描述符,等有数据、可读、可写、出异常 或超时、就会返回;返回后通过遍历fdset整个数组来找到就绪的描述符fd,然后进行对应的IO操作。
优点:
几乎在所有的平台上支持,跨平台支持性好
缺点:
- 由于是采用轮询方式全盘扫描,会随着文件描述符FD数量增多而性能下降。
- 每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间)
- 默认单个进程打开的FD有限制是1024个,可修改宏定义,但是效率仍然慢。
(2)poll机制
基本原理与select一致,只是没有最大文件描述符限制,因为采用的是链表存储fd。
(3)epoll机制
epoll之所以高性能是得益于它的三个函数
1、epoll_create()系统启动时,在Linux内核里面申请一个B+树结构文件系统,返回epoll对象,也是一个fd。
- epoll_ctl() 每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数。
- epoll_wait() 轮训所有的callback集合,并完成对应的IO操作
优点:
- 没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄
- 效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降
- 内核和用户空间mmap同一块内存实现
如图
四、aio(异步io)
异步 IO 是基于事件和回调机制实现的,也就是应⽤操作之后会直接返回,不会堵塞在那⾥,当后台处理完成,操作系统会通知相应的线程进⾏后续的操作。
⽬前来说 AIO 的应⽤还不是很⼴泛,Netty 之前也尝试使⽤过 AIO,对性能提升不大,所以⼜放弃了。
JAVA7的时候,基于异步Channel的IO,在java.nio.channels包下增加了多个以Asynchronous开头的channel接口和类用于AIO通信,Java7称它们为NIO.2。