NIO聊天室设计思路及实现

NIO聊天室设计思路及实现

   各个客户端通过连接服务器端,通过向缓冲区写如数据,然后利用通道向服务器端发送消息,服务器端通过通道获取缓冲区的内的数据,通过广播的形式广播给所有的用户。客户端可以多开,也可以多次发送,以bye字符为结束标志。服务器端循环获取,循环广播。

服务器端

属性:

//ServerSocketChannel (服务器监听通道);Selector(选择器,帮助通道去监听通道事件);port(限定)
private ServerSocketChannel listenerChannel; //监听通道
private Selector selector;//选择器对象
private static final int PORT = 9999; //服务器端口

构造方法:

public ChatServer() {
try {
    // 1. 得到监听通道  老大
    listenerChannel = ServerSocketChannel.open();
    // 2. 得到选择器  间谍
    selector = Selector.open();
    // 3. 绑定端口
    listenerChannel.bind(new InetSocketAddress(PORT));
    // 4. 设置为非阻塞模式
    listenerChannel.configureBlocking(false);
    // 5. 将选择器绑定到监听通道并监听accept事件
    listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
    printInfo("Chat Server is ready.......");
} catch (IOException e) {
    e.printStackTrace();
}

}

注意:为了代码的可读性将基本配置单独抽取出来

执行方法:

public void start() throws Exception {
        try {
            while (true) { //不停监控
            //利用选择器循环监控客户端连接服务器端的情况(非阻塞的优势,在此体现,可以在不停监控的过程中做一些其他的事情);
                if (selector.select(2000) == 0) {
                    //System.out.println("Server:服务准备就绪");
                    continue;
                }
                //如果有客户端成功连接,则利用选择器调用selectedKeys()方法,来返回已就绪通道的集合;
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                //利用hasNext()判断,循环通过迭代器的方式,来获取每一个通道;
                    SelectionKey key = iterator.next();
                    //判断是否是连接请求事件;
                    if (key.isAcceptable()) { //连接请求事件
                        SocketChannel sc = listenerChannel.accept();
                        sc.configureBlocking(false);
                        sc.register(selector, SelectionKey.OP_READ);
                        //得到客户端地址和端口号
                        System.out.println(sc.getRemoteAddress().toString().substring(1) + "上线了...");
                    }
                    //判断是否是读取数据事件;
                    if (key.isReadable()) { //读取数据事件
                        readMsg(key);
                    }
                    //一定要把当前key删掉,防止重复处理
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

读取消息方法:

private void readMsg(SelectionKey key) throws Exception {
    //根据Key值获取每个通道
    SocketChannel channel = (SocketChannel) key.channel();
    //获取通道内的缓冲区
    ByteBuffer byteBuffer  = ByteBuffer.allocate(1024);
    //判断标志
    int read = channel.read(byteBuffer);
    if(read>0){
        //获取消息
         String msg = new String(byteBuffer.array());
         //打印消息
         printInfo(msg);//为了控制输出,抽取了一个打印方法
         //发广播
        broadCast(channel,msg);//同样抽取了方法
    }
}

广播方法:

public void broadCast(SocketChannel except, String msg) throws Exception {
	//提示
    System.out.println("服务器发送了广播...");
    注意:
    selector调用keys()和selectedKeys(),一个是选择器选中的键集,另一个是选择器中的键集
    for (SelectionKey key : selector.keys()) {
    	//这里通过key获得通道,但是这个通道不一定是SocketChannel
        Channel targetChannel = key.channel();
        //进行判断
        if (targetChannel instanceof SocketChannel && targetChannel != except) {
        	//赋值给SocketChannel
            SocketChannel destChannel = (SocketChannel) targetChannel;
            //写到缓存中
            ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
            //写到通道中
            destChannel.write(buffer);
        }
    }
}

打印方法:

private void printInfo(String str) { //往控制台打印消息
	//格式化实践
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    //输出拼接的字符串
    System.out.println("[" + sdf.format(new Date()) + "] -> " + str);
}

服务器端启动程序:

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

客户端:

客户端首先需要连接服务器,第一次失败,第二次需要换方法进行连接,然后向服务器写数据,同时向数据库读数据,为了方便,我们需要单独开个线程,只负责读数据

属性:

private final String HOST = "127.0.0.1"; //服务器地址,确保所有客户端在一个服务器上
private int PORT = 9999; //服务器端口,确保使用同一个服务器上同一个应用服务
private SocketChannel socketChannel; //网络通道
private String userName; //聊天用户名,为了美化

构造方法:

public ChatClient() throws IOException {
    //1. 得到一个网络通道
    socketChannel = SocketChannel.open();
    //2. 设置非阻塞方式
    socketChannel.configureBlocking(false);
    //3. 提供服务器端的IP地址和端口号
    InetSocketAddress address = new InetSocketAddress(HOST, PORT);
    //4. 连接服务器端
    if (!socketChannel.connect(address)) {
        while (!socketChannel.finishConnect()) {  //nio作为非阻塞式的优势
            。。。。。。。。。可以干一些其他事情
        }
    }
    //5. 得到客户端IP地址和端口信息,作为聊天用户名使用
    userName = socketChannel.getLocalAddress().toString().substring(1);
    System.out.println("---------------Client(" + userName + ") is ready---------------");
}

发送数据方法:

public void sendMsg(String msg) throws Exception {
	//检测到发送的字符串中有‘bye’,则关闭通道
    if (msg.equalsIgnoreCase("bye")) {
    	//关闭通道
        socketChannel.close();
        return;
    }
    msg = userName + "说:" + msg;
    //写道缓存中
    ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
    //通道将缓存写入通道中
    socketChannel.write(buffer);
}

接受数据方法:

public void receiveMsg() throws Exception {
	//建立缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    //将通道内的数据写入到缓冲区
    int size = socketChannel.read(buffer);
    if (size > 0) {
        String msg = new String(buffer.array());
        System.out.println(msg.trim());
    }
}

客户端启动程序:

public class TestChat {
    public static void main(String[] args) throws Exception {
        ChatClient chatClient=new ChatClient();
        //开器多线程进行读取数据
        new Thread(){
            public void run(){
                while(true){
                    try {
                        chatClient.receiveMsg();
                        Thread.sleep(2000);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        Scanner scanner=new Scanner(System.in);
        //只要有输入,就一直发送
        while (scanner.hasNextLine()){
            String msg=scanner.nextLine();
            chatClient.sendMsg(msg);
        }
    }
}

注意:这里一定要搞清楚几个关系

1、ServerSocketChannel, 用来在服务器端监听新的客户端 Socket 连接 ;(主要是selector在为他服务,在监听),常在服务器端使用

2、SocketChannel, 网络 IO 通道, 具体负责进行读写操作 ;常在客户端使用

    public int write(ByteBuffer src), 往通道里写数据

    public int read(ByteBuffer dst), 从通道里读数据 

3、通道和缓冲区的关系:通道发送数据,缓冲区是存储数据,这里常用的是ByteBuffer;

	数据总是从通道读取到缓冲区中,将缓冲区中的内容写到到通道中

	allocate(int):创建指定的缓冲区容量

	wrap(byteArray):创建一个缓冲区对象但是不分配任何空间来储存数据元素。它使用你所提供的数组作为存储空间来储存缓冲区中的数据元素;例如:char [] myArray = new char [100];  CharBuffer charbuffer = CharBuffer.wrap (myArray);
posted @ 2019-05-05 22:02  Kill(Bug)  阅读(103)  评论(0)    收藏  举报