一站式学习Java网络编程 全面理解BIO_NIO_AIO,学习手记(六)

大家好,我是方圆
把这篇肝完就休息!


1. NIO模型分析

在这里插入图片描述

  • 在服务器端创建一个Selector,将ServerSocketChannel注册到Selector上,被Selector监听的事件为Accept

在这里插入图片描述

  • Client1请求与服务器建立连接,Selector接收到Accept事件,服务器端对其进行处理(handles),服务器与客户端连接成功

在这里插入图片描述

  • 建立连接过程中,服务器通道(ServerSocketChannel)调用accept方法,获取到与客户端进行连接的通道(SocketChannel),也将其注册到Selector上,监听READ事件,这样,客户端向服务器发送消息,就能触发该READ事件进行响应,读取该消息。

Tips: 我们处理这个建立连接并接收从客户端传过来的消息,都是在一个线程内完成的。在bio中,则会为单个客户端单独开辟一个线程,用于处理消息,并且客户端在不发送消息的过程中,该线程一直是阻塞的。


在这里插入图片描述

  • 同样,两个客户连接过来也是一个线程在起作用,将客户端2的SocketChannel注册到服务器的Selector,并监听READ事件,随时响应随时处理。即一个客户端有一个SocketChannel,两个客户端就有两个SocketChannel,这个就是我们使用nio编程模型来用一个selector对象在一个线程里边监听以及处理多个通道的io的操作

各个Channel是被配置为非阻塞式的(configureBlocking(false)),但是Selector本身调用的select()方法,它是阻塞式的,当监听在Selector上的事件都没有触发时,那么它就会被阻塞,直到有事件对其进行响应

2. 聊天室项目代码重点知识

2.1 服务器端

2.1.1 字段

在这里插入图片描述

2.1.2 主方法

在这里插入图片描述

2.1.3 处理方法

在这里插入图片描述

2.1.4 转发消息方法

在这里插入图片描述

2.1.5 接收消息方法

在这里插入图片描述

2.2 客户端

2.2.1 字段

在这里插入图片描述

2.2.2 主方法

在这里插入图片描述

2.2.3 处理方法

在这里插入图片描述

2.2.4 接收方法

在这里插入图片描述

2.2.5 发送方法

在这里插入图片描述


3. 测试结果

  • 服务器端显示信息正确
    在这里插入图片描述

4. 完整代码

4.1 服务器端

package server;

import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Set;

public class ChatServer {
    private static final int DEFAULT_PORT = 8888;
    private static final String QUIT = "quit";
    private static final int BUFFER = 1024;
    private int port;

    private ServerSocketChannel serverSocketChannel;
    private Selector selector;
    private ByteBuffer rBuffer = ByteBuffer.allocate(BUFFER);
    private ByteBuffer wBuffer = ByteBuffer.allocate(BUFFER);
    private Charset charset = Charset.forName(String.valueOf(StandardCharsets.UTF_8));

    public ChatServer(){
        this(DEFAULT_PORT);
    }

    public ChatServer(int port) {
        this.port = port;
    }

    public boolean readyToQuit(String msg){
        return QUIT.equals(msg);
    }

