2014年5月4日 星期日

毛詩

今天看了這篇 "習近平五四訪北大 學生誦毛詩", 文中提到毛澤東的詞 "沁園春.長沙", 印象中只記得 "沁園春.雪" 較知名, 所以查了一下, 找到這篇 :

# 從歷史和文學的角度看毛澤東的兩首《沁園春》

經此文解析,  方明瞭毛澤東詩詞之意境優劣之處.

作者為姚立民氏, 但此君非同名之前立委, 而是中央社記者, 安徽桐城人. 桐城出文人, 是清代文學桐城派的故鄉, 方苞, 劉大魁, 姚鼐為開派之祖. 明代東林黨左光斗, 康熙文字獄南山案的戴名世, 雍正寵臣軍機大臣張廷玉 (明史總修官), 都是桐城人.


Java 包裹類別的 auto-boxing 與 auto-unboxing

在 Java 程式裡有一個類別庫 java.lang, 即使沒有用 import 指令匯入, JDK 在編譯時也會自動匯入, 因為此類別庫中有我們最常用的 wrapper 類別, String 類別, StringBuffer/StringBuilder 類別, Math 類別等等.

這裡要測試一下 wrapper 包裹類別的自動裝箱 (auto-boxing) 與自動拆箱 (auto-unboxing) 功能. 關於包裹類別參見 :

# Java 複習筆記 : 資料型態與語法

包裹類別的用途是將 8 種基本資料型態資料包裹成物件型態, 通常是利用其兩種建構子, 例如 :

Integer i=new Integer(100);  
Integer i=new Integer("100");

亦即建構子的傳入參數有兩種, 一是基本資料本身, 二是其字串型態, 8 種基本型態的包裹類別皆是如此, 參考 Java API :

# http://docs.oracle.com/javase/7/docs/api/

但是注意, 使用字串為建構子時, 其格式必須正確, 否則會產生格式例外 NumberFormat Exception, 例如下列範例雖然編譯成功, 但會有執行時期錯誤 :

public class wrapper1{
  public static void main(String[] args) {
    Integer i=new Integer("100.0");  //執行時發生格式錯誤例外
    System.out.println(i);
    }
  }

這是因為整數沒有小數點, 不能傳入 "100.0", 否則格式錯誤.

但下面範例則可以順利執行 :

public class wrapper1{
  public static void main(String[] args) {
    Double d=new Double("100");  //輸出 100.0
    System.out.println(d);
    }
  }

這是因為浮點數包括了整數, 本來就不一定會有小數點, 因此傳入整數格式是合法的.

在 JDK 1.5 版之後提供了包裹類別的 auto-boxing 與 auto-unboxing 功能, 亦即不需要利用建構子, JDK 會自動呼叫, 稱為 auto-boxing, 例如 :

public class wrapper1{
  public static void main(String[] args) {
    Integer i=100;
    System.out.println(i); //輸出 100
    Double d=100.0;
    System.out.println(d); //輸出 100.0
    }
  }

反過來看,  如果把包裹物件指定給基本型態變數, 在 JDK 1.5 版以前, 必須呼叫包裹物件的 xxxValue() 方法進行拆箱 (boxing) 才行.

public class wrapper1{
  public static void main(String[] args) {
    Integer i=100;
    System.out.println(i.intValue());  //輸出 100
    Double d=100.0;
    System.out.println(d.doubleValue());  //輸出 100.0
    }
  }

同樣地, 在 JDK 1.5 之後, 也不一定要用 xxxValue(), JDK 會自動呼叫 xxxValue() 進行拆箱 :


public class wrapper1{
  public static void main(String[] args) {
    Integer i=100;
    int ui=i;  //自動呼叫 intValue() 拆箱
    System.out.println(ui);  //輸出 100
    Double d=100.0;
    double ud=d;  //自動呼叫 doubleValue() 拆箱
    System.out.println(ud); //輸出 100.0
    }
  }

但要注意的是, 自動拆箱時的傳回值資料型態, 若指定給比傳回值還大的資料型態就 OK, 否則編譯時會出現 incompatible types 錯誤 :

public class wrapper1{
  public static void main(String[] args) {
    Integer i=100;
    //byte ui=i;  //自動拆箱型態不匹配 : 不可指定給較小的資料型態
    long ui=i; //自動拆箱型態匹配
    System.out.println(ui);
    double d=100.0;
    //float ud=f;  //自動拆箱型態不匹配 : 不可指定給較小的資料型態
    double ud=d;  //自動拆箱型態匹配
    System.out.println(ud);
    }
  }


