NIO三大核心:(缓冲区)Buffer、(通道)Channel、(选择器)Selector

缓冲区(Buffer)

  一个用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类。Java NIO 中的 Buffer 主要用于与 NIO 通道进行 交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。

在这里插入图片描述

Buffer 类及其子类

Buffer 就像一个数组,可以保存多个相同类型的数据。根 据数据类型不同 ,有以下 Buffer 常用子类:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

上述 Buffer 类,他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个 Buffer 对象:

static XxxBuffer allocate(int capacity) : 创建一个容量为capacity 的 XxxBuffer 对象

缓冲区的基本属性

Buffer 中的重要概念:

  • 容量 (capacity) :作为一个内存块,Buffer具有一定的固定大小,也称为"容量",缓冲区容量不能为负,并且创建后不能更改。
  • 限制 (limit):表示缓冲区中可以操作数据的大小(limit 后面数据不能进行读写)。缓冲区的限制不能为负,并且不能大于其容量。 写入模式下,limit等于buffer的容量。读取模式下,limit等于写入的数据量
  • 位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制。
  • 标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法 指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这 个 position。
    标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity
  • 图示:
    在这里插入图片描述

Buffer常见方法

Buffer clear()          清空缓冲区并返回对缓冲区的引用
Buffer flip()           为将缓冲区的界限设置为当前位置,并将当前位置设置为 0
int capacity()          返回Buffer 的 capacity 大小
boolean hasRemaining()  判断缓冲区中是否还有元素
int limit()             返回Buffer 的界限(limit) 的位置
Buffer limit(int n)     将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象
Buffer mark()           对缓冲区设置标记
int position()          返回缓冲区的当前位置 position
Buffer position(int n)  将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象
int remaining()         返回 position 和 limit 之间的元素个数
Buffer reset()          将位置 position 转到以前设置的 mark 所在的位置
Buffer rewind()         将位置设为为 0, 取消设置的 mark

缓冲区的数据操作

Buffer 所有子类提供了两个用于数据操作的方法:get()put() 方法
取获取Buffer中的数据
get()                     读取单个字节
get(byte[] dst)			  批量读取多个字节到 dst 中
get(int index)			  读取指定索引位置的字节(不会移动 position)
    
放数据到 Buffer中
put(byte b)				  将给定单个字节写入缓冲区的当前位置
put(byte[] src)   		  将 src 中的字节写入缓冲区的当前位置
put(int index, byte b)    将指定字节写入缓冲区的索引位置(不会移动 position)

使用Buffer读写数据一般遵循以下四个步骤:

  • 1.写入数据到Buffer
  • 2.调用flip()方法,转换为读取模式
  • 3.从Buffer中读取数据
  • 4.调用buffer.clear()方法或者buffer.compact()方法清除缓冲区

演示案例

public class BufferTest {
    /**
     * 显示Buffer中的  position  limit  capacity 位置
     *
     * @param buffer
     */
    public static void showBufferInfo(Buffer buffer) {
        System.out.println("起始位置--" + buffer.position());
        System.out.println("界限--" + buffer.limit());
        System.out.println("缓冲区容量--" + buffer.capacity());
        System.out.println("--------------------------");
    }

    @Test
    public void test01() {
        // 1、分配一个非直接(后面会说到)缓冲区,容量设置成 10
        ByteBuffer buffer = ByteBuffer.allocate(10);
        showBufferInfo(buffer);  // 0  10  10

        // 2、put()往缓冲区中添加数据
        String name = "0123456";
        buffer.put(name.getBytes());
        showBufferInfo(buffer);  // 7  10  10

        // 3、flip() 为 将缓冲区的界限设置为当前位置,并将当前位置设值为 0: 可读模式
        buffer.flip();
        showBufferInfo(buffer);  // 0  7  10

        // 4、get数据的读取
        char ch = (char) buffer.get();
        System.out.println(ch);
        showBufferInfo(buffer);  // 1  7  10

        //5、rewind() 重复读取数据  取消标记,并还原起始位置
        buffer.rewind();
        showBufferInfo(buffer);  // 0  7  10

        //6、clear()  清空缓冲区. 但是缓冲区中的数据依然存在,但是处于“被遗忘”状态
        buffer.clear();
        System.out.println((char) buffer.get());
        showBufferInfo(buffer);  // 1  10  10

    }

