NIO网络编程

NIO的基本介绍

java NIO 全称 java non-blocking IO,是同步非阻塞的。

NIO有三大核心部分

  • Channel(通道):传统IO流中内置了通道,通道是双向的,可读可写,Channel不存储数据,主要配合缓冲区进行数据的传输。
  • Buffer(缓冲区):客户端与通道之间的缓冲区,两者间的数据读写交互都会经过这个缓冲区
  • Selector(选择器):会监听选择器下的所有通道,如果某个通道发生读写事件,这个选择器就会去处理这个通道的读写事件,通道没有发生事件,也不会阻塞。

三大核心组件的简单关系图如下所示:

NIO非阻塞模式:如果客户端向通道中写入了数据,这时线程就可以从通道中读取这个数据;如果通道中没有数据可用,线程也不会阻塞在这里,直至通道变的可以读取之前,该线程可以做其他事情。而BIO中建立连接后线程读完数据后会阻塞,直到新的数据过来。

NIO是基于事件驱动的,如果通道中没有事件,便不会处理。

NIO与BIO的区别

  • BIO阻塞的,NIO非阻塞的
  • BIO通过流处理数据,NIO通过缓冲区处理数据,缓冲区IO效率比流IO效率高很多
  • BIO基于字节流和字符流进行操作,而NIO基于通道和缓冲区进行操作,数据总是从通道读取到缓冲区,或是从缓冲区写入到通道,选择器用于监听多个通道的事件,因此一个线程就可以监听多个客户端通道。

NIO的三大核心组件

Buffer缓冲区

缓冲区的基本使用

缓冲区:本质上是一个可以读写的内存块,可以看成一个容器对象(数组),通道与客户端的数据传输都需要经过这个缓冲区。

数据的读写交互都是通过Buffer缓冲区进行的,例举一个Buffer的基本使用如下所示:

public class BufferTest {
    public static void main(String[] args) {
        //创建一个int型的缓冲区,分配空间为5,可以存放5个int
        IntBuffer intBuffer = IntBuffer.allocate(5);
        for (int i = 1; i <= 5; i++) {
            intBuffer.put(i);
        }
        //读取缓冲区数据,调用flip方法,读写切换
        intBuffer.flip();
        //有剩余数据就读取,内部有一个指针移动,直到返回false表示读完
        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }
    }
}

Buffer类的属性

Buffer类中定义了所有缓冲区都具有的四个属性,这些属性可以体现当对缓冲区写数据或读数据时,缓冲区内部状态的变化。

  • mark:标记
  • position:缓冲区中当前的位置
  • limit:缓冲区的终点(索引最后的位置)
  • capacity:缓冲区可以容纳的最大数据量

举例如下:

缓冲区分配内部容量为5,内部数组hb元素都为0,position当前位置为0,limit缓冲区界限5

每添加一个元素,position的位置都加1,直到limit界限,如果循环次数超过容量大小,会报BufferOverflowException异常

调用flip()方法切换状态,从写状态转为读状态,position指向的位置从尾部移动到首部

每读到缓冲区一个数据,position位置往后移动一位,值+1

Buffer的分散和合并

NIO支持通过多个Buffer完成读写操作

  • Scattering:将数据写入到Buffer时,可以采用buffer数组,依次写入
  • Gathering:从buffer读取数据时,可以采用buffer数组,依次读

服务端代码,监听端口8000

