(一)NIO原理实现分析之总体简介和FileChannel、SocketChannel和ServerSocketChannel三种实现Demo

目录

一、NIO体系

1.NIO和IO的区别

2.NIO组成关系

2.1 协同关系图

2.2 UML类图

3.NIO常用组件

3.1 Buffer

3.2 Channel

3.3 Selector

3.4 SelectionKey

3.5 SelectorProvider

3.6 SelectableChannel

二、使用实例

1.FileChannel简单实例

2.SocketChannel和ServerSocketChannel简单实例

3.Socket多路复用


一、NIO体系

自从JDK1.4发布NIO以来,随着各种框架的使用,NIO已经成为了一个不得不了解的体系,如果想要了解注入Tomcat、Zookeeper和Netty等框架原理和源码,NIO是一个不得不跨过去的坎。以前大致学习过NIO体系,但并未梳理成文章,此时便大致梳理一下顺便复习复习。

1.NIO和IO的区别

传统的IO(阻塞型IO)是基于流实现的,并且其是单向通信,不能同时写和同时读,具有执行排他性。而NIO(非阻塞型IO)则是基于通道实现的,可以实现双向通信,即同时写和读。因为NIO读取和写入数据是面向非阻塞的缓冲区,而不是单向的流。

相比于IO而言有以下三个优点:

  1. 通过Channel注册到Selector上的状态来实现一种客户端与服务端的通信;
  2. Channel中数据的读取是通过Buffer,一种非阻塞的读取方式;
  3. Selector多路复用器单线程模型,线程的资源开销相对比较小。

2.NIO组成关系

NIO一共有三个部分:

  1. Selector;
  2. Channel;
  3. Buffer。

2.1 协同关系图

这三个部分的协同关系如下图:

由上图可以看出三者之间的关系:Selector负责控制管理通道Channel,而Buffer则负责程序和通道之间的数据读写。

2.2 UML类图

常用通道类图:

由UML类图可以大致看出来,文件通道和一般的NIO通道些许有些不一样,因为FileChannel是可以通过IO文件输入输出流的getChannel来获取的,而SocketChannel这些通道更多的是使用Selector和SelectionKey搭配SelectionProvider来创建的,因此从UML类图上看有两条线。但是毫无疑问的是Channel通道的读写都是使用Buffer的实现子类来完成的,也就是协同关系图所画的那样。

具体详细的组件看到后面介绍。

3.NIO常用组件

常用组件图:

接下来详细介绍一下这六个组件的大致作用。

3.1 Buffer

Buffer对象是用于特定基元类型数据的容器。翻译成人话就是处理Java基础类型及衍生类的数据容器对象。缓冲区是特定基元类型的元素的线性有限序列,即存储数据结构是一个一元数组。除了它的内容外,缓冲区的基本属性是它的容量、限制和位置。

几个重要参数的意义:

  • capacity:作为一个内存块,Buffer固定的大小容量值。只能往里写capacity个基本类型类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据;
  • position:当写数据到Buffer中时,position表示当前的位置。初始的position值为0,当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元,position最大可为capacity–1;当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0,当从Buffer的position处读取数据时,position向前移动到下一个可读的位置;
  • limit:在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity;当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)。

3.2 Channel

用于I/O操作的连接通道。这些通道表示与实体(如硬件设备、文件、网络套接字或程序组件)的开放连接,这些实体能够执行一个或多个不同的I/O操作,例如读或写操作。通道要么是打开的,要么是关闭的,没有中间状态,当创建时通道将会打开,而一旦关闭后通道则无法操作。NIO的Channel多线程访问是线程安全的。

通道常用的有三种类型:

3.2.1 FileChannel

针对文件的通道FileChannel,用于读取、写入、映射和操作文件的通道。这个通道对象的所有操作都是基于Buffer实现子类完成的。相较于普通的IO文件操作不同的是这个通道是线程安全的,但是如果多个操作来操作同一个位置将会被阻塞。

该类的实例提供的文件视图保证与同一程序中其他实例提供的同一文件的其他视图一致。然而,这个类的实例所提供的视图可能与其他并发运行的程序所看到的视图一致,也可能不一致,这是由于底层操作系统所执行的缓存以及网络文件系统协议所导致的延迟。不管其他程序是用什么语言编写的,也不管它们是在同一台机器上运行还是在其他机器上运行,都是如此。任何这种不一致的确切性质都是依赖于系统的,因此是不确定的。

