1、Java的I/O演进之路
BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销
NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理
AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用
2、 伪异步I/O
伪异步I/O采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机
客户端:
public class SocketClient {
public static void main(String[] args) throws IOException {
System.out.println("客户端启动");
Socket socket=new Socket("127.0.0.1",8888);
// socket.bind(new InetSocketAddress("127.0.0.1",8888)); 会报错Address already in use: JVM_Bind
OutputStream out= socket.getOutputStream();
PrintStream ps=new PrintStream(out);
Scanner scanner=new Scanner(System.in);
while(true){
System.out.println("please enter in:");
String msg=scanner.nextLine();
ps.println(msg);
ps.flush();;
}
}
}
服务端:
public class SocketServer {
public static void main(String[] args) {
try{
System.out.println("服务端启动");
ServerSocket serverSocket=new ServerSocket(8888);
ServerSocketPoolHandler poolHandle=new ServerSocketPoolHandler(3,10);
while(true){
Socket socket= serverSocket.accept();
poolHandle.execute(new ServerSocketRunnable(socket));
}
} catch (Exception e) {
System.out.println("服务端接收到客户端请求失败");
e.printStackTrace();
}
}
}
启动服务端并启动多个客户端发送消息,由于核心线程数=最大线程数=3,当客户端数>3时,客户端的Socket任务会到线程池的阻塞队列中等待,关闭客户端,当客户端数<=3时,Socket任务将会被服务端处理
3、 NIO
面向缓冲区,基于通道的io操作,同步非阻塞的io,可配置socket为非阻塞模式
channel(通道):打开到IO设备(例如:文件、套接字)的连,可以从通道中读取或者写入数据【一个channel对应一个buffer 】
buffer(缓冲区,):一块写入/读取数据的内存
selector(选择器):检查一个或多个channel,并确定哪些通道已准备好读取/写入【一个selector对应多个channel】
注:Channel负责传输,Buffer负责存取数据
NIO和BIO区别:
1)bio以流方式处理数据,而NIO以数据块的方式
2)BIO同步阻塞,NIO同步非阻塞
3)bio基于字节流和字符流进行操作,而nio基于channel和buffer进行操作,数据从通道读到缓冲区,或者从缓冲区写入通道,Selector用于监听多个通道事件(如:连接请求、数据读写等),使用单线程就可以监听多个客户端通道
3.1 Buffer
常用Buffer类型:ByteBuffer,charBuffer,IntBuffer,LongBuffer,ShortBuffer,loatBuffer、DoubleBuffer,MapByteBuffer
缓冲区基本属性:
-
capacity(容量):allocate(int capacity);初始化是固定值
-
limit(限制):缓冲区中可以操作数据的大小,写模式大小等于buffer容量,读取模式时,等于写入的数据量
-
position(位置):下一个要读取或者写入的数据的索引。
-
mark(标记)和reset(重置):标记是一个索引,mark指定buffer中的一个特定的position,之后通过调用reset方法恢复到position【position為mark的位置】
-
flip:写模式切换成读模式
-
clear:清空所有的数据【允许所有数据被覆盖】,是下次put的时候【读模式切换为写模式】
-
compact:清空所有读取过的数据,没有读取的数据不会被清空,可以继续读取【读模式切换为写模式】
-
put:添加数据
-
rewind:数据可以重复读取,移动position=0;
-
remaining:写入模式的情况下,就是还可以写多少个,读模式下就是还可以读多少个
注: 1、标记、位置、限制、容量遵守不变式:
0<=mark<=position<=limit<=capacity
2、重复读取解决方案
-
通过rewind【从头开始】
-
从指定的地方读取重复数据,先通过mark,后通过reset
3、buffer读写数据遵循以下步骤
-
写入数据到Buffer
-
调用flip方法,转为读模式
-
从buffer中读取数据
-
clear()或者compact清除缓冲区–【如果还未进行put,则下步骤仍然可以获得get的数据】
ByteBuffer buffer=ByteBuffer.allocate(1024);执行完position=0,limit=1024 capacity=1024 //buffer.put("徐迎燕test".getBytes()); buffer.putInt(1234);//执行完position=4,limit=1024 capacity=1024 buffer.putChar('a');//执行完position=6,limit=1024 capacity=1024 //转为读模式 buffer.flip();//执行完position=0,limit=6 capacity=1024 System.out.println(buffer.getInt());//执行完position=4,limit=6 capacity=1024 //转为写模式 buffer.clear();//执行完position=0,limit=1024 capacity=1024 System.out.println(buffer.getInt());//执行完position=4,limit=1024 capacity=1024 System.out.println(buffer.getChar());//执行完position=0,limit=1024 capacity=1024 buffer.putChar('g');//执行完position=8,limit=1024 capacity=1024 //转为读模式 buffer.flip();//执行完position=0,limit=8 capacity=1024 System.out.println(buffer.getChar()); 最终数据结果打印 1234 1234 a 最后一次的结果未输出
3.2 channel
FileChannel:读取、写入,映射和操作文件的管道【阻塞】
SocketChannel:通过TCP读写网络中的数据通道
ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel
DatagramChannelChannel:通过udp读写网络中的数据通道
从目标通道中去复制原通道数据:targetChannel.transferFrom(srcChannel,srcChannel.position(),srcChannel.size());
把原通道数据复制到目标通道:srcChannel.transferTo(srcChannel.position(),srcChannel.size(),targetChannel);
分散读取(Scatter):是指把Channel通道的数据读入到多个缓冲区中去
聚集写入(Gather):是指将多个Buffer中的数据聚集到Channel
3.3 selector
选择器(Selector)是非阻塞IO的核心,是SelectableChannel对象的多路复用器,Selector可以同时监控多个SelectableChannel的IO状况,也就是说,利用Selector可使一个单独的线程管理多个Channel
优点:
- selector可以监听多个注册的通道上的事件
- 只有连接或者读写事件发生时才进行读写操作,减少开销,且不必为每个连接创建线程,不用维护多个线程
- 避免多线程上下切换导致的开销
selector应用:
1、创建selector
Selector selector = Selector.open();
2、向selector注册通道
ServerSocketChannel serverSocketChannel= ServerSocketChannel.open() ;//获取通道
serverSocketChannel.bind(new InetSocketAddress(2222));//绑定连接
serverSocketChannel.configureBlocking(false);//切换为非阻塞
Selector selector=Selector.open();//获取选择器
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);//将通道注册到选择器上,并且监听接收事件
3、轮询的获取选择器上已经准备就绪的事件
while(selector.select()>0){
Set<SelectionKey> selectionKeys = selector.selectedKeys();//获取当前选择器中所有注册的选择键(已就绪的监听事件)
Iterator<SelectionKey> keys = selectionKeys.iterator();
while(keys.hasNext()){
SelectionKey selectionKey = keys.next();
if(selectionKey.isAcceptable()){//接收事件
socketchannel.register(selector,SelectionKey.OP_READ);
}else if(selectionKey.isReadable()){//读事件
socketchannel.register(selector,SelectionKey.OP_WRITE);
}else if(selectionKey.isWritable()){//写事件
it.remove();// 15)取消选择键SelectionKey
}
}