我的Java学习之路(13)-- Java NIO网络编程制作简易聊天室

Java NIO网络编程制作简易聊天室

  • 一、NIO简介
  • 二、编程模型
  • 三、BIO网络模型
  • 四、NIO网络模型
  • 五、具体代码实现
  • 六、演示效果图

一、NIO简介

  • NIO全称:Non-blocking IO 或 New IO,是非阻塞式的IO
  • JDK版本:JDK1.4+
  • 应用场景:高并发网络服务编程

二、编程模型

  • 模型:对事物共性的抽象
  • 编程模型:对编程共性的抽象

三、BIO网络模型

  1. BIO网络模型介绍
    我的Java学习之路(13)-- Java NIO网络编程制作简易聊天室_第1张图片
    从图中可以看出,一个线程到第5步的时候,会阻塞在那等待客户端的下次请求,每新增一个客户端,就会启动一个新的线程来处理这个客户端的请求,如果客户端量很大的话,就会造成服务器压力过大而崩溃。

  2. BIO网络模型缺点

  • 阻塞式IO模型
  • 弹性伸缩能力差
  • 多线程非常消耗资源

四、NIO网络模型

  1. NIO网络模型介绍
    我的Java学习之路(13)-- Java NIO网络编程制作简易聊天室_第2张图片
    相对于BIO,NIO的网络模型就要复杂的多,服务端提供一个单线程的Selector组件,它是一个事件注册监听器,负责监听管理注册到它上面的连接和事件,但是本身不负责处理客户端的业务逻辑。当它监听到指定的事件,就会启动相应的事件处理器去处理请求,事件处理器可以依次处理多个请求,处理完成之后,由事件处理器去响应客户端,Selector本身继续监听其他事件。

  2. NIO模型的优点

  • 非阻塞式IO模型
  • 弹性伸缩能力强
  • 单线程节约资源
  1. 原生NIO的缺点
  • NIO类库和API比较复杂
  • 可靠性能力补齐,工作量和难度都非常大
  • Selector空轮询,导致CPU占用100%的bug还没有修复

五、具体代码实现

  1. 服务端代码NioServer.java
package com.feonix;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * NIO服务器端
 */
public class NioServer {
    /**
     * 映射客户端channel
     */
    private Map<String, SocketChannel> clientsMap = new HashMap<String, SocketChannel>();