可見, 因為 Integer 自動拆箱時會呼叫 intValue() 傳回 int 型態資料, 若指定給比 int 小的 byte 變數就會不匹配, 無法編譯成功. 同樣地, Double 自動拆箱時呼叫 doubleValue() 會傳回 double, 如果指定給 float 也會發生編譯錯誤.


2014年5月3日 星期六

如何製作可執行的 Java 壓縮檔 jar

雖然會用 Eclipse, 但工具很龐大, 啟動要一點時間, 所以我一直都習慣用 DOS 畫面執行 Java. 但編譯後可能會產生多個 class 檔 (例如內部類別), 攜帶不方便, 而且要使用者打開 DOS 視窗, 敲入一長串執行指令, 在 GUI 為王道的今天, 實在太委屈了, 應該將整個程式壓縮成單一的可執行 jar 檔較好. 下面將以之前測試 JTextField 時的一個範例檔案為例來說明.

# Java Swing 測試 : JLabel 與文字欄位

這篇文章被 Google 翻譯弄亂了, 因為小狐貍們也跟我一樣用 Chrome, 結果菁菁有一次啟動了 "Google 翻譯" 我不知道, 存檔後才發現它把程式碼也翻譯了, 目前暫時還沒時間去一一修復, 擱著吧. 我們要用的是其中幾乎無損的範例 9 (JTextField6.java), 但此處要做個修改, 就是為了製作 jar 檔時抓相關的 class 方便, 希望將編譯出來的 class 檔都集中放在指定目錄中, 這就要用到 package 觀念了, 我們在原來的 JTextField6.java 前面加上 package mypackage :

package mypackage;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

public class JTextField6 implements ActionListener {
  JFrame f;
  JTextField text1,text2;
  JButton cut, copy, paste;
  public static void main(String argv[]) {
    new JTextField6(); 
    }
  public JTextField6() {
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTextField6");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    cp.setLayout(null);
    text1=new JTextField();
    text1.setBounds(20,20,200,25);
    cp.add(text1);
    cut=new JButton("剪下");
    cut.setBounds(20,70,60,25);
    cut.addActionListener(this);
    cp.add(cut);
    copy=new JButton("複製");
    copy.setBounds(90,70,60,25);
    copy.addActionListener(this);
    cp.add(copy);
    paste=new JButton("貼上");
    paste.setBounds(160,70,60,25);
    paste.addActionListener(this);
    cp.add(paste);
    text2=new JTextField();
    text2.setBounds(20,120,200,25);
    cp.add(text2);
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);    
    f.setVisible(true);
    f.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        int result=JOptionPane.showConfirmDialog(f,
                   "確定要結束程式嗎?",
                   "確認訊息",
                   JOptionPane.YES_NO_OPTION,
                   JOptionPane.WARNING_MESSAGE);
        if (result==JOptionPane.YES_OPTION) {System.exit(0);}
        }   
      });
    }
  public void actionPerformed(ActionEvent e) {
    if (e.getSource()==cut) {text1.cut();}
    else if (e.getSource()==copy) {text1.copy();}
    else if (e.getSource()==paste) {text2.paste();}
    }
  }

編譯時要加點料, 不是 javac JTextField6.java, 這樣仍然是在目前目錄下產生 class 檔, 亦即有沒有加 package 都一樣. 要讓 class 檔放在 package 目錄下, 必須這樣編譯 :

javac -d . JTextField6.java

注意, 多了一個選項 -d 與小數點, 表示要在目前目錄下建立一個 package 所指定之子目錄, 並將產生的 class 檔放在裡面. 例如 JTextField6.java 是在 G:\java\JT 下, 則編譯後, 會建立一個 mypackage 子目錄, 並將產生的兩個 class 檔 : JTextField6.class 與 JTextField6$1.class 放在此子目錄下 :


 一個專案通常由多個 java 檔組成, 可以將這些 java 檔都指定為相同 package 名稱, 然後只用下面指令一次編譯全部該專案的 java 檔, 產生的 class 檔就全部放在 package 所指定的子目錄下 :

javac -d . *.java

然後在工作目錄, 即 java 檔所在目錄, 下達壓縮指令 :

jar cvf JTextField6.jar mypackage

第二個參數是壓縮後的檔案, 可自訂; 但最後一個參數則必須是要壓縮的 package 目錄名. 這樣就會在工作目錄下產生 JTextField6.jar 檔案 :