文件通道可以通过该类的open()方法创建,也可以通过File文件输出输入流对象的getChannel()方法来获取,返回的通道都是关联到对应流的文件上,无论是显式地或通过读取或写入字节来改变通道的位置、通过通道改变文件的长度、通过通道改变文件内容这三个操作都将改变原始对象的对应属性,反之亦然。

文件通道有很多的打开方式,如"open for reading"、"open for writing"或者"open for reading and writing"这几种,通过不同的文件流打开的将会是不同模式,如输入流是"open for reading",输出流就是"open for writing"等(当然,如果输出流是append模式,通道对应的也会是append模式)。

3.2.2 SocketChannel

面向流连接的可选择通道SocketChannel,Socket通道通过open()方法创建,不可能为任意预先存在的套接字创建通道,一个新创建的Socket通道只是打开但是并没有连接,只有调用connect方法后才会连接。只要连接了通道将会维护这个连接直到被关闭,通道没连接上时进行任何操作都会抛出异常,但是调用isConnected()判断方法是被允许的。

Socket通道支持非阻塞连接,通过connect()方法创建套接字连接,并通过finishConnect()方法完成连接。连接操作是否正在进行可以通过调用isConnectionPending()方法来判断确定。Socket通道也支持异步关闭。

Socket连接可以通过setOption()方法来设置不同的模式,具体模式在StandardSocketOptions类中标明了。

Socket通道也是线程安全的,支持多线程同时的读和写,但是connect和finishConnect方法相互同步,当调用这些方法时,试图发起读或写操作将会阻塞,直到调用完成。通道的读写也是通过Buffer缓冲实现子类完成的。

3.2.3 ServerSocketChannel

面向流监听的可选择通道ServerSocketChannel,这个通道和SocketChannel通道不一样的地方在于ServerSocketChannel通道是一个Socket服务器提供方,而SocketChannel通道则是连接这个服务器的Socket连接,这也是为什么说一个是面向连接一个是面向监听。

此通道可以通过这个类的open()方法来创建,但是不能为任意预先存在的ServerSocket对象来创建,新建的通道也是一样只打开但是并未连接。如果尚未绑定的服务器Socket调用了accept()方法将会抛出异常,可以通过bind()方法来实现ip和端口绑定。当然,这个也是线程安全的。

3.3 Selector

可选通道对象的多路复用器。调用本类的静态方法open(),这个方法中将会调用系统默认的提供器SelectorProvider的方法openSelector()来创建一个新的Selector对象,调用close()方法将会关闭该对象。一个可选择的通道与选择器的注册是由一个SelectionKey对象代表的。

一个Selector会维护三个SelectionKey集合,新建时都会为空:

  1. HashSet<SelectionKey> keys:表示此选择器当前通道注册的SelectionKey对象,可以由keys()方法返回;
  2. Set<SelectionKey> selectedKeys:在预选操作时至少一个SelectionKey对应的通道被检测到一就绪时将会被添加到这个集合中,这个集合一定是keys集合的子集;
  3. Set<SelectionKey> cancelledKeys:此集合中的SelectionKey是已取消但尚未取消注册的对象集合,这个集合不能直接访问,这个集合也一定是keys的子集。

当调用SelectableChannel的register方法时,将会创建一个SelectionKey,同时会将这个Key和传入的Selector关联起来(Key存在Selector中的keys中,而Key中指向当前Selector)。

3.4 SelectionKey

表示使用Selector注册SelectableChannel的标记。每次向Selector中注册一个通道时都会相应的创建一个SelectionKey注册到Selector中,但是当调用了cancel或者close方法后这个Key将会失效,但是已经取消的Key不会立即删除,而是会保存到Selector的cancelledKey集合中以等待下次选择操作来删除。使用时也需要注意SelectionKey的默认实现中将会包含Channel对象和Selector对象,因此可以把SelectionKey看成Selector和Channel的中间桥梁。

其共有四种操作类型:

  1. OP_READ:1 << 0,二进制为00001,对应十进制1。即绑定的socket可读,当selector调用select()方法时将会阻塞;
  2. OP_WRITE:1 << 2,二进制为00100,对应十进制4。即绑定的socket可写,当selector调用select()方法时不会阻塞,而是一直读取;
  3. OP_CONNECT:1 << 3,二进制为01000,对应十进制8。绑定的socket申请和server端进行连接;
  4. OP_ACCEPT:1 << 4,二进制为10000,对应十进制16。即server端调用selector的select()方法捕获到有socket要连接进来。

