网络编程(3)——NIO

概念

  • Java NIO:Channel and Buffers(通道与缓冲区)

    • 标准的IO基于字节流字符流进行操作的,而NIO是基于**通道(channel)缓冲区(Buffer)**进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
  • Java NIO:Non-blocking IO(非阻塞IO)

    • Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
  • Java NIO:Selectors(选择器)

    • Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。

NIO的本质就是避免原始的TCP建立连接使用3次握手的操作,减少连接的开销

###NIO概念模型

这里写图片描述

1. Buffer

Buffer是一个对象,它包含一些要写入或者要读取的数据。在NIO类库中加入Buffer对象,体现了新库与原IO的一个重要的区别。在面向流的IO中(即BIO中),可以将数据直接写入或读取到Stream对象中。在NIO库中,所有数据都是用缓冲区处理的(读写)。缓冲区实质上是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类型的数组。这个数组为缓冲区提供了数据的访问读写等操作属性,如位置、容量、上限等概念。

Byte类型:我们最常使用的就是ByteBuffer,实际上每一种java基本类型都对于一种缓冲区(除了Boolean类型)

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
2. Channel

java NIO的通道类似流,但又有些不同:

  • 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
  • 通道可以异步地读写
  • 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

通道,它像自来水管道一样,网络数据通过Channel读取和写入,通道与流不同之处在于通道是双向的,而流只是一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者二者同时进行,最关键的是可以与多路复用器结合起来,有多重状态位,方便多路复用器去识别。事实上通道分为两大类,一类是网络读写的,一类是用于文件操作的,我们使用SocketChannelServerSocketChannel都是SelectableChannel的子类。

从通道读取数据到缓冲器,从缓冲区写入数据到通道

这里写图片描述

以下有集中java NIO中最重要的通道的实现

  • FileChannel: 从文件中读写数据
  • DatagramChannel: 能通过UDP读写网络中的数据
  • SocketChannel: 能通过TCP读写网络中的数据
  • ServerSocketChannel: 可以监听新进来的TCP连接,像Web服务器那样,对每一个新进来的连接都会创建一个SocketChannel

这里Buffer覆盖了你能通过IO发送的基本数据类型:byte、short、int、long、float、double和char

3. Buffer

javaNIO中的Buffer用于和NIO通道进行交互。如你所知,数据从通道读入缓冲区,从缓冲区写入到通道中。

缓冲区本质是一块可以写入数据,然后可以从中读取数据的内存,这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块区域。

使用Buffer读写数据一般遵循以下四个步骤:

  • 写入数据到Buffer
  • 调用flip()方法
  • Buffer中读取数据
  • 调用clear()方法或者compact()方法

当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读的模式。在读模式下,可以读取之前写入到Buffer中的数据。一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或者compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将被放到缓冲区未读数据的后面。

以下是Java NIO里关键的Buffer实现:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer
(1)Buffer的三个属性
  • capacity
    • 作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”。你只能往里写capacity个byte、long、char等类型。一旦Buffer满了,需要将其清空才能继续往里写数据。
  • position
  • limit

position和limit的含义取决于Buffer处在读模式还是写模式。

这里写图片描述

3. Selector

这里写图片描述

Selector允许单线程处理多个Channel。如果你的应用打开了多个通道,但每个连接的流量都很低,使用Selector就会很方便。

多路复用器(Selector),他是NIO编程的基础,非常重要。多路复用器提供选择已经就绪的任务的能力。 简单的说,就是Selector会不断地轮询注册在其上的管道,如果某个管道发生了读写操作,这个通道就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。

一个多路复用器可以负责成千上万Channel通道,没有上限,这也是JDK使用了epoll代替了传统的select实现,获得连接句柄没有限制。这也就以为着我们只要一个线程负责Selector的轮询,就可以接入成千上万个客户端,这是JDK NIO库的巨大进步。(注意epoll有一个非常大的bug)

Selector线程就类似一个管理者,管理了成千上万个管道,然后轮询那个管道的数据已经准备好,通知CPU执行IO的读取或者写入操作。

Selector模式:当IO事件(管道)注册到选择器以后,selector会分配给每个管道一个key值,相当于标签。selector选择器是以轮询的方式进行查找注册的所有IO事件(管道),当我们的IO事件(管道)准备就绪后,select就会识别,会通过key值来找到相应的管道,进行相关的数据处理操作(从管道里读或者写数据,写到我们数据缓冲区中)。

为了将Channel和Selector配合使用,必须将channel注册到selector上。通过SelectableChannel.register()方法来实现。

ssc.configureBlocking(false);
			//4. 绑定地址
			ssc.bind(new InetSocketAddress(port));
			//5. 把服务器通道注册到多路复用器上,并且监听阻塞事件。这个channel一直在等着
			ssc.register(this.selector, SelectionKey.OP_ACCEPT);

与Selector一起使用时,Channel必须处于非阻塞模式下。

register()方法有两个参数,第二个参数的意思是通过Selector监听channel时对什么事件感兴趣。每个管道都会对选择器进行注册不同的事件状态,以便选择器查找。

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

例子代码

Server类
package socket_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;

/**
 * 服务端channel也要注册到多路复用器上
 * @author lenovo
 *
 */

public class Server implements Runnable{

	//1. 建立一个多路复用器(管理所有的通道)
	private Selector selector;
	//2. 建立缓冲区
	private ByteBuffer readBuf = ByteBuffer.allocate(1024);
	private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
	
