java之非阻塞IO(NIO)

来源:互联网 发布:淘宝好看的跑步鞋店铺 编辑:程序博客网 时间:2024/05/15 10:04

一、NIO类库简介

1、缓冲区Buffer

在NIO库中,所有的数据都是有缓冲区处理的。在读取数据时,是直接读到缓冲区中的;在写入数据时,是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区操作。
缓冲区实质上是一个数组,但是又不仅仅是数组,缓冲区还提供了对数据的结构化访问以及维护读写位置(limit)等信息。
最常用的缓冲区是ByteBuffer(字节缓冲区),一个ByteBuffer提供了一组功能用来操作byte数组。除了ByteBuffer缓冲区,还有以下缓冲区:
CharBuffer:字符缓冲区
ShortBuffer:短整型缓冲区
IntBuffer:整型缓冲区
LongBuffer:长整型缓冲区
FloatBuffer:浮点型缓冲区
DoubleBuffer:双精度浮点型缓冲区

事实上,每个java基本类型都对应有一个缓冲区,Buffer接口是每个缓冲区的父接口。

2、通道Channel

Channel是一个通道,网络数据通过Channel读取和写入,通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OuputStream的子类),而通道可以用于读、写或者是二者同时进行。
实际上Channel可以分为两大类:用于网络读写的SelectableChannel和用于文件操作的FileChannel。其中ServerSocketChannel和SocketChannel都是SelectableChannel的子类。

3、多路复用器Selector

多路复用器提供选择已经就绪的任务的能力。简单的来讲,Selector会不断的轮询注册在其上的Channel,如果某个Channel上发生读写操作,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的Io操作。
一个多路复用器可以同时轮询多个Channel。

二、NIO编程步骤

1、服务端开发:

步骤一:创建ServerSocketChannel,配置它为非阻塞模式。
步骤二:绑定监听,配置TCP参数,例如backlog的大小。
步骤三:创建一个独立的IO线程,用于轮询多路复用器Selector。
步骤四:创建Selector,将之前创建的ServerSocketChannel注册到Selector上,监听SelectionKey.OP_ACCEPT。
步骤五:启动IO线程,在循环体中执行selector.select()方法,轮询就绪的Channel。
步骤六:当轮询到了就绪状态的Channel时,需要对其进行判断,如果是OP_ACCEPT状态,说明是新的客户端接入,则调用serverSocketChannel.accept()方法接收新的客户端。
步骤七:设置新接入的客户端链路SocketChannel为非阻塞模式,配置其他一些TCP参数。
步骤八:将SocketChanenl注册到Selector上,监听OP_READ操作位。
步骤九:如果轮询的Channel为OP_READ状态,则说明SocketChannel中有新的就绪的数据包需要读取,则构造ByteBuffer对象,读取数据包。
步骤十:如果轮询的Channel为OP_WRITE,说明还有数据没有发送完成,需要继续发送。

2、客户端开发:

步骤一:打开SocketChannel,绑定客户端本地地址。
步骤二:设置SocketChannel为非阻塞模式,同时设置客户端连接的TCP参数,比如:接收缓冲区的大小、发送缓冲区的大小
步骤三:异步连接服务端
步骤四:判断是否连接成功,若连接成功,直接注册状态位到多路复用器中,如果当前没有连接成功,(异步连接,返回false,说明客户端已经发送sync包,服务端没有返回ack包,物理链路还没有建立)。
步骤五:向Reactor线程的多路复用器注册OP_CONNECT状态位,监听服务端的TCP ACK应答。
步骤六:创建Reactor线程,创建多路复用器并启动线程。
步骤七:多路复用器在run方法的无限循环体内轮询准备就绪的key。
步骤八:接收Connection事件进行处理
步骤九:判断连接结果,如果连接成功,注册读事件到多路复用器。
步骤十:异步读客户端请求消息到缓冲区。
步骤十一:对ByteBuffer进行编解码,如果有半包消息接收缓冲区Reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程中,进行业务逻辑编排。
步骤十二:将消息编码到ByteBuffer,调用SocketChannel的异步发送write接口,将消息异步发送给客户端。

三、编码实现:

1、服务器端:

