【网络编程】JAVA中的io之NIO

NIO原理与实践
本文深入探讨了NIO(非阻塞I/O)的核心概念,包括缓冲区Buffer、通道Channel和多路复用器Selector,并提供了详细的代码示例,展示了如何使用NIO实现服务器与客户端的通信。

本文原地址:https://www.dyzhello.club/static/page/readArticle.html?id=15450

原创内容转载请说明

在讨论NIO之前我们需要明白几个概念:

  • 缓冲区Buffer
  • 通道Channel
  • 多路复用器Selector

Buffer:

顾名思义这是一种缓冲区,在原来的IO操作中,我们可以直接把数据写到Stream里,但是在NIO中我们要对数据进行读取或者写入都是对Buffer进行操作。

常见的Buffer类有ByteBuffer、CharBuffer、IntBuffer、ShortBuffer、LongBuffer、FloatBuffer、DoubleBuffer。显而易见的是这些缓冲区类都是针对基本数据类型的。

Channel:

这是一种通道,我们就是通过他进行数据的传递。可以类比为现实生活中的水管,不同的是水管的流向是可以双向但不同时的但是Channel的读取和写入是可以双向同时的,这就是Stream和Channel主要的区别。

Selector:

一般称 为选择器 ,当然你也可以称之为多路复用器 。用于检查注册在其上的Channel的状态并进行处理,由于多个Channel可以都在一个Selector上注册,从而实现一个线程管理多个Channel,所以称之为多路复用(当然多路复用这个概念不局限于此)。

接下来我们就来实现一下NIO通信:

服务端Server代码:

public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(9090));
        new Thread(new SelectorHandler(serverSocketChannel)).start();
    }
}

首先我们创建一个ServerSocketChannel对象来监听9090端口,然后开启一个线程把这个ServerSocketChannel交给SelectorHandler处理

接下来实现SelectorHandler来进行具体的IO处理

SelectorHandler:

@SuppressWarnings("Duplicates")
public class SelectorHandler implements Runnable {
    ServerSocketChannel serverSocketChannel;
    Selector selector;

