nio简介
bio必须一个线程处理一个客户端连接,并发低。
nio:非阻塞io,单线程多路复用(多个客户端复用一个线程)。相比bio支持更高并发。
nio服务端只用一个线程。上图是一个简易的线程模型图,实际服务端和客户端都有自己的selector和socketChannel。此图只为突出服务端只用一个线程处理客户都多个连接。后面看完实例和请求过程就可用详细了解。
NIO实例
maven依赖:
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.35.Final</version>
</dependency>
</dependencies>
实例也可从我github上下载:https://github.com/chendaliu/architectCourse/tree/master/nettyDemo/src/main/java/com/czl/netty/nio
1.server端NIOServer.java:
package com.czl.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NIOServer {
//public static ExecutorService pool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws IOException {
// 创建一个在本地端口进行监听的服务Socket通道.并设置为非阻塞方式
ServerSocketChannel ssc = ServerSocketChannel.open();
//必须配置为非阻塞才能往selector上注册,否则会报错,selector模式本身就是非阻塞模式
ssc.configureBlocking(false);
ssc.socket().bind(new InetSocketAddress(9000));
// 创建一个选择器selector
Selector selector = Selector.open();
// 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
System.out.println("等待事件发生。。");
// 轮询监听channel里的key,select是阻塞的,accept()也是阻塞的
int select = selector.select();
System.out.println("有事件发生了。。");
// 有客户端请求,被轮询监听到
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
//删除本次已处理的key,防止下次select重复处理
it.remove();
handle(key);
}
}
}
private static void handle(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
System.out.println("有客户端连接事件发生了。。");
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为是发生了连接事件,所以这个方法会马上执行完,不会阻塞
//处理完连接请求不会继续等待客户端的数据发送
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//通过Selector监听Channel时对读事件感兴趣
sc.register(key.selector(), SelectionKey.OP_READ);
} else if (key.isReadable()) {
System.out.println("有客户端数据可读事件发生了。。");
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//NIO非阻塞体现:首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定是发生了客户端发送数据的事件
int len = sc.read(buffer);
if (len != -1) {
System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len));
}
ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());
sc.write(bufferToWrite);
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} else if (key.isWritable()) {
SocketChannel sc = (SocketChannel) key.channel();
System.out.println("write事件");
// NIO事件触发是水平触发
// 使用Java的NIO编程的时候,在没有数据可以往外写的时候要取消写事件,
// 在有数据往外写的时候再注册写事件
key.interestOps(SelectionKey.OP_READ);
//sc.close();
}
}
}
2.client端NioClient.java
package com.czl.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NioClient {
//通道管理器
private Selector selector;
/**
* 启动客户端测试
*
* @throws IOException
*/
public static void main(String[] args) throws IOException {
NioClient client = new NioClient();
client.initClient("127.0.0.1", 9000);
client.connect();
}
/**
* 获得一个Socket通道,并对该通道做一些初始化的工作
*
* @param ip 连接的服务器的ip
* @param port 连接的服务器的端口号
* @throws IOException
*/
public void initClient(String ip, int port) throws IOException {
// 获得一个Socket通道
SocketChannel channel = SocketChannel.open();
// 设置通道为非阻塞
channel.configureBlocking(false);
// 获得一个通道管理器
this.selector = Selector.open();
// 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
//用channel.finishConnect() 才能完成连接
channel.connect(new InetSocketAddress(ip, port));
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
channel.register(selector, SelectionKey.OP_CONNECT);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
*
* @throws IOException
*/
public void connect() throws IOException {
// 轮询访问selector
while (true) {
selector.select();
// 获得selector中选中的项的迭代器
Iterator<SelectionKey> it = this.selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
// 删除已选的key,以防重复处理
it.remove();
// 连接事件发生
if (key.isConnectable()) {
SocketChannel channel = (SocketChannel) key.channel();
// 如果正在连接,则完成连接
if (channel.isConnectionPending()) {
channel.finishConnect();
}
// 设置成非阻塞
channel.configureBlocking(false);
//在这里可以给服务端发送信息哦
ByteBuffer buffer = ByteBuffer.wrap("HelloServer".getBytes());
channel.write(buffer);
//在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ); // 获得了可读的事件
} else if (key.isReadable()) {
read(key);
}
}
}
}
/**
* 处理读取服务端发来的信息 的事件
*
* @param key
* @throws IOException
*/
public void read(SelectionKey key) throws IOException {
//和服务端的read方法一样
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = channel.read(buffer);
if (len != -1) {
System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));
}
}
}
跑起实例
先跑起NIOServer。控制台“等待事件发生。。”说明启动成功。
然后启动NioClient。控制台“客户端收到信息:HelloClient”说明启动成功。
调试查看请求流程
停掉所有服务,debug启动NIOServer,然后debug启动NioClient。需要耐心debug。可按照如下请求过程debug
过程:
# selector+selectionKey(SocketChannel+selector)可监听连接、读、写 1.服务端:启动,新建ServerSocketChannel,注册到selector,生成selectionKey(ServerSocketChannel+selector),负责监听连接事件。 2.客户端:启动,新建SocketChannel和selector,然后与服务端端口建立连接。 3.服务端:selector监听到连接,取出第1步的selectionKey,取到ServerSocketChannel,用ServerSocketChannel新建一个SocketChannel,注册到selector负责监听读操作。 4.客户端:建立连接成功后,把SocketChannel注册到客户端的selector,生成selectionKey,负责监听连接事件。 5.客户端:监听到第4步连接成功。取出第4步新建的selectionKey,取出SocketChannel,向该Channel写入"HelloServer",并把该Channel注册到selector,负责监听读事件。 6.服务端:第3步中监听读事件的selector,监听到第5步客户端的事件。从selector中取出channel(第3步中的那个channel),从通道中读取到数据"HelloServer"。然后向通道写数据"HelloClient"。 7.客户端:第5步中最后负责监听的selector,监听到第6步中服务端的数据,收到"HelloClient"。客户端完成,客户端的selector继续轮询事件。 8.服务端:监听到第6步中自己的写事件,取到channel,取消监听写事件,只监听读事件。
我就是这么debug一遍了解流程的