NIO 多人聊天室
阿新 • • 發佈:2018-05-27
fig pin 仿真 all listen HR code cas ole
一前言
在家休息沒事,敲敲代碼,用NIO寫個簡易的仿真聊天室。下面直接講聊天室設計和編碼。對NIO不了解的朋友,推薦一個博客,裏面寫的很棒:
https://javadoop.com/ 裏面有NIO的部分
二設計
1.進入的時候,提示輸入聊天昵稱,重復的話,重新輸入,成功後進到聊天室。
2.成功進到聊天室,廣播通知,XXX進到了聊天室;離開聊天室,XXX離開了聊天室。
3.@XXX 給XXX發消息,只有雙方可以看到。
4.服務端收到的內容會轉發給其他客戶端。
三代碼
目前版本(設計工程3,4有待改進)
服務端:
1 package com.lee.demo.nio; 23 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,固定顯示選擇的線程的輸出;
右側紅框,作用是線程選擇顯示哪個線程的輸出。
NIO 多人聊天室