Java网络编程杂谈(二)

前序:
上一篇Java网络编程杂谈(一)主要讲了一些IO相关的概念和BIO编程模型。这一篇文章接着详谈线程池版BIO和NIO,粗略介绍AIO,然后概述4种IO模型对比。

线程池版BIO

针对与前面的BIO,它是一个连接对应一个线程的,也就是连接上来的客户端数量和服务器端的线程的数量是一比一的关系。而线程池版的BIO就是利用一个线程池来处理多个客户端的请求,线程池中的线程数M可以小于连接上来的客户端的数量N。通过线程池可以灵活的调配线程资源,设置线程数的最大值,防止由于海量并发连接导致线程耗尽资源。

线程池版BIO的实现简述

这里还是接着上篇时间服务器的例子。线程池版BIO只需要在服务器的主函数中创建一个时间服务器处理类的线程池,将请求socket封装成一个task(也就是一个实现了runnable接口的类,该类以socket作为构造器参数,可以用前面BIO中的TimeServerHandler),然后调用线程池的execute方法执行该task。

线程池版BIO的缺点分析及线程池的好处

上篇分析了BIO为什么是同步阻塞的,究其原因还是因为两个IO API——InputStream和OutputStream类方法是同步的,而线程池版BIO依然使用了这两个IO API,所以不可避免的线程池版的BIO依然还是同步阻塞的,线程池只是从线程模型上做了优化,无法从根本上解决同步IO操作引起的通信线程阻塞问题,下面结合IO同步和线程池的工作策略来分析因为对方返回应答时间长而引起的级联故障:

(1)现有某服务器处理缓慢,返回应答耗时长;
(2)线程池版BIO下的线程正在读取该服务器的响应,由于读入流是同步的,所以该线程会被同步阻塞;
(3)假设所有线程都被该服务器阻塞,那么后续的所有IO任务将会加入阻塞队列中;
(4)由于线程池采用阻塞队列实现,当队列积满后,队列被阻塞,有任务也不可以再加入队列;
(5)此时,accept线程在前端接受到一个客户端的链接请求,但这个线程会被阻塞在线程池的阻塞队列之上,新的客户端请求会被拒绝,进而发生大量客户端连接超时;
(6)由于几乎所有的连接都超时,调用者会认为系统已经崩溃。

NIO模型

NIO库在JDK1.4中引入,NIO库提供了面向块的、高速的IO。

NIO模型的三大核心要素

通道channel

channel是指通道,网络数据通过channel进行读取和写入。NIO中的channel对应着BIO中的流,流中的数据只能在一个方向上移动,要么读取、要么写入;而channel中的数据是双向的,全双工的。

缓冲区buffer

在NIO中,所有数据都是用缓冲区(buffer)处理的,读的时候需要读取到缓冲区中,写入的时候也需要写入到缓冲区中。任何时候访问NIO中的数据都需要通过缓冲区进行。

多路复用器selector

Selector是NIO模型中的多路复用器,底层调用的是epoll系统调用,关于支持IO多路复用的系统调用包括select、poll、epoll等的讨论在Java网络编程杂谈(一)一文中的5大IO模型那块做过简短说明,这里不再赘述。

NIO的实现关键是IO多路复用技术,而多路复用器Selector提供选择已经就绪的任务的能力。简单来讲,就是Selector会不断地轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel处于就绪状态,就会被Selector轮询出来,然后通过SelectKey可以获取到就绪的channel集合,进行后续的IO操作。

NIO代码实现

在NIO代码实现这块注意几个类的使用:
ByteBuffer——channel读写数据都必须通过Buffer对象;
Selector——多路复用器,将channel注册在多路复用器上,以便多路复用器为我们选出就绪channel;
ServerSocketChannel——服务端channel,有accept()方法等待客户端连接请求,返回一个SocketChannel;
SocketChannel——每一个与服务器建立TCP链接的客户端都是一个SocketChannel;
SelectionKey——获取就绪channel、判断channel所属就绪状态;

NIO服务器端代码实现

public class TimeServer {
    public static void main(String[] args) {
        int port = 9090;
        MutiplexTimeServer timeServer = new MutiplexTimeServer(port);

        new Thread(timeServer,"NIO-MutiplexTimeServer-001").start();
    }