L:\Java\JT>jar cvf JTextField6.jar mypackage
已新增資訊清單
新增: mypackage/ (讀=0)(寫=0)(儲存 0%)
新增: mypackage/JTextField6$1.class (讀=801)(寫=557)(壓縮 30%)
新增: mypackage/JTextField6.class (讀=2132)(寫=1191)(壓縮 44%)
使用 RAR 或 Winzip 解壓縮此 jar 檔就可以得到 class 檔案群. 事實上 jar 檔就是 zip 檔, 直接把副檔名 .jar 改成 .zip 解壓縮如下 :


可見 jar 指令除了將 package 目錄壓進去外, 還加了一個 META-INF 目錄, 裡面有一個 MENIFEST.MF 檔案, 內容如下 :

Manifest-Version: 1.0
Created-By: 1.7.0_51 (Oracle Corporation)

執行 jar 檔指令如下 :

java -cp JTextField6.jar mypackage/JTextField6

必須指定 package 名稱以及主 class 名稱才行. 但是在檔案總管直接點擊 jar 檔是無法執行的, 那要怎麼做呢? 其實只要在壓縮 jar 檔時, 在上面那個 MANIFEST.MF 檔案裡添加 Main-Class 屬性, 指定主類別名稱就可以了.

首先我們要準備一個文字檔, 例如 manifest.txt, 其內容如下 :

Main-Class: mypackage.JTextField6

注意, 打完須 enter, 否則無效. 這裡主類別是在 mypackage 套件下, 所以務必註明套件名稱, 如果只寫 JTextField6 將找不到主類別. 然後重新壓縮 :

L:\Java\JT>jar cvmf manifest.txt JTextField6.jar mypackage
已新增資訊清單
新增: mypackage/ (讀=0)(寫=0)(儲存 0%)
新增: mypackage/JTextField6$1.class (讀=801)(寫=557)(壓縮 30%)
新增: mypackage/JTextField6.class (讀=2132)(寫=1191)(壓縮 44%)

這樣就可以直接用 -jar 選項執行 jar 檔了.

L:\Java\JT>java -jar JTextField6.jar

而且直接在檔案總管點擊 jar 檔也可以執行了.

把 jar 副檔名改為 zip 去看壓縮檔中 META-INF 目錄下的 MANIFEST.MF 內容 :

Manifest-Version: 1.0
Created-By: 1.7.0_51 (Oracle Corporation)
Main-Class: mypackage.JTextField6

可見在壓縮時, jar 工具程式會讀取我們指定的 manifest.txt 檔內容, 然後寫入 MANIFEX.MF 檔裡. 當然, 如果不要設 package 也是可以的, 把專案中全部要用到 class 都放在同一目錄下, 然後於 manifest.txt 檔案中指定主類別, 然後在 jar 指令中列舉各 class 即可 :

jar cvmf manifest.txt ABC.jar A.class B.class C.class

參考資料 :

# Java Gossip: 製作 Executable JAR
# 可執行的 jar 檔

菁菁上敦煌英語

菁菁在科見的 K7 課程到上週一結束, 因人數不足併班, 時間上僅 3, 6 適合, 但菁不願週六上課 (她說是難得的假期), 加上師資上的考慮, 所以就決定停止. 但英文課還是必須繼續, 所以上週三 (4/23) 我帶她去明華路的敦煌詢問課程, 覺得與科見大不同, 採全外師制, 中師僅負責輔導, 以及與家長溝通, 不參與實際教學. 我看了課程安排與所用課本, 當即決定轉到敦煌. 其實, 早該轉的, 是因為科見離家近, 走路即可達, 交通方便才沒考慮.  次日帶菁菁給高主任面試, 以便選擇適當班級, 決定本週試讀, 前天 Francy 老師告訴我外師 Laurent 老師的意見 OK, 詢問加入意願, 於是前天 5/1 發下課本, 開始上課了.

關於小狐狸們的英文, 我因太忙只能做到幫忙校訂作業, 其實這樣學習效果不是很好. 他們三個都從小二去科見, 只有姐姐主動學習, 完成科見 K12 後去李麥克, 那裡以英文小說為學習主體, 雖然不是外師, 但效果很好, 可直接看原文小說. 至於科見, 我覺得師資似乎流動太頻繁, 中師有些不齊,  以前教二哥 K7 那位常講些讓學生沒信心的話, 我了解後幫他轉班才繼續唸完 K12.

2014年5月2日 星期五

到訪過 6 萬

晚上自圖書館回來, 發現部落格到訪已經超過 6 萬, 抓圖如下 :


晚上收到仲仔 Line 通知, 老同學建興今晚動換肝手術, 祝福他順利成功. 年後忙於世務, 久無聯繫, 不知他有幸換肝配對成功. 大約要一週方能轉普通病房探視.


Java Telnet Client 類別

