JDK原生网络编程-NIO基础入门

文章目录

  • 初识NIO
    • 什么是NIO
    • NIO和BIO的主要区别
      • 面向流与面向缓冲
      • 阻塞与非阻塞
      • Selector选择器机制
  • NIO核心组件
    • Selector
      • 关于SelectionKey
      • SelectionKey类型
      • 服务端和客户端分别感兴趣的类型
    • Channels
    • buffer缓冲区
      • 重要属性
      • Buffer的分配
      • Buffer的读写
      • Buffer方法总结
  • NIO之Reactor模式
    • 单线程 Reactor 模式流程
    • 单线程Reactor,工作者线程池
    • 多Reactor线程模式
  • NIO实战示例
    • 服务端
    • 客户端
    • 测试


初识NIO

什么是NIO

NIO 库是在JDK 1.4 中引入的,为弥补了原先 I/O 的不足,它在标准Java代码中提供了高速的、面向块的 I/O。NIO可以翻译成 no-blocking io或者new io

NIO和BIO的主要区别

面向流与面向缓冲

JDK提供的NIO和BIO之间最大的一个区别是,BIO是面向流的,而NIO则是面向缓冲区的。BIO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。除此之外,它也不能前后移动流中的数据。如果需要前后移动从流中读取的数据,则需要先将它缓存到一个缓冲区。而NIO 的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据

阻塞与非阻塞

BIO属于同步阻塞模型,意味着,当一个线程调用read()或者write()方法时,该线程被阻塞,直到有一些数据被读取,或数据完全写入,否则该线程在此期间不能再干任何事情了
NIO属于非阻塞模型,一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而且不会让线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO 操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)

Selector选择器机制

NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道

NIO核心组件

Selector

Selector的英文含义是“选择器”,也可以称为为“轮询代理器”、“事件订阅器”、“channel容器管理机”都行

应用程序将向Selector选择器注册需要它关注的Channel,以及对应的Channel会对哪些IO事件感兴趣。Selector中也会维护一个“已经注册的Channel”的容器

关于SelectionKey

SelectionKey是一个抽象类,表示selectableChannel在Selector中注册的标识.每个Channel 向 Selector 注册时,都将会创建一个 SelectionKey。SelectionKey将Channel与Selector 建立了关系,并维护了channel事件。可以通过cancel方法取消键,取消的键不会立即从selector 中移除,而是添加到cancelledKeys中,在下一次select操作时移除它,所以在调用某个 key时,需要使用isValid进行校验

SelectionKey类型

在向Selector对象注册感兴趣的事件时,NIO共定义了四种:OP_READ、OP_WRITE、 OP_CONNECT、OP_ACCEPT(定义在 SelectionKey类中),分别对应读、写、请求连接、接受连接等网络Socket操作

操作类型 就绪条件及说明
OP_READ 当操作系统读缓冲区有数据可读时就绪。并非时刻都有数据可读,所 以一般需要注册该操作,仅当有就绪时才发起读操作,有的放矢,避免浪 费 CPU
OP_WRITE 当操作系统写缓冲区有空闲空间时就绪。一般情况下写缓冲区都有空 闲空间,小块数据直接写入即可,没必要注册该操作类型,否则该条件不 断就绪浪费 CPU;但如果是写密集型的任务,比如文件下载等,缓冲区很 可能满,注册该操作类型就很有必要,同时注意写完后取消注册
OP_CONNECT 当 SocketChannel.connect()请求连接成功后就绪。该操作只给客户端 使用
OP_ACCEPT 当接收到一个客户端连接请求时就绪。该操作只给服务器使用

服务端和客户端分别感兴趣的类型

ServerSocketChannel 和 SocketChannel 可以注册自己感兴趣的操作类型,当对应操作类 型的就绪条件满足时 OS 会通知 channel,下表描述各种 Channel 允许注册的操作类型,Y 表 示允许注册,N 表示不允许注册,其中服务器SocketChannel 指由服务器 ServerSocketChannel.accept()返回的对象

OP_READ OP_WRITE OP_CONNECT OP_ACCEPT
服务器 ServerSocketChannel Y
服务器 SocketChannel Y Y
客户端 SocketChanne Y Y Y

服务器启动 ServerSocketChannel,关注 OP_ACCEPT 事件

客户端启动 SocketChannel,连接服务器,关注 OP_CONNECT 事件

