阻塞式IO
阻塞式IO即在进行IO时,不能同时进行其它的计算任务。因此即使是在使用多线程的情况下,如果有多个IO操作同时进行,也可能导致CPU被占用且闲置,出现CPU利用率不高的情况。一个阻塞式多线程IO示例图如下:
为了解决上述问题,加入了Selector(选择器)进行协调。通过将每一个Channel(通道)都注册到选择器上,选择器的作用即监视这些通道的IO情况。当某一个IO请求事件完全准备就绪时,选择器才会将其任务分配到服务端的一个或者多个线程上再去运行。
使用阻塞式NIO单线程传递一张图片的示例代码如下:
客户端:
@Test
public void client() throws IOException {
//1.获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
FileChannel inChannel = FileChannel.open(Paths.get("1.png"), StandardOpenOption.READ);
//2.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//3.读取本地文件,并发送到服务端
while (inChannel.read(buf) != -1) {
buf.flip();
sChannel.write(buf);
buf.clear();
}
//4.关闭通道
inChannel.close();
sChannel.close();
}
服务端:
@Test
public void server() throws IOException {
//1.获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//2.绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//3.获取客户端连接的通道
SocketChannel socketChannel = ssChannel.accept();
//4.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//5.结构客户端的数据,并保存到本地
while (socketChannel.read(buf)!=-1) {
buf.flip();
outChannel.write(buf);
buf.clear();
}
//6.关闭通道
socketChannel.close();
outChannel.close();
ssChannel.close();
}
若要在客户端接收服务端的反馈,需要客户端显式调用shutdownOutput()方法,告诉服务端数据已经传输完毕。改进的代码变化不大(客户端也可以使用通道之间使用直接内存进行传递的方式)。
客户端:
@Test
public void client() throws IOException {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
FileChannel inChannel = FileChannel.open(Paths.get("1.png"), StandardOpenOption.READ);
inChannel.transferTo(0, inChannel.size(), socketChannel); //没有声明缓冲区,可以直接使用通道传输(直接内存)
socketChannel.shutdownOutput();//显式调用shutdownOutput()告诉服务端数据已经传输完毕
//接收服务端的反馈
ByteBuffer buf = ByteBuffer.allocate(1024);
while (socketChannel.read(buf) != -1) {
buf.flip();
System.out.println(new String(buf.array(), 0, buf.position()));
buf.clear();
}
socketChannel.close();
inChannel.close();
}
服务端:
@Test
public void server() throws IOException {
ServerSocketChannel ssChannel = ServerSocketChannel.open();
FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
ssChannel.bind(new InetSocketAddress(9898));
SocketChannel socketChannel = ssChannel.accept();
ByteBuffer buf = ByteBuffer.allocate(1024);
while (socketChannel.read(buf) != -1) {
buf.flip();
outChannel.write(buf);
buf.clear();
}
//发送反馈给客户端
buf.put("服务端接受数据成功".getBytes());
buf.flip();
socketChannel.write(buf);
socketChannel.close();
outChannel.close();
ssChannel.close();
}
非阻塞式IO
关于非阻塞式IO,使用的关键点如下:
1.对于一个通道,若要切换到非阻塞模式,需要调用方法configureBlocking(false),将通道置为非阻塞。
2.服务端需要声明一个Selector(选择器对象),可以调用Selector.open()方法获得。
3.需要将通道注册到选择器上,需要调用某通道对象的register方法。该方法需要指定注册到哪一个选择器,并且需要指定SelectionKey(注册哪一种事件)。可以监听的事件类型共有4种,在SelectionKey中用4个常量表示,可以用|连接多种状态。
- 读:SelectionKey.OP_READ 1
- 写:SelectionKey.OP_WRITE 4
- 连接:SelectionKey.OP_CONNECT 8
- 接收:SelectionKey.OP_ACCEPT 16
SelectionKey表示SelectableChannel和Selector之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。SelectionKey类提供了以下主要方法:
客户端
@Test
public void client() throws IOException {
//1.获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
//2.切换非阻塞模式
socketChannel.configureBlocking(false);
//3.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//4.发送数据给服务端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String str = scanner.next();
buf.put((new Date().toString()+":"+str).getBytes());
buf.flip();
socketChannel.write(buf);
buf.clear();
}
//5.关闭通道
socketChannel.close();
}
服务端(和epoll的实现原理类似,可以理解为epoll的简化版本)
@Test
public void server() throws IOException {
//1.获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2.切换非阻塞模式
serverSocketChannel.configureBlocking(false);
//3.绑定连接
serverSocketChannel.bind(new InetSocketAddress(9898));
//4.获取选择器
Selector selector = Selector.open();
//5.将通道注册到选择器上,并且指定“监听事件”
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//第二个参数时选择键,用于通道向选择器注册哪种事件,可以将多个事件使用|进行组合。
//6.轮询式获取选择器上已经“准备就绪”的事件
while (selector.select() > 0) {
//7.获取当前选择器中所有注册的“选择键()已经就绪的事件”
Iterator iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
//8.获取“准备就绪”的事件
SelectionKey sk = iterator.next();
//9.判断具体时什么事件准备就绪
if (sk.isAcceptable()) {
//10.若“接收事件就绪”,获取客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
//11.切换非阻塞模式
socketChannel.configureBlocking(false);
//12.将该通道注册到选择器上
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
//13.获取当前选择器上“读就绪”状态的通道
SocketChannel socketChannel = (SocketChannel) sk.channel();
//14.读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len;
while ((len = socketChannel.read(buf)) > 0) {
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
}
//15.取消选择键SelectionKey
iterator.remove();
}
}
}
上方使用的都是基于TCP协议的通道,在Java NIO中的DatagramChannel时一个能收发UDP包的通道。使用方式也很简单,只需要将网络通道声明为DatagramChannel即可,其它基本相同。
客户端
@Test
public void send() throws IOException {
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String str = scanner.next();
buf.put((new Date().toString() + ":\n" + str).getBytes());
buf.flip();
dc.send(buf, new InetSocketAddress("127.0.0.1", 9898));
buf.clear();
}
dc.close();
}
服务端
@Test
public void receive() throws IOException {
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
dc.bind(new InetSocketAddress(9898));
Selector selector = Selector.open();
dc.register(selector, SelectionKey.OP_READ);
while (selector.select() > 0) {
Iterator iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
if (sk.isReadable()) {
ByteBuffer buf = ByteBuffer.allocate(1024);
dc.receive(buf);
buf.flip();
System.out.println(new String(buf.array(), 0, buf.limit()));
buf.clear();
}
}
}
}