1. 程式人生 > >NIO 多人聊天室

NIO 多人聊天室

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;
  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,固定顯示選擇的線程的輸出;
右側紅框,作用是線程選擇顯示哪個線程的輸出。

NIO 多人聊天室