Netty权威指南 第2版学习笔记2——NIO入门

传统的BIO编程

网络编程的基本模型是Client/Server模型,通过三次扬建立连接,如果连接建立成功,双方就可以通过网络套接字进行通信。

BIO通信模型图

这里写图片描述

采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这是典型的一请求一应答通信模型。

该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。

同步阻塞式I/O创建的TimeServer源码分析

TimeServer.java

package com.phei.netty.bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class TimeServer {
    public static void main(String[] args) throws IOException {
        int port=8080;
        if(args!=null && args.length>0){
            try{
                port=Integer.valueOf(args[0]);
            }catch(NumberFormatException e){

            }
        }
        ServerSocket server=null;
        try{
            server=new ServerSocket(port); //创建服务监听
            System.out.println("The time server is start in port:" + port);
            Socket socket=null;
            while(true){
                socket=server.accept();
                new Thread(new TimeServerHandler(socket)).start(); //启动新的线程来处理客户端请求
            }
        }
        finally{
            if(server!=null)
            {
                System.out.println("The time server close");
                server.close();
                server=null;
            }
        }
    }
}

TimeServerHandler

package com.phei.netty.bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class TimeServerHandler implements Runnable {

    private Socket socket;
    public TimeServerHandler(Socket socket){
        this.socket=socket;

    }
    @Override
    public void run() {
        // TODO Auto-generated method stub
        BufferedReader in =null;
        PrintWriter out=null;
        try{
            in=new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
            out=new PrintWriter(this.socket.getOutputStream(),true);
            String currentTime=null;
            String body=null;
            while(true){
                body=in.readLine();
                if(body==null)break;
                System.out.println("The time server receive order:"+body);
                currentTime="QUERY TIME ORDER".equalsIgnoreCase(body)?new java.util.Date(
                        System.currentTimeMillis()).toString():"BAD ORDER";
                out.println(currentTime);
            }
        }
        catch(Exception e){
            if(in!=null){
                try{
                    in.close();
                }
                catch(IOException e1){
                    e1.printStackTrace();
                }
            }
            if(out!=null){
                out.close();
                out=null;
            }
            if(this.socket!=null){
                try{
                    this.socket.close();

                }catch(IOException e1){
                    e1.printStackTrace();
                }
                this.socket=null;
            }
        }
    }
}

Server运行后,使用Java VisualVM查看线程:
这里写图片描述

可看到线程处于阻塞状态。

BIO 客户端源码分析

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class TimeClient {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int port=8080;
        if(args!=null && args.length>0){
            try{
                port=Integer.valueOf(args[0]);
            }catch(NumberFormatException e){

            }
        }
        Socket socket=null;
        BufferedReader in = null;
        PrintWriter out=null;
        try{
        socket=new Socket("127.0.0.1",port);
        in=new BufferedReader(new InputStreamReader(socket.getInputStream()));
        out=new PrintWriter(socket.getOutputStream(),true);
        out.println("QUERY TIME ORDER");
        System.out.println("Send order 2 server success.");
        String resp=in.readLine();
        System.out.println("Now is : " + resp);
        }catch(Exception e){

        }finally{
            if(out!=null){
                out.close();
                out=null;
            }
            if(in!=null){
                try{
                    in.close();
                }catch(IOException e){
                    e.printStackTrace();
                }
                in=null;
            }
            if(socket!=null){
                try{
                    socket.close();
                }catch(IOException e){
                    e.printStackTrace();
                }
                socket=null;
            }
        }
    }
}

BIO的主要的问题在于,每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理接入的客户端链路,一个线程只能处理一个客户端连接。在高性能服务器应用领域,往往需要面向成千上万个客户端的并发连接,这种模型显然无法满足要求。
为了改进一线程一连接模型,后来又演进出一种通过线程池或者消息队列实现1个或者多个线程处理N个客户端的模型,由于它的底层通信机制依然使用同步阻塞I/O,所以被称为“伪异步”

伪异步I/O编程

源码分析

TimeServer.java

