【JAVA】传统的IO和NIO区别

前言

        在分布式系统横行的现在,传统阻塞式的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通信的一些总结,欢迎大家斧正。

猜你喜欢

转载自blog.csdn.net/sds15732622190/article/details/78515187