    @Test
    public void test02() {
        // 分配一个缓冲区,容量设置成 10
        ByteBuffer buf = ByteBuffer.allocate(10);
        String n = "0123456";
        buf.put(n.getBytes());
        buf.flip();
        // 读取数据
        byte[] b = new byte[2];
        buf.get(b);
        String rs = new String(b);
        System.out.println(rs); //01
        showBufferInfo(buf);  // 2  7  10
        buf.mark(); // 标记此刻这个位置! 2

        byte[] b2 = new byte[3];
        buf.get(b2);
        System.out.println(new String(b2));//234
        showBufferInfo(buf);  // 5  7  10

        buf.reset(); // 回到标记位置
        if (buf.hasRemaining()) {// 0 1 2 3 4 5 6
            //返回当前位置和限制之间的元素数 limit - position   7 - 2
            System.out.println(buf.remaining());//5
        }
    }

    @Test
    public void test03() {
        // 1、创建一个直接内存的缓冲区
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        System.out.println(buffer.isDirect()); //isDirect()告诉这个字节缓冲区是否是直接的
    }

}

直接与非直接缓冲区

byte byffer可以是两种类型,一种是基于直接内存(也就是非堆内存);另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理。

从数据流的角度,非直接内存是下面这样的作用链:

本地IO-->直接内存-->非直接内存-->直接内存-->本地IO

而直接内存是:

本地IO-->直接内存-->本地IO

  很明显,在做IO处理时,比如网络发送大量数据时,直接内存会具有更高的效率。直接内存使用allocateDirect创建,但是它比申请普通的堆内存需要耗费更高的性能。不过,这部分的数据是在JVM之外的,因此它不会占用应用的内存。所以呢,当你有很大的数据要缓存,并且它的生命周期又很长,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明显的性能提升,还是推荐直接使用堆内存。字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。

使用场景

  • 1 有很大的数据需要存储,它的生命周期又很长
  • 2 适合频繁的IO操作,比如网络并发场景

通道(Channel)

  由 java.nio.channels 包定义。Channel 表示 IO 源与目标打开的连接。 Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。

1、 NIO 的通道类似于流,但有些区别如下:

  • 通道可以同时进行读写,而流只能读或者只能写

  • 通道可以实现异步读写数据

  • 通道可以从缓冲读数据,也可以写数据到缓冲:

2、BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)
是双向的,可以读操作,也可以写操作。

3、Channel 在 NIO 中是一个接口

public interface Channel extends Closeable{}

常用的Channel实现类

  • FileChannel:用于读取、写入、映射和操作文件的通道。
  • DatagramChannel:通过 UDP 读写网络中的数据通道。
  • SocketChannel:通过 TCP 读写网络中的数据。
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】

FileChannel 类

获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:

  • FileInputStream
  • FileOutputStream
  • RandomAccessFile
  • DatagramSocket
  • Socket
  • ServerSocket

获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道

FileChannel的常用方法

int  read(ByteBuffer dst) 		 从 Channel 中读取数据到 ByteBuffer
long read(ByteBuffer[] dsts) 	 将 Channel 中的数据“分散”到 ByteBuffer[]
int  write(ByteBuffer src) 	 	 将 ByteBuffer 中的数据写入到 Channel
long write(ByteBuffer[] srcs)    将 ByteBuffer[] 中的数据“聚集”到 Channel
long position() 				 返回此通道的文件位置
FileChannel position(long p) 	 设置此通道的文件位置
long size() 					 返回此通道的文件的当前大小
FileChannel truncate(long s) 	 将此通道的文件截取为给定大小
void force(boolean metaData) 	 强制将所有对此通道的文件更新写入到存储设备中

案例一:本地文件读取数据

需求:使用前面学习后的 ByteBuffer(缓冲) 和 FileChannel(通道), 将 “Hello,打工人!” 写入到 data01.txt 中.