public class ScatteringAndGatheringTest {
    public static void main(String[] args) throws IOException {
        //创建ServerSocketChannel绑定端口8000
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(8000);
        serverSocketChannel.socket().bind(inetSocketAddress);
        //创建Buffer数组
        ByteBuffer[] byteBuffers = new ByteBuffer[2];
        byteBuffers[0] = ByteBuffer.allocate(5);
        byteBuffers[1] = ByteBuffer.allocate(3);
        //等待客户端的连接
        SocketChannel socketChannel = serverSocketChannel.accept();
        long byteRead = socketChannel.read(byteBuffers);
        System.out.println("从客户端读取到的字节数:"+byteRead);
        //输出当前Buffer里的position和limit
        Arrays.asList(byteBuffers).stream().map(
                buffer -> "position=" + buffer.position() + ",limit=" + buffer.limit()
        ).forEach(System.out::println);
        //将所有buffer切换状态
        Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
        //将byteBuffers数组的数据写出
        long byteWrite = socketChannel.write(byteBuffers);
        System.out.println("从buffer数组中写出的字节数:"+byteWrite);
    }
}

客户端测试

连接服务端:telnet 127.0.0.1 8000

客户端发送数据:send hello123

服务端打印如下,buffer数组中,第一个buffer元素大小为5存了3字节,第二个buffer存了3字节

客户端发送的数据分散到了buffer数组中的各个元素里存储。

获取的时候从buffer数组里获取,会将buffer数组里各个元素的数据合并。

从客户端读取到的字节数:8
position=5,limit=5
position=3,limit=3
从buffer数组中写出的字节数:8

Channel通道

channel基本介绍

负责配合缓冲区进行数据的传递

channel通道与流的区别

  • 通道可以同时进行读写,而流只能读或只能写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲区读数据,也可以向缓冲区写数据

Channel是一个接口,主要实现类有:FileChannel,SocketChannel,ServerSocketChannel

FileChannel类

FileChannel类主要对本地文件进行IO操作,常见方法有:

  1. public int read(ByteBuffer dst):从通道读取数据放到缓冲区中

  2. public int write(ByteBuffer src):把缓冲区中数据写入到通道中

  3. public long transferTo(long position, long count, WritableByteChannel target):把数据从当前通道复制给目标通道

  4. public long transferFrom(ReadableByteChannel src, long position, long count):从目标通道复制数据到当前通道 

举例1:将字符串数据通过FileChannel通道写入到D盘test.txt文件中

public class FileChannelTest {
    public static void main(String[] args) throws Exception {
        String str = "hello FileChannel";
        //创建输出流FileOutputStream,获取FileChannel
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\test.txt");
        FileChannel fileChannel = fileOutputStream.getChannel();
        //创建一个字节缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //将字符串放入到缓冲区
        buffer.put(str.getBytes(StandardCharsets.UTF_8));
        //切换缓冲区状态为读
        buffer.flip();
        //从缓冲区中读数据,然后将读取到的数据写入到通道中
        fileChannel.write(buffer);
        fileOutputStream.close();
    }
}

举例2:读取文件数据,并在控制台输出

public class FileChannelTest {
    public static void main(String[] args) throws Exception {
        //创建输入流FileInputStream,获取FileChannel
        FileInputStream fileInputStream = new FileInputStream("d:\\test.txt");
        FileChannel fileChannel = fileInputStream.getChannel();
        //创建一个字节缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(fileInputStream.available());
        //将通道数据读入到缓冲区
        fileChannel.read(buffer);
        System.out.println(new String(buffer.array()));
        fileInputStream.close();
    }
}

举例3:通过Buffer完成文件拷贝

public class FileChannelTest {
    public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("d:\\1.txt");
        FileChannel inputChannel = fileInputStream.getChannel();
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\2.txt");
        FileChannel outputChannel = fileOutputStream.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(10);
        while (true) {
            //这里需要调用clear方法,重置position位置
            buffer.clear();
            int read = inputChannel.read(buffer);
            /*
              假设1.txt文件内容字节长度为20,字节缓冲区大小为10,
              需要循环两遍读取才能读完。第一次读取然后写入到文件
              2.txt结束时,position=limit=10
              第二次循环如果没有调用clear()方法,重置position=0,
              那么读到的长度read=0,if判断为false,会导致死循环无法退出
             */
            if (read == -1) {
                break;
            }
            //需要将写状态切换为读状态
            buffer.flip();
            //将buffer中的数据写入到outputChannel中,相当于写到了文件2.txt中
            outputChannel.write(buffer);
        }
        fileInputStream.close();
        fileOutputStream.close();
    }
}