    @Test
    public void test(){
        ByteBuffer byteBuffer = ByteBuffer.allocate(12);
        System.out.println(byteBuffer.isDirect());
    }
}

MutiplexTimeServer 类的实现

public class MutiplexTimeServer implements Runnable {
    private Selector selector;

    private ServerSocketChannel serverSocketChannel;

    private volatile boolean stop;

    public MutiplexTimeServer(int port){
        try {
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(port),1024);
            serverSocketChannel.register(selector, SelectionKey.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){
            try {
                selector.select(1000); //阻塞方法,直到有就绪的channel,退出方法
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                SelectionKey key = null;
                while(iterator.hasNext()){
                    key = iterator.next();
                    iterator.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();
                sc.configureBlocking(false);
                sc.register(selector,SelectionKey.OP_READ);
            }

            if(key.isReadable()){
                SocketChannel sc = (SocketChannel) key.channel();
                ByteBuffer readbuffer = ByteBuffer.allocate(1024);
                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("The time server receive order : " + body);
                    String currentTime = "QUERY TIME ORDER".equals(body) ? new Date(System.currentTimeMillis()).toString():"BAD ORDER";
                    doWrite(sc,currentTime);
                }else if(readBytes < 0){
                    //对端链路关闭
                    key.cancel();
                    sc.close();
                }else{
                    ;//读到0字节,忽略
                }
            }
        }
    }

    private void doWrite(SocketChannel sc,String response) throws IOException {
        if(response != null && response.length()>0){
            byte[] bytes = response.getBytes();
            ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            sc.write(writeBuffer);
        }
    }
}

NIO客户端代码实现

public class TimeClient {
    public static void main(String[] args) {
        int port = 9090;
        new Thread(new TimeClientHandle("127.0.0.1",port),"TimeClient-001").start();
    }
}

TimeClientHandle 的实现

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;
        this.port = port;
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
        }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 {
                selector.select(1000);
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                SelectionKey key = null;
                while(iterator.hasNext()){
                    key = iterator.next();
                    iterator.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);
            }
        }

        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);
                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 ; //读到0字节,忽略
            }
        }
    }

    public 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);
    }

    public 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.");
        }
    }
}

NIO代码分析

NIO服务器端建立步骤:
(1)创建ServerSocketChannel,配置为非阻塞模式;
(2)绑定监听端口,配置TCP参数;
(3)创建一个IO线程,用于轮询多路复用器Selector;
(4)创建Selector,将之前创建的ServerSocketChannel注册到Selector上,并监听OP_ACCEPT操作位;
(5)启动IO线程,在循环体中执行Selector.select()方法,选出就绪channel;
(6)当select()方法返回时,通过selector.selectedKeys()方法获取SelectionKey对象,并判断channel就绪事件:
(6.1)当就绪事件为OP_ACCEPT时,表示有客户端连接请求,调用ServerSocketChannel.accept()方法建立连接,并返回SocketChannel,将其注册在Selector上监听OP_READ操作位;
(6.2)当就绪事件为OP_READ时,创建Buffer对象读取channel中的数据;
(6.3)当就绪事件为OP_WRITE时,说明可以执行向该channel中写数据的操作;

AIO模型简述

AIO是NIO 2.0,可以进行真正的异步IO操作,需要调用内核的IO接口来完成;
在实现代码上比较复杂。NIO2.0 提供了异步套接字通道的实现,异步通道提供以下两种方式获取操作结果:
1、通过java.util.concurrent.Future类来表示异步操作的结果;
2、在执行异步操作的时候传入一个CompletionHandler接口的实现类作为操作完成的回调;

第一种方法是通过Future类获取异步操作结果,在做后面的操作;第二种方法是异步操作结束直接调用回调方法;

在AIO的代码实现上注意以下几个类的异步方法的调用:
1、AsynchronousServerSocketChannel.accept()
2、AsynchronousSocketChannel的read方法和write()方法
上面三个异步方法都可以传入一个CompletionHandler接口的实现类作为操作完成的回调,该接口需要重写两个方法分别是:completed()方法和failed()方法。

由于AIO不是本次网络编程重点,这里不贴代码了,代码理解也比较复杂,不过抓住异步操作的流程来理解还是能看懂书上的代码的。

4种IO模型对比

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值