服务器接受连接,启动一个服务器的 SocketChannel,这个 SocketChannel 可以关注 OP_READ、OP_WRITE 事件,一般连接建立后会直接关注 OP_READ 事件

客户端这边的客户端 SocketChannel 发现连接建立后,可以关注 OP_READ、OP_WRITE 事件,一般是需要客户端需要发送数据了才关注 OP_READ 事件

连接建立后客户端与服务器端开始相互发送消息(读写),根据实际情况来关注OP_READ、 OP_WRITE 事件

Channels

Channels称为通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操 作系统)。那么既然是和操作系统进行内容的传递,那么说明应用程序可以通过通道读取数 据,也可以通过通道向操作系统写数据,而且可以同时进行读写

  1. 所有被 Selector(选择器)注册的通道,只能是继承了 SelectableChannel 类的子类
  2. ServerSocketChannel:应用服务器程序的监听通道。只有通过这个通道,应用程序才 能向操作系统注册支持“多路复用 IO”的端口监听。同时支持 UDP 协议和 TCP 协议
  3. ScoketChannel:TCP Socket 套接字的监听通道,一个 Socket 套接字对应了一个客户 端 IP:端口 到 服务器 IP:端口的通信连接

通道中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入

buffer缓冲区

Buffer 用于和 NIO 通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的。 以写为例,应用程序都是将数据写入缓冲,再通过通道把缓冲的数据发送出去,读也是一样, 数据总是先从通道读到缓冲,应用程序再读缓冲的数据
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存( 其实就是数组)。 这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存

重要属性

capacity

作为一个内存块,Buffer 有一个固定的大小值,也叫“capacity”.你只能往里写 capacity 个 byte、long,char 等类型。一旦 Buffer 满了,需要将其清空(通过读数据或者清除数据) 才能继续写数据往里写数据

position
当你写数据到 Buffer 中时,position 表示当前能写的位置。初始的 position 值为 0.当一 个 byte、long 等数据写到 Buffer 后, position 会向前移动到下一个可插入数据的 Buffer 单 元。position 最大可为 capacity – 1

当读取数据时,也是从某个特定位置读。当将 Buffer 从写模式切换到读模式,position 会被重置为 0. 当从 Buffer 的 position 处读取数据时,position 向前移动到下一个可读的位置

limit
在写模式下,Buffer 的 limit 表示你最多能往 Buffer 里写多少数据。 写模式下,limit 等 于 Buffer 的 capacity。当切换 Buffer 到读模式时, limit 表示你最多能读到多少数据。因此,当切换 Buffer 到 读模式时,limit 会被设置成写模式下的 position 值。换句话说,你能读到之前写入的所有数 据(limit 被设置成已写数据的数量,这个值在写模式下就是 position)

Buffer的分配

堆内存分配

要想获得一个 Buffer 对象首先要进行分配。 每一个 Buffer 类都有allocate方法,分配 48 字节 capacity 的 ByteBuffer 的例子:

ByteBuffer buf = ByteBuffer.allocate(48);

直接内存分配

HeapByteBuffer 与 DirectByteBuffer,在原理上,前者可以看出分配的 buffer 是在 heap 区域的,其实真正 flush 到远程的时候会先拷贝到直接内存,再做下一步操作;在 NIO 的框 架下,很多框架会采用 DirectByteBuffer 来操作,这样分配的内存不再是在 java heap 上,经 过性能测试,可以得到非常快速的网络交互,在大量的网络交互下,一般速度会比 HeapByteBuffer 要快速好几倍

直接内存与堆内存比较

直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
直接内存 IO 读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显

Buffer的读写

向Buffer中写数据

读取Channel写到Buffer

//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);

通过Buffer的put()方法写到Buffer

//将字节数组复制到缓冲区
writeBuffer.put(bytes);

flip()方法

flip()方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值

public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
}

从Buffer中读取数据

从Buffer读取数据写入到Channel

//发送缓冲区的字节数组
channel.write(writeBuffer);

使用get()方法从Buffer中读取数据

byte aByte = buffer.get()

使用Buffer读写数据常见步骤

  1. 写入数据到Buffer
  2. 调用flip()方法
  3. 从Buffer中读取数据
  4. 调用clear()方法或者compact()方法

当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面

Buffer方法总结