今天寫完周報後, 花了一整天把 Telnet Client 程式物件化, 到下班前終於執行成功, 搞定. 我不把它寫進 JT 類別, 因為那很彆扭, 而且也不是一般常用的工具, 應寫成獨立類別為宜, 為了簡單起見, 我將其命名為 Telnet.java, 改寫自上一篇文章 :

# Java 的 Telnet 實作 (續)

Telnet.java

import java.io.*;
import java.net.*;
public class Telnet {
  Socket socket;
  BufferedInputStream in;
  PrintWriter out;
  public boolean connect(String host) {
    return connect(host,23);
    }
  public boolean connect(String host,int port) {
    try {
      socket=new Socket(host, port);
      socket.setKeepAlive(true);
      in=new BufferedInputStream(socket.getInputStream());
      out=new PrintWriter(socket.getOutputStream(),true);
      return true;
      }
    catch (Exception e) {e.printStackTrace();return false;}
    }
  public boolean login(String usr, String pwd) {
    try {
      String res=readUntil("Enter User Name");
      if (res!=null) {
        res=readUntil(">");
        if (res != null) {
          send(usr);
          Thread.sleep(500);
          res=readUntil("Enter Password");
          if (res != null) {
            send(pwd, false);
            Thread.sleep(500);
            res=readUntil(">");
            if (res != null) {return true;}
            }
          }
        }
      return false;
      }
    catch(Exception e) {e.printStackTrace();return false;}
    }
  public void send(String command) {
    send(command, true);
    }
  public void send(String command, boolean echo) {
    try {
      out.println(command + "\n");
      out.flush();
      if (echo) {System.out.println(command);}      
      }
    catch (Exception e) {e.printStackTrace();}
    }
  public String readUntil(String pattern) {
    try {
      char lastChar=pattern.charAt(pattern.length()-1);
      StringBuffer sb=new StringBuffer();
      char ch=(char)in.read();    
      while(true) {
        System.out.print(ch);
        sb.append(ch);
        if (ch==lastChar && sb.toString().endsWith(pattern)) {
          return sb.toString();
          }  
        ch=(char)in.read();
        }
      }
    catch(Exception e) {e.printStackTrace();}
    return null;
    }
  public void disconnect() {
    try {in.close();out.close();socket.close();}
    catch(Exception e) {e.printStackTrace();}
    }
  }

此 Telnet 類別不是可以泛用的類別, 因為其中的 login() 方法乃是針對公司某主機所特製, 如果是另外一台 Telnet 伺服主機, 其登入情況不同, 必須另行撰寫, 這樣好像失去寫進類別的意義咧. 但先求有再求好, 以後再來看看是否能一般化吧.

編譯後放在同一目錄下, 接著就可以使用它來連線了, 測試程式如下 :

public class test12 {
  public static void main(String[] args) {
    Telnet client=new Telnet();
    if (client.connect("aa.bbb.cc.dd",23)) {  //連線主機
      String usr="asabulu";
      String pwd="blabla";  
      if (client.login(usr,pwd)) { //登入主機
        client.send("lis table users all;");  //傳送指令
        String res=client.readUntil("BOTTOM");  //取得主機回應資料
        }
      else {System.out.println("login failed!");}
      }
    else {System.out.println("connect failed!");}
    client.disconnect();  
    }
  }

執行後成功列印出所需資料. 下一步要改成將主機回應資料儲存至指定檔案. 其次 login() 方法應該自 Telnet.java 中移出到應用程式中來, 因為它會因伺服器不同而需改寫. (2014-05-02)

2014-05-05 補充 :

今天改寫上周完成的 Telnet.java 類別如下 :

import java.io.*;
import java.net.*;
public class Telnet {
  Socket socket;
  BufferedInputStream in;
  PrintWriter out;
  public boolean connect(String host) {
    return connect(host,23);
    }
  public boolean connect(String host,int port) {
    try {
      socket=new Socket(host, port);
      socket.setKeepAlive(true);
      in=new BufferedInputStream(socket.getInputStream());
      out=new PrintWriter(socket.getOutputStream(),true);
      return true;
      }
    catch (Exception e) {e.printStackTrace();return false;}
    }
  public void send(String command) {
    send(command, true);
    }
  public void send(String command, boolean echo) {
    try {
      out.println(command + "\n");
      out.flush();
      if (echo) {System.out.println(command);}      
      }
    catch (Exception e) {e.printStackTrace();}
    }
  public String readUntil(String pattern) {
    return readUntil(pattern,100000);
    }
  public String readUntil(String pattern,int maxLoops) {
    try {
      char lastChar=pattern.charAt(pattern.length()-1);
      StringBuffer sb=new StringBuffer();
      char ch=(char)in.read();
      int cnt=0;
      while(true) {
        System.out.print(ch);
        sb.append(ch);
        if (ch==lastChar && sb.toString().endsWith(pattern)) {
          return sb.toString();
          }  
        ch=(char)in.read();
        if (++cnt > maxLoops) {break;}
        }
      }
    catch(Exception e) {e.printStackTrace();}
    return null;
    }
  public void wait(int ms) {
    try {Thread.sleep(ms);}
    catch(Exception e) {e.printStackTrace();}
    }  
  public void disconnect() {
    try {in.close();out.close();socket.close();}
    catch(Exception e) {e.printStackTrace();}
    }
  }

