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 -th visitors to this page.
抓 Lab09.jar (含有這習題所有檔案)

      回到作業目錄             回到課程目錄

 
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 (含有這習題所有檔案)

      回到作業目錄             回到課程目錄