limit(), limit(1 0)等 其中读取和设置这 4 个属性的方法的命名和 jQuery 中的 val(),val(10)类似,一个负 责 get,一个负责 set
reset() 把 position 设置成 mark 的值,相当于之前做过一个标记,现在要退回到之前标记 的地方
clear() position = 0;limit = capacity;mark = -1; 有点初始化的味道,但是并不影响底 层 byte 数组的内容
flip() limit = position;position = 0;mark = -1; 翻转,也就是让 flip 之后的 position 到 limit 这块区域变成之前的 0 到 position 这块,翻转就是将一个处于存数据状态的缓 冲区变为一个处于准备取数据的状态
rewind() 把 position 设为 0,mark 设为-1,不改变 limit 的值
remaining() return limit - position;返回 limit 和 position 之间相对位置差
hasRemaining () return position < limit 返回是否还有未读内容
compact() 把从 position 到 limit 中的内容移到 0 到 limit-position 的区域内,position 和 li mit 的取值也分别变成 limit-position、capacity。如果先将 positon 设置到 limit,再 c ompact,那么相当于 clear()
get() 相对读,从 position 位置读取一个 byte,并将 position+1,为下次读写作准备
get(int index) 绝对读,读取 byteBuffer 底层的 bytes 中下标为 index 的 byte,不改变 position
get(byte[] dst, int offset, int len gth) 从 position 位置开始相对读,读 length 个 byte,并写入 dst 下标从 offset 到 offs et+length 的区域
put(byte b) 相对写,向 position 的位置写入一个 byte,并将 postion+1,为下次读写作准备
put(int index, byte b) 绝对写,向 byteBuffer 底层的 bytes 中下标为 index 的位置插入 byte b,不改变 p osition
put(ByteBuffer src) 用相对写,把 src 中可读的部分(也就是 position 到 limit)写入此 byteBuffer
put(byte[] src, int offset, int len gth) 从 src 数组中的 offset 到 offset+length 区域读取数据并使用相对写写入此 byteBuffer
wrap(byte[] array)、 wrap(byte [] array, int offset, int length) 把一个 byte 数组或 byte 数组的一部分包装成 ByteBuffer

NIO之Reactor模式

“反应”即“倒置”,“控制逆转”,具体事件处理程序不调用反应器,而向反应器注册一个事件处理器,表示自己对某些事件感兴趣,有时间来了,具体事件处理程序通过事件处理器对某个指定的事件发生做出反应

单线程 Reactor 模式流程

  1. 服务器端的 Reactor 是一个线程对象,该线程会启动事件循环,并使用 Selector(选 择器)来实现 IO 的多路复用。注册一个 Acceptor 事件处理器到 Reactor 中,Acceptor 事件处 理器所关注的事件是 ACCEPT 事件,这样 Reactor 会监听客户端向服务器端发起的连接请求 事件(ACCEPT 事件)
  2. 客户端向服务器端发起一个连接请求,Reactor 监听到了该 ACCEPT 事件的发生并将 该 ACCEPT 事件派发给相应的 Acceptor 处理器来进行处理。Acceptor 处理器通过 accept()方 法得到与这个客户端对应的连接(SocketChannel),然后将该连接所关注的 READ 事件以及对 应的 READ 事件处理器注册到 Reactor 中,这样一来 Reactor 就会监听该连接的 READ 事件了
  3. 当 Reactor 监听到有读或者写事件发生时,将相关的事件派发给对应的处理器进行 处理。比如,读处理器会通过 SocketChannel 的 read()方法读取数据,此时 read()操作可以直 接读取到数据,而不会堵塞与等待可读的数据到来
  4. 每当处理完所有就绪的感兴趣的 I/O 事件后,Reactor 线程会再次执行 select()阻塞等 待新的事件就绪并将其分派给对应处理器进行处理

注意:Reactor 的单线程模式的单线程主要是针对于 I/O 操作而言,也就是所有的 I/O 的 accept()、read()、write()以及 connect()操作都在一个线程上完成的

但在目前的单线程 Reactor 模式中,不仅 I/O 操作在该 Reactor 线程上,连非 I/O 的业务 操作也在该线程上进行处理了,这可能会大大延迟 I/O 请求的响应。所以我们应该将非 I/O 的业务逻辑操作从 Reactor 线程上卸载,以此来加速 Reactor 线程对 I/O 请求的响应

