LAB09 Modify Java program for chatting room 聊天室(Network programming) Problem: Add some features to the example ChatServer.java Purpose: Master Thread programming and socket programming (writting TCP server) using java.net package through modifing an example of Java chatting server. You will also learn how to deal critical section in Java, as well as how to use java.util.Hashtable and the interface java.util.Enumeration Due: 2010/05/08 Sunday 23:59 請注意: 做好習題請依規定到本題討論區貼文(post), 且注意標題 Subject: LAB09 from 學號姓名 (當然是你的學號與姓名); 內容用簡單HTML版本, 先放心得, 接著 Running Script, 然後帶 Line numbers程式碼; 全部資料也要壓縮成一個 .jar 檔案, 當作夾檔附件; 阿整個就是類似這題目啦! Description: 阿就是修改我給的範例 ChatServer.java : (1) 把你學號姓名寫在程式裡面, 有人連線時要讓他看到你歡迎他 (2) 把裡面未完成的 change nick name 功能完成, 注意 nick name不可重複 (3) 把 Console 上的 訊息也同時 Log 到 Log file 啟動 ChatServer 時詢問 Logfile name, 只按 ENTER 則用 ChatServer.log, 注意 要用 append 方式, 不可毀掉舊 Log (4) 加入類似 Facebook 的「戳一下(Poke)」功能 以下這項功能必須期中考有及格或上學期85分以上的才可以做(不做只是分數較少一點:): (5) 自行發揮創意加入至少一項新功能或祕技 例如可以開多間聊天室(包廂), 室主可以有權限踢走或控管人員進出 若有多間聊天室則剛進入時在大廳, 可察看有哪些"包廂"? (例如 /rlist 查看包廂; /cr ??? 切換包廂, ...) 又室主離開包廂時若只剩一人, 則該人自動成為新室主, 若仍留有多人, 詢問室主要把室主權指定給誰 室主的提示符號要不同於其他人 (例如 >> ) 離開包廂則會到大廳, 大廳主人是啟動此聊天室的人 Extra credit:(也是期中考有及格或上學期85分以上的才可以做) 參考 Client2.java 修改為有自己特色的聊天室 Client 端 Turn in: 心得 + Running script + source code with line numbers. Plus a .jar file contains all your files. Note: You should also write what you have learned from this Lab.(心得) 本題心得要包括研究此程式中用到 Java 各種 API 的心得.You are the
Hint to 聊天室 ChatServer : interface, Hashtable, Enumeration (1) 關於常數與C語言的 header file 在 C/C++ 中, 我們常把一些常數用 #define 定義成巨集(macro), 放在 .h 檔 然後所有需要用到的 file 都 #include 那個 .h 檔 但這在 Java 行不通! 因為 Java 不支持 preprocessing ! 那怎辦? 很簡單 .. 把原先 C/C++ 的 .h 檔寫成一個 interface, 各 class 需要就 implements 它! 該 interface 裡面就是放常數宣告: 把 C/C++ 的 #define CMD_DATA -1 改成 public static final int CMD_DATA = -1; 例如: 54 interface CMD_Constant 55 { // 把所有共用常數集中在一個 interface 中是個不錯的用法 56 public final int CMD_DATA = -1; 57 public final int CMD_QUIT = 0; 58 public final int CMD_MSG = 1, CMD_LIST = 2; 59 public final int CMD_QUERY = 3, CMD_NICK = 4; 60 public final int CMD_HELP = 999; 61 public final String SECRET_NAME = "IloveGiGi IwantGiGi"; 62 }//interface 之後各class 只要 implements CMD_Constant 就能看到以上常數 (2) 聊天室的網路Server部份與 Thread 部份應該大家都會了 剩下是要如何表示聊天室? 如何把 message 傳給聊天室內所有成員 ? (a)如何表示聊天室? 方法很多, 這邊我們用一個 Hashtable 資料結構: 74 static private Hashtable<String,ClientThread> onlineUsers = 75 new Hashtable<String,ClientThread> ( ); // MAP 在 JDK1.5 (5.0)以前不支援 Generic (Template)須寫: static private Hashtable onlineUsers = new Hashtable( ); // MAP 請參考Java API 手冊網頁 或 javap java.util.Hashtable Hashtable 是關聯式陣列, 由許多 key - value 配對組成 ! 以下範例都參用到 onlineUsers (是 Hashtable ).. 我們用 nickname 當作 key, 用該 nickname 的 "連線"當作 value *要加入聊天室: 使用 Hashtable 的 put 222 onlineUsers.put(name, this); 此例中的 this 就是該 function 所在的 "物件", 是 class ClientThread, ClientThread 在每次有人連入時會被 new 出來, 代表一個連線物件 該連線物件內有許多連線相關資料! **但加入聊天室之前要先 check 看是否已經存在該nickname? if( onlineUsers.containsKey(name) ) ... *離開時, 要從聊天室移除: onlineUsers.remove(myName); (b)如何把 message 傳給聊天室內所有成員 ? 這時重點在如何取得聊天室內所有成員的連線? 所有成員順序不重要, 重點是要能一個一個抓來用, 如何做? 要用另一個資料結構 Enumeration, 先把聊天室 Hashtable 複製成一個 Enumeration (列舉物件): Enumeration e = onlineUsers.elements( ); 然後這個 e 就可用 nextElement( ) 一次取出一個元素, 每次取之前先 check 還有沒有: if( e.hasMoreElements( )) ... 於是, 這光播的工作就很簡單, 請看: 265 private void doBcast(String msg) { 266 for ( Enumeration e = onlineUsers.elements( ); e.hasMoreElements( ); ) { 267 ClientThread c = (ClientThread) e.nextElement( ); 268 if ( c != this ) c.writeLine(msg); // 廣播給除自己之外所有人 269 }//for 270 }//doBcast( Line 268 的 c.writeLine(msg); 是我們寫的 function, 自己研究一下就懂了.. 阿就是從連線的 "輸出水管" (OutputStream) 把 msg 給 print 出去啦! (c)若是只要把訊息丟給一個人? 也很簡單: 參看 void doMsg(String cmd) 內這列: (重點在用 get 取得該連線 c) ClientThread c = (ClientThread) onlineUsers.get(dst); 然後當然也是用 c.writeLine(msg); 把 msg 丟給他囉... |
01 //ChatServer.java --- by tsaiwn@csie.nctu.edu.tw and jwwang@csie.nctu 02 ////// http://www.csie.nctu.edu.tw/~tsaiwn/course/java/examples/network/ 03 //--Demostrate using ServerSocket and Socket to build a chat server 04 // javac ChatServer.java 05 // java ChatServer 1234 (Default port 6789 is used if port == 0 ) 06 ////// 要進入聊天室的用 telnet host_name port_listening 測試即可 07 import java.io.*; 08 import java.net.*; 09 import java.util.*; 10 11 public class ChatServer // the class with main program should be public 12 { 13 private final static int DEFAULT_PORT = 6789; 14 private ServerSocket serverSock; 15 private int portNumber; 16 private Socket clientSock; 17 18 public ChatServer(int port) { // constructor 19 portNumber = port; 20 try { 21 serverSock = new ServerSocket( portNumber ); 22 System.out.println("ChatServer started on port " + 23 portNumber + " at " + new Date().toString() ); 24 while ( true ) { // wait for a client to connect into 25 clientSock = serverSock.accept(); 26 // after accept a connection, fork a thread to handle it 27 ClientThread guest = new ClientThread(clientSock); 28 guest.start( ); //it will call run( ); 用thread 處理連線 29 }//while( 30 // 任何可能丟出 Exception 的句子都要用 try{ }夾住 31 // 或是其所在的 function 要宣告 throws Exception 32 } catch ( Exception e ) { 33 System.err.println("Cannot startup ChatServer!"); 34 System.exit(38); 35 }//try..catch.. 36 }// ChatServer constructor 37 38 public static void main(String[ ] argv) { 39 if ( argv.length != 1 ) { 40 System.err.println("Usage: java ChatServer [port number]"); 41 System.exit(0); 42 }//if 43 int port = 0; 44 try { port = Integer.parseInt( argv[0] ); 45 } catch ( Exception e ) { 46 System.err.println("Invalid port number"); 47 System.exit(1); 48 }//try..catch.. 49 if(port == 0) port = DEFAULT_PORT; 50 new ChatServer( port ); 51 } // main 52 }// class ChatServer 53 54 interface CMD_Constant 55 { // 把所有共用常數集中在一個 interface 中是個不錯的用法 56 public final int CMD_DATA = -1; 57 public final int CMD_QUIT = 0; 58 public final int CMD_MSG = 1, CMD_LIST = 2; 59 public final int CMD_QUERY = 3, CMD_NICK = 4; 60 public final int CMD_HELP = 999; 61 public final String SECRET_NAME = "IloveGiGi IwantGiGi"; 62 }//interface 63 //// Any Class that implements CMD_Constant can use the above constants 64 65 class ClientThread extends Thread implements CMD_Constant 66 { 67 private Socket mySock; 68 private String myName; 69 private String remoteAddr; // 連著我的對方 IP address 70 private BufferedReader in; 71 private PrintWriter out; 72 private PrintStream tty = System.out; // tty 只是簡寫而已 73 74 static private Hashtable<String,ClientThread> onlineUsers = 75 new Hashtable<String,ClientThread> ( ); // MAP 76 static private String lock = "This is a lock"; // static ensure one copy 77 78 private java.util.Date now; 79 80 public ClientThread(Socket skt) { 81 mySock = skt; 82 } // ClientThread constructor 83 84 private String getNickname() { return myName; } 85 private String getRemoteAddr() { return remoteAddr; } 86 87 public void run() { 88 now = new Date(); 89 try { 90 remoteAddr = mySock.getInetAddress().getHostAddress(); 91 // 然後取得InputStream並包成 BufferedReader 方便 readLine() 92 in = new BufferedReader( 93 new InputStreamReader(mySock.getInputStream()) ); 94 out = new PrintWriter( // 再取得 OutputStream 並包成 PrintWriter 95 new OutputStreamWriter(mySock.getOutputStream()), true ); 96 }catch ( Exception e ) { log(e.getMessage()); } 97 /// I/O Streams ready NOW 98 try { // 接著, 要求連線者輸入 nickname 99 myName = askNickname(); 100 if(myName == null) return; // give up 101 }catch ( Exception e ) { log(e.getMessage()); } 102 // 若輸入怪異的nickname例如Control_C 則終止連線 103 if (stranger(myName) ){ close(); return; } 104 // 廣播給所有聊天室的人 105 doBcast("CHAT *** " + myName + " is coming in ***"); 106 // 並在 console 上顯示 Log (tty == System.out) 107 log(myName + "@"+remoteAddr+" enters the Chat Room " + now); 108 // writeLine(msg) 會把 msg 寫到目前連線者終端機 109 writeLine("CHAT *** Welcome 歡迎 "+myName+" 進入聊天室 ***"); 110 writeLine("You can type '/help' for help"); 111 /// keep chatting 112 try { 113 String cmd, msg; int whatCMD=0; 114 FOO: 115 while ( (cmd = in.readLine()) != null ) { 116 StringTokenizer stkn = new StringTokenizer(cmd, " \t"); 117 String command = " "; 118 if(stkn.countTokens( ) >= 1) command = stkn.nextToken(); 119 msg = " "; 120 if(stkn.hasMoreTokens()) msg = stkn.nextToken("\n"); 121 whatCMD = parseCommand(command.toUpperCase()); 122 switch ( whatCMD ) { 123 case CMD_MSG: doMsg(msg); break; 124 case CMD_LIST: doList(); break; 125 case CMD_QUERY: doQuery(cmd.substring(6)); break; 126 case CMD_NICK: doNick(msg); break; 127 case CMD_HELP: doHelp(); break; 128 case CMD_QUIT: 129 now = new Date(); 130 log(myName + " said BYE at " + now); 131 doBcast("["+myName + " saied Bye Bye! ]"); 132 break FOO; 133 case CMD_DATA: doBcast("["+myName + "] " + cmd); 134 log(myName + ": " + cmd); 135 break; 136 } // switch 137 } // while FOO: 138 } catch ( Exception e ) { log(e.getMessage()); } 139 log(" .. one thread stop at " + new Date( ) ); 140 close(); 141 }// run( // in class ClientThread(Socket skt) 142 143 private void writeLine(String msg) { 144 try { out.println(msg+"\r"); // with carriage-return 145 } catch ( Exception e ) {;} // simply ignore the Exception 146 } // writeLine(msg) 把 msg 由此連線的 out 寫到連線者的 InputStream 147 private void write(String msg) { 148 try { out.print(msg); out.flush(); // NO newline/CarriageReturn 149 } catch ( Exception e ) {;} 150 } // write 151 152 private void close( ) { 153 if(myName!=null && ! myName.equals(SECRET_NAME)) 154 doBcast("CHAT *** " + myName + " has left the chatroom ***"); 155 synchronized ( lock ) { 156 try { onlineUsers.remove(myName); } catch ( Exception e ) { } 157 } // synchronized critical section 158 try { 159 if ( in != null ) in.close(); 160 if ( out != null ) out.close(); 161 if ( mySock != null ) mySock.close(); 162 } catch ( Exception e ) {;} 163 } // close( ) 關閉此連線之 socket 及其 In/Out Stream 164 165 private boolean stranger(String myName) { 166 //log("=+= " + (myName.getBytes()[0])+" =" ); 167 //log("=+= " + Character.isISOControl(myName.charAt(0))); 168 if (Character.isISOControl(myName.charAt(0)) ) myName = "QUIT"; 169 if (myName.equals("QUIT")){ writeLine("! Thank you for coming!"); } 170 if(myName == null || myName.equals("QUIT") ) { 171 log(" --- from "+ remoteAddr +" at "+now.toString()); 172 return true; 173 } // "QUIT" is not allowed as a nick name 174 if(myName.equals(SECRET_NAME)){ 175 log("GiGi from "+remoteAddr+" at "+now.toString()); 176 doList(); return true; 177 }//if 178 return false; // a usual user name 179 }//stranger( 180 181 private String askNickname() throws Exception { 182 String name = null; 183 boolean ok = false; 184 write("\r\n=> Enter Your Nickname: "); 185 while(!ok) { 186 name = in.readLine(); 187 while ( onlineUsers.containsKey(name) ) { 188 writeLine("=> " + name + " exists! Re-enter your nickname: "); 189 name = in.readLine(); 190 } 191 ok = true; 192 if (name.equalsIgnoreCase("yoshiki") ){ 193 log("!!! " + name + " is trying ... "); 194 writeLine("? Yoshiki 你這日本的走狗滾開!"); 195 close(); 196 return null; 197 } 198 if (name.endsWith("ki") || name.endsWith("kli") || 199 name.endsWith("KI") || name.endsWith("KLI") ) { 200 log("!!! " + name + " is trying ... "); 201 writeLine("? No Japanese! Sorry! Re-enter your nickname: "); 202 ok=false; 203 } 204 if (name.equals("tsaiwn") ){ 205 writeLine("? Do Not use God's name -- 別盜用別人 username"); 206 writeLine("=> Re-Enter Your Nickname: "); 207 ok=false; 208 } 209 if (name.startsWith(" ") || name.startsWith("/") ){ 210 writeLine("? Do Not cheat me ! 別亂打!"); 211 writeLine("=> Re-Enter Your Nickname: "); 212 ok=false; 213 } 214 if (name.equalsIgnoreCase("quit") ){ return "QUIT"; } 215 } // while(!ok) 216 if(! name.equals(SECRET_NAME)) setNickname(name); 217 return name; 218 }//askNickname() 219 220 private void setNickname(String name) { 221 synchronized ( lock ) { 222 onlineUsers.put(name, this); 223 } 224 }//setNickname( 225 226 private void doList( ) { 227 for(Enumeration e = onlineUsers.elements(); e.hasMoreElements(); ) { 228 ClientThread c = (ClientThread) e.nextElement(); 229 writeLine("CHAT *** [" + c.getNickname() + 230 "@" + c.getRemoteAddr() + "] ***"); 231 } // for 232 } // doList 233 234 private void doMsg(String cmd) { 235 cmd = cmd.trim(); 236 int pos = cmd.indexOf(' '); // find tell whom 237 if ( pos < 0 ) return; // no message on the line 238 String dst = cmd.substring(0, pos); 239 String msg = cmd.substring(pos+1); 240 msg = msg.trim(); 241 if ( onlineUsers.containsKey(dst) ) { 242 ClientThread c = (ClientThread) onlineUsers.get(dst); 243 c.writeLine("*** [" + myName + "] " + msg); 244 } else { 245 writeLine("CHAT *** " + dst + " not in the ROOM ***"); 246 }//if..else.. 247 }//doMsg( 248 249 private void doQuery(String cmd) { 250 cmd = cmd.trim(); // remove leading blanks 251 if ( onlineUsers.containsKey(cmd) ) { 252 ClientThread c = (ClientThread) onlineUsers.get(cmd); 253 writeLine("CHAT *** " + c.getNickname() + " is from " + 254 c.getRemoteAddr() + " ***"); 255 } else { 256 writeLine("CHAT *** " + cmd + " is UNKNOWN ***"); 257 }//if..else.. 258 }//doQuery( 259 260 private void doNick(String cmd) { 261 // not implemented yet 262 writeLine("Not implemented yet to change nick name!"); 263 }//doNick( 264 265 private void doBcast(String msg) { 266 for( Enumeration e = onlineUsers.elements(); e.hasMoreElements(); ) { 267 ClientThread c = (ClientThread) e.nextElement(); 268 if ( c != this ) c.writeLine(msg); 269 }//for 270 }//doBcast( 271 272 private int parseCommand(String x) { 273 if ( x.startsWith("/QUIT") || x.startsWith("/BYE") ) { 274 return CMD_QUIT; 275 } else if ( x.startsWith("/MSG") || x.startsWith("/TELL") ) { 276 return CMD_MSG; 277 } else if ( x.startsWith("/LIST") ) { 278 return CMD_LIST; 279 } else if ( x.startsWith("/QUERY") ) { 280 return CMD_QUERY; 281 } else if ( x.startsWith("/NICK") ) { 282 return CMD_NICK; 283 } else if ( x.startsWith("/HELP") || x.startsWith("/?") ) { 284 return CMD_HELP; 285 } else if ( x.startsWith("/") ) { 286 return CMD_HELP; 287 } else { // 不是 "/"開始的都當作 message data 288 return CMD_DATA; 289 }//if... 290 } // parseCommand 291 292 private void doHelp() { 293 writeLine("Available commands: /MSG /LIST /QUERY /NICK /HELP /QUIT"); 294 } 295 private void log(String s) { 296 tty.println(s); 297 } 298 }//class ClientThread抓 Lab09.jar (含有這習題所有檔案)