举例4:通过transferFrom完成文件拷贝

public class FileChannelTest {
    public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("d:\\1.jpg");
        FileChannel sourceChannel = fileInputStream.getChannel();
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\2.jpg");
        FileChannel destChannel = fileOutputStream.getChannel();
        destChannel.transferFrom(sourceChannel,0,sourceChannel.size());
        sourceChannel.close();
        destChannel.close();
        fileInputStream.close();
        fileOutputStream.close();
    }
}

Selector选择器

Selector基本介绍

NIO是非阻塞的IO方式,可以用一个线程处理多个客户端的连接,这里就会使用到选择器Selector(也叫多路复用器)。多个Channel可以以事件的方式注册到同一个Selector,就是通过一个数据结构将多个Channel和Selector的关系关联起来,这样Selector就能够检测到这些注册的通道上是否有事件发生。如果有事件发生,便获取事件然后针对每个事件做对应的处理,这样就完成了一个线程处理多个请求/连接的功能,不用像BIO那样一个线程处理一个连接。

Selector选择器通过属性selectedKeys关联Channel通道

NIO网络编程相关的(Selector,SelectionKey,ServerSocketChannel,SocketChannel)流程

  1. 服务端ServerSocketChannel监听端口,等待客户端连接
  2. 每个客户端连接时,都会通过ServerSocketChannel得到SocketChannel
  3. 将SocketChannel通过方法register(Selector sel, int ops, Object att),注册到Selector上,一个Selector可以注册多个的SocketChannel
  4. 注册后,register方法的返回值是SelectionKey
  5. Selector中有个集合存放所有返回的SelectionKey
  6. Selector通过方法select()监听所有通道,返回值是有事件发生的通道个数。
  7. 如果通道有事件发生了,获得有事件发生的SelectionKey
  8. 根据SelectionKey的channel()方法得到SocketChannel处理业务

SelectionKey API

SelectionKey表示Selector和网络通道的注册关系,共四种:

  • int OP_READ = 1 << 0;  代表读操作
  • int OP_WRITE = 1 << 2;  代表写操作
  • int OP_CONNECT = 1 << 3;  代表连接已经建立
  • int OP_ACCEPT = 1 << 4;   代表有新的网络连接可以accept

其它方法

  • public abstract SelectableChannel channel()   得到与之关联的通道
  • public abstract Selector selector();  得到选择器Selector对象
  • public final Object attachment();  得到与之关联的共享数据
  • public final boolean isReadable()  是否可读
  • public final boolean isWritable()    是否可写
  • public final boolean isAcceptable()  是否可accept

NIO编程案例

NIO网络编程案例

  1. 服务端启动
  2. 客户端启动,连接到服务端,然后发送数据
  3. 服务端接收到客户端发送过来的数据,打印

服务端代码

public class NIOServer {
    public static void main(String[] args) throws IOException {
        //创建一个ServerSocketChannel,监听端口8000
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(8000));
        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //创建Selector对象
        Selector selector = Selector.open();
        //将serverSocketChannel注册到Selector中,关心事件为OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //循环等待连接
        while (true) {
            if (selector.select(1000) == 0) {
                System.out.println("服务器等待1s,无连接");
                continue;
            }
            //如果返回大于0,表示有连接,select方法返回的值就是有事件发生的通道数目
            //Set<SelectionKey>:存储所有发生事件的通道对应的SelectionKey
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()) {
                SelectionKey selectionKey = keyIterator.next();
                //根据key对应通道发生的事件做相应的处理
                if (selectionKey.isAcceptable()) {
                    //事件是OP_ACCEPT,有新的客户端连接,分配一个SocketChannel
                    //这里accept方法不会阻塞,因为本身就是一个连接的事件到这里的
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    //将socketChannel注册到Selector中,关注事件为OP_READ
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if (selectionKey.isReadable()) {
                    //获取selectionKey对应的通道
                    SocketChannel channel = (SocketChannel)selectionKey.channel();
                    //获取通道对应的缓冲区Buffer
                    ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    channel.read(buffer);
                    System.out.println("接收来自客户端的数据为:" + new String(buffer.array(),0,buffer.position()));
                }
                //移除当前key
                keyIterator.remove();
            }
        }
    }
}