3.5 SelectorProvider

用于选择器Selector和可选择通道SelectableChannel的服务提供程序类。这个类的具体实现子类将会有一个无参构造函数和若干抽象实现方法。当调用该类的provider方法时将会调用返回虚拟机的默认实现实例,如果需要通过提供类获取不同类型的Channel,只需要调用对应的open()方法即可,如常用的Socket调用openSocketChannel()方法即可。

3.6 SelectableChannel

可以通过选择器多路复用的通道。如果想要和Selector一起使用,则必须通过该类的register()注册方法将该类和对应的Selector关联起来,这个方法将会返回SelectionKey对象,这个对象将会包含可选择Channel和Selector两个对象。一旦使用选择器注册,通道将保持注册状态,直到取消注册为止。这涉及到释放选择器分配给通道的所有资源。

通道是不能直接取消注册的,只能注销SelectionKey来达到这种效果。注销SelectionKey要求通道在选择器的下一个选择操作期间注销。当Channel被关闭时将会在暗中注销这个Channel所有关联的SelectionKey,无论是通过调用close方法或者通过中断在通道上的I/O操作中阻塞的线程。

但是当Selector被关闭时Channel将会被注销,SelectionKey也将会立即失效。通道在被注册时最多只能被任一一个Selector选择器注册一次,通道是否注册到一个或多个选择器可以通过调用isRegistered()方法来确定。

需要注意的是SelectableChannel是线程安全的。我们可以选择阻塞模式,也可以选择非阻塞模式,可以通过isBlocking()方法来判断是否阻塞。新创建的可选择Channel都是阻塞模式,只有搭配Selector多路复用非阻塞模式才能起到最大的作用,通道在注册到选择器之前必须被放置到非阻塞模式,并且在它被注销之前不能被返回到阻塞模式。

二、使用实例

1.FileChannel简单实例

上面简单介绍过,FileChannel可以使用open()方法来直接获取文件通道,也可以使用IO的输入输出流来获取,接下来使用一个简单的读取文件实例来说明其大致用法。代码如下:

public class NIOTest {
    private static final int K_BUFFER_SIZE = 512;

    /**
     * 测试文件通道
     */
    @Test
    public void testFileChannel() throws Exception {
        // 确定文件路径等属性
        String fileName = "application.yml";
        File file = new File(ClassLoader.getSystemResource(fileName)
                .getPath());
        Path path = file.toPath();
        // 重点是这里,可以直接使用FileChannel.open()方法也可以通过IO的输入
        // 输出流的getChannel()方法获取文件通道
        // 使用FileChannel的open()方法直接获取
        FileChannel channel = FileChannel.open(path, 
                StandardOpenOption.CREATE_NEW, StandardOpenOption.READ);
        // 通过IO的输入输出流获取FileChannel对象
        //FileChannel channel = new FileInputStream(file).getChannel();
        
        // 为缓存分配大小,暂时分配512字节以方便测试
        ByteBuffer buffer = ByteBuffer.allocate(K_BUFFER_SIZE);
        long position = 0;
        int value;
        StringBuilder stringBuilder = new StringBuilder();
        // read方法将会从position位置开始读取,读取的内容会为0,但是为-1
        // 的时候说明文件流已经结束
        while ((value = channel.read(buffer)) != -1) {
            // 手动复制数组内容是因为buffer.clear()方法只会将position等属性
            // 初始化,但里面byte数组内容依然存在,且调用get(byte[],int,int)
            // 方法也还是调用arraycopy,因此这里直接自己调用复制算了
            byte[] values = new byte[value];
            System.arraycopy(buffer.array(), 0, values, 0, value);
            stringBuilder.append(new String(values));
            // 清除将属性等初始化,否则读取时buffer被填满,通道的read()
            // 方法将不会再往里面添加数据
            buffer.clear();
        }
        // 关闭流
        channel.close();
        System.out.println(stringBuilder.toString());
    }
}

其实FileChannel读取文件和写入文件的方式和普通的IO输入输出流没多大差别,并且底层调用的都是同一个方法(指的是C语言层面),所以就单纯的读写文件而言这两者差别不是很大,只是FileChannel提供了比输入输出流更灵活的操作方式而已。

但如果是复制或者移动一个文件到另一个位置,使用FileChannel的transferTo或者transferFrom这两个方法会比普通的IO流读取出来再写入要快很多。

