在第一篇里面我们了解了传统IO,这篇我们就来介绍一下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;
/**
* NIO服务端
*
* @author -Jimmy_zjf888-
*/
public class NIOServer {
// 通道管理器
private Selector selector;
/**
* 获得一个ServerSocket通道,并对该通道做一些初始化的工作
* @param port绑定的端口号
* @throws IOException
*/
public void initServer(int port) throws IOException {
// 获得一个ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置通道为非阻塞
serverChannel.configureBlocking(false);
// 将该通道对应的ServerSocket绑定到port端口
serverChannel.socket().bind(new InetSocketAddress(port));
// 获得一个通道管理器
this.selector = Selector.open();
// 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
// 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
*
* @throws IOException
*/
public void listen() throws IOException {
System.out.println("服务端启动成功!");
// 轮询访问selector
while (true) {
// 当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// 获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator<?> ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
handler(key);
}
}
}
/**
* 处理请求
*
* @param key
* @throws IOException
*/
public void handler(SelectionKey key) throws IOException {
// 客户端请求连接事件
if (key.isAcceptable()) {
handlerAccept(key);
// 获得了可读的事件
} else if (key.isReadable()) {
handelerRead(key);
}
}
/**
* 处理连接请求
*
* @param key
* @throws IOException
*/
public void handlerAccept(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 获得和客户端连接的通道
SocketChannel channel = server.accept();
// 设置成非阻塞
channel.configureBlocking(false);
// 在这里可以给客户端发送信息哦
System.out.println("新的客户端连接");
// 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ);
}
/**
* 处理读的事件
*
* @param key
* @throws IOException
*/
public void handelerRead(SelectionKey key) throws IOException {
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if(read > 0){
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服务端收到信息:" + msg);
//回写数据
ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());
channel.write(outBuffer);// 将消息回送给客户端
}else{
System.out.println("客户端关闭");
key.cancel();
}
}
/**
* 启动服务端测试
*
* @throws IOException
*/
public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.initServer(8888);
server.listen();
}
}
运行上述代码,发现selector.select();这里是一个阻塞点
OK,我们看看能否正常运行吧,做程序就是这样,跑一下测试一下,这里我们测试的代码运行一下也不会怎么样,为了学习嘛
OK,我们看看能否正常运行吧,做程序就是这样,跑一下测试一下,这里我们测试的代码运行一下也不会怎么样,为了学习嘛
学到这里我们总结总结一下吧,传统IO单线程情况下只能有一个客户端,多线程的情况下可以有多个客户端,但非常消耗内存,但NIO就不一样了,他是全能的,一个线程就可以接入多个客户端,为了解释清楚笔者就用如下图片来描述传统IO和NIO的区别吧
如图所示,整个办公室就是我们的系统,ServerSocket就是我们的前台大厅,他监听着端口,
看看有没有客人来,客人我们就理解为socket吧,传统的多线程是不是就是需要多个线程给多个客户端服务啊,要是公司招那么多前台妹子是不是要花一笔不小的开支啊,虽然乐坏了我们这些程序猿了,但对公司来说就是一笔不小的开支,如图所示公司就那么大,人太多都装不下了,连前台妹子都被挤到外面去了( >﹏<。) ,那么介绍完传统IO以后,我们来介绍介绍我们的必杀技NIO吧,如图所示。
如上图所示,整个办公室就是我们的系统,ServerSocketChannel就是我们的前台大厅,他监听着端口,看看有没有客人来,客人我们就理解为socket吧,与传统IO不同的是,咱们公司的妹子(线程池)不需要那么多了,这次我们的前台经过特殊培训,她已经不是一般能力的前台妹子了(神马妹子都升级了?能力惊人的逆天),她比较牛能有同时服务多个客人的能力,同时她还可以监听前台大厅,看看有没有新的客人进来,那么她是怎么欢迎新来的可能呢?
//首先我们先让她知道前台大厅在哪
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置通道为非阻塞的,当然也只能设为非阻塞的,设为true会抛异常的
serverChannel.configureBlocking(false);
// 获得一个通道管理器
this.selector = Selector.open();
// 讲大厅交给一个selector,这个selector我们就想象成一个前台妹子吧,SelectionKey我们就理解为一个标记吧
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
通过上述相信你们应该NIO有所了解了吧,是不是比对着电子书看有意思多了,有意思给个赞把,对了笔者讲讲代码中的几个关键点吧,也是在学习过程中发现的,当然如上代码已经进行bug修复
1.当关闭客户端窗口的时候会报错?为什么会报错
答:那是因为客户端关闭的时候我们还在给它写数据,解决方案:
int read = channel.read(buffer);
if(read > 0){
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服务端收到信息:" + msg);
//回写数据
ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());
channel.write(outBuffer);// 将消息回送给客户端
}else{
System.out.println("客户端关闭");
key.cancel();
}
2.我们发现selector.select();这里是一个阻塞点,那么为什么说NIO是非阻塞的?
答:这里我们所说的阻塞并不是select阻塞,而是channel.read(buffer)的时候是否阻塞,而传统IO是会阻塞在那里的,当然我们这里的select可以说是阻塞的,也可以说是非阻塞的,为什么这么说呢?其实select也有其他方法
1).selector.select(timeOut);这里有个带毫秒值的方法,当然这里阻塞timeOut毫秒后会返回,只不过返回0而已
2).selector.wakeup();也可以唤醒selector(官方描述:如果当前的select是阻塞的,用.select()或者select(timeOut),这个方法调用后这些方法都会立马返回结果值),当然在后面讲解Netty源码的时候我会讲解该方法,让大家更好的去理解,这里就不多说了。
3.key.OP_WRITE事件神马时候注册呢?
答:其实真正写的时候很少用,其实WRITE主要描述底层缓存区是否有空间,当然正常的时候缓冲区都是存在足够的空间的,如果存在空间就返回true