NIO实现多人聊天室

1. 前言

首先对应NIO来说是同步非阻塞的,这里再看一下什么是同步什么是异步,什么是阻塞什么是非阻塞

IO模型

2. BIO多人聊天室阻塞发生在哪里?

ServserSocket.accept()是阻塞的
② 所有输入流和输出流都是阻塞的,接收方等待发送方发送消息的时候是阻塞等待的,如果发送方一直不发送消息,那么接收方就要一直阻塞等待干不了其他事
③ 在BIO多人聊天室的案例中,由于BIO输出输出的阻塞,我们不得不为他们分配不同的线程

3. NIO实现多人聊天室

对于NIO来说最核心的三个部件就是Channle+Buffer+selector

Channel : Channel是程序和磁盘中间的通道,我们可以把他理解为生活当作的铁路,只是用于连接,铁路自己本身不能完成运输,想完成运输要依赖于火车

Buffer : Buffer就是火车,Buffer在Java NIO 中负责数据的存取,把数据装到缓冲区从铁路传输缓冲区到目的地,Buffer可以在Channel上双向移动(可读可写)

Selector : 同步非阻塞就是Selector来实现的,将 Channel 注册到 Selector 中,通过Selector来管理Channel,Selector不断检查Channel的状态,当Channel准备好读写了,Selector就会感知到并进行处理

所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。

需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在"干活",而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时

传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。

对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。

NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程

NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)

换句话说,BIO里用户最关心“我要读”,NIO里用户最关心"我可以读了"

3.1 服务端ChatServer

public class ChatServer {
    private static final int DEFAULT_PORT = 8888;
    private static final String QUIT = "quit";//退出关键词
    private static final int BUFFER = 1024;//缓冲区大小

    private ServerSocketChannel server;
    private Selector selector;//selector
    private ByteBuffer rBuffer = ByteBuffer.allocate(BUFFER);//从用户channel读的buffer
    private ByteBuffer wBuffer = ByteBuffer.allocate(BUFFER);//用于向其他用户channel写的buffer
    private Charset charset = Charset.forName("UTF-8");
    private int port;//可以自定义服务端端口

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

    public ChatServer() {
        this(DEFAULT_PORT);
    }

    public synchronized boolean readyToQuit(String msg){
        if (msg.equals(QUIT)){
            return true;
        }
        return false;
    }

    public void start(){//主要逻辑
        try {
            //打开服务端 Socket
            server = ServerSocketChannel.open();//ServerSocketChannel默认是阻塞的
            //转换为非阻塞
            server.configureBlocking(false);
            //监听端口
            server.socket().bind(new InetSocketAddress(port));
            selector = Selector.open();
            //注册ServerSocketChannel的accept
            server.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("启动服务器,监听端口"+port+"....");
            while (true){
                //select是阻塞式的,如果没有注册的通道发生监听的事件就会阻塞
                selector.select();
                //有事件发生了
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                //处理触发事件
                for (SelectionKey key : selectionKeys) {
                    //处理
                    handles(key);
                }
                //清空处理完的
                selectionKeys.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void handles(SelectionKey key) throws IOException {
        //触发了服务端的Accept
        if(key.isAcceptable()){
            //得到服务端通道
            ServerSocketChannel server = (ServerSocketChannel) key.channel();
            //客户端通道
            SocketChannel client = server.accept();
            //转化为非阻塞
            client.configureBlocking(false);
            //注册Read事件
            client.register(selector,SelectionKey.OP_READ);
            System.out.println("客户端"+client.socket().getPort()+"已连接");
        }else if(key.isReadable()){//触发了客户端的Read
            SocketChannel client = (SocketChannel) key.channel();
            //读
            String fwdMsg = receive(client);
            if(fwdMsg.isEmpty()){
                //客户端异常
                key.cancel();//移除
            }else{
                //转发数据
                forwardMessage(client, fwdMsg);
                //检查用户是否退出
                if(readyToQuit(fwdMsg)){
                    //断开
                    key.cancel();
                }
            }
        }
    }

    private void forwardMessage(SocketChannel client, String fwdMsg) throws IOException {
        //找到目前在线的客户端
        Set<SelectionKey> keys = selector.keys();
        for (SelectionKey key : keys) {
            //服务端的跳过
            if(key.channel() instanceof ServerSocketChannel){
                continue;
            }
            if(key.isValid() && !key.channel().equals(client)){
                wBuffer.clear();
                wBuffer.put(charset.encode(fwdMsg));
                wBuffer.flip();
                while (wBuffer.hasRemaining()){
                    ((SocketChannel)key.channel()).write(wBuffer);
                }
            }
        }
    }

    private String receive(SocketChannel client) throws IOException {
        rBuffer.clear();
        while((client.read(rBuffer))>0){//只要还能读就读
        }
        rBuffer.flip();
        return String.valueOf(charset.decode(rBuffer));
    }

    public static void main(String[] args) {

    }
}

3.2 客户端ChatClient


public class ChatClient {
    private final String DEFAULT_SERVER_HOST = "127.0.0.1";
    private final int DEFAULT_SERVER_PORT = 8888;
    private final String QUIT = "quit";
    private static final int BUFFER = 1024;//缓冲区大小

    private SocketChannel client;
    private Selector selector;//selector
    private ByteBuffer rBuffer = ByteBuffer.allocate(BUFFER);//从用户channel读的buffer
    private ByteBuffer wBuffer = ByteBuffer.allocate(BUFFER);//用于向其他用户channel写的buffer
    private Charset charset = Charset.forName("UTF-8");


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

    public void start(){
        try {
            client = SocketChannel.open();
            //非阻塞
            client.configureBlocking(false);
            selector = Selector.open();
            //注册事件
            client.register(selector, SelectionKey.OP_CONNECT);
            client.connect(new InetSocketAddress(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT));
            while(true){//轮询
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                for (SelectionKey key : selectionKeys) {
                    handles(key);
                }
                selectionKeys.clear();

            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void handles(SelectionKey key) throws IOException {
        //CONNECT事件 连接就绪
        if(key.isConnectable()){
            SocketChannel channel = (SocketChannel)key.channel();
            if(client.isConnectionPending()){//就绪
                client.finishConnect();//正式建立连接
                //新线程做用户输入
                new Thread(new NIOUserInputHandler(this)).start();
            }
            //注册,监听read
            client.register(selector,SelectionKey.OP_READ);
        }else if(key.isReadable()){
            //READ事件 服务器转发的消息
            SocketChannel channel = (SocketChannel) key.channel();
            String msg = receive(client);
            if(msg.isEmpty()){
                selector.close();
            }else{
                System.out.println(msg);
            }
        }

    }

    private String receive(SocketChannel client) throws IOException {
        rBuffer.clear();
        while(client.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()){
            client.write(wBuffer);
        }
    }
}

3.3 客户端的写线程NIOUserInputHandler

public class NIOUserInputHandler implements Runnable{//处理用户的输入
    private ChatClient chatClient;

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

    public void run() {

        try {
            BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
            while(true){
                String input = consoleReader.readLine();
                chatClient.send(input);
                if(chatClient.readyToQuit(input)){
                    break;
                }
            }

        }catch (IOException e){
            e.printStackTrace();
        }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值