public class ServerDemo {private static final int PORT = 10000;public static void main(String[] args) {/** * 创建一个名为MultiplexerServer的多路复用类,它是一个独立的线程,用来轮询多路复用器Selector, * 可以处理多个客户端的并发接入 */MultiplexerServer server = new MultiplexerServer(PORT);new Thread(server,"NIO-MultiplexerServer-001").start();}}
public class MultiplexerServer implements Runnable {private Selector selector;private ServerSocketChannel serverChannel;private volatile boolean stop;/** * 初始化多路复用器,绑定监听端口 *  * @param port */public MultiplexerServer(int port) {try {selector = Selector.open();serverChannel = ServerSocketChannel.open();// 设置为非阻塞模式serverChannel.configureBlocking(false);// 绑定端口serverChannel.socket().bind(new InetSocketAddress("localhost", port), 1024);// 将serverSocketChannel注册到多路复用器Selector上,并监听ACCEPT(连接请求)事件(SelectionKey.OP_ACCEPT)serverChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("Initialize finished,the server is start in port " + port);} catch (IOException e) {e.printStackTrace();System.exit(1);}}public void stop() {this.stop = true;}@Overridepublic void run() {while (!stop) {try {// 表示当有处于就绪状态(有读写等事件发生)的Channel时,selector将返回该Channel的SelectionKey集合selector.select();// 表示无论是否有读写事件发生,selector每隔一秒都会被唤醒一次。// selector.select(1000);//通过对就绪状态的Channel集合进行迭代,可以进行网络的异步读写操作。Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> it = selectedKeys.iterator();SelectionKey selectedKey = null;while (it.hasNext()) {selectedKey = it.next();it.remove();try {handlerInput(selectedKey);} catch (Exception e) {e.printStackTrace();if (selectedKey != null) {selectedKey.cancel();if (selectedKey.channel() != null) {selectedKey.channel().close();}}}}} catch (IOException e) {e.printStackTrace();}}//关闭多路复用器,释放资源。//注意:多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动取消注册并关闭,所以并不需要重复释放资源。if(selector != null){try {selector.close();} catch (IOException e) {e.printStackTrace();}}}/** * 根据SelectionKey操作为来判断接入的网络事件类型, * accept:新接入请求 * readable:读操作请求 * @param selectedKey * @throws IOException */private void handlerInput(SelectionKey selectedKey) throws IOException {//请求有效if(selectedKey.isValid()){//处理新接入的请求if(selectedKey.isAcceptable()){ServerSocketChannel ssc = (ServerSocketChannel) selectedKey.channel();SocketChannel sc = ssc.accept();//设置SocketChannel为异步非阻塞类型//同时,还可以对TCP参数进行设置,例如TCP接收和发送数据的缓冲区的大小等sc.configureBlocking(false);//将SocketChannel注册到多路复用器Selector,并监听SelectionKey.OP_READ操作位sc.register(selector, SelectionKey.OP_READ);}//处理读操作的请求if(selectedKey.isReadable()){SocketChannel sc = (SocketChannel) selectedKey.channel();//创建ByteBuffer缓冲区,由于无法得知客户端发送的码流大小,因此在这里设置为1MByteBuffer readBuffer = ByteBuffer.allocate(1024);//读取请求码流到缓冲区int readBytes = sc.read(readBuffer);//由于我们将SocketChannel设置为异步非阻塞模式,,因此它的read 也为非阻塞的,//使用返回值进行判断,看读取到的字节数,分别有以下三种情况://1、返回值大于0,读到了字节,对字节进行编解码//2、返回值等于0,没有读到字节,属于正常情况,忽略。//3、返回值为-1,链路已经关闭,需要关闭SocketChannel,释放资源if(readBytes > 0){//情况一:读到了码流//首先执行flip操作,将缓冲区当前的limit设置为position,position设置为0,用于//后续对缓冲区的读取操作。readBuffer.flip();//根据缓冲区中可读的字节个数,创建字节数组byte[] bytes = new byte[readBuffer.remaining()];//将缓冲区中的数据读取到字节数组中readBuffer.get(bytes);String order = new String(bytes,"UTF-8");System.out.println("The server recived order is "+order);String currentTime = null;if("QUERY CURRENT TIME".equalsIgnoreCase(order.trim())){DateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");currentTime = format.format(new Date());}else{currentTime = "BAD REQUEST";}//发送结果doWrite(sc,currentTime);}else if(readBytes < 0){//对端链路关闭,进行资源的释放selectedKey.cancel();//取消注册sc.close();//关闭Channel}else{//读到了0字节,忽略}}}}private void doWrite(SocketChannel sc, String response) throws IOException {//这里会有写半包问题!!!!if(response != null && response.length() > 0){byte[] bytes = response.getBytes();ByteBuffer buffer  = ByteBuffer.allocate(bytes.length);buffer.put(bytes);buffer.flip();sc.write(buffer);}}}

2、客户端:

public class ClientDemo {private static final String HOST = "localhost";private static final int PORT = 10000;public static void main(String[] args) {new Thread(new ClientHandlerDemo(HOST,PORT)).start();}}
public class ClientHandlerDemo implements Runnable {private String host;private int port;private Selector selector;private SocketChannel socketChannel;private volatile boolean stop;public ClientHandlerDemo(String host, int port) {this.host = host == null ? "localhost" : host;this.port = port;try {this.selector = Selector.open();this.socketChannel = SocketChannel.open();socketChannel.configureBlocking(false);} catch (IOException e) {e.printStackTrace();System.exit(1);}}@Overridepublic void run() {try {//建立连接doConnect();} catch (IOException e) {e.printStackTrace();}while(!stop){try {selector.select();Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> it = selectedKeys.iterator();SelectionKey selectedKey = null;while(it.hasNext()){selectedKey = it.next();it.remove();try {handleInput(selectedKey);} catch (Exception e) {e.printStackTrace();if(selectedKey != null){selectedKey.cancel();if(selectedKey.channel() != null){selectedKey.channel().close();}}}}} catch (IOException e) {e.printStackTrace();System.exit(1);}}//多路复用器被关闭后,所有注册在其上的Channel和Pipe等资源会自动取消注册并关闭,因此不需要我们重复释放资源if(selector != null){try {selector.close();} catch (IOException e) {e.printStackTrace();}}}private void handleInput(SelectionKey selectedKey) throws IOException {//判断是否有效if(selectedKey.isValid()){SocketChannel sc = (SocketChannel)selectedKey.channel();//判断是否是连接操作请求if(selectedKey.isConnectable()){//判断是否连接成功if(sc.finishConnect()){sc.register(selector, SelectionKey.OP_READ);doWrite(sc);}else{System.out.println("连接失败!进程退出");System.exit(1);}}//判断是否是读操作请求if(selectedKey.isReadable()){ByteBuffer buffer = ByteBuffer.allocate(1024);//1Mint readBytes  = sc.read(buffer);if(readBytes > 0){buffer.flip();byte[] dst = new byte[buffer.remaining()];buffer.get(dst);String body = new String(dst,"UTF-8");System.out.println("Now is "+body);this.stop = true;}else if(readBytes < 0){//对端链路关闭,释放资源selectedKey.cancel();sc.close();}else{//读取到的字节数为0,正常情况,忽略}}}}private void doConnect() throws IOException{//如果直接连接成功,则注册到多路复用器上,发送请求,读应答if(socketChannel.connect(new InetSocketAddress(host,port))){socketChannel.register(selector, SelectionKey.OP_READ);doWrite(socketChannel);}else{socketChannel.register(selector, SelectionKey.OP_CONNECT);}}//连接成功,发送请求private void doWrite(SocketChannel socketChannel) throws IOException {byte[] bytes = "QUERY CURRENT TIME".getBytes();ByteBuffer buffer = ByteBuffer.allocate(bytes.length);buffer.put(bytes);buffer.flip();socketChannel.write(buffer);if(!buffer.hasRemaining()){//发送数据完成,没有丢包System.out.println("send order to server success!!!");}}}

四、优点分析

1、客户端发起的连接操作时异步的,可以通过在多路复用器上注册OP_CONNECT等待后续结果,不像BIO会被同步阻塞。
2、SocketChannel的读写操作是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样IO通信线程就可以处理其他的链路,不需要同步等待该链路可用。
3、一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降,因此非常适合高性能、高并发的网络服务器。




阅读全文
0 0
原创粉丝点击