package com.phei.netty.bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class TimeServer {
    public static void main(String[] args) throws IOException {
        int port=8080;
        if(args!=null && args.length>0){
            try{
                port=Integer.valueOf(args[0]);

            }catch(NumberFormatException e){

            }
        }
        ServerSocket server=null;
        try{
            server=new ServerSocket(port);
            System.out.println("The time server is start in port:" + port);
            Socket socket=null;
            TimeServerHandlerExecutePool signleExecutor=new TimeServerHandlerExecutePool(50,10000);
            while(true){
                socket=server.accept();
                signleExecutor.execute(new TimeServerHandler(socket));
            }
        }
        finally{
            if(server!=null)
            {
                System.out.println("The time server close");
                server.close();
                server=null;
            }
        }
    }
}

主函数首先创建一个时间服务器处理类的线程池,当接收到新的客户端连接时,将请求Socket封装成一个Task,然后调用线程池的execute方法执行,从而避免了每个请求接入都创建一个新的线程。
TimeServerHandler.java

package com.phei.netty.bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class TimeServerHandler implements Runnable {

    private Socket socket;
    public TimeServerHandler(Socket socket){
        this.socket=socket;

    }
    @Override
    public void run() {
        // TODO Auto-generated method stub
        BufferedReader in =null;
        PrintWriter out=null;
        try{
            in=new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
            out=new PrintWriter(this.socket.getOutputStream(),true);
            String currentTime=null;
            String body=null;
            while(true){
                body=in.readLine();
                if(body==null)break;
                System.out.println("The time server receive order:"+body);
                currentTime="QUERY TIME ORDER".equalsIgnoreCase(body)?new java.util.Date(
                        System.currentTimeMillis()).toString():"BAD ORDER";
                out.println(currentTime);
            }
        }
        catch(Exception e){
            if(in!=null){
                try{
                    in.close();
                }
                catch(IOException e1){
                    e1.printStackTrace();
                }
            }
            if(out!=null){
                out.close();
                out=null;
            }
            if(this.socket!=null){
                try{
                    this.socket.close();

                }catch(IOException e1){
                    e1.printStackTrace();
                }
                this.socket=null;
            }
        }
    }
}

TimeServerHandlerExecutePool

package com.phei.netty.bio;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TimeServerHandlerExecutePool {
    private ExecutorService executor;
    public TimeServerHandlerExecutePool(int maxPoolSize,int queueSize){
        executor=new ThreadPoolExecutor(Runtime.getRuntime()
                .availableProcessors(),maxPoolSize,120L,TimeUnit.SECONDS,
                new ArrayBlockingQueue<java.lang.Runnable>(queueSize));
    }
    public void execute(java.lang.Runnable task){
        executor.execute(task);
    }
}
ThreadPoolExecutor

线程池类为Java.util.concurrent.ThreadPoolExecutor,常用的构造方法是:
ThreadPoolExecutor(int corePoolSize, 线程池维护线程的最少数量
int maximumPoolSize, 线程池维护线程的最大数量
long keepAliveTime, 线程池维护线程所允许的空闲时间
TimeUnit unit, 线程池维护线程所允许的空闲时间的单位
BlockingQueue workQueue, 线程池所使用的缓冲队列
RejectedExecutionHandler handler) 线程池对拒绝任务的处理策略
一个任务通过execute(Runnable)方法被添加到线程池,任务就是一个Runnable类型的对象,任务的执行方法就是Runnable类型对象的run()方法。

当一个任务通过execute(Runnable)方法欲添加到线程池时,几种情况:
  1. 如果紫时线程池中的数量小于corePoolSize,即使线程池中的线程处于空闲状态,也要创建新的线程来处理被添加的任务
  2. 如果此时线程池中的数量等于corePoolSize,但缓冲队列workQueue未满,那么任务被放入缓冲队列
  3. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于aximumPoolSize,建新的线程来处理被添加的任务
  4. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务
  5. 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数
handler