主要是加入 wait() 方法, readUntil() 加入無限迴圈控制, 以及將 login 移出. 應用程式如下 :

public class fetchA {
  Telnet client;
  public static void main(String[] args) {
    new fetchA();
    }
  public fetchA() {
    StringBuffer sb=new StringBuffer();  //儲存擷取之資料
    client=new Telnet();
    if (client.connect("aa.bbb.cc.dd",23)) {
      String usr="asabulu";
      String pwd="blabla";
      String res=login(usr,pwd);
      if (res != null) {
        sb.append(res);
        client.send("LIST TABLE USERS ALL;");
        res=client.readUntil("BOTTOM");
        if (res != null) {
          sb.append(res);
          res=logout();
          sb.append(res);
          }
        }
      else {System.out.println("login failed!");}
      }
    else {System.out.println("connect failed!");}
    JT.writeFile("test.log",sb.toString());  //擷取之資料寫入檔案
    client.disconnect();
    }
  public String login(String usr, String pwd) {
    String res=client.readUntil("Enter User Name");
    if (res!=null) {
      res=client.readUntil(">");
      if (res != null) {
        client.send(usr);
        client.wait(500);
        res=client.readUntil("Enter Password");
        if (res != null) {
          client.send(pwd, false);
          client.wait(500);
          res=client.readUntil(">");
          if (res != null) {return res;}
          }
        }
      }
    return null;
    }
  public String logout() {
    /*
    >LOGOUT
    BYE BYE
    Asabulu Logged out on 2014/05/05 at 11:18:29.
    */
    StringBuffer sb=new StringBuffer();
    String res=client.readUntil(">");
    sb.append(res);
    if (res!=null) {
      client.send("LOGOUT;");
      res=client.readUntil(".");
      if (res != null) {
        sb.append(res);
        return sb.toString();
        }
      }
    return null;
    }
  }

這個 fetchA 程式是專為公司的 A 主機而寫, 每一台主機的登入方式都不一樣, 所以必須移出 Telnet 類別加以客製化, 分別撰寫 login() 與 logout() 方法. 此處 A 主機 LOGOUT 後會輸出登出時間, 為了保留完整資訊, readUntil() 讀取到日期後面的小數點時, 表示已經輸出登出資訊了.


Era-The Mass

這是今天中午在 YouTube 上看這部 "原子彈落到廣島的那一秒" 短片時聽到的, 影片內容來自 BBC, 以前在看高能物理的科普書籍時我就上網看過了. 重點在這首配樂我聽後非常震撼, 有一種既空幻但又氣勢磅礡的感覺. 剪輯者有註明配樂取自德國軍歌 Era-The Mass, 因此很快就找到這首歌了.

依據網路上的說明, 這首歌曾被誤認為是納粹軍歌 (第一裝甲師戰歌), 但其實不是, 只是同樣改編自布蘭詩歌 (Carmina Burana), 因此曲風很類似的緣故. 歌詞原文並非德文, 而是拉丁文 :

"哦命運,像月亮般變化無常,盈虛交替;可惡的生活把苦難和幸福交織; 無論貧賤與富貴都如冰雪般融化消亡。可怕而虛無的命運之輪,你無情地轉動,你惡毒兇殘,搗毀所有的幸福 和美好的企盼,陰影籠罩迷離莫辨,你也把我擊倒;災難降臨,我赤裸的背脊被你無情地碾壓。命運摧殘著我的健康與意志,無情地打擊,殘暴地壓迫,使我終生受到奴役。在此刻切莫有一絲遲疑;因為那最無畏的勇士也已被命運擊垮,讓琴弦撥響,一同與我悲歌泣號!"

看這歌詞, 似乎也不適合當鼓舞戰鬥的軍歌吧! 拿來形容核裂變的威力倒是很貼切 ~~~ 空幻但又氣勢磅礡.