    public void close(Closeable closeable){
        if(closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void start(){
        try {
            //创建ServerSocketChannel通道
            serverSocketChannel = ServerSocketChannel.open();
            //设置非阻塞模式,默认情况也是阻塞调用的
            serverSocketChannel.configureBlocking(false);
            //绑定端口号
            serverSocketChannel.bind(new InetSocketAddress(port));
            //创建selector
            selector = Selector.open();
            //将accept事件注册到selector上
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务器启动成功,监听端口号:" + port + "...");

            //开始进入监听模式
            while(true){
                //阻塞式调用
                selector.select();

                //获取所有的监听事件,
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                for (SelectionKey selectionKey : selectionKeys) {
                    //处理事件
                    handles(selectionKey);
                }
                //将已经处理完成的事件进行清空
                selectionKeys.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            close(selector);
        }
    }

    /**
     * 处理事件 处理accept事件 和 read事件
     * @param selectionKey 与selector绑定的channel的key
     */
    private void handles(SelectionKey selectionKey) throws IOException {
        if(selectionKey.isAcceptable()){
            //处理accept事件
            //先要获取ServerSocketChannel
            ServerSocketChannel server =(ServerSocketChannel) selectionKey.channel();
            // 我觉得这样写也行 直接调用全局变量 SocketChannel socketChannel = serverSocketChannel.accept();
            //获取对应的客户端的通道
            SocketChannel socketChannel = server.accept();
            socketChannel.configureBlocking(false);
            //将客户端通道绑定到selector上,监听read事件
            socketChannel.register(selector,SelectionKey.OP_READ);
            System.out.println("客户端" + socketChannel.socket().getPort() + ":已经连接");
        }else if(selectionKey.isReadable()){
            //处理read事件
            SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
            String fwdMsg = receive(clientChannel);

            if(fwdMsg.isEmpty()){
                //接不到消息了,那么就把这个通道给他移除了
                selectionKey.cancel();
                //通知selector有注册的通道被移除了,更新状态
                selector.wakeup();
            }else {
                //转发消息
                forwardMessage(clientChannel,fwdMsg);
                if(readyToQuit(fwdMsg)){
                    selectionKey.cancel();
                    selector.wakeup();
                }
            }
        }
    }

    /**
     * 转发消息
     * @param clientChannel 客户端通道
     * @param fwdMsg 转发的消息
     */
    private void forwardMessage(SocketChannel clientChannel, String fwdMsg) throws IOException {
        //keys方法区别于selectedKeys,这个方法返回的是接下来需要被处理的通道key
        //而keys则返回与selector绑定的所有通道key
        //跳过ServerSocketChannel和本身
        for (SelectionKey selectionKey : selector.keys()) {
            SelectableChannel channel = selectionKey.channel();
            if(channel instanceof ServerSocketChannel)
                System.out.println("客户端" + clientChannel.socket().getPort() + ":" + fwdMsg);
            else if(selectionKey.isValid() && !channel.equals(clientChannel)){
                wBuffer.clear();
                //写入消息
                wBuffer.put(charset.encode("客户端" + clientChannel.socket().getPort() + ":" + fwdMsg));
                //转换为读模式
                wBuffer.flip();
                //有数据就一直读
                while(wBuffer.hasRemaining())
                    ((SocketChannel)channel).write(wBuffer);
            }
        }
    }

    /**
     * 从客户通道上读取消息
     * @param clientChannel 客户通道
     * @return 消息
     */
    private String receive(SocketChannel clientChannel) throws IOException {
        //将当前指针置于初始位置,覆盖已有的消息(清空消息)
        rBuffer.clear();
        //不停的向缓存中写
        while(clientChannel.read(rBuffer) > 0);
        //由写模式到读模式
        rBuffer.flip();
        return String.valueOf(charset.decode(rBuffer));
    }

    public static void main(String[] args) {
        ChatServer chatServer = new ChatServer();
        chatServer.start();
    }
}

4.2 客户端

package client;

import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Set;

public class ChatClient {

    private static final String DEFAULT_SERVER_HOST = "127.0.0.1";
    private static final int DEFAULT_SERVER_PORT = 8888;
    private static final String QUIT = "quit";

    private static final int BUFFER = 1024;
    private String host;
    private int port;
    private SocketChannel clientChannel;
    private Selector selector;
    private ByteBuffer rBuffer = ByteBuffer.allocate(BUFFER);
    private ByteBuffer wBuffer = ByteBuffer.allocate(BUFFER);
    private Charset charset = Charset.forName(String.valueOf(StandardCharsets.UTF_8));

    public ChatClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public ChatClient() {
        this(DEFAULT_SERVER_HOST,DEFAULT_SERVER_PORT);
    }

    public boolean readyToQuit(String msg){
        return QUIT.equals(msg);
    }

    public void close(Closeable closeable){
        if(closeable != null){
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void start(){
        try {
            //创建用户通道
            clientChannel = SocketChannel.open();
            clientChannel.configureBlocking(false);//这一步千万不能忘了
            //创建selector,并且将用户通道的connect请求注册上去
            selector = Selector.open();
            clientChannel.register(selector, SelectionKey.OP_CONNECT);
            //尝试与服务器创建连接
            clientChannel.connect(new InetSocketAddress(host,port));

            while (selector.isOpen()){
                //一直监听selector上注册的channel请求
                selector.select();

                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                for (SelectionKey selectionKey : selectionKeys) {
                    //响应请求
                    handles(selectionKey);
                }
                selectionKeys.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }catch (ClosedSelectorException e){

        } finally {
            close(selector);
        }
    }

    private void handles(SelectionKey selectionKey) throws IOException {
        //处理connect事件
        if(selectionKey.isConnectable()){
            //如果能够与服务器响应了
            SocketChannel channel = (SocketChannel) selectionKey.channel();
            if(channel.isConnectionPending()){
                channel.finishConnect(); //正式建立连接
                new Thread(new UserInputHandler(this)).start();
            }
            channel.register(selector,SelectionKey.OP_READ);
        }else if(selectionKey.isReadable()){
            String msg = receive(clientChannel);
            SocketChannel channel = (SocketChannel) selectionKey.channel();
            if(msg.isEmpty()){
                //服务端异常
                close(selector);
            }else {
                //TODO 看看这里信息对不对
                System.out.println(msg);
            }

        }
    }

    private String receive(SocketChannel clientChannel) throws IOException {
        rBuffer.clear();
        //一直读数据
        while (clientChannel.read(rBuffer) > 0);
        rBuffer.flip();
        return String.valueOf(charset.decode(rBuffer));
    }

    public void send(String msg) throws IOException {
        if(msg.isEmpty())
            return;

        wBuffer.clear();
        wBuffer.put(charset.encode(msg));
        wBuffer.flip();
        while(wBuffer.hasRemaining()){
            clientChannel.write(wBuffer);
        }

        if(QUIT.equals(msg))
            close(selector);
    }

    public static void main(String[] args) {
        ChatClient chatClient = new ChatClient();
        chatClient.start();
    }
}

4.3 客户端监听用户输入进程

package client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class UserInputHandler implements Runnable{

    private ChatClient chatClient;

    public UserInputHandler(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    @Override
    public void run() {
        //等待用户输入信息
        BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));

        while(true){
            try {
                String msg = consoleReader.readLine();
                chatClient.send(msg);
                if(chatClient.readyToQuit(msg)) break;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

爽到!

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 在Java中,网络IO的实现方式有BIONIOAIO三种。 1. BIO(Blocking I/O) BIOJava最早的一种网络IO实现方式。它通过阻塞式的方式来进行网络IO操作,即一个线程在进行IO操作时会一直阻塞,直到IO操作完成。这种方式的优点是编程简单易懂,但缺点是并发性能较差,不能满足高并发的需求。 2. NIO(Non-blocking I/O) NIOJava在1.4版本中引入的一种新的网络IO实现方式。NIO的核心是Selector(选择器),它能够监控多个通道的状态,当某个通道有数据可读或可写时,Selector会通知相应的线程进行处理。NIO采用非阻塞式的方式进行IO操作,可以支持更高的并发性能。但是,NIO编程难度较大,需要掌握较多的概念和技巧。 3. AIO(Asynchronous I/O) AIOJava在1.7版本中引入的一种新的网络IO实现方式。AIO采用异步的方式进行IO操作,即一个线程在进行IO操作时不需要一直阻塞,而是可以继续执行其他任务,当IO操作完成后再由系统通知线程进行处理。AIO可以支持更高的并发性能,并且编程模型比NIO更加简单。但是,AIO的兼容性不如NIO,需要在操作系统和JVM等多个方面进行支持。 ### 回答2: 在Java中,网络IO可以通过不同的方式实现,这些方式包括BIO(阻塞IO)、NIO(非阻塞IO)和AIO(异步IO)。 BIO是最早的实现方式,也是最简单直观的方式。它的特点是使用阻塞模式,即当一个线程在进行IO操作时,其他线程必须等待IO操作完成才能继续执行。在BIO中,每个连接都需要一个独立的线程来处理,这可能导致服务器资源浪费,无法满足高并发的需求。 NIO是在Java 1.4中引入的新IO模型,相较于BIO,它具有更高的并发处理能力。NIO使用了多路复用器(Selector)来管理多个连接,一个线程可以通过一个选择器同时处理多个连接的IO操作,从而避免了每个连接都需要一个独立线程的问题。 AIO是在Java 1.7中引入的新IO模型,也被称为NIO.2。AIO是基于事件和回调机制的,它的特点是IO操作后不需要对应的线程进行阻塞等待,而是通过回调方式来通知IO操作已经完成。AIO适合处理连接数较多且连接时间较长的场景,例如聊天服务器。 综上所述,BIONIOAIOJava中实现网络IO的三种方式。BIO适用于连接数较小的场景,NIO适用于连接数较多但连接时间较短的场景,而AIO适用于连接数较多且连接时间较长的场景。选择适合的IO模型能够提高服务器的并发处理能力和效率。 ### 回答3: JAVA中有三种主要的网络IO实现方式,分别是BIO(Blocking IO)、NIO(Non-blocking IO)和AIO(Asynchronous IO)。 BIOJAVA IO最传统的模型,采用阻塞方式进行IO操作。在BIO模型中,每个连接都需要一个独立的线程来处理,当有大量的连接时,就需要大量线程,会导致性能下降。 NIOJAVA IO的改进版本,引入了通道(Channel)和缓冲区(Buffer)的概念。NIO采用非阻塞方式进行IO操作,可以提高IO的效率。在NIO模型中,一个线程可以处理多个连接,通过选择器(Selector)来监听多个通道的事件,当有事件发生时,线程可以进行处理。 AIOJAVA NIO的进一步改进,引入了异步通道(AsynchronousChannel)和回调机制。AIO采用异步方式进行IO操作,可以在IO操作完成之后再通知线程进行处理,而不需要线程一直等待IO操作完成。AIO适用于高并发的场景,可以大大提高系统的吞吐量和性能。 总结来说,BIO适用于连接数较少且业务处理较简单的场景;NIO适用于连接数较多但每个连接的并发处理量不大的场景;AIO适用于连接数较多且每个连接的并发处理量大的场景。选择合适的IO模型可以根据业务需求和实际场景进行选择,来提高系统的性能和并发能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

方圆想当图灵

嘿嘿,小赏就行,不赏俺也不争你

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值