2.SocketChannel和ServerSocketChannel简单实例

这次我们写个实例,直接使用通道连接,暂时不借助Selector多路复用来实现一个简单的Demo。Server端的代码如下:

public class NIOTest {
    private static final int K_BUFFER_SIZE = 512;
    private static final String IP = "localhost";
    private static final int PORT = 8888;
    
    /**
     * 测试Socket通道
     */
    @Test
    public void testSocketChannel() {
        // 确定IP和端口
        String ip = "localhost";
        int port = 8888;
        ByteBuffer buffer = ByteBuffer.allocate(K_BUFFER_SIZE);
        try {
            // 直接通过Provider来打开一个服务端监听通道
            ServerSocketChannel channel = SelectorProvider.provider()
                    .openServerSocketChannel();
            // 绑定IP和地址,一个IP和地址只能由一个通道绑定,其它的再来进行绑定
            // 则会报地址已被使用的异常
            channel.bind(new InetSocketAddress(IP, PORT));
            // 监听通道是否有连接进入,如果没有则会阻塞,有则会执行后续的流程
            SocketChannel socketChannel = channel.accept();
    
            StringBuilder builder = new StringBuilder();
            int value;
            // 接收监听到通道的内容并读取
            while ((value = socketChannel.read(buffer)) != -1) {
                System.out.println(value);
                System.out.println(buffer);
                builder.append(new String(buffer.array()));
                buffer.clear();
            }
            System.out.println("message:" + builder.toString());
    
            channel.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Client端的代码如下:

public class NIOTest {
    private static final String IP = "localhost";
    private static final int PORT = 8888;
    
    /**
     * 测试Socket通道
     */
    @Test
    public void testSocketChannel() {
        // 确定IP、端口和要发送的消息
        String hello = "Hello!!";
        SocketAddress address = new InetSocketAddress(IP, PORT);
        ByteBuffer buffer = ByteBuffer.wrap(hello.getBytes());
        try {
            // 先打开通道
            SocketChannel channel = SelectorProvider.provider()
                    .openSocketChannel();
            // 进行连接
            channel.connect(address);
        
            // 连接成功后将信息写入到通道中
            System.out.println(channel.write(buffer));
            System.out.println(new String(buffer.array()));
        
            channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上面的这个例子是一个NIO Socket最简单的使用,可以初步了解NIO这一块最核心的API。

3.Socket多路复用

模拟一个Server服务器端和多个客户端进行通讯发送消息,Server端具体代码如下:

public class NIOTest {
    private Selector selector;
    private BlockingDeque<SocketChannel> channels = new LinkedBlockingDeque<>();
    private static final int K_BUFFER_SIZE = 512;
    private static final String IP = "localhost";
    private static final int PORT = 8888;

    @Test
    public void testSelectorSocket() throws IOException {
        // 创建一个ServerSocketChannel对象
        ServerSocketChannel serverChannel = SelectorProvider.provider().openServerSocketChannel();
        serverChannel.configureBlocking(false);
        // 进行IP和端口绑定
        serverChannel.bind(new InetSocketAddress(IP, PORT));
    
        // 创建selector并和ServerSocketChannel关联
        selector = SelectorProvider.provider().openSelector();
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    
        // 创建一个定时发送消息的线程
        new Thread(this::intervalRegister).start();
    
        // 对服务器的多路复用进行监听
        listen();
    }
    
    /**
     * 间隔触发向client发送消息
     */
    private void intervalRegister() {
        while (true) {
            try {
                System.out.println("当前缓存channel数量:" + channels.size());
                // 接入的channel数量不为空才发送消息
                if (channels.size() > 0) {
                    Iterator<SocketChannel> ite = channels.iterator();
                    int number = 0;
                    // 遍历发送消息
                    while (ite.hasNext()) {
                        SocketChannel channel = ite.next();
                        // 如果通道被关闭了,则从缓存中移除
                        if (!channel.isOpen()) {
                            ite.remove();
                            continue;
                        }
                        channel.configureBlocking(false);
    
                        // 对缓存的SocketChannel发送消息
                        System.out.println("channel连接状态:" + channel.isConnected());
                        String msg = "time: " + System.currentTimeMillis() + " invoke channel with number " + (number++);
                        System.out.println("发送消息为:" + msg);
                        channel.write(ByteBuffer.wrap(msg.getBytes()));
                    }
                }
                System.out.println("-------------------------------------------");
                Thread.sleep(11111);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    /**
     * 监听方法
     */
    private void listen() {
        while (true) {
            try {
                // 执行到这里将会阻塞,知道有新的Channel向服务器发起请求
                selector.select();
                Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
    
                while (keys.hasNext()) {
                    SelectionKey key = keys.next();
                    if (key.isValid()) {
                        // 对不同的操作进行判断
                        if (key.isAcceptable()) {
                            System.out.println("handle acceptable.");
                            handleAcceptable(key);
                        } else if(key.isReadable()) {
                            System.out.println("handle readable.");
                            handleReadable(key);
                        } else if(key.isWritable()) {
                            System.out.println("handle writable.");
                            handleWritable(key);
                        }
                    }
                    // 移除以防止没有操作也循环调用
                    keys.remove();
                }
                System.out.println("-------------------------------------------");
                Thread.sleep(5000);
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
        }
    }
    
    /**
     * handle acceptable.
     *
     * @param key
     */
    private void handleAcceptable(SelectionKey key) throws IOException {
        if (key.channel() instanceof ServerSocketChannel) {
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            // 获取SocketChannel,通道发送具体的请求才会往下走,否则这里将会被阻塞
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.socket().setTcpNoDelay(true);
            // 将此通道注册到selector中监听read事件
            socketChannel.register(key.selector(), SelectionKey.OP_READ);
    
            // 发送消息
            ByteBuffer buffer = ByteBuffer.wrap("Hello client.".getBytes());
            while (buffer.hasRemaining()) {
                socketChannel.write(buffer);
            }
    
            // 将通道缓存
            channels.addFirst(socketChannel);
        } else {
            System.out.println("not ServerSocketChannel type.");
        }
    }
    
    /**
     * handle readable.
     *
     * @param key
     */
    private void handleReadable(SelectionKey key) throws IOException {
        if (key.channel() instanceof SocketChannel) {
            SocketChannel socketChannel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(K_BUFFER_SIZE);
            try {
                int byteRead;
                // 读取消息
                StringBuilder builder = new StringBuilder();
                while ((byteRead = socketChannel.read(buffer)) != -1) {
                    if (byteRead > 0) {
                        System.out.println("value byte read:" + byteRead);
                        // 直接复制整个消息
                        byte[] values = new byte[byteRead];
                        System.arraycopy(buffer.array(), 0, values, 0, byteRead);
                        builder.append(new String(values));
                        buffer.clear();
                    } else {
                        break;
                    }
                }
                System.out.println("server read content:" + builder.toString());
            } catch (IOException e) {
                e.printStackTrace();
                key.cancel();
                socketChannel.close();
            }
        }
    }
    
    /**
     * 处理写入
     *
     * @param key
     */
    private void handleWritable(SelectionKey key) throws IOException {
        if (key.channel() instanceof SocketChannel) {
            SocketChannel channel = (SocketChannel) key.channel();
            channel.configureBlocking(false);
            key.interestOps(SelectionKey.OP_WRITE);
    
            String msg = "Hello client, this's msg send by myself.Time:" + System.currentTimeMillis();
            ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
            System.out.println("write byte:" + buffer.array().length);
            System.out.println("write content:" + new String(buffer.array()));
            while (buffer.hasRemaining()) {
                channel.write(buffer);
            }
            key.interestOps(SelectionKey.OP_READ);
        }
    }
}

Client端代码如下:

public class NIOClientTest {
    private static final int K_BUFFER_SIZE = 512;
    private static final String IP = "localhost";
    private static final int PORT = 8888;

    private Selector selector = null;

    @Test
    public void testClientChannel() throws IOException {
        selector = SelectorProvider.provider().openSelector();
    
        for (int i = 0; i < 5; i++) {
            startClient(i);
        }
    
        invokeServer();
    }
    
    /**
     * 启动客户端
     *
     * @throws IOException
     */
    private void startClient(int i) throws IOException {
        // 先初始化socket通道并连接
        SocketChannel socketChannel = SelectorProvider.provider().openSocketChannel();
        socketChannel.configureBlocking(false);
        socketChannel.socket().setTcpNoDelay(true);
        socketChannel.connect(new InetSocketAddress(IP, PORT));
    
        // 注册到Selector多路复用器
        SelectionKey key = socketChannel.register(selector, SelectionKey.OP_CONNECT, i);
        new ChatThread(selector, socketChannel, key).start();
    }
    
    /**
     * 调用服务
     */
    private void invokeServer() {
        while (true) {
            try {
                selector.select();
                Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
    
                while (keys.hasNext()) {
                    SelectionKey key = keys.next();
    
                    if (key.isValid()) {
                        if (key.isReadable()) {
                            System.out.println("read");
                            handleRead(key);
                        } else if (key.isConnectable()) {
                            System.out.println("connect.");
                            handleConnect(key);
                        } else if (key.isWritable()) {
                            System.out.println("write.");
                            handleWrite(key);
                        }
                    }
    
                    keys.remove();
                }
                System.out.println("-------------------------------------------");
                Thread.sleep(5000);
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
        }
    }
    
    /**
     * 连接
     *
     * @param key
     * @throws IOException
     */
    private void handleConnect(SelectionKey key) throws IOException {
        if (key.channel() instanceof SocketChannel) {
            SocketChannel channel = (SocketChannel) key.channel();
            if (channel.isConnectionPending()) {
                channel.finishConnect();
            }
            channel.configureBlocking(false);
    
            String msg = "Hello Server, I'm number " + channel.hashCode() + " client.Time:" + System.currentTimeMillis();
            ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
            System.out.println("write byte:" + buffer.array().length);
            System.out.println("write content:" + new String(buffer.array()));
            while (buffer.hasRemaining()) {
                channel.write(buffer);
            }
    
            // 将此通道注册到selector中只监听read事件
            channel.register(key.selector(), SelectionKey.OP_READ);
        }
    }
    
    /**
     * 读取
     *
     * @param key
     * @throws IOException
     */
    private void handleRead(SelectionKey key) throws IOException {
        if (key.channel() instanceof SocketChannel) {
            ByteBuffer buffer = ByteBuffer.allocate(K_BUFFER_SIZE);
            SocketChannel channel = (SocketChannel) key.channel();
    
            int byteRead;
            StringBuilder builder = new StringBuilder();
            while ((byteRead = channel.read(buffer)) != -1) {
                if (byteRead > 0) {
                    System.out.println("value byte read:" + byteRead);
                    byte[] values = new byte[byteRead];
                    System.arraycopy(buffer.array(), 0, values, 0, byteRead);
                    builder.append(new String(values));
                    buffer.clear();
                } else {
                    break;
                }
            }
    
            System.out.println("channel " + channel.hashCode() + " client read msg from server:" + builder.toString());
        }
    }
    
    /**
     * 写入
     *
     * @param key
     */
    private void handleWrite(SelectionKey key) throws IOException {
        if (key.channel() instanceof SocketChannel) {
            SocketChannel channel = (SocketChannel) key.channel();
            if (channel.isConnectionPending()) {
                channel.finishConnect();
            }
            channel.configureBlocking(false);
            key.interestOps(SelectionKey.OP_WRITE);
    
            String msg = "Hello server, I'm channel " + channel.hashCode() + ", this's msg send by myself.Time:" + System.currentTimeMillis();
            ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
            System.out.println("write byte:" + buffer.array().length);
            System.out.println("write content:" + new String(buffer.array()));
            while (buffer.hasRemaining()) {
                channel.write(buffer);
            }
    
            key.interestOps(SelectionKey.OP_READ);
        }
    }
    
    class ChatThread extends Thread {
        private Selector selector;
        private SocketChannel socketChannel;
        private SelectionKey key;
    
        ChatThread(Selector selector, SocketChannel socketChannel, SelectionKey key) {
            this.selector = selector;
            this.socketChannel = socketChannel;
            this.key = key;
        }
    
        @Override
        public void run() {
            while (true) {
                if (!socketChannel.isConnected()) {
                    continue;
                }
                try {
                    Thread.sleep(15000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
                String msg = "Hello server, I'm " + socketChannel.hashCode() +  ", positive send msg.";
    
                // 注册写事件,将输入的消息发送给客户端
                key.interestOps(SelectionKey.OP_WRITE);
                key.attach(ByteBuffer.wrap(msg.getBytes()));
                this.selector.wakeup();
            }
        }
    }
}

其中有个细节,如果把SelectionKey设置为OP_WRITE模式,则会一直触发write操作,因此在写完之后把OP_WRITE改成OP_READ,这样便可以做到写一次读一次。当然这样弄在写操作时无法触发读操作,可以用OP_WRITE | OP_READ来达到同时写和同时读的操作,后面在弄成OP_READ即可。

这个Demo偶尔会产生粘包现象,作为简单的Demo就不做处理了。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值