前言
在分布式系统横行的现在,传统阻塞式的I/O流在系统通信方面显得心有余而力不足,多线程的消耗是服务器支撑不起的,这时候,NIO应运而生,NIO又可以称为非阻塞式的IO,它类似于IO,如下,NIO包里有几个重要的概念:
buffer:NIO是基于缓冲的,buffer是最底层的必要类,这也是IO和NIO的根本不同,虽然stream等有buffer开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而NIO却是直接读到buffer中进行操作。
channel:类似于IO的stream,但是不同的是除了FileChannel,其他的channel都能以非阻塞状态运行。FileChannel执行的是文件的操作,可以直接DMA操作内存而不依赖于CPU。其他比如socketchannel就可以在数据准备好时才进行调用。
selector:用于分发请求到不同的channel,这样才能确保channel不处于阻塞状态就可以收发消息。
接下来,我们从代码中讨论他们各自的优缺点。
传统IO服务器-客户端通信
package OIO;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/*
* 单线程,只能有一个socket访问
*/
public class IOServer {
@SuppressWarnings("resource")
public static void main(String[] args) throws Exception {
//创建socket服务,监听10101端口
ServerSocket server = new ServerSocket(10101);
System.out.print("服务器启动!");
while(true){
//获取一个套接字(阻塞)
final Socket socket = server.accept();
System.out.println("来了一个新客户端!");
//业务处理
handler(socket);
}
}
public static void handler(Socket socket){
try {
byte[] bytes = new byte[1024];
InputStream inputStream = socket.getInputStream();
while(true){
//读取数据(阻塞)
int read = inputStream.read(bytes);
if(read != -1){
System.out.println(new String(bytes,0,read));
}else{
break;
}
}
} catch (Exception e) {
System.out.println("socket关闭!");
try {
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
接下来我们启动服务端,测试连接一下,可以看到控制台打印出了“来了一个新客户端!”。
那么,我们试着向服务器发送一条消息,会发现服务器控制台接收并显示了这条消息。
但是,在这种情况下,再次通过telnet连接该服务器,会发现连接不上,原因很简单,我们通过注释可以看到,这段代码中一共有两个阻塞点,一个是在sever.accept()的时候,他在等待客户端连接的请求,如果没有,他会一直阻塞下去直到超时;那么第二个阻塞点就是inputStream.read()的时候,字节流在读取数据的时候是阻塞的,看代码我们不难发现,如果内存中有可读取的字节,那么会读取出来,如果没有,该线程会一直阻塞在这里,直到有数据被读取,或者超时。
那么这段代码结论出来了,只有一个线程去服务一个客户端,就好比说,有一家饭店,只有一个服务生,这个服务生只服务第一个客人。
这样显然是不行的,那么怎么修改这段代码使之可以服务多个客人呢?在IO里,我们采用了多线程的方案,加入 一个线程池(多招聘几个服务生),接下来我们看一下,使用多线程能否解决这个问题。
package OIO;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*
* 加入了线程池,可接受多个socket客户端的消息;
* 但是一个线程只能为一个socket服务
*/
public class IOServer {
@SuppressWarnings("resource")
public static void main(String[] args) throws Exception {
//创建线程池
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//创建socket服务,监听10101端口
ServerSocket server = new ServerSocket(10101);
System.out.print("服务器启动!");
while(true){
//获取一个套接字(阻塞)
final Socket socket = server.accept();
System.out.println("来了一个新客户端!");
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
//业务处理
handler(socket);
}
});
}
}
public static void handler(Socket socket){
try {
byte[] bytes = new byte[1024];
InputStream inputStream = socket.getInputStream();
while(true){
//读取数据(阻塞)
int read = inputStream.read(bytes);
if(read != -1){
System.out.println(new String(bytes,0,read));
}else{
break;
}
}
} catch (Exception e) {
System.out.println("socket关闭!");
try {
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
我们可以看到,这段代码和之前相比,仅仅是加入了一个线程池,将handler方法加入线程池中去执行。启动后,结果如下:
发现可以执行多个客户端的请求了,虽然解决了这个问题,但这样真的可以吗?我们还是从代码去分析,加入了线程池,当有大规模的访问时,会消耗掉大量的资源,并且会影响新线程。这就好比是一个餐厅有十个服务员(对于一个服务器,线程是有上限的),还是每个线程响应一个客户端(每个服务员只能服务一桌客人,不能离开),如果此时请求连接的客户端过多(吃饭的人太多),服务器还是会挂(没有多余的服务生去服务后来的客人)。那现在怎么办呢?非阻塞式的NIO应运而生。
NIO
NIO作为非阻塞式的IO,它的优点就在于,1、它由一个专门的线程去处理所有的IO事件,并负责分发;2、事件驱动,只有事件到了才会触发,而不是同步的监听这个事件;3、线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。话不多说,看代码。
package 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 {
//通道管理器
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
*/
@SuppressWarnings("unchecked")
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();
// 客户端请求连接事件
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key
.channel();
// 获得和客户端连接的通道
SocketChannel channel = server.accept();
// 设置成非阻塞
channel.configureBlocking(false);
//在这里可以给客户端发送信息
channel.write(ByteBuffer.wrap(new String("向客户端发送了一条信息").getBytes()));
//在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ);
// 获得了可读的事件
} else if (key.isReadable()) {
read(key);
}
}
}
}
/**
* 处理读取客户端发来的信息 的事件
* @param key
* @throws IOException
*/
public void read(SelectionKey key) throws IOException{
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服务端收到信息:"+msg);
ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
channel.write(outBuffer);// 将消息回送给客户端
}
/**
* 启动服务端测试
* @throws IOException
*/
public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.initServer(8000);
server.listen();
}
}
这种方式就简单了,我们通过代码可以看到,将通道设置为非阻塞,并将访问事件注册到通道管理器中,同时,listen()方法在同步的监听,并且会一直阻塞在selector.select()方法,一旦注册的事件进入,迭代器会接收到该请求的key,并判断该key是访问accept类型的还是read类型的,进而执行相应的方法,只要是有注册事件的访问,该线程就会一直执行,直到无注册事件访问,线程继续阻塞。
总结
并不是说有了NIO,传统的IO就毫无用处了,当我们在执行持续性的操作(如上传下载)时,IO的方式是要优于 NIO的。分清情况,合理选用。
分布式的网络通信netty就是基于NIO的这种机制,在接下来的博客会说到netty通信的一些总结,欢迎大家斧正。