一、概念
socke本身是阻塞的,非阻塞IO要求tsocket被设置为non-blocking。
- 系统调用:
1)在内核缓冲区没有数据的情况下,会立即返回一个调用失败的信息。
2)在内核缓冲区有数据的情况下,是阻塞的。直到内核缓冲区的数据全部复制到进程缓冲区,系统调用成功。
- NIO特点:
每次的IO调用,在内核数据未就绪的情况下,应用程序需要不停地进入IO调用,轮询查看数据是否就绪,如果没有就绪,继续轮询;如果就绪,才会返回。 - 优点:在内核缓冲区没有数据的情况下,发起的系统调用不会阻塞,用户程序不会阻塞。
- 缺点:需要不断地反复地发起IO调用,这种不断轮询,不断询问内核的方式,会占用CPU大量时间,资源利用率比较低;在内核缓冲区有数据的情况下,也是阻塞的。NIO模型在高并发场景下是不可用的。
- Java中NIO提供了选择器(Selector)类似操作系统提供的 select/epoll,也叫做IO多路复用器。作用是检查一个channel(通道)的状态是否是可读、可写、可连接、可接收的状态,为了实现单线程管理多个channel,也就是管理多个网络请求。
- Channel通道
即可以读,又可以写,不直接和数据源打交道,主要和缓冲区Buffer进行交互java.nio.channels
ServerSocketChannel 服务器端
SocketChannel 客户端 - Buffer缓冲区
IO流中的数据通过缓冲区交给Channel
二、NIO编程
- 服务器端
1)实例化ServerSocketChannel
2)绑定端口 通过ServerSocketChannel调用bind() 方法
3)设置ServerSocketChannel 为非阻塞 configir…
4)实例化Selector选择器
5)将ServerSocketChannel注册到选择器上ServerSocketChannel.register()
6)监听是否有新的事件 接收(连接)事件/读写事件/ Selector.select()
7)获取已完成事件的集合,对于这个集合进行遍历。
8)如果发现是Accept事件,则进行accept调用,获取SocketChannel,注册到Selector上,关注read事件
9)监听是否有read读事件
10)通过SocketChananel通道来读取数据,其中通过buffer作为传输介质
11)关闭资源
SocketChannel
Selector
ServerSocketChannel
public class MyNIOServer {
public static void main(String[] args) {
ServerSocketChannel serverSocketChannel = null;
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
try {
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9999));
System.out.println("服务器端已经启动...");
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//等待监听事件结果,这个select是一个阻塞的方法,直到有事件才会返回
while(selector.select() > 0){
//获取事件的集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
//遍历这个集合,选择事件
while(iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isAcceptable()){
//通过key获取通道channel
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
//接收连接
SocketChannel accept = channel.accept();
System.out.println("客户端:"+accept.getRemoteAddress()+"已连接...");
//设置accept为非阻塞
accept.configureBlocking(false);
//将accept通道注册到selector选择器上关注读事件
accept.register(selector, SelectionKey.OP_READ);
}
if(key.isReadable()){
//通过key获取通道channel
SocketChannel channel = (SocketChannel) key.channel();
//进行读取操作
int read = channel.read(readBuffer);
if(read == -1){
//通道关闭
channel.close();
//连接关闭
key.cancel();
continue;
}
//进行读写模式切换
readBuffer.flip();
//将数据从buffer读取
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
System.out.println("客户端:"+channel.getRemoteAddress()+",发送消息:"+new String(bytes, 0, bytes.length));
readBuffer.clear();
//将channel通道注册到selector选择上关注写事件
channel.register(selector, SelectionKey.OP_WRITE);
}
if(key.isWritable()){
//通过key获取通道channel
SocketChannel channel = (SocketChannel) key.channel();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String info = reader.readLine();
//将info写入channel
writeBuffer.put((info+"\n").getBytes());
writeBuffer.flip();
channel.write(writeBuffer);
writeBuffer.clear();
//将channel通道注册到selector选择上关注读事件
channel.register(selector, SelectionKey.OP_READ);
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(serverSocketChannel != null){
try {
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
- 客户端
1)实例化 SocketChannel
2)设置 SocketChannel 为非阻塞
3)实例化 Selector
4)连接服务器connect,在这个方法中提供ip地址和端口号,注意:这个方法不是一个阻塞方法,如果连接失败返回false,连接成功返回true。
5)如果是false,则将SocketChannel注册到Selector选择器中,监听connect可连接事件
6)监听selector中是否有可完成事件,遍历可完成事件的集合,判断该事件是否是可连接事件
7)connect方法返回true
8)给服务器端发送消息,channel.write
9)关闭资源,selector SocketChannel
public class MyNIOClient {
public static void main(String[] args) {
SocketChannel socketChannel = null;
try {
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
if(!socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999))){
socketChannel.register(selector, SelectionKey.OP_CONNECT);
while(selector.select() > 0){
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isConnectable()){
SocketChannel channel = (SocketChannel) key.channel();
//完成连接
channel.finishConnect();
System.out.println("客户端已启动...");
}
}
}
}
Scanner scanner = new Scanner(System.in);
while(true){
//连接成功,给服务器端发送消息
ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println("客户端要发送消息到服务器端:");
String info = scanner.nextLine();
if("".equals(info) || "exit".equals(info)){
break;
}
buffer.put(info.getBytes(StandardCharsets.UTF_8));
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
//获取服务器端的响应消息
int read = socketChannel.read(buffer);
if(read == -1){
break;
}
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
System.out.println("服务器端响应消息:"+new String(bytes, 0, bytes.length));
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(socketChannel != null){
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
多线程(Server端)
思路:主线程接收到客户端连接,将连接所对用的socketChannel交给子线程处理读写请求。
代码实现:
//子线程 实现Runnable接口
class NIOServerHandler implements Runnable{
private SocketChannel socketChannel;
//创建Selector实例
private Selector selector;
public NIOServerHandler(SocketChannel socketChannel){
this.socketChannel = socketChannel;
if(selector == null){
try {
selector = Selector.open();//创建IO复用器
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {//关注socketChannel通道的读写就绪事件,进而处理
try {
socketChannel.configureBlocking(false);//设置为非阻塞
socketChannel.register(selector, SelectionKey.OP_READ);//把要关注的读事件注册到selector上
while(selector.select() > 0){//有 读/写 事件发生
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isReadable()){//是可读事件
SocketChannel channel = (SocketChannel) key.channel();//获取key所对应的Channel
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
buffer.flip();//读写模式切换
byte[] bytes = new byte[buffer.remaining()];//buffer.remaining -> 可读大小
buffer.get(bytes);
String msg = new String(bytes, 0, bytes.length);
System.out.println(Thread.currentThread().getName()+", 客户端:"+channel.getRemoteAddress()+", 消息: "+msg);
buffer.clear();
buffer.put(("hello client\n").getBytes());
buffer.flip();
channel.write(buffer);
if("".equals(msg) || "exit".equals(msg)){
System.out.println(Thread.currentThread().getName()+"客户端:"+channel.getRemoteAddress()+"下线了");
key.cancel();
channel.close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//主线程
public class MyMultiThreadNIOServer {
public static void main(String[] args) {
ServerSocketChannel serverSocketChannel = null;
try {
//创建ServerSocketChannel
serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(9998));
//将SerSocketChannel置为非阻塞
serverSocketChannel.configureBlocking(false);
//创建Selector选择器
Selector selector = Selector.open();
//将serverSocketChannel注册到选择器上,关注可接受事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//使用固定数量的线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
while(selector.select() > 0){
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//获取当前迭代器
while(iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isAcceptable()){//是可接受事件
System.out.println(Thread.currentThread().getName()+"关注可接受事件");
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) key.channel();//获取通道
SocketChannel channel = serverSocketChannel1.accept();
System.out.println("客户端:"+channel.getRemoteAddress()+"已连接");
//将SocketChannel channel提交给子线程
pool.submit(new NIOServerHandler(channel));
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
NIO中重要组件
Channel
- 类似流,即可以从通道中读取数据,也可以写数据到通道中, 通道可以异步地读写,通道中的数据先要读取到一个Buffer中,或者从一个Buffer中写入。
读数据:将数据从 channel读到 buffer
写数据:从 buffer中写入到 channel通道中 - 常用实现类:
DatagramChannel 通过UDP的方式读写网络中的数据通道
SocketChannel 通过TCP的方式读写网络中的数据,一般用于客户端
ServerSocketChanneel 通过TCP的方式读写网络中的数据,一般用于服务器端
FileChannel 用于读写操作文件的通道
Buffer
- 缓冲区,与NIO Channel交互,数据是从通道读取进入缓冲区,从缓冲区写入到通道中。
- 使用Buffer注意点:
(1) 写数据到buffer
(2) 调用buffer.flip
(3) 从Buffer中读取数据
(4) buffer.clear/buffer.compact
当从buffer中读取数据,调用 clear方法清空buffer中的数据,或者调用 compact方法清空已经读过的数据。任何未读到数据会被移动缓冲区的起始位置,新写入的数据将放到缓冲区未读数据的后面。 - Buffer实现依赖3个指针 :position limit capacity
position:取决于Buffer处于读模式还是写模式,写数据到Buffer中,position表示当前位置,初始的值为0;读数据时,从某个特定位置开始去读,需要将buffer从写模式切换为读模式,position会被重置为0。
limit:写模式下,表示最多能往里写多少数据;读模式下,表示最多能读到的数据 。
capacity:
作为buffer内存块,有一个固定的大小
示例:
ByteBuffer.allocate(100);
1、position = 0 limit = capacity = 100
2、buffer.put(“hello\n”) position=6 limit = capacity = 100
3、buffer.flip() position=0 limit = position capacity = 100
4.buffer.get() 3 position=3 limit = 6 capacity = 100
- Buffer 的方法:
ByteBuffer.allocate() ;在堆上分配空间
ByteBuffer.allocateDirect() ;在堆外分配空间
ByteBuffer.wrap(byte[] bytes); 通过 byte数据创建一个缓冲区
flip()
capacity()
limit()
position()
Selector
- 选择器,也叫做多路复用器,作用是检查一个或多个channel通道是否处于可读、可写、可连接(管理多个网络请求)
- 优势:
单线程管理多个网络连接,相比于多线程使用了更多的线程,效率反而更高,减少了线程上下文切换带来的资源耗费。 - Selector的使用
(1) Selector.open();
(2) channel.register;(xxx, SelectionKey的四种事件)
(3) selector.select() ;这个方法是一个阻塞方法,如果有事件就绪则返回
- Selector维护三种类型的selectionKey集合
selector.selectedKeys();已选择键的集合
selector.keys();已注册键的集合
selector.cancelKey();已取消键的集合 - SelectionKey的四种事件
一个通道可以注册多个事件 通过SelectionKey.OP_READ | SelectionKey.OP_WRITE
NIO和BIO的比较
- BIO以 流的方式处理数据,NIO以 块的方式处理数据,块I/O的效率比流I/O高很多。
- BIO是阻塞的(线程等待数据,没有数据发送过来,继续等待),NIO是非阻塞的(通道没有数据时不用等待)。
- BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
NIO | BIO |
---|---|
面向缓冲区(Buffer) | 面向流(Stream) |
非阻塞(Non Blocking IO) | 阻塞(Blocking IO) |
选择器(Selectors) |