JDK原生网络编程-NIO基础入门_第1张图片

单线程Reactor,工作者线程池

与单线程 Reactor 模式不同的是,添加了一个工作者线程池,并将非 I/O 操作从 Reactor 线程中移出转交给工作者线程池来执行。这样能够提高 Reactor 线程的 I/O 响应,不至于因 为一些耗时的业务逻辑而延迟对后面 I/O 请求的处理

使用线程池的优势

  1. 通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和 销毁过程产生的巨大开销
  2. 另一个额外的好处是,当请求到达时,工作线程通常已经存在,因此不会由于等待 创建线程而延迟任务的执行,从而提高了响应性
  3. 通过适当调整线程池的大小,可以创建足够多的线程以便使处理器保持忙碌状态。 同时还可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败

改进的版本中,所有的 I/O 操作依旧由一个 Reactor 来完成,包括 I/O 的 accept()、read()、 write()以及 connect()操作

对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发或大数据量 的应用场景却不合适,主要原因如下:

  1. 一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,即便 NIO 线程的 CPU 负 荷达到 100%,也无法满足海量消息的读取和发送
  2. 当 NIO 线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时 之后往往会进行重发,这更加重了 NIO 线程的负载,最终会导致大量消息积压和处理超时, 成为系统的性能瓶颈

JDK原生网络编程-NIO基础入门_第2张图片

多Reactor线程模式

Reactor 线程池中的每一 Reactor 线程都会有自己的 Selector、线程和分发的事件循环逻辑

mainReactor 可以只有一个,但 subReactor 一般会有多个。mainReactor 线程主要负责接 收客户端的连接请求,然后将接收到的 SocketChannel 传递给 subReactor,由 subReactor 来 完成和客户端的通信

核心流程

  1. 注册一个 Acceptor 事件处理器到 mainReactor 中,Acceptor 事件处理器所关注的事 件是 ACCEPT 事件,这样 mainReactor 会监听客户端向服务器端发起的连接请求事件(ACCEPT 事件)。启动 mainReactor 的事件循环
  2. 客户端向服务器端发起一个连接请求,mainReactor 监听到了该 ACCEPT 事件并将该 ACCEPT 事件派发给 Acceptor 处理器来进行处理。Acceptor 处理器通过 accept()方法得到与这 个客户端对应的连接(SocketChannel),然后将这个 SocketChannel 传递给 subReactor 线程池
  3. subReactor 线程池分配一个 subReactor 线程给这个 SocketChannel,即,将 SocketChannel 关注的 READ 事件以及对应的 READ 事件处理器注册到 subReactor 线程中。当 然你也注册 WRITE 事件以及 WRITE 事件处理器到subReactor 线程中以完成 I/O 写操作。 Reactor 线程池中的每一 Reactor 线程都会有自己的 Selector、线程和分发的循环逻辑
  4. 当有 I/O 事件就绪时,相关的 subReactor 就将事件派发给响应的处理器处理。注意, 这里 subReactor 线程只负责完成 I/O 的 read()操作,在读取到数据后将业务逻辑的处理放入 到线程池中完成,若完成业务逻辑后需要返回数据给客户端,则相关的 I/O 的 write 操作还 是会被提交回 subReactor 线程来完成

注意:所有的 I/O 操作(包括,I/O 的 accept()、read()、write()以及 connect()操作)依旧还 是在 Reactor 线程(mainReactor 线程 或 subReactor 线程)中完成的。Thread Pool(线程池)仅用 来处理非 I/O 操作的逻辑

多Reactor线程模式将“接受客户端的连接请求”和“与该客户端的通信”分在了两个Reactor线程来完成。mainReactor完成接收客户端连接请求的操作,它不负责与客户端的通信,而是将建立好的连接转交给subReactor线程来完成与客户端的通信,这样一来就不会因为read()数据量太大而导致后面的客户端连接请求得不到即时处理的情况。并且多Reactor线程模式在海量的客户端并发请求的情况下,还可以通过实现subReactor线程池来将海量的连接分发给多个subReactor线程,在多核的操作系统中这能大大提升应用的负载和吞吐量

JDK原生网络编程-NIO基础入门_第3张图片

NIO实战示例

服务端

public class NioServerHandle implements Runnable{