1.ThreadPoolExecutor.AbortPolicy() 抛出java.util.concurrent.RejectedExecutionException异常
2.ThreadPoolExecutor.CallerRunsPolicy()
当抛出RejectedExecutionException异常时,会调用rejectedExecution方法
3.ThreadPoolExecutor.DiscardOldestPolicy()
抛弃旧的任务
4. ThreadPoolExecutor.DiscardPolicy()
抛弃当前的任务

由于线程池和消息队列都是有界的,因此,无论客户端并发连接数多大,它都不会导致线程个数过于膨胀或者内存溢出,相比于传统的一连接一线程模型,是一种改良。但它底层仍是同步阻塞模型,无法从根本上解决问题。
此段关于伪异步的弊端分析,《Netty权威指南第2版》23页描述比较详细。

基于NIO的非阻塞编程

Non-block I/O

NIO类库简介

新的输入/输出(NIO)库是JDK1.4中引入的。下面是几个相关概念

1.缓冲区Buffer
Buffer是一个对象,包含一些要写入或者要读出的数据。在NIO对象中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中,可以将数据直接写入或者将数据直接读到Stream对象中。
在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
缓冲区实质是一个数组,通常是ByteBuffer,也可以使用其它种类的数组。缓冲区提供了对数据的结构化访问以及维护读写位置limit等信息。
最常用的缓冲区ByteBuffer,提供了一组功能用于操作byte数组。除了ByteBuffer,还有其它一些缓冲区。事实上,每一种Java基本类型(除了Boolean类型)都对应一种缓冲区,类图继承关系:
这里写图片描述
2.通道 Channel
Channel,就像自来水管一样,网络数据通过Channel读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者二者同时进行。
因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是UNIX网络编程模型中,底层操作系统的通道都是全双工的。
Channel继承关系类图:
这里写图片描述

Channel可以分为两大类:用于网络读写的SelectableChannel和用于文件操作的FileChannel
3.多路复用器Selector
它是JavaNIO编程的基础,提供选择已经就绪任务的能力。Selector会不断地轮询注册在其上的Channel,如果某个Channel上面发生读/写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
一个Selector可以同时轮询多个Channel,由于JDK使用epoll()代替select实现,它没有最大连接句柄1024/2048的限制,一个线程负责Selector的轮询,就可以接入成千上万的客户端。

NIO 服务端序列图

这里写图片描述
NIO服务端的主要创建过程:
1.步骤一:打开ServerSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道

ServerSocketChannel acceptorSvr=ServerSocketChannel.open();

2.步骤二:绑定监听端口,设置连接为非阻塞模式

acceptorSvr.socket().bind(new InetSocketAddress(InetAddress.getByName("IP"),port));
acceptorSvr.configureBlocking(false);

3.步骤三:创建Reactor线程,创建多路复用器并启动线程

selector selector=Selector.open();
New Thread(new ReactorTask()).start();

4.步骤四:将ServerSocketChannel注册到Reactor线程的Selector上,监听Accept事件

SelectionKey key=acceptorSvr.register(selector,SelectionKey.OP_ACCEPT,ioHandler);

5.步骤五:Selector在线程run方法的无限循环体内轮询准备就绪的Key

int num=selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator it =selecterdKeys.iterator();
while(it.hasNext()){
   SelectionKey key=(SelectionKey)it.next();
}

6.步骤六 Selector监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路

SocketChannel channel = svrChannel.accept();

7.步骤七 设置客户端链路为非阻塞模式

channel.configureBlocking(false);
channel.socket().setReuseAddress(true);

8.步骤八 将新接入的客户端连接注册到Reactor线程的Selector上,监听读操作,读取客户端发送的网络消息

SelectionKey key=socketChannel.register(selector,SelectionKey.OP_READ,ioHandler);

9.步骤九 异步读取客户端请求消息到缓冲区

int readNumber=channel.read(receivedBuffer);

10.步骤十 对ByteBuffer进行硬编码,如果有半包消息指针reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排

