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

沒有留言 :