NIO 多人聊天室

时间:2021-04-01 10:17:03

一前言

在家休息没事,敲敲代码,用NIO写个简易的仿真天室。下面直接讲聊天室设计和编码。对NIO不了解的朋友,推荐一个博客,里面写的很棒:

https://javadoop.com/     里面有NIO的部分

 

二设计

1.进入的时候,提示输入聊天昵称,重复的话,重新输入,成功后进到聊天室。

2.成功进到聊天室,广播通知,XXX进到了聊天室;离开聊天室,XXX离开了聊天室。

3.@XXX 给XXX发消息,只有双方可以看到。

4.服务端收到的内容会转发给其他客户端。

 

三代码

目前版本(设计工程3,4有待改进)

服务端:

 

  1 package com.lee.demo.nio;
  2 
  3 import java.io.IOException;
  4 import java.net.InetSocketAddress;
  5 import java.nio.ByteBuffer;
  6 import java.nio.channels.Channel;
  7 import java.nio.channels.SelectionKey;
  8 import java.nio.channels.Selector;
  9 import java.nio.channels.ServerSocketChannel;
 10 import java.nio.channels.SocketChannel;
 11 import java.nio.charset.Charset;
 12 import java.util.HashSet;
 13 import java.util.Iterator;
 14 import java.util.Set;
 15 
 16 public class ChatServer {
 17 
 18     private Selector  selector = null;
 19     private Charset charset = Charset.forName("UTF-8");
 20     public static final int PORT = 8765; 
 21     private static String USER_CONTENT_SPILIT = "#";
 22     private static HashSet<String> users = new HashSet<String>();
 23     
 24     public void init() throws IOException {
 25         selector = Selector.open();
 26         ServerSocketChannel server = ServerSocketChannel.open();
 27         server.socket().bind(new InetSocketAddress(PORT));
 28         // 将其注册到 Selector 中,监听 OP_ACCEPT 事件
 29         server.configureBlocking(false);
 30         server.register(selector, SelectionKey.OP_ACCEPT);
 31 
 32         while (true) {
 33             int readyChannels = selector.select();
 34             if (readyChannels == 0) {
 35                 continue;
 36             }
 37             Set<SelectionKey> readyKeys = selector.selectedKeys();
 38             // 遍历
 39             Iterator<SelectionKey> iterator = readyKeys.iterator();
 40             while (iterator.hasNext()) {
 41                 SelectionKey key = iterator.next();
 42                 iterator.remove();
 43                 dealWithKey(server, key);
 44                 
 45             }
 46         }
 47         
 48     }
 49     
 50     private void dealWithKey(ServerSocketChannel server, SelectionKey key) throws IOException {
 51         String content = null;
 52         if (key.isAcceptable()) {
 53             // 有已经接受的新的到服务端的连接
 54             SocketChannel socketChannel = server.accept();
 55 
 56             // 有新的连接并不代表这个通道就有数据,
 57             // 这里将这个新的 SocketChannel 注册到 Selector,监听 OP_READ 事件,等待数据
 58             socketChannel.configureBlocking(false);
 59             socketChannel.register(selector, SelectionKey.OP_READ);
 60             //将此对应的channel设置为准备接受其他客户端请求
 61             key.interestOps(SelectionKey.OP_ACCEPT);
 62             System.out.println("Server is listening from client " + socketChannel.getRemoteAddress());
 63             socketChannel.write(charset.encode("Please input your name: "));
 64             
 65         } else if (key.isReadable()) {
 66             // 有数据可读
 67             // 上面一个 if 分支中注册了监听 OP_READ 事件的 SocketChannel
 68             SocketChannel socketChannel = (SocketChannel) key.channel();
 69             ByteBuffer readBuffer = ByteBuffer.allocate(1024);
 70             int num = socketChannel.read(readBuffer);
 71             if (num > 0) {
 72                 content = new String(readBuffer.array()).trim();
 73                 // 处理进来的数据...
 74                 System.out.println("Server is listening from client " + 
 75                                     socketChannel.getRemoteAddress() + 
 76                                     " data received is: " + 
 77                                     content);
 78 /*                ByteBuffer buffer = ByteBuffer.wrap("返回给客户端的数据...".getBytes());
 79                 socketChannel.write(buffer);*/
 80                 //将此对应的channel设置为准备下一次接受数据
 81                 key.interestOps(SelectionKey.OP_READ);
 82                 
 83                 String[] arrayContent = content.split(USER_CONTENT_SPILIT);
 84                 //注册用户
 85                 if(arrayContent != null && arrayContent.length ==1) {
 86                     String name = arrayContent[0];
 87                     if(users.contains(name)) {
 88                         socketChannel.write(charset.encode("system message: user exist, please change a name"));
 89                         
 90                     } else {
 91                         users.add(name);
 92                         int number = OnlineNum(selector);
 93                         String message = "welcome " + name + " to chat room! Online numbers:" + number;
 94                         broadCast(selector, null, message);
 95                     }
 96                 } 
 97                 //注册完了,发送消息
 98                 else if(arrayContent != null && arrayContent.length >1){
 99                     String name = arrayContent[0];
100                     String message = content.substring(name.length() + USER_CONTENT_SPILIT.length());
101                     message = name + " say " + message;
102                     if(users.contains(name)) {
103                         //不回发给发送此内容的客户端
104                         broadCast(selector, socketChannel, message);
105                     }
106                 }
107             } else if (num == -1) {
108                 // -1 代表连接已经关闭
109                 socketChannel.close();
110             }
111         }
112     }
113     
114     private void broadCast(Selector selector, SocketChannel except, String content) throws IOException {
115         //广播数据到所有的SocketChannel中
116         for(SelectionKey key : selector.keys())
117         {
118             Channel targetchannel = key.channel();
119             //如果except不为空,不回发给发送此内容的客户端
120             if(targetchannel instanceof SocketChannel && targetchannel!=except)
121             {
122                 SocketChannel dest = (SocketChannel)targetchannel;
123                 dest.write(charset.encode(content));
124             }
125         }
126     }
127     
128     public static int OnlineNum(Selector selector) {
129         int res = 0;
130         for(SelectionKey key : selector.keys())
131         {
132             Channel targetchannel = key.channel();
133             if(targetchannel instanceof SocketChannel)
134                 res++;
135         }
136         return res;
137     }
138     
139     public static void main(String[] args) throws IOException {
140         new ChatServer().init();
141     }
142 
143 }