    public static void main(String[] args) {
        NioServer nioServer = new NioServer();
        try {
            // 启动服务端
            nioServer.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 启动
     *
     * @throws IOException
     */
    public void start() throws IOException {
        // 1. 创建Selector
        Selector selector = Selector.open();

        // 2. 通过ServerSocket创建channel通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        // 3. 为channel通道绑定监听端口
        serverSocketChannel.bind(new InetSocketAddress(8000));

        // 4. 将channel设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);

        // 5. 将channel注册到selector上,监听连接事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器端启动成功!");

        // 6. 循环等待新接入的连接
        for (; ; ) {
            // 获取可用的channel数量
            int readyChannels = selector.select();
            // 屏蔽未就绪的连接,防止selector空轮询
            if (readyChannels == 0) {
                continue;
            }
            // 获取可用的channel集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                // 获取selectionKey实例
                SelectionKey selectionKey = iterator.next();
                // 移除当前的selectionKey
                iterator.remove();
                // 7. 根据就绪状态,调用对应的方法处理业务逻辑
                // 接入事件处理逻辑
                if (selectionKey.isAcceptable()) {
                    acceptHandler(serverSocketChannel, selector);
                }
                // 可读事件处理逻辑
                if (selectionKey.isReadable()) {
                    readHandler(selectionKey, selector);
                }
            }
        }
    }

    /**
     * 接入事件处理器
     *
     * @param serverSocketChannel
     * @param selector
     */
    private void acceptHandler(ServerSocketChannel serverSocketChannel, Selector selector) throws IOException {
        // 创建socketChannel
        SocketChannel socketChannel = serverSocketChannel.accept();
        // 将socketChannel设置为非阻塞模式
        socketChannel.configureBlocking(false);
        // 将channel注册到selector上,监听 可读事件
        socketChannel.register(selector, SelectionKey.OP_READ);
        // 回复客户端提示信息
        socketChannel.write(Charset.forName("UTF-8").encode("系统提示:你与聊天室内的其他人都不是好友关系,请注意隐私安全!"));
    }

    /**
     * 可读事件处理器
     *
     * @param selectionKey
     * @param selector
     * @throws IOException
     */
    private void readHandler(SelectionKey selectionKey, Selector selector) throws IOException {
        // 从selectionKey中获取到已经就绪的channel
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        // 创建buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 循环读取客户端的请求信息
        String request = "";
        while (socketChannel.read(byteBuffer) > 0) {
            // 切换buffer为读模式
            byteBuffer.flip();
            // 读取buffer中的内容
            request += Charset.forName("UTF-8").decode(byteBuffer);
        }
        // 将channel再次注册到selector上,监听可读事件
        socketChannel.register(selector, SelectionKey.OP_READ);
        // 将客户端发送的信息广播给其他客户端
        if (request.length() > 0) {
            // 设置昵称的处理逻辑
            if (request.contains("name:::")) {
                String[] res = request.split(":::");
                if (res.length == 2) {
                    // 添加昵称和客户端channel的映射
                    clientsMap.put(res[1], socketChannel);
                    System.out.printf("客户端%s加入聊天室\n", res[1]);
                    // 广播给其他客户端,有新用户加入聊天室
                    broadCast(socketChannel, res[1] + "加入聊天室,和Ta打个招呼吧~");
                }
            } else { // 普通消息的处理逻辑
                // 先判断客户端channel映射map是否空
                if (!clientsMap.isEmpty()) {
                    // 遍历客户端channel映射map
                    for (Entry<String, SocketChannel> clientSet : clientsMap.entrySet()) {
                        // 找到当前客户端channel
                        if (socketChannel.equals(clientSet.getValue())) {
                            // 拿到昵称
                            String name = clientSet.getKey();
                            System.out.printf("客户端%s消息:%s\n", name, request);
                            // 广播给其他客户端
                            broadCast(socketChannel, name + "说:" + request);
                        }
                    }
                }
            }
        }
    }

    /**
     * 广播消息
     */
    private void broadCast(SocketChannel sourceChannel, String request) {
        if (!clientsMap.isEmpty()) {
            // 遍历所有已接入的客户端channel,循环向所有已接入客户端channel广播信息
            clientsMap.entrySet().forEach(clientSet -> {
                // 获取一个已接入的客户端channel
                SocketChannel targetChannel = clientSet.getValue();
                // 剔除发送消息的客户端
                if (!sourceChannel.equals(targetChannel)) {
                    try {
                        // 将消息发送到targetChannel客户端
                        targetChannel.write(Charset.forName("UTF-8").encode(request));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
  1. 客户端代码NioClient.java
package com.feonix;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Scanner;

/**
 * NIO客户端
 */
public class NioClient {
    public static void main(String[] args) {
        NioClient nioClient = new NioClient();
        try {
            // 启动客户端
            nioClient.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 启动
     */
    public void start() throws IOException {
        // 连接服务器,指定ip和端口号
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8000));
        System.out.println("客户端启动成功!");

        // 接收服务器端的响应
        Selector selector = Selector.open();
        // 设置非阻塞
        socketChannel.configureBlocking(false);
        // 向selector注册可读事件
        socketChannel.register(selector, SelectionKey.OP_READ);
        // 启动一个线程处理消息读取事件
        new Thread(new NioClientHandler(selector)).start();

        // 向服务器端发送消息
        Scanner key = new Scanner(System.in);
        System.out.print("请输入你的昵称:");
        // 等待用户键盘输入昵称
        String name = key.nextLine();
        if (name != null && name.length() > 0) {
            // 发送昵称给服务端
            socketChannel.write(Charset.forName("UTF-8").encode("name:::" + name));
        }
        while (key.hasNextLine()) {
            // 等待用户键盘输入消息内容
            String request = key.nextLine();
            if (request != null && request.length() > 0) {
                // 发送消息给服务端
                socketChannel.write(Charset.forName("UTF-8").encode(request));
            }
        }
    }
}
  1. 客户端消息读取处理器NioClientHandler.java
package com.feonix;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

/**
 * 客户端消息读取处理器
 */
public class NioClientHandler implements Runnable {

    private Selector selector;

    public NioClientHandler(Selector selector) {
        this.selector = selector;
    }

    @Override
    public void run() {
        try {
            for (; ; ) {
                // 获取可用的channel数量
                int readyChannels = selector.select();
                // 屏蔽未就绪的连接
                if (readyChannels == 0) {
                    continue;
                }
                // 获取可用的channel集合
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    // 获取selectionKey实例
                    SelectionKey selectionKey = iterator.next();
                    // 移除当前的selectionKey
                    iterator.remove();

                    // 可读事件处理逻辑
                    if (selectionKey.isReadable()) {
                        readHandler(selectionKey, selector);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 可读事件处理器
     */
    private void readHandler(SelectionKey selectionKey, Selector selector) throws IOException {
        // 从selectionKey中获取到已经就绪的channel
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        // 创建buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 循环读取服务器端的响应信息
        String response = "";
        while (socketChannel.read(byteBuffer) > 0) {
            // 切换buffer为读模式
            byteBuffer.flip();
            // 读取buffer中的内容
            response += Charset.forName("UTF-8").decode(byteBuffer);
        }
        // 将channel再次注册到selector上,监听可读事件
        socketChannel.register(selector, SelectionKey.OP_READ);
        // 将服务器端响应的信息打印到本地
        if (response.length() > 0) {
            System.out.println(response);
        }
    }
}

六、演示效果图

我的Java学习之路(13)-- Java NIO网络编程制作简易聊天室_第3张图片
我的Java学习之路(13)-- Java NIO网络编程制作简易聊天室_第4张图片
我的Java学习之路(13)-- Java NIO网络编程制作简易聊天室_第5张图片
我的Java学习之路(13)-- Java NIO网络编程制作简易聊天室_第6张图片


以上就是本次分享的全部内容,欢迎留言讨论,记得点赞哦(๑¯∀¯๑)~
我的Java学习之路(13)-- Java NIO网络编程制作简易聊天室_第7张图片

你可能感兴趣的:(Java)