Object message=null;
while(buffer.hasRemain())
{
    byteBuffer.mark();
    Object message=decode(byteBuffer);
    if(message==null)
    {
      byteBuffer.reset();
      break;
    }
    messagerList.add(message);
}
if(!byteBuffer.hasRemain())
   byteBuffer.clear();
else
   byteBuffer.compact();
if(messageList!=null & !messageList.isEmpty())
{
   for(Object messageE:messageList)
      handlerTask(messageE);
}   
  1. 步骤十一 将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送给客户端
socketChannel.write(buffer);

如果发送区TCP缓冲区满,会导致写半包,此时,需要注册监听写操作位,循环写,直到整包消息写入TCP缓冲区。

源码分析

TimeServer.java

package com.phei.netty.bio;

import java.io.IOException;

public class TimeServer {
    public static void main(String[] args) throws IOException {
        int port=8080;
        if(args!=null && args.length>0){
            try{
                port=Integer.valueOf(args[0]);

            }catch(NumberFormatException e){

            }
        }
        MultiplexerTimeServer timeServer=new MultiplexerTimeServer(port);
        new Thread(timeServer,"NIOMultiplexerTimeServer-001").start();
    }
}

MultiplexerTimeServer

package com.phei.netty.bio;

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;
import java.util.Set;

public class MultiplexerTimeServer implements Runnable {
    private Selector selector;
    private ServerSocketChannel servChannel;
    private volatile boolean stop;

    public MultiplexerTimeServer(int port) {
        try {
            //资源初始化
            selector = Selector.open();   //创建Selector
            servChannel = ServerSocketChannel.open();   //创建ServerSocketChannel
            servChannel.configureBlocking(false);   //设置为异步非阻塞模式
            servChannel.socket().bind(new InetSocketAddress(port), 1024);  backlog为1024
            servChannel.register(selector, SelectionKey.OP_ACCEPT);   //注册channel到Selector,监听OP_ACCEPT操作位
            System.out.println("The time server is start in port : " + port);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void stop() {
        this.stop = true;
    }

    @Override
    public void run() {
        while (!stop) {
        //遍历selector,休眠时间为1s,无论是否有读写等事件发生,每隔1s selector唤醒一次
            try {
                selector.select(1000);
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> it = selectedKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null)
                                key.channel().close();
                        }
                    }
                }
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
        if (selector != null)
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

    }
    private void handleInput(SelectionKey key)throws IOException{
        if(key.isValid()){
            //处理新接入的请求消息
            if(key.isAcceptable()){
                ServerSocketChannel ssc=(ServerSocketChannel)key.channel();
                SocketChannel sc=ssc.accept();    //接收客户端的连接请求并创建SocketChannel实例。完成上述操作后,相当于完成了TCP的三次握手,物理链路正式建立
                sc.configureBlocking(false);   //异步非阻塞,同时可以对TCP参数进行设置,如TCP接收和发送缓冲区的大小等
                sc.register(selector, SelectionKey.OP_READ);
            }
            if(key.isReadable()){
                //读取客户端的请求消息
                SocketChannel sc=(SocketChannel)key.channel();
                ByteBuffer readBuffer=ByteBuffer.allocate(1024);   //创建一个ByteBuffer缓冲区
                int readBytes=sc.read(readBuffer);   //读取请求码流,read是非阻塞的
                if(readBytes>0){ //读到了字节
                    readBuffer.flip();   //将缓冲区当前的limit设置为position,position设置为0,用于对缓冲区的读取操作,然后根据缓冲区可读的字节个数创建字节数组
                    byte[] bytes=new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);   //将缓冲区可读的字节数组复制到新创建的字节数组中
                    String body=new String(bytes,"UTF-8");
                    System.out.println("The time server receive order : "+body);
                    String currentTime="QUERY TIME ORDER".equalsIgnoreCase(body)?new java.util.Date(System.currentTimeMillis()).toString():"BAD ORDER";
                    doWrite(sc,currentTime);
                }else if(readBytes<0){ //链路已经关闭,需要关闭SocketChannel,释放资源
                    key.cancel();
                    sc.close();
                }else
                    ; //读到0字节,忽略
            }
        }
    }
    private void doWrite(SocketChannel channel,String response) throws IOException{
        if(response!=null && response.trim().length()>0){
            byte[] bytes=response.getBytes();
            ByteBuffer writeBuffer=ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            channel.write(writeBuffer);
            //由于SocketChannel是异步非阻塞的,它并不保证一次能够把需要发送的字节数组发送完,此时会出现“写半包”问题。我们需要注册写操作,不断轮询Selector将没有发送完的ByteBuffer发送完毕,然后可以通过ByteBuffer的hasRemain()方法判断消息是否发送完成。此代码没有演示这部分操作
        }
    }
}

