Java NIO(三):网络编程NIO

Java NIO(二):标准输入输出NIO

目录

一、Selector

二、SelectionKey

三、一个简单的Server-Client实例


Selector是网络编程NIO中的核心组件

一、Selector

Selector(选择器)这个组件用于采集各个通道的状态(事件)。Selector轮询每个注册的Channel,一旦发现Channel有注册的事件发生,便获取事件然后进行处理。Selector允许单线程处理多个Channel(一个Channel就是一个连接),如果每个连接的流量都很小,使用Selector就会很方便。如下图:

要使用Selector,首先要进程注册,向selector注册Channel,然后调用selector的select方法。这个方法会阻塞到某个注册的事件就绪,在获取了到达事件之后,就可以逐个地对这些事件进行响应处理。

其实这就是Selector存在的意义:如果用阻塞I/O需要多线程(浪费内存),如果用非阻塞I/O,需要不断重试(耗费CPU)。而使用Selector在非阻塞模式下,通过Selector线程只为已就绪的通道工作,不会不断的重试了。比如,当所有通道都没有数据到达时,也就没有Read事件发生,我们的线程会在select()方法处被挂起,从而让出了CPU资源。

通道有4个事件可供监听:

  • Accept:有可以接受的连接
  • Connect:连接成功
  • Read:有数据可读
  • Write:可以写入数据了

Selector使用方法

1、调用Selector.open()方法来创建一个Selector

Selector selector = Selector.open()

2、向Selector注册通道

channel.configureBloacking(false);  //需要将Channel设置为非阻塞模式,否则会抛异常

SelectionKey key = channel.register(selector,SeletionKey.OP_READ);

register()方法的第二个参数名叫“interest set”,也就是你所关心的事件集合。如果你关心多个事件,用一个“按位或运算符”分隔,比如:SelectionKey.OP_READ | SelectionKey.OP_WRITE,这很好理解,可见源码:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

也就是说

二、SelectionKey

在上述Selector注册通道时,channel.register返回的是SelectionKey类型的对象,这个对象包含了本次注册的信息,可以通过它修改注册信息。

SelectionKey对象中含有如下属性:

  • interest集合(使用&操作SelectionKey.OP_ACCEPT和key.interestOps())
  • ready集合(key.readyOps(),可以使用&操作检测该集合,也可以使用is方法)
  • Channel(key.channel())
  • Selector(key.selector())
  • 附加对象(key.attach(obj)   Object obj = key.attachment())

selector通道选择:

  • int select():阻塞
  • int select(long timeout):超时之前阻塞
  • int selectNow():不阻塞

一但调用select方法并且返回了,说明有一个或多个通道就绪了,然后通过该方法选择已经就绪的集合,然后遍历这些集合对每个通道进行处理。

Selector selector = Selector.open();
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
	SelectionKey key = (SelectionKey)keyIterator.next();
	if(key.isAcceptable()) {
		// a connection was accepted by a ServerSocketChannel.
	}else if(key.isConnectable()) {
		// a connection was established with a remote server.
	}else if(key.isReadable()) {
		// a channel is ready for reading
	}else if(key.isWritable()) {
		// a channel is ready for writing
	}
	keyIterator.remove();
}

三、一个简单的Server-Client实例

Server

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;

public class NIOServer {
	public static void main(String[] args) throws IOException {
        // 创建一个selector
        Selector selector = Selector.open();

        // 初始化TCP连接监听通道
        ServerSocketChannel listenChannel = ServerSocketChannel.open();
        listenChannel.bind(new InetSocketAddress(8099));  //设置端口号
        listenChannel.configureBlocking(false);	 //将Channel设置为非阻塞模式
        
        // 注册到selector(监听其ACCEPT事件)
        listenChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 创建一个缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(100);

        while (true) {
            selector.select(); //阻塞,直到有监听的事件发生
            Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();

            // 通过迭代器依次访问select出来的Channel事件
            while (keyIter.hasNext()) {
                SelectionKey key = keyIter.next();

                if (key.isAcceptable()) { // 有连接可以接受
                    SocketChannel channel = ((ServerSocketChannel) key.channel()).accept();
                    channel.configureBlocking(false);
                    channel.register(selector, SelectionKey.OP_READ);
                    System.out.println(channel.getRemoteAddress() + "上线了!");
                } else if (key.isReadable()) { // 有数据可以读取
                	// 读取到流末尾说明TCP连接已断开,因此需要关闭通道或者取消监听READ事件,否则会无限循环
                	buffer.clear();
                    if (((SocketChannel) key.channel()).read(buffer) == -1) {
                        key.channel().close();
                        continue;
                    } 
                    // 按字节遍历数据
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        byte b = buffer.get();
                        //以字符 '\0'(一个值为0的字节) 来标识消息结束
                        if (b == 0) { // 客户端消息末尾的\0
                            System.out.println();

                            // 响应客户端
                            buffer.clear();
                            buffer.put("Hello, Client!\0".getBytes());
                            buffer.flip();
                            while (buffer.hasRemaining()) {
                                ((SocketChannel) key.channel()).write(buffer);
                            }
                        } else {
                            System.out.print((char) b);
                        }
                    }
                }

                // 已经处理的事件要手动移除
                keyIter.remove();
            }
        }
    }
}

Client

public class NIOClient {

    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("localhost", 8099);
        InputStream is = socket.getInputStream();
        OutputStream os = socket.getOutputStream();

        // 先向服务端发送数据
        os.write("Hello, Server!\0".getBytes());

        // 读取服务端发来的数据
        int b;
        while ((b = is.read()) != 0) {
            System.out.print((char) b);
        }
        System.out.println();

        socket.close();
    }
}

NIO擅长1个线程管理多条连接,节约系统资源,但是如果每条连接要传输的数据量很大的话,因为是同步I/O,会导致整体的响应速度很慢;而传统I/O为每一条连接创建一个线程,能充分利用处理器并行处理的能力,但是如果连接数量太多,内存资源会很紧张。所以。连接数多且数据量小用NIO,连接数少用I/O~


参考资料

https://blog.csdn.net/javaxuexi123/article/details/81910644

https://blog.csdn.net/geekcome/article/details/23868411

猜你喜欢

转载自blog.csdn.net/qq_39192827/article/details/86568260