@Test
public void write() {
    try {
        // 1、字节输出流通向目标文件
        FileOutputStream fos = new FileOutputStream("data01.txt");
        // 2、得到字节输出流对应的通道Channel
        FileChannel channel = fos.getChannel();
        // 3、分配缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put("Hello,打工人!".getBytes());
        // 4、把缓冲区切换成写出模式
        buffer.flip();
        channel.write(buffer);
        channel.close();
        System.out.println("写数据到文件中!");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

案例二:本地文件读数据

需求:使用前面学习后的 ByteBuffer(缓冲) 和 FileChannel(通道), 将 data01.txt 中的数据读入到程序,并显示在控制台屏幕

@Test
public void read() {
    try {
        // 1、定义一个文件字节输入流与源文件接通
        FileInputStream is = new FileInputStream("data01.txt");
        // 2、需要得到文件字节输入流的文件通道
        FileChannel channel = is.getChannel();
        // 3、定义一个缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 4、读取数据到缓冲区
        channel.read(buffer);
        channel.close();
        buffer.flip();
        // 5、读取出缓冲区中的数据并输出即可
        String rs = new String(buffer.array(), 0, buffer.remaining());
        System.out.println(rs);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

案例三:使用Channel和Buffer完成文件复制

需求:将D盘下的test.jpg文件复制一份

@Test
public void copy() throws Exception {
    // 源文件
    File srcFile = new File("D:\\test.jpg");
    //目标文件
    File destFile = new File("D:\\test_new.jpg");
    // 得到一个字节输入流
    FileInputStream fis = new FileInputStream(srcFile);
    // 得到一个字节输出流
    FileOutputStream fos = new FileOutputStream(destFile);
    // 得到文件通道
    FileChannel isChannel = fis.getChannel();
    FileChannel osChannel = fos.getChannel();
    // 分配缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    while (true) {
        // 必须先清空缓冲然后再写入数据到缓冲区
        buffer.clear();
        // 开始读取一次数据
        if (isChannel.read(buffer) == -1) {
            break;
        }
        // 已经读取了数据 ,把缓冲区的模式切换成可读模式
        buffer.flip();
        // 把数据写出到
        osChannel.write(buffer);
    }
    isChannel.close();
    osChannel.close();
    System.out.println("复制完成!");
}

案例四:分散 (Scatter) 和聚集 (Gather)

  • 分散读取(Scatter )是指把Channel通道的数据读入到多个缓冲区中去
  • 聚集写入(Gathering )是指将多个 Buffer 中的数据“聚集”到 Channel。

需求:将data01.txt文件复制一份

@Test
public void testScatterAndGather() {
    try {
        // 1、字节输入管道
        FileInputStream is = new FileInputStream("data01.txt");
        FileChannel isChannel = is.getChannel();
        // 2、字节输出流管道
        FileOutputStream fos = new FileOutputStream("data02.txt");
        FileChannel osChannel = fos.getChannel();
        // 3、定义多个缓冲区做数据分散
        ByteBuffer buffer1 = ByteBuffer.allocate(4);
        ByteBuffer buffer2 = ByteBuffer.allocate(1024);
        ByteBuffer[] buffers = {buffer1, buffer2};
        // 4、从通道中读取数据分散到各个缓冲区
        isChannel.read(buffers);
        // 5、从每个缓冲区中查询是否有数据读取到了
        for (ByteBuffer buffer : buffers) {
            buffer.flip();// 切换到读数据模式
            System.out.println(new String(buffer.array(), buffer.position(), buffer.remaining()));
        }
        // 6、聚集写入到通道
        osChannel.write(buffers);
        isChannel.close();
        osChannel.close();
        System.out.println("文件复制成功");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

案例五:transferFrom() 与 transferTo()

@Test
public void testFromAndTo() {
    try {
        // 1、字节输入管道
        FileInputStream is = new FileInputStream("data01.txt");
        FileChannel isChannel = is.getChannel();
        // 2、字节输出流管道
        FileOutputStream fos = new FileOutputStream("data04.txt");
        FileChannel osChannel = fos.getChannel();
        // 3、复制数据
        //(1):将字节从给定的可读字节通道传输到此通道的文件中
        //osChannel.transferFrom(isChannel, isChannel.position(), isChannel.size());
        //(2):将字节从此通道的文件传输到给定的可写字节通道
        isChannel.transferTo(isChannel.position(), isChannel.size(), osChannel);
        isChannel.close();
        osChannel.close();
        System.out.println("完成复制!");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

选择器(Selector)

选择器(Selector) 是 SelectableChannel 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。
在这里插入图片描述

  • Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)。
  • Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个
    Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管
    理多个通道,也就是管理多个连接和请求。
  • 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都
    创建一个线程,不用去维护多个线程。
  • 避免了多线程之间的上下文切换导致的开销。

选择器(selector)的应用

创建 Selector:通过调用 Selector.open() 方法创建一个 Selector。

Selector selector = Selector.open();

向选择器注册通道:SelectableChannel.register(Selector sel, int ops)

//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。可以监听的事件类型(可使用 SelectionKey 的四个常量表示):

  • 读 : SelectionKey.OP_READ (1)
  • 写 : SelectionKey.OP_WRITE (4)
  • 连接 : SelectionKey.OP_CONNECT (8)
  • 接收 : SelectionKey.OP_ACCEPT (16)
  • 若注册时不止监听一个事件,则可以使用“位或”操作符连接。
int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE 

Selector 示意图和特点说明

  Selector可以实现一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
在这里插入图片描述

NIO非阻塞式网络通信入门案例

服务端流程

public class Server {
    public static void main(String[] args) throws Exception {
        // 1、获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        // 2、切换为非阻塞模式
        ssChannel.configureBlocking(false);
        // 3、绑定连接的端口
        ssChannel.bind(new InetSocketAddress(9999));
        // 4、获取选择器Selector
        Selector selector = Selector.open();
        // 5、将通道都注册到选择器上去,并且开始指定监听接收事件
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 6、使用Selector选择器轮询已经就绪好的事件
        while (selector.select() > 0) {
            // 7、获取选择器中的所有注册的通道中已经就绪好的事件
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            // 8、开始遍历这些准备好的事件
            while (it.hasNext()) {
                // 提取当前这个事件
                SelectionKey sk = it.next();
                // 9、判断这个事件具体是什么
                if (sk.isAcceptable()) {
                    // 10、直接获取当前接入的客户端通道
                    SocketChannel schannel = ssChannel.accept();
                    // 11 、切换成非阻塞模式
                    schannel.configureBlocking(false);
                    // 12、将本客户端通道注册到选择器
                    schannel.register(selector, SelectionKey.OP_READ);
                } else if (sk.isReadable()) {
                    // 13、获取当前选择器上的读就绪事件
                    SocketChannel sChannel = (SocketChannel) sk.channel();
                    // 14、读取数据
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    while ((sChannel.read(buffer)) > 0) {
                        buffer.flip();
                        System.out.println(new String(buffer.array(), buffer.position(), buffer.remaining()));
                        buffer.clear();// 清除之前的数据
                    }
                }
                it.remove(); // 处理完毕之后需要移除当前事件
            }
        }
    }
}

客户端流程

/**
 *	用IDEA并发启动即可
 **/
public class Client {
    public static void main(String[] args) throws Exception {
        // 1、获取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
        // 2、切换成非阻塞模式
        sChannel.configureBlocking(false);
        // 3、分配指定缓冲区大小
        ByteBuffer buf = ByteBuffer.allocate(1024);
        // 4、发送数据给服务端
        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.print("客户端:");
            String msg = sc.nextLine();
            buf.put(("客户端1:" + msg).getBytes());
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
    }
}

NIO 网络编程应用实例-群聊系统

需求:服务端接收客户端的连接请求,并接收多个客户端发送过来的事件。

需求:进一步理解 NIO 非阻塞网络编程机制,实现多人群聊

  • 编写一个 NIO 群聊系统,实现客户端与客户端的通信需求(非阻塞)
  • 服务器端:可以监测用户上线,离线,并实现消息转发功能
  • 客户端:通过 channel 可以无阻塞发送消息给其它所有客户端用户,同时可以接受其它客户端用户通过服务端转发来的消息

服务端代码实现

public class GroupChatServer {
    //定义属性
    private Selector selector;
    private ServerSocketChannel listenChannel;
    private static final int PORT = 6667;

    //构造器
    //初始化工作
    public GroupChatServer() {
        try {
            //得到选择器
            selector = Selector.open();
            //ServerSocketChannel
            listenChannel = ServerSocketChannel.open();
            //绑定端口
            listenChannel.socket().bind(new InetSocketAddress(PORT));
            //设置非阻塞模式
            listenChannel.configureBlocking(false);
            //将该listenChannel 注册到selector
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 监听
     */
    public void listen() {
        System.out.println("监听线程: " + Thread.currentThread().getName());
        try {
            //循环处理
            while (true) {

                int count = selector.select();
                if (count > 0) {//有事件处理

                    //遍历得到selectionKey 集合
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        //取出selectionkey
                        SelectionKey key = iterator.next();

                        //监听到accept
                        if (key.isAcceptable()) {
                            SocketChannel sc = listenChannel.accept();
                            sc.configureBlocking(false);
                            //将该 sc 注册到seletor
                            sc.register(selector, SelectionKey.OP_READ);
                            //提示
                            System.out.println(sc.getRemoteAddress() + " 上线 ");
                        }
                        if (key.isReadable()) { //通道发送read事件,即通道是可读的状态
                            //处理读 (专门写方法..)
                            readData(key);
                        }
                        //当前的key 删除,防止重复处理
                        iterator.remove();
                    }
                } else {
                    System.out.println("等待....");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //发生异常处理....
        }
    }

    //读取客户端消息
    private void readData(SelectionKey key) {
        //取到关联的channle
        SocketChannel schannel = null;
        try {
            //得到channel
            schannel = (SocketChannel) key.channel();
            //创建buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = schannel.read(buffer);
            //根据count的值做处理
            if (count > 0) {
                buffer.flip();
                //把缓存区的数据转成字符串
                String msg = new String(buffer.array(), buffer.position(), buffer.remaining());
                //输出该消息
                System.out.println("form 客户端: " + msg);
                //向其它的客户端转发消息(去掉自己), 专门写一个方法来处理
                sendInfoToOtherClients(msg, schannel);
            }
        } catch (IOException e) {
            try {
                System.out.println(schannel.getRemoteAddress() + " 离线了..");
                //取消注册
                key.cancel();
                //关闭通道
                schannel.close();
            } catch (IOException e2) {
                e2.printStackTrace();
            }
        }
    }

    //转发消息给其它客户(通道)
    private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException {
        System.out.println("服务器转发消息中...");
        System.out.println("服务器转发数据给客户端线程: " + Thread.currentThread().getName());
        //遍历 所有注册到selector 上的 SocketChannel,并排除 self
        for (SelectionKey key : selector.keys()) {
            //通过 key  取出对应的 SocketChannel
            Channel targetChannel = key.channel();
            //排除自己
            if (targetChannel instanceof SocketChannel && targetChannel != self) {
                //转型
                SocketChannel dest = (SocketChannel) targetChannel;
                //将msg 存储到buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                //将buffer 的数据写入 通道
                dest.write(buffer);
            }
        }
    }

    public static void main(String[] args) {
        //创建服务器对象
        GroupChatServer groupChatServer = new GroupChatServer();
        groupChatServer.listen();
    }
}

客户端代码实现

public class GroupChatClient {

    //定义相关的属性
    private final String HOST = "127.0.0.1"; // 服务器的ip
    private final int PORT = 6667; //服务器端口
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    //构造器, 完成初始化工作
    public GroupChatClient() throws IOException {
        selector = Selector.open();
        //连接服务器
        socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //将channel 注册到selector
        socketChannel.register(selector, SelectionKey.OP_READ);
        //得到username
        username = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(username + " is ok...");
    }

    //向服务器发送消息
    public void sendInfo(String info) {
        info = username + " 说:" + info;
        try {
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //读取从服务器端回复的消息
    public void readInfo() {
        try {
            int readChannels = selector.select();
            if (readChannels > 0) {//有可以用的通道
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    if (key.isReadable()) {
                        //得到相关的通道
                        SocketChannel sc = (SocketChannel) key.channel();
                        //得到一个Buffer
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        //读取
                        sc.read(buffer);
                        //把读到的缓冲区的数据转成字符串
                        String msg = new String(buffer.array());
                        System.out.println(msg.trim());
                    }
                }
                iterator.remove(); //删除当前的selectionKey, 防止重复操作
            } else {
                //System.out.println("没有可以用的通道...");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        //启动我们客户端
        GroupChatClient chatClient = new GroupChatClient();
        //启动一个线程, 每个3秒,读取从服务器发送数据
        new Thread(() -> {
            while (true) {
                chatClient.readInfo();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        //发送数据给服务器端
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String s = scanner.nextLine();
            chatClient.sendInfo(s);
        }
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值