	private volatile boolean isStarted;
	//创建服务端serverSocketChannel
	private ServerSocketChannel serverSocketChannel;
	//创建选择器
	private Selector selector;
	
	public NioServerHandle() {
		try {
			//创建选择器
			selector = Selector.open();
			//创建serverSocketChannel
			serverSocketChannel = ServerSocketChannel.open();
			//设置为非阻塞
			serverSocketChannel.configureBlocking(false);
			//绑定端口
			serverSocketChannel.socket().bind(new InetSocketAddress(8761));
			//注册事件
			serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
			isStarted=true;
			System.out.println("服务器已启动,端口号:" + 8761);
		} catch (IOException e) {
			isStarted=false;
			e.printStackTrace();
		}
	}

	@Override
	public void run() {
		while(isStarted){
			try {
				 //阻塞,只有当至少一个注册的事件发生的时候才会继续
				selector.select();
				Set<SelectionKey> selectionKeys= selector.selectedKeys();
				Iterator<SelectionKey> it = selectionKeys.iterator();
				while(it.hasNext()){
					SelectionKey selectionKey=it.next();
					it.remove();
					hanldeInput(selectionKey);
				}
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} finally {
				// TODO: handle finally clause
			}
		}
	}

	private void hanldeInput(SelectionKey selectionKey) throws IOException {
		//先确认是否可用
		if(selectionKey.isValid()){
			//接收连接事件
			if(selectionKey.isAcceptable()){
			   ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
			   SocketChannel socketChannel= serverSocketChannel.accept();
			   socketChannel.configureBlocking(false);
			   System.out.println("=接受连接,并注册读事件");
			   socketChannel.register(selector, SelectionKey.OP_READ);
			}
			//读事件
			if(selectionKey.isReadable()){
				SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
				//创建buffer
				ByteBuffer buffer = ByteBuffer.allocate(1024);
				int read=socketChannel.read(buffer);
				if(read>0){
					//读取
					buffer.flip();
					byte[] bytes=new byte[buffer.remaining()];
					buffer.get(bytes);
					String message = new String(bytes,"UTF-8");
	                System.out.println("服务器端接收的消息为:" + message);
	                String response ="Hello,"+message+",Now is "+new java.util.Date(
	                        System.currentTimeMillis()).toString();
	                //应答消息
	                doWrite(socketChannel,response);
				}else{//链路已经关闭,释放资源 
					selectionKey.cancel();
					socketChannel.close();
				}
			}
            //写事件
            if(key.isWritable()){
                SocketChannel sc = (SocketChannel) key.channel();
                ByteBuffer att = (ByteBuffer)key.attachment();
                if(att.hasRemaining()){
                    int count = sc.write(att);
                    System.out.println("write:"+count+ "byte ,hasR:"
                            +att.hasRemaining());
                }else{
                    //只监听读事件,移出写事件
                    key.interestOps(SelectionKey.OP_READ);
                }
            }
		}
	}

	//发送应答消息
    private void doWrite(SocketChannel channel,String response)
            throws IOException {
        //将消息编码为字节数组
        byte[] bytes = response.getBytes();
        //根据数组容量创建ByteBuffer
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        //将字节数组复制到缓冲区
        writeBuffer.put(bytes);
        //flip操作
        writeBuffer.flip();
        //同时注册读和写的事件
        channel.register(selector,SelectionKey.OP_WRITE|SelectionKey.OP_READ,
                writeBuffer);
    }
}

启动服务端

public class NioServer {

	private static NioServerHandle nioServerHandle;

    public static void start(){
        nioServerHandle = new NioServerHandle();
        new Thread(nioServerHandle,"Server").start();
    }
    public static void main(String[] args){
        start();
    }

}

客户端

public class NioClientHandle implements Runnable{
    private String host;
    private int port;
    private volatile boolean started;
    private Selector selector;
    private SocketChannel socketChannel;