# 紀錄片 : 核爆廣島 2/2
# 歌詞的 MV
# Era-Ameno 改配 Era-The Mass MV
# 電影 兵臨城下
# Era 樂團音樂會 - 演唱 The Mass

Java 的 Telnet 實作 (續)

昨晚搞定 Telnet 連線程式後, 早上到公司立刻改寫以便測試是否能連線某主機, 然後遠端下指令後, 取得系統資訊, 經過剖析處理後放入資料庫以方便營運管理. 改寫的程式參考前篇文章 :

# Java 的 Telnet 實作

此主機連線後會先要求輸入帳號與密碼認證 :

Enter User Name
>asabulu

Enter Password
>********

因此這裡必須持續攔截輸入串流是否出現 "Enter User Name" 以及 ">", 是的話就將帳號寫入輸出串流, 然後再攔截輸入串流是否出現 "Enter Password" 以及 ">", 是的話就將密碼寫入輸出串流, 這樣就可以登入系統, 可以下達指令了, 例如顯示某資料表內容. 此系統在顯示結束時會以 "BOTTOM" 字串結尾, 因此可用來辨認下達一個指令後, 伺服主機回應的資料是否列印完畢, 達成同步的目的.

之前使用 Javascrip 去呼叫 Putty 做 Telnet 連線取得資料的做法, 因為 Javascript 無法取得輸出入串流, 因此無法與主機同步, 下達指令後還要預估資料要花多少時間才會列印完畢, 然後安插適當的 wait 秒數, 才能下達下一個指令, 否則不同資料表的內容就會前後穿插列印, 導致後續剖析程式分析錯誤. 這種不同步的通訊方式現在看起來還真可笑, 但卻成功運作了兩年之久.



import java.io.*;
import java.net.*;
public class test9_1 {
  static Socket socket;
  static BufferedInputStream bis;
  static BufferedReader r;
  static PrintWriter w;
  public static void main(String[] args) {
    try {
      String host="aa.bbb.cc.dd";  //Telnet 伺服主機
      int port=23;
      String usr="asabulu\n";  //帳號
      String pwd="blabla\n";  //密碼
      if (connect(host,port,usr,pwd)) {System.out.println("connected");}
      else {System.out.println("not connected");}
      socket.close();      
      }
    catch (Exception e) {e.printStackTrace();}
    }
  public static boolean connect(String host,String usr,String pwd) {
    return connect(host,23,usr,pwd);  //預設 port 23
    }
  public static boolean connect(String host,int port,String usr,String pwd) {
    try {
      socket=new Socket(host, port);
      socket.setKeepAlive(true);
      bis=new BufferedInputStream(socket.getInputStream());
      w=new PrintWriter(socket.getOutputStream(),true);
      if (readUntil("Enter User Name")==null) {return false;}
      if (readUntil(">")==null) {return false;}
      Thread.sleep(2000);
      send(usr);
      Thread.sleep(500);
      if (readUntil("Enter Password")==null) {return false;}
      if (readUntil(">")==null) {return false;}
      Thread.sleep(500);
      send(pwd, false);  //不於本機顯示密碼
      Thread.sleep(500);
      if (readUntil(">")==null) {return false;}
      Thread.sleep(2000);
      send("lis table aaa;");
      if (readUntil("BOTTOM")==null) {return false;}
      Thread.sleep(5000);
      return true; // connect OK
      }
    catch (Exception e) {e.printStackTrace();return false;}
    }
  public static void send(String command) {
    send(command, true);  //預設本機顯示應傳送之命令
    }
  public static void send(String command, boolean echo) {  //傳送指令
    try {
      w.println(command);
      w.flush();
      if (echo) {System.out.println(command);}  //顯示所傳送之指令      
      }
    catch (Exception e) {e.printStackTrace();}
    }
  public static String readUntil(String pattern) {
    try {
      char lastChar=pattern.charAt(pattern.length()-1);   //紀錄要攔截的最後一個字元
      StringBuffer sb=new StringBuffer();  //暫存輸入串流讀入之內容
      char ch=(char)bis.read();  //一次讀取一個字元  
      while(true) {   //無限迴圈
        System.out.print(ch);  //顯示讀入之字元
        sb.append(ch);  //讀入之字元存入緩衝器
        if (ch==lastChar && sb.toString().endsWith(pattern)) {
          return sb.toString();  //找到目標字串, 傳回讀取之串流字串 (可用來存檔)
          }    
        ch=(char)bis.read(); /續讀
        }
      }
    catch(Exception e) {e.printStackTrace();}
    return null;
    }
  }