客户端代码

public class NIOClient {
    public static void main(String[] args) throws Exception {
        //创建一个SocketChannel用于与服务端通讯
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1",8000);
        //连接服务器
        if (!socketChannel.connect(inetSocketAddress)) {
            while (!socketChannel.finishConnect()) {
                System.out.println("服务器连接失败,请重试!");
                TimeUnit.MILLISECONDS.sleep(10);
            }
        }
        //连接成功,发送数据到服务端
        String data = "hello,server";
        ByteBuffer buffer = ByteBuffer.wrap(data.getBytes());
        socketChannel.write(buffer);
        System.in.read();
    }
}

NIO群聊功能案例

案例要求

  • 编写一个NIO群聊系统,实现服务端和客户端的简单通讯(非阻塞)
  • 实现多人群聊
  • 服务器端:可以监测用户上线,离线,并实现消息转发功能
  • 客户端:通过channel可以无阻塞的发送消息给其它所有用户,同时可以接收其它用户发送的消息(由服务器转发得到)

服务端代码

@Slf4j
public class ChatServer {
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    public static final int PORT = 8080;
    public ChatServer() {
        try {
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(PORT));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //监听通道事件
    private void listen() {
        try {
            //循环判断是否有连接等事件发生
            while (true) {
                int count = selector.select();
                if (count > 0) {
                    //有事件发生
                    Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                    while (keyIterator.hasNext()) {
                        SelectionKey key = keyIterator.next();
                        //连接事件
                        if (key.isAcceptable()) {
                            SocketChannel socketChannel = serverSocketChannel.accept();
                            socketChannel.configureBlocking(false);
                            socketChannel.register(selector,SelectionKey.OP_READ);
                            log.info("{} ===> 上线了",socketChannel.getRemoteAddress());
                        }
                        //读事件
                        if (key.isReadable()) {
                            //读取通道中的数据
                            readData(key);
                        }
                        keyIterator.remove();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

        }
    }
    //读取通道中的数据
    private void readData(SelectionKey key) {
        SocketChannel channel = null;
        try {
            channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = channel.read(buffer);
            String message = new String(buffer.array(),0,buffer.position());
            if (count > 0) {
                log.info("接收到来自客户端的消息:{}",message);
            }
            //向其它客户端转发消息
            sendInfoToOtherClients(message, channel);
        } catch (IOException e) {
            try {
                log.info("{} ===> 离线了",channel.getRemoteAddress());
                //取消注册
                key.cancel();
                //关闭通道
                channel.close();
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
    }
    //将消息转发到其它客户端
    private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException {
        log.info("服务器开始转发消息.....");
        //将消息发送到所有的SocketChannel中,排除掉自己self
        for (SelectionKey key: selector.keys()) {
            Channel channel = key.channel();
            if (channel instanceof SocketChannel && channel != self) {
                SocketChannel socketChannel = (SocketChannel) channel;
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                socketChannel.write(buffer);
            }
        }
    }
    public static void main(String[] args) {
        ChatServer chatServer = new ChatServer();
        chatServer.listen();
    }
}

客户端代码

@Slf4j
public class ChatClient {
    private Selector selector;
    private SocketChannel socketChannel;
    public static final String HOST = "127.0.0.1";
    public static final int PORT = 8080;
    private String username;

    public ChatClient() throws IOException {
        selector = Selector.open();
        socketChannel = socketChannel.open(new InetSocketAddress(HOST, PORT));
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
        username = socketChannel.getLocalAddress().toString().substring(1);
        log.info("客户端【{}】登录成功",username);
    }

    //向服务器发送消息
    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> keyIterator = selector.selectedKeys().iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    if (key.isReadable()) {
                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        channel.read(buffer);
                        String msg = new String(buffer.array(),0,buffer.position());
                        log.info("服务端发送过来的数据为:{}",msg);
                    }
                }
                keyIterator.remove();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws IOException {
        ChatClient chatClient = new ChatClient();
        //启动一个线程,每隔3秒,读取从服务器发送的数据
        new Thread(() -> {
            while (true) {
                chatClient.readInfo();
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        //发送数据给服务端
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String msg = scanner.nextLine();
            chatClient.sendInfo(msg);
        }
    }
}

演示

启动服务端,然后启动3个客户端

服务端效果

20:34:33.978 [main] INFO com.dingmb.nio.ChatServer - /127.0.0.1:5573 ===> 上线了
20:34:36.127 [main] INFO com.dingmb.nio.ChatServer - /127.0.0.1:5581 ===> 上线了
20:34:37.858 [main] INFO com.dingmb.nio.ChatServer - /127.0.0.1:5589 ===> 上线了
20:34:48.956 [main] INFO com.dingmb.nio.ChatServer - 接收到来自客户端的消息:127.0.0.1:5573 说:我是5573
20:34:48.957 [main] INFO com.dingmb.nio.ChatServer - 服务器开始转发消息.....
20:34:56.464 [main] INFO com.dingmb.nio.ChatServer - 接收到来自客户端的消息:127.0.0.1:5581 说:我是5581
20:34:56.465 [main] INFO com.dingmb.nio.ChatServer - 服务器开始转发消息.....
20:35:04.682 [main] INFO com.dingmb.nio.ChatServer - 接收到来自客户端的消息:127.0.0.1:5589 说:我是5589
20:35:04.682 [main] INFO com.dingmb.nio.ChatServer - 服务器开始转发消息.....
20:35:14.314 [main] INFO com.dingmb.nio.ChatServer - /127.0.0.1:5573 ===> 离线了
20:35:16.590 [main] INFO com.dingmb.nio.ChatServer - /127.0.0.1:5581 ===> 离线了
20:35:18.728 [main] INFO com.dingmb.nio.ChatServer - /127.0.0.1:5589 ===> 离线了

客户端1效果

20:34:33.978 [main] INFO com.dingmb.nio.ChatClient - 客户端【127.0.0.1:5573】登录成功
我是5573
20:34:56.466 [Thread-0] INFO com.dingmb.nio.ChatClient - 服务端发送过来的数据为:127.0.0.1:5581 说:我是5581
20:35:04.682 [Thread-0] INFO com.dingmb.nio.ChatClient - 服务端发送过来的数据为:127.0.0.1:5589 说:我是5589

客户端2效果

20:34:36.133 [main] INFO com.dingmb.nio.ChatClient - 客户端【127.0.0.1:5581】登录成功
20:34:48.958 [Thread-0] INFO com.dingmb.nio.ChatClient - 服务端发送过来的数据为:127.0.0.1:5573 说:我是5573
我是5581
20:35:04.683 [Thread-0] INFO com.dingmb.nio.ChatClient - 服务端发送过来的数据为:127.0.0.1:5589 说:我是5589

客户端3效果

20:34:37.864 [main] INFO com.dingmb.nio.ChatClient - 客户端【127.0.0.1:5589】登录成功
20:34:48.958 [Thread-0] INFO com.dingmb.nio.ChatClient - 服务端发送过来的数据为:127.0.0.1:5573 说:我是5573
20:34:56.465 [Thread-0] INFO com.dingmb.nio.ChatClient - 服务端发送过来的数据为:127.0.0.1:5581 说:我是5581
我是5589

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值