NIO客户端序列图

这里写图片描述

1.步骤一 打开SocketChannel
2.步骤二 设置SocketChannel为非阻塞模式,同时设置客户端连接的TCP参数
3.步骤三 异步连接服务端
4.步骤四 判断是否连接成功…
5.步骤五 向Reacctor线程的Selector注册OP_CONNECT状态位,监听服务端的TCP ACK应答

clientChannel.register(selector,SelectionKey.OP_CONNECT,ioHandler);

6.步骤六 创建Reactor线程,创建Selector
7.步骤七 Selector在线程run方法的无限循环体内轮询准备就绪的Key
8.步骤八 接收connect事件进行处理

if(key.isConnectable())
   //handlerConnect();

9.步骤九 判断连接结果,如果连接成功,注册读事件到Selector

if(channel.finishConnect())
   registerRead();
  1. 步骤十 注册读事件到Selector
clientChannel.register(selector,SelectionKey.OP_READ,ioHandler);
  1. 步骤十一 异步读客户端请求消息到缓冲区
int readNumber=channel.read(receivedBuffer);
  1. 步骤十二 对ByteBuffer进行编解码,如果有半包消息接收缓冲区Reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排。
Object message=null;
while(buffer.hasRemain()){
   byteBuffer.mark();
   Object message = decode(byteBuffer);
   if(message==null)
   {
      byteBuffer.reset();
      break;
   }
   messageList.add(message);
}
if(!byteBuffer.hasRemain())
   byteBuffer.clear();
else
   byteBuffer.compact();
if(messageList!=null & !messageList.isEmpty())
{
   for(Object messageE : messageList)
      handlerTask(messageE);
}
  1. 步骤十三 将POJO对象encode成ByteBuffer,调用SockeChannel的异步write接口,将消息异步发送给客户端
socketChannel.write(buffer);

示例:
TimeClient.java

import com.phei.netty.nio.TimeClientHandle;

public class TimeClient {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int port = 8080;
        if (args != null && args.length > 0) {
            try {
                port = Integer.valueOf(args[0]);
            } catch (NumberFormatException e) {

            }
        }
        new Thread(new TimeClientHandle("127.0.0.1",port),"TimeClien-001").start();
    }
}

TimeClientHandle.java