    SelectorHandler(ServerSocketChannel serverSocketChannel) throws IOException {
        this.serverSocketChannel = serverSocketChannel;
        selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

    @Override
    public void run() {
        while (true) {
            try {
                selector.select(1000);
                Set<SelectionKey> selectionKeySet = selector.selectedKeys();
                Iterator<SelectionKey> iterable = selectionKeySet.iterator();
                while (iterable.hasNext()) {
                    SelectionKey key = iterable.next();
                    iterable.remove();
                    handlerKey(key);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void handlerKey(SelectionKey key) throws IOException {
        if (key.isAcceptable()) {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            SocketChannel sc = ssc.accept();
            // 不设置将会Connect reset by peer;
            sc.configureBlocking(false);
            sc.register(selector, SelectionKey.OP_READ);
        }
        if (key.isReadable()) {
            SocketChannel sc = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(10240);
            int byteLen = sc.read(buffer);
            if (byteLen > 0) {
                buffer.flip();
                byte[] bytes = new byte[buffer.remaining()];
                buffer.get(bytes);
                String request = new String(bytes, "utf-8");
                System.out.println(request);
                ByteBuffer responseBuffer = ByteBuffer.allocate(10240);
                responseBuffer.put("我已经收到了你的请求".getBytes());
                responseBuffer.flip();
                sc.write(responseBuffer);
            }
        }
    }

}

 

首先在构造函数里我们创建一个Selector对象并将serverSocketChannel注册在其上。

再来看run方法里的实现:

可以看到我们写了一个死循环目的就是为了不断的轮询注册在Selector上的Channel

Set<SelectionKey> selectionKeySet = selector.selectedKeys();

 

在这里我们获取注册在selector上的Channel的key,那么这个key是什么呢?其实就是对channel的状态的标记。

当我么获取到keySet之后我们遍历所有的key然后交给handlerKey方法处理。

while (iterable.hasNext()) {
                    SelectionKey key = iterable.next();
                    iterable.remove();
                    handlerKey(key);
                }

 

在handlerKey里我们通过获取当前的key值来判断channel的状态

if (key.isAcceptable())
 if (key.isReadable())

这两种状态我么做不同的处理,当isAcceptable为true时我们将调用

SocketChannel sc = ssc.accept();

来获取SocketChannel这里需要注意的是在BIO中accept是一个阻塞的方法,直到有连接请求才会继续往下执行,但是由于我们在创建ServerSocketChannel的时候将其置为no-blacking所以这里如果没有连接会直接返回null。下面是java api中关于该方法的描述:

Accepts a connection made to this channel's socket.

If this channel is in non-blocking mode then this method will immediately return null if there are no pending connections. Otherwise it will block indefinitely until a new connection is available or an I/O error occurs.

The socket channel returned by this method, if any, will be in blocking mode regardless of the blocking mode of this channel.

This method performs exactly the same security checks as the accept method of the ServerSocket class. That is, if a security manager has been installed then for each new connection this method verifies that the address and port number of the connection's remote endpoint are permitted by the security manager's checkAccept method.

我把重要的两段翻译了一下:

如果该通道处于非阻塞模式,那么如果没有挂起的连接,该方法将立即返回null。否则,它将无限期阻塞,直到有新的连接可用或出现I/O错误。

该方法返回的套接字通道(如果有的话)将处于阻塞模式,而不管该通道的阻塞模式如何。

在我第一次编写的该代码时就在这里遇到了问题,我的程序不能接受连接请求,一旦有请求过来就会抛出异常提示:Connect reset by peer;

因为返回的Channel的阻塞模型和改对象的阻塞模型无关,所以要重新设置为no-blocking

 // 不设置将会Connect reset by peer;
            sc.configureBlocking(false);

然后我们把他注册到selector上并把状态设置为可读,当再一次轮询到这个key的时候就会进入下面的逻辑:

 ByteBuffer buffer = ByteBuffer.allocate(10240);
            int byteLen = sc.read(buffer);
            if (byteLen > 0) {
                buffer.flip();
                byte[] bytes = new byte[buffer.remaining()];
                buffer.get(bytes);
                String request = new String(bytes, "utf-8");
                System.out.println(request);
                ByteBuffer responseBuffer = ByteBuffer.allocate(10240);
                responseBuffer.put("我已经收到了你的请求".getBytes());
                responseBuffer.flip();
                sc.write(responseBuffer);

在这里我们创建一个ByteBuffer来获取channel中的内容,并给请求方一个响应,在这里注意我们只需要把ByteBuffer写入Channel并不需要flush,因为这不是一个流

到这里我们的服务端代码就完成了接下来我们完成客户端的编写:

Client:

public class Client {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 9090));
        socketChannel.configureBlocking(false);
        new Thread(new ClientSelectorHandler(socketChannel)).start();
    }
}

和服务端思路相同但不同的是这里我们创建的是SocketChannel而非ServerSocketChannel

ClientSelectorHandler:

@SuppressWarnings("Duplicates")
public class ClientSelectorHandler implements Runnable {
    SocketChannel socketChannel;
    Selector selector;

    ClientSelectorHandler(SocketChannel socketChannel) throws IOException {
        this.socketChannel = socketChannel;
        selector = Selector.open();
        if (this.socketChannel.isConnected()) {
            this.socketChannel.register(selector, SelectionKey.OP_READ);
            ByteBuffer requestBuffer = ByteBuffer.allocate(10240);
            requestBuffer.put("这是一个请求".getBytes("utf-8"));
            requestBuffer.flip();
            this.socketChannel.write(requestBuffer);
        } else {
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                selector.select(1000);
                Set<SelectionKey> selectionKeySet = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeySet.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    handlerKey(key);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void handlerKey(SelectionKey key) throws IOException {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        if (!key.isValid()) {
            return;
        }
        if (key.isConnectable()) {
            if (socketChannel.finishConnect()) {
                socketChannel.register(selector, SelectionKey.OP_READ);
            }
        }
        if (key.isReadable()) {
            ByteBuffer buffer = ByteBuffer.allocate(10240);
            int readState = socketChannel.read(buffer);
            if (readState > 0) {
                buffer.flip();
                byte[] bytes = new byte[buffer.remaining()];
                buffer.get(bytes);
                String response = new String(bytes, "utf-8");
                System.out.println(response);
            }
        }

    }
}

客户端实现思路和服务端大体相同只是对状态的判断上略有不同

 if (key.isConnectable()) {
            if (socketChannel.finishConnect()) {
                socketChannel.register(selector, SelectionKey.OP_READ);
            }
        }

 

在这里我们判断是否连接成功,如果连接成功就将socketChannel注册到selector上并标记为可读。到此,我们就完成了一个非常简单的NIO示例,可以看到即使如此相比传统的BIO依然复杂了很多。

在后续文章中我们将实现JDK7引进的AIO示例,相比繁杂的多路复用实现的NIO,AIO是一种更加简化的异步IO。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值