實際測試可成功登入主機並下達顯示資料表之命令. 此處我還沒有處理主機傳回資料的存檔問題, 雖然這是我真正的目的, 但我想把程式改成物件化與多緒執行再來.




2014年5月1日 星期四

Java 的 Telnet 實作

這兩天在看湯秉翰的 "網路程式設計初學指引" (博碩), 第八章是講 TELNET 通訊協定, 看後獲益良多, 這本書是我五本網路程式書籍中寫得最棒的一本, 淺顯易懂, 特別是作者功力深, 光是 TELNET 協定實作方面, 沒有一本書像這本這樣從協定觀念簡介, 到實際用 Java 實作都一五一十介紹的. 以下是閱讀摘要 :
  1. Telnet 協定係主從式架構, 客戶端與伺服端使用 TCP 傳輸層建立連線, 利用 port 23 進行對話, 它只支援文字模式介面. 其規範文件參考 RFC854. 
  2. TELNET 協定內容主要有四 : (1) NVT (2) 命令與資料辨識 (3) 選項 (4) 終端模式
  3. 因為主從兩方控制字元可能不同, 因此 TELNET 採用了 NVT (Network Virtual Terminal) 網路虛擬終端機作為中介. 客戶端與伺服端互傳資料時均須符合 NVT 規定, 以 ASCII 碼傳送資料.
  4. NVT 字元集有兩類 : (1) 命令 (2) 資料, 前者最高位元為 1, 後者為 0. 所有的命令以 255 開頭, 稱為 IAC (Interprete As Command), 當收到 255 時, 表示後續字元都是命令 (控制字元), 否則都屬於資料.
  5. NVT 僅僅是 TELNET 的基本功能, 像伺服端要不要回應 (ECHO), 通知終端機型式或畫面字元寬度等並未在 NVT 中規定, 而是透過跟在 IAC 後面的選項 (Option) 或子溝通選項 (sub-negotiation) 來補強雙方溝通用語. 
  6. TELNET 使用 DO (253), DONT (254), WILL (251), WONT (252) 四個詞彙來溝通選項 :
    DO : 要求對方執行某選項.  或回應認同對方想要執行的選項.
    DONT : 要求對方停止已經在執行之選項, 或回應拒絕對方所要求執行之選項
    WILL : 表示本身將執行某選項, 或回應認可或確認對方所要求執行的選項
    WONT : 拒絕對方的 DO 要求
    所以如果要求對方執行某選項, 對方也答應執行的話, 是 DO-WILL; 對方不同意就是 DO-WONT, 本身想要執行某選項, 對方也認同是 WILL-DO; 對方不認可就是 WILL-DONT 
下面是我從書中範例改寫, 用來測試公司主機用的, 因為不需要中文, 因此拿掉 BIG5 碼轉換. 此程式可以順利連線該主機的 Telnet Server, 經由該範例可以深刻了解應用層協定對話的方式 :

import java.io.*;
import java.net.*;

public class test {
  public static void main(String[] args) {
    String host="localhost";  //這裡用 localhost 代替, 實際使用主機 IP
    int port=23;
    telnet(host,port);
    }
  public static void telnet(String host, int port) {
    Socket socket;
    InputStream in;
    OutputStream out;
    try {
      socket=new Socket(host, port);
      in=socket.getInputStream();
      out=socket.getOutputStream();
      while (true) {
        int data=in.read();
        if (data==255) { //IAC:Interpre As Command (後面是控制指令)
          int tone=in.read();  //第一個 Byte=溝通類型
          int option=in.read();  //第二個 Byte=選項
          switch (tone) {        //檢查伺服端傳來的溝通類型
            case 250 : {          //SB (Sub-negotiation Begin) : 子溝通選項
              switch (option) { //只實作終端機型態
                case 24 : {       //Negotiate terminal type
                  //read remaining 3 bytes from server 後面還有三個 byte 須讀完
                  in.read();      //"1":SEND
                  in.read();      //"255":IAC
                  in.read();      //"240":SE
                  //reply with terminal type="VT100" 回覆伺服器 VT100
                  out.write(255); //IAC
                  out.write(250); //SB
                  out.write(24);  //Terminal type
                  out.write(0);   //"IS"
                  out.write("VT100".getBytes());
                  out.write(255); //IAC
                  out.write(240); //SE
                  out.flush();
                  }
                }
              break;
              }
            case 253 : { //DO (Request or Allow option) 要求我方執行
              if (option==24) { //Allow option : terminal type
                if (tone==253) { //DO
                  out.write(255);     //IAC
                  out.write(251);     //WILL
                  out.write(option);  //Allow option
                  out.flush();
                  }
                else if (tone==251) { //WILL  認可我方要求
                  out.write(255);     //IAC
                  out.write(253);     //DO
                  out.write(option);  //DO option
                  out.flush();
                  }
                }
              break;
              }
            default : { //Deny option 預設 : 拒絕選項
              if (tone==253) {      //DO:reply with WONT
                out.write(255);     //IAC
                out.write(252);     //WONT
                out.write(option);  //rejected option
                out.flush();
                }
              else if (tone==251) { //WILL:reply with DONT
                out.write(255);     //IAC
                out.write(254);     //DONT
                out.write(option);  //rejected option
                out.flush();
                }
              } //end of default
            } //end of switch
          } //end of if
        else { //NOT IAC : display received
          System.out.print((char)data);
          }
        }    
      }
    catch (Exception e){System.out.println(e);}
    }
  }