	// 在Server的构造函数中进行了一系列的操作
	public Server(int port) {
		try {
			//1. 打开多路复用器
			this.selector = Selector.open();
			//2. 打开服务器通道, open是一个静态方法
			ServerSocketChannel ssc = ServerSocketChannel.open();
			//3. 设置服务器通道为非阻塞模式
			ssc.configureBlocking(false);
			//4. 绑定地址
			ssc.bind(new InetSocketAddress(port));
			//5. 把服务器通道注册到多路复用器上,并且监听阻塞事件。这个channel一直在等着
			ssc.register(this.selector, SelectionKey.OP_ACCEPT);
			
			System.out.println("Server start, port : " + port);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}	
	}
	@Override
	// 循环Client端注册到多路复用器的channel,看看这些channel的标志位都是什么状态
	public void run() {
		while(true) {
			try {
				//1. 必须要让多路复用器开始监听。表示注册在多路复用器上的通道都在被监听
				this.selector.select();
				//2. 返回多路复用器已经选择的结果集(每次循环都要遍历注册到selector上的所有key)
				Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
				//3. 进行遍历
				while(keys.hasNext()) {
					//4. 获取一个选择的元素
					SelectionKey key = keys.next();
					//5. 直接从容器中移除就可以了,因为这个操作已经在下面被处理完了,
					//   如果之后channel中又有数据了,就需要重新注册
					keys.remove();
					//6. 是否是有效的,比如Client与服务器端断掉了就不是有效的
					if(key.isValid()) {
						//7. 如果为阻塞状态
						if(key.isAcceptable())
							this.accept(key);
						
						//8. 如果为可读状态
						if(key.isReadable())
							this.read(key);
						
						//9. 写数据
						if(key.isWritable()){
							this.write(key);
						}
					}
					
				}
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
		}
	}
	
	private void write(SelectionKey key) throws IOException {
		/*SocketChannel sc = (SocketChannel) key.channel();
		this.writeBuf.clear();
		byte[] bytes = new byte[1024];
		System.in.read(bytes);
		this.writeBuf.put(bytes);
		writeBuf.flip();
		sc.write(writeBuf);
		sc.register(this.selector, SelectionKey.OP_READ);*/
	}
	private void read(SelectionKey key) {
		try {
			//1. 清空缓冲区旧的数据
			this.readBuf.clear();
			//2. 获取之前注册的socket通道对象
			SocketChannel sc = (SocketChannel) key.channel();
			//3. 读取数据
			int count = sc.read(this.readBuf);
			//4. 如果没有数据
			if(count == -1) {
				key.channel().close();
				key.cancel();
				return;
			}
			//5. 有数据则进行读取,读取之前需要进行复位方法(把position和limit进行复位
			this.readBuf.flip();
			//6. 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据
			byte[] bytes = new byte[this.readBuf.remaining()];
			//7. 接收缓冲区数据,将readBuf中的数据写到bytes中
			this.readBuf.get(bytes);
			//8. 打印结果
			String body = new String(bytes).trim();
			System.out.println("Server : " + body);
			
			//9. 可以写回给客户端数据
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	// 通过key即可以拿到对应可以的channel  如果是阻塞的情况下,就执行accept方法
	private void accept(SelectionKey key) {
		try {
			//1. 获取服务通道
			ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
			
			//2. 执行阻塞方法。(等待客户端的通道是否注册上了)
			SocketChannel sc = ssc.accept();
			
			//3. 设置阻塞模式
			sc.configureBlocking(false);
			
			//4. 注册到多路复用器上,并设置读取标志
			sc.register(this.selector, SelectionKey.OP_READ);
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	
	public static void main(String[] args) {
		new Thread(new Server(8765)).start();
	}
}

Server类的步骤

Server类的成员变量
这里写图片描述

Server类的构造函数

这里写图片描述

Server类的run()方法

首先这个方法的整个方法体都在一个while中

这里写图片描述

client类
package socket_nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Client {
	// 需要一个Selector
	private static ByteBuffer readBuf = ByteBuffer.allocate(1024);
	private static ByteBuffer writeBuf = ByteBuffer.allocate(1024);
	public static void main(String[] args) {
		// 创建连接的地址
		InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8765);
		
		// 声明连接通道
		SocketChannel sc = null;
		
		//建立缓冲区
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		try {
			//打开通道
			sc = SocketChannel.open();
			// 建立连接,并不是一个TCP的,而是简单的一个channel的连接
			sc.connect(address);
			
			while(true) {
				/*int count = sc.read(buf);
				System.out.println(count);
				if(count != -1) {
					buf.flip();
					byte[] res = new byte[buf.remaining()];
					buf.get(res);
					String body = new String(res).trim();
					System.out.println("接收到来自server的数据:" + body);
				}*/
				// 定义一个字节数组,然后使用系统录入功能
				byte[] bytes = new byte[1024];
				System.in.read(bytes);
				
				// 把数据放到缓冲区中
				buf.put(bytes);
				// 对缓冲区进行复位
				buf.flip();
				// 写出数据
				sc.write(buf);
				// 清空缓冲区数据
				buf.clear();
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			if(sc != null) {
				try {
					sc.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}
}

这里写图片描述

参考并感谢

[1]. http://ifeve.com/overview/

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值