    public NioClientHandle() {
        try {
            /*创建选择器*/
            this.selector = Selector.open();
            /*打开监听通道*/
            socketChannel = SocketChannel.open();
            /*如果为 true,则此通道将被置于阻塞模式;
            * 如果为 false,则此通道将被置于非阻塞模式
            * 缺省为true*/
            socketChannel.configureBlocking(false);
            started = true;
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }
    public void stop(){
        started = false;
    }


    @Override
    public void run() {
        //连接服务器
        try {
            doConnect();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(-1);
        }
        /*循环遍历selector*/
        while(started){
            try {
                /*阻塞方法,当至少一个注册的事件发生的时候就会继续*/
                selector.select();
                /*获取当前有哪些事件可以使用*/
                Set<SelectionKey> keys = selector.selectedKeys();
                /*转换为迭代器*/
                Iterator<SelectionKey> it = keys.iterator();
                SelectionKey key = null;
                while(it.hasNext()){
                    key = it.next();
                    /*我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。
                    如果我们没有删除处理过的键,那么它仍然会在事件集合中以一个激活
                    的键出现,这会导致我们尝试再次处理它。*/
                    it.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if(key!=null){
                            key.cancel();
                            if(key.channel()!=null){
                                key.channel().close();
                            }
                        }
                    }
                }

            } catch (IOException e) {
                e.printStackTrace();
                System.exit(-1);
            }
        }

        if(selector!=null){
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    /*具体的事件处理方法*/
    private void handleInput(SelectionKey key) throws IOException {
        if(key.isValid()){
            /*获得关心当前事件的channel*/
            SocketChannel sc =(SocketChannel)key.channel();
            /*处理连接就绪事件
            * 但是三次握手未必就成功了,所以需要等待握手完成和判断握手是否成功*/
            if(key.isConnectable()){
                /*finishConnect的主要作用就是确认通道连接已建立,
                方便后续IO操作(读写)不会因连接没建立而
                导致NotYetConnectedException异常。*/
                if(sc.finishConnect()){
                    /*连接既然已经建立,当然就需要注册读事件,
                    写事件一般是不需要注册的。*/
                    socketChannel.register(selector,SelectionKey.OP_READ);
                }else System.exit(-1);
            }

            /*处理读事件,也就是当前有数据可读*/
            if(key.isReadable()){
                /*创建ByteBuffer,并开辟一个1k的缓冲区*/
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                /*将通道的数据读取到缓冲区,read方法返回读取到的字节数*/
                int readBytes = sc.read(buffer);
                if(readBytes>0){
                    buffer.flip();
                    byte[] bytes = new byte[buffer.remaining()];
                    buffer.get(bytes);
                    String result = new String(bytes,"UTF-8");
                    System.out.println("客户端收到消息:"+result);
                }
                /*链路已经关闭,释放资源*/
                else if(readBytes<0){
                    key.cancel();
                    sc.close();
                }

            }
        }
    }

    /*进行连接*/
    private void doConnect() throws IOException {
        /*如果此通道处于非阻塞模式,则调用此方法将启动非阻塞连接操作。
        如果连接马上建立成功,则此方法返回true。
        否则,此方法返回false,
        因此我们必须关注连接就绪事件,
        并通过调用finishConnect方法完成连接操作。*/
        if(socketChannel.connect(new InetSocketAddress("127.0.0.1",8761))){
            /*连接成功,关注读事件*/
            socketChannel.register(selector,SelectionKey.OP_READ);
        }
        else{
            socketChannel.register(selector,SelectionKey.OP_CONNECT);
        }
    }

    /*写数据对外暴露的API*/
    public void sendMsg(String msg) throws IOException {
        doWrite(socketChannel,msg);
    }

    private void doWrite(SocketChannel sc,String request) throws IOException {
        byte[] bytes = request.getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        writeBuffer.put(bytes);
        writeBuffer.flip();
        sc.write(writeBuffer);
    }
}

启动客户端

public class NioClient {

	 private static NioClientHandle nioClientHandle;

	    public static void start(){
	        nioClientHandle = new NioClientHandle();
	        new Thread(nioClientHandle,"Server").start();
	    }
	    //向服务器发送消息
	    public static boolean sendMsg(String msg) throws Exception{
	        nioClientHandle.sendMsg(msg);
	        return true;
	    }
	    public static void main(String[] args) throws Exception {
	        start();
	        Scanner scanner = new Scanner(System.in);
	        while(NioClient.sendMsg(scanner.next()));

	    }
}

测试

启动服务端
在这里插入图片描述
启动客户端,服务端接收到连接事件
在这里插入图片描述
客户端输入信息,并接收到服务端返回的消息

在这里插入图片描述
服务端接收到客户端信息,并返回消息
在这里插入图片描述

你可能感兴趣的:(网络编程,网络通信,nio,reactor,网络编程,bio)