NIO 多人聊天室
一前言
在家休息没事,敲敲代码,用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的结果,需要开启多个控制台,要不会发生混乱。
方法:
第一步:
第二步:
左侧红框,pin console ,作用是锁定console,固定显示选择的线程的输出;
右侧红框,作用是线程选择显示哪个线程的输出。
努力到无能为力,奋斗到感动上天!