客户端:

  1 package com.lee.demo.nio;
  2 
  3 import java.io.IOException;
  4 import java.net.InetSocketAddress;
  5 import java.nio.ByteBuffer;
  6 import java.nio.channels.SelectionKey;
  7 import java.nio.channels.Selector;
  8 import java.nio.channels.SocketChannel;
  9 import java.nio.charset.Charset;
 10 import java.util.Iterator;
 11 import java.util.Scanner;
 12 import java.util.Set;
 13 
 14 public abstract class ChatClient {
 15 
 16     private Selector selector = null;
 17     public static final int port = 8765;
 18     private Charset charset = Charset.forName("UTF-8");
 19     private SocketChannel sc = null;
 20     private String name = "";
 21     private static String USER_EXIST = "system message: user exist, please change a name";
 22     private static String USER_CONTENT_SPILIT = "#";
 23     
 24     public void init() throws IOException
 25     {
 26         selector = Selector.open();
 27         //连接远程主机的IP和端口
 28         sc = SocketChannel.open(new InetSocketAddress("127.0.0.1", port));
 29         sc.configureBlocking(false);
 30         sc.register(selector, SelectionKey.OP_READ);
 31         //开辟一个新线程来读取服务器端的数据
 32         new Thread(new ClientThread()).start();
 33         //在主线程中 从键盘读取数据输入到服务器端
 34         Scanner scan = new Scanner(System.in);
 35         try {
 36             while (scan.hasNextLine()) {
 37                 String line = scan.nextLine();
 38                 if ("".equals(line))
 39                     continue; // 不允许发空消息
 40                 if ("".equals(name)) {
 41                     name = line;
 42                     line = name + USER_CONTENT_SPILIT;
 43                 } else {
 44                     line = name + USER_CONTENT_SPILIT + line;
 45                 }
 46                 sc.write(charset.encode(line));// sc既能写也能读,这边是写
 47             }
 48         } finally {
 49             scan.close();
 50         }
 51         
 52         
 53     }
 54     private class ClientThread implements Runnable
 55     {
 56         public void run()
 57         {
 58             try
 59             {
 60                 while(true) {
 61                     int readyChannels = selector.select();
 62                     if(readyChannels == 0) continue; 
 63                     //可以通过这个方法,知道可用通道的集合
 64                     Set<SelectionKey> selectedKeys = selector.selectedKeys();  
 65                     Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
 66                     while(keyIterator.hasNext()) {
 67                          SelectionKey sk = (SelectionKey) keyIterator.next();
 68                          keyIterator.remove();
 69                          dealWithSelectionKey(sk);
 70                     }
 71                 }
 72             }
 73             catch (IOException io)
 74             {}
 75         }
 76 
 77         private void dealWithSelectionKey(SelectionKey sk) throws IOException {
 78             if(sk.isReadable())
 79             {
 80                 //使用 NIO 读取 Channel中的数据,这个和全局变量sc是一样的,因为只注册了一个SocketChannel
 81                 //sc既能写也能读,这边是读
 82                 SocketChannel sc = (SocketChannel)sk.channel();
 83                 
 84                 ByteBuffer buff = ByteBuffer.allocate(1024);
 85                 String content = "";
 86                 while(sc.read(buff) > 0)
 87                 {
 88                     buff.flip();
 89                     content += charset.decode(buff);
 90                 }
 91                 //若系统发送通知名字已经存在,则需要换个昵称
 92                 if(USER_EXIST.equals(content)) {
 93                     name = "";
 94                 }
 95                 System.out.println(content);
 96                 sk.interestOps(SelectionKey.OP_READ);
 97             }
 98         }
 99     }
100     
101 }

自己生成一个类,将Client这个抽象类继承,执行就可以了。观看结果的时候,会有个小地方需要注意,就是Eclipse中,console的结果,需要开启多个控制台,要不会发生混乱。

方法:

第一步:

NIO 多人聊天室

第二步:

NIO 多人聊天室

 左侧红框,pin console ,作用是锁定console,固定显示选择的线程的输出;
 右侧红框,作用是线程选择显示哪个线程的输出。