其實不需要從 RFC854 實做 Telnet, 直接以字元 IO 方式讀取伺服器回應再傳送指令也是可以的, 首先在下列網站找到一個 Telnet 伺服器 :

# http://www.telnet.org/htm/places.htm

挑選了其中的 rainmaker.wunderground.com 這個 TELNET 伺服器作為測試標的, 當以 TELNET 連線此 IP 時, 首先會出現 "Press Return to continue:" 提示文字, 按 ENTER 後出現 "Press Return for menu or enter 3 letter forecast city code--" 回應, 隨便輸入 3 個字元按 ENTER 後, 出現選單. 

我參考了下列兩個網站資料, 改寫為如下之範例 :

# http://letrungthang.blogspot.tw/2011/12/telnet-in-java.html
# http://stackoverflow.com/questions/6399557/java-simple-telnet-client-using-sockets


import java.io.*;
import java.net.*;
public class telnet {
  static Socket socket;
  static BufferedInputStream bis;
  static BufferedReader r;
  static PrintWriter w;
  public static void main(String[] args) {
    try {
      String host="rainmaker.wunderground.com";
      int port=23;
      if (connect(host,port)) {
        System.out.println("connected");
        String prompt1="Press Return to continue:";
        String prompt2="city code-- ";
        if (readUntil(prompt1) != null) {
          send("\n");
          Thread.sleep(500);
          if (readUntil(prompt2) != null) {
            send("TPE\n");
            Thread.sleep(500);
            System.out.println("connected");
            }
          }
        }
      else {System.out.println("not connected");}
      socket.close();      
      }
    catch (Exception e) {e.printStackTrace();}
    }
  public static boolean connect(String host,int port) {
    try {
      socket=new Socket(host, port);
      socket.setKeepAlive(true);
      bis=new BufferedInputStream(socket.getInputStream());
      w=new PrintWriter(socket.getOutputStream(),true);
      return true; // connect OK
      }
    catch (Exception e) {e.printStackTrace();return false;}
    }
  public static void send(String value) {
    try {
      w.println(value);
      w.flush();
      System.out.println(value);
      }
    catch (Exception e) {e.printStackTrace();}
    }
  public static String readUntil(String pattern) {
    try {
      char lastChar=pattern.charAt(pattern.length()-1);
      StringBuffer sb=new StringBuffer();
      int numRead=0;
      char ch=(char)bis.read();    
      while(true) {
        System.out.print(ch);
        numRead++;
        sb.append(ch);
        if (ch==lastChar) {
          if (sb.toString().endsWith(pattern)) {return sb.toString();}
          }              
        if (bis.available()==0){break;}
        ch=(char)bis.read();
        if (numRead > 2000) {break;} // can not read the pattern
        } //end of while
      } //end of if
    catch(Exception e) {e.printStackTrace();}
    return null;
    }
  }



參考資料 :

# 逢甲大學資訊工程學系專題報告: 實作 Telnet Client
# 瞭解 Telnet
# java simple telnet client using sockets
# Telnet client library
# Places to telnet

關於 XSS 攻擊

最近同事貼工作日誌時, 被資安發現是 XSS 攻擊而要求檢討. 事實上只是將公文上的 EMAIL 內容貼上去以利追蹤而已, 可能 mail 中含有 HTML 程式碼的緣故. 所以要研究一下防治之道.

何謂 XSS? 搜尋了一些資料如下 :
  1. 防範Cross-Site-Scripting(XSS)
  2. [ASP.NET]基礎防範XSS攻擊函式
  3. 網站防範XSS攻擊的關鍵思考
  4. 【ASP.NET】跨網站攻擊防範Xss
  5. XSS (Cross-site scripting) 跨網站指令碼攻擊 
  6. XSS (Cross Site Scripting) Prevention Cheat Sheet