package com.phei.netty.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.SocketChannel;
import java.util.Iterator;
import java.util.Set;

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

    public TimeClientHandle(String host,int port){
        this.host=host==null?"127.0.0.1":host;
        this.port=port;
        try{
            selector=Selector.open();
            socketChannel=SocketChannel.open();
            socketChannel.configureBlocking(false);   //设置为异步非阻塞模式,下面可以设置SocketChannel的TCP参数,例如接收和发送的TCP缓冲区大小
        }catch(IOException e){
            e.printStackTrace();
            System.exit(1);
        }
    }

    @Override
    public void run() {
        try{
            //发送连接请求
            doConnect();
        }catch(IOException e){
            e.printStackTrace();
            //作为示例,这里认为连接是成功的,没有进行重连操作
            System.exit(1);
        }
        while(!stop){
            try{
                //轮询,当有就绪的Channel时,执行handleInput(key)方法
                selector.select(1000);
                Set<SelectionKey> selectedKeys=selector.selectedKeys();
                Iterator<SelectionKey> it=selectedKeys.iterator();
                SelectionKey key=null;
                while(it.hasNext()){
                    key=it.next();
                    it.remove();
                    try{
                        handleInput(key);
                    }catch(Exception e){
                        if(key!=null){
                            key.cancel();
                            if(key.channel()!=null)
                                key.channel().close();
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
                System.exit(1);
            }
        }
        //Selector关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源。
        if(selector !=null)
            try{
                selector.close();
            }catch(IOException e){
                e.printStackTrace();
            }
    }
    private void handleInput(SelectionKey key) throws IOException{
        if(key.isValid()){
            //判断是否连接成功
            SocketChannel sc=(SocketChannel)key.channel();
            if(key.isConnectable()){
                if(sc.finishConnect()){
                     //客户端连接成功,监听网络读操作,然后发送请求消息给服务端
                    sc.register(selector, SelectionKey.OP_READ);
                    doWrite(sc);
                }else
                    //客户端连接失败
                    System.exit(1);
            }
            if(key.isReadable()){
                ByteBuffer readBuffer=ByteBuffer.allocate(1024);   //分配1M ByteBuffer
                int readBytes=sc.read(readBuffer);
                if(readBytes>0){
                    readBuffer.flip();
                    byte[] bytes=new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body=new String(bytes,"UTF-8");
                    System.out.println("Now is : " + body);
                    this.stop=true;
                }else if(readBytes<0){
                    key.cancel();
                    sc.close();
                }else
                    ;
            }
        }
    }
    private void doConnect() throws IOException{
        if(socketChannel.connect(new InetSocketAddress(host,port))){
            //如果连接成功,则将SocketChannel注册到Selector上,注册SectionKey.OP_READ
            socketChannel.register(selector, SelectionKey.OP_READ);
            doWrite(socketChannel);
        }else
            //如果没有连接成功,则说明服务端没有返回TCP握手应答消息,但这并不代表连接失败。需要将SocketChannel注册到Selector上,注册SelectionKey.OP_CONNECT。当服务端返回TCP syn-ack消息后,Selector就能够轮询到这个SocketChannel处于连接就绪状态
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
    }
    private void doWrite(SocketChannel sc)throws IOException{
        byte[] req="QUERY TIME ORDER".getBytes();
        ByteBuffer writeBuffer=ByteBuffer.allocate(req.length);
        writeBuffer.put(req);
        writeBuffer.flip();
        sc.write(writeBuffer);     //这里同样存在“半包写”问题 
        if(!writeBuffer.hasRemaining())   //如果发送结果全部发送完成
            System.out.println("Send order 2 server succeed.");
    }
}

JDK1.7升级了NIO类库,升级后的NIO类库被称为NIO 2.0 。JAVA正式提供了异步文件I/O操作,同时提供了与UNIX网络编程事件驱动I/O对应的AIO。

基于NIO2.0的异步非阻塞(AIO)编程

NIO 2.0 引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。异步通道提供以下两种方式获取操作结果:

  • 通过java.util.concurrent.Future 类来表示异步操作的结果
  • 在执行异步操作的时候传入一个java.nio.channels

CompletionHandler接口的实现类作为操作完成的回调。
AIO 相对于NIO编程更为简单,异步SocketChannel是被动执行对象,不需要像NIO编程那样创建一个独立的I/O来处理读写操作。

目前国内商用的主流Java版本仍是JDK1.6 ,NIO2.0使用相对较少。

为什么要使用NIO

不同I/O模型对比

这里写图片描述

JAVA原生NIO一些问题

  • NIO类库和API繁杂,使用麻烦
  • 需要具备其它的额外技能做铺垫,如熟悉JAVA多线程编程。这是因为NIO编程涉及到Reactor模式,必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序。
  • 可靠性能力补齐的工作量和难度都非常大,如:客户端面临重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题
  • JDK NIO的BUG,如epoll bug,会导致Selector空轮询,最终导致CPU100%
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程圈子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值