570 likes | 754 Views
課程參與度之評估方式. 上課時必須專心聽講,跟上進度,參與討論 扣分項目 玩線上遊戲一次扣 1 分 玩手機一次扣 1 分 睡覺一次扣 1 分 聊天一次扣 1 分 無法回答老師提出的問題一次扣 1 分 加分項目 主動回答老師的問題一次加 2 分 找出老師程式中的錯誤一次加 1 分 修正老師程式中的錯誤一次加 4 分. 網路程式設計 第十章 TELNET(BBS) 程式設計 -NIO. 鄧姚文. 大綱. 連線並讀取資料 設計回應命令的機制 物件導向設計 TelnetClientNio 類別 Telnet 伺服器. 10-1 連線並讀取資料.
E N D
課程參與度之評估方式 • 上課時必須專心聽講,跟上進度,參與討論 • 扣分項目 • 玩線上遊戲一次扣1分 • 玩手機一次扣1分 • 睡覺一次扣1分 • 聊天一次扣1分 • 無法回答老師提出的問題一次扣1分 • 加分項目 • 主動回答老師的問題一次加2分 • 找出老師程式中的錯誤一次加1分 • 修正老師程式中的錯誤一次加4分
大綱 連線並讀取資料 設計回應命令的機制 物件導向設計 TelnetClientNio 類別 Telnet伺服器
10-1 連線並讀取資料 InetSocketAddressaddr = new InetSocketAddress("localhost", 23); SocketChannelchann = SocketChannel.open(addr); ByteBufferbuf = ByteBuffer.allocate(1024); chann.read(buf); buf.flip(); • 利用InetSocketAddress類別建立物件 • 呼叫SocketChannel的open方法 • 準備緩衝區物件(ByteBuffer) • 資料讀取至緩衝區 • 讀取指標位置移至第一個位置,準備後續循序讀取處理
10-1 連線並讀取資料 while(buf.hasRemaining()){ byte b = buf.get(); int data = b & 0xFF; System.out.print(data+","); } 利用while迴圈循序讀取緩衝區內的位元組,並顯示於主控台上: 上述程式碼中,由於Java的byte資料型態將第一個位元認定為正負辨認位元,因此筆者將得到的位元組轉為整數型態,以便後續能使用它以辨識是否為命令或資料。完整範例程式碼如下:
10-1 連線並讀取資料 01 package com.ch10; 02 03 import java.io.IOException; 04 import java.net.InetSocketAddress; 05 import java.nio.ByteBuffer; 06 import java.nio.channels.SocketChannel; 07 08 public class TelnetTester { 09 public static void main(String[] args) throws IOException { 10 InetSocketAddressaddr = new InetSocketAddress( 11 "localhost", 23); 12 SocketChannelchann = SocketChannel.open(addr); 13 ByteBufferbuf = ByteBuffer.allocate(1024); 14 chann.read(buf); 15 System.out.println("緩衝區有效資料個數:" + buf.position()); 16 buf.flip(); 17 while (buf.hasRemaining()) {
10-1 連線並讀取資料 18 byte b = buf.get(); 19 int data = b & 0xFF; 20 System.out.print(data + ","); 21 } 22 } 23 } ▌執行結果 緩衝區內的有效資料個數:21 255,253,37,255,251,1,255,251,3,255,253,39,255,253,31,255,253,0,255,251,0,
10-1 連線並讀取資料 • 依照第8章的TELNET協定內容說明以及RFC854規範,解讀後可得知伺服器送來的命令為: • IAC DO AUTH:請執行認證選項。 • IAC WILL ECHO:想要執行回音選項。 • IAC WILL SG:想要執行subnegotiation選項。 • IAC DO NEW-ENV:請執行傳送環境變數選項。 • IAC DO NAWS:請執行客戶端畫面資訊選項。 • IAC DO BINARY:請執行二元制資料傳送選項。 • IAC WILL BINARY:想要執行二元制資料傳送選項。
10-2 設計回應命令的機制 • ByteBuffersbuf = ByteBuffer.allocate(1024); • sbuf.put((byte)IAC); • sbuf.put((byte)WONT); • sbuf.put((byte)option); • sbuf.flip(); • chann.write(sbuf); • 準備一個傳送專用的緩衝區 • 緩衝區的put方法,將資料放入緩衝中(放入255,252,37三個byte) • 最後呼叫write方法將資料送出
10-2 設計回應命令的機制 • 回應功能實作 • 設計一個專門處理命令的方法handleCommand,如果傳入的值是255(IAC),即交由該方法處理
01 public static void handleCommand(SocketChannelchann, 02 ByteBufferbuf) throws IOException { 03 ByteBufferoutBuf = ByteBuffer.allocate(1024); 04 int tone = buf.get() & 0xFF; 05 int option = buf.get() & 0xFF; 06 System.out.println(tone + "," + option); 07 outBuf.clear(); 08 if (tone == DO) { 09 outBuf.put((byte) IAC); 10 outBuf.put((byte) WONT); 11 outBuf.put((byte) option); 12 outBuf.flip(); 13 chann.write(outBuf); 14 System.out.println(" SENT:" + IAC + "," + WONT + "," 15 + option);
10-2 設計回應命令的機制 • 回應功能實作 • 設計一個專門處理命令的方法handleCommand,如果傳入的值是255(IAC),即交由該方法處理
16 } else if (tone == WILL) { 17 outBuf.put((byte) IAC); 18 outBuf.put((byte) DONT); 19 outBuf.put((byte) option); 20 outBuf.flip(); 21 chann.write(outBuf); 22 System.out.println(" SENT:" + IAC + "," + DONT + "," 23 + option); 24 } 25 }
10-2 設計回應命令的機制 ▌程式碼說明 ‧第1-2行,定義handleCommand的規格,接收兩個參數,一是SocketChannel物件chann,另一個是緩衝區物件buf。 ‧第3行,預先準備傳送資料專用緩衝區outBuf。 ‧第4行,讀取IAC後面的語氣值,可能是DO或WILL。 ‧第5行,讀取語氣值後面的選項。 ‧第8-15行,當伺服器傳來請求(DO)執行時,回應否定(WONT)該選項。 ‧第16-24行,當伺服器傳來想要(WILL)執行時,回應拒絕(DONT)該選項。 • 回應功能實作
10-2 設計回應命令的機制 • 整合測試 • 最後加上部份顯示資料,目的是讓讀者在執行過程中能看到互動情形,最後完成的初階應用程式TelnetTester類別程式碼如下:
01 package com.ch10; 02 03 import java.io.IOException; 04 import java.net.InetSocketAddress; 05 import java.nio.ByteBuffer; 06 import java.nio.channels.SocketChannel; 07 08 public class TelnetTester { 09 public static finalint IAC = 255; 10 public static finalint WILL = 251; 11 public static finalint WONT = 252;
10-2 設計回應命令的機制 • 12 public static finalint DO = 253; • public static finalint DONT = 254; • 14 public static finalint TERMINAL_TYPE = 24; • 15 public static finalint SB = 250; • 16 public static finalint SE = 240; • 17 • 18 public static void main(String[] args) throws IOException { • 19 InetSocketAddressaddr = new InetSocketAddress( • 20 "localhost", 23); • 21 SocketChannelchann = SocketChannel.open(addr); • 22 ByteBufferbuf = ByteBuffer.allocate(1024); • 23 chann.read(buf); • 24 System.out.println("緩衝區有效資料個數:" + buf.position()); • 25 buf.flip(); • 26 while (buf.hasRemaining()) { • 27 byte b = buf.get(); • 28 int data = b & 0xFF; • 29 if (data == IAC) { • 30 System.out.println();
10-2 設計回應命令的機制 31 System.out.print(data + ","); 32 handleCommand(chann, buf); 33 } else { 34 System.out.print((char) data); 35 } 36 } 37 } 38 39 public static void handleCommand(SocketChannelchann, 40 ByteBufferbuf) throws IOException { 41 ByteBufferoutBuf = ByteBuffer.allocate(1024); 42 int tone = buf.get() & 0xFF; 43 int option = buf.get() & 0xFF; 44 System.out.println(tone + "," + option); 45 outBuf.clear(); 46 if (tone == DO) { 47 outBuf.put((byte) IAC); 48 outBuf.put((byte) WONT);
10-2 設計回應命令的機制 49 outBuf.put((byte) option); 50 outBuf.flip(); 51 chann.write(outBuf); 52 System.out.println(" SENT:" + IAC + "," + WONT + "," 53 + option); 54 } else if (tone == WILL) { 55 outBuf.put((byte) IAC); 56 outBuf.put((byte) DONT); 57 outBuf.put((byte) option); 58 outBuf.flip(); 59 chann.write(outBuf); 60 System.out.println(" SENT:" + IAC + "," + DONT + "," 61 + option); 62 } 63 } 64 }
10-2 設計回應命令的機制 ▌執行結果 緩衝區有效資料個數:21 255,253,37 SENT:255,252,37 255,251,1 SENT:255,254,1 255,251,3 SENT:255,254,3 255,253,39 SENT:255,252,39 255,253,31 SENT:255,252,31 255,253,0 SENT:255,252,0 255,251,0 SENT:255,254,0
10-3 物件導向設計TelnetClientNio類別 TelnetClientNio client = new TelnetClientNio("localhost", 23); client.connect(); • 類別規劃 • 設計類別時可先以未來該類別的使用者的觀點開始,如果是個可重覆使用的類別,應該可以用以下方式產生,並進行連線功能:
10-3 物件導向設計TelnetClientNio類別 public class TelnetClientNio extends Thread{ ... } String host; int port; SocketChannel channel; public TelnetClientNio(String host, int port) { this.host = host; this.port = port; } • 屬性與建構子 • TelnetClientNio類別繼承了java.lang.Thread,目的是為取得使用者在鍵盤輸入的資料: • 屬性 • 建構子
10-3 物件導向設計TelnetClientNio類別 • 連線並讀取資料-connect方法 • 呼叫後產生SocketChannel物件 • 以while迴圈不斷地讀取伺服器傳來的資料(包括命令) • 判斷傳來的資料是命令時則交由handleCommand方法處理 的資料是命令時 • 若為一般資料則將資料轉為字元並顯示於主控台
10-3 物件導向設計TelnetClientNio類別 • 連線並讀取資料-connect方法 • 利用上節TelnetTester的程式碼後實作的connect()如下:
01 public void connect() { 02 try { 03 InetSocketAddressaddr = new InetSocketAddress( 04 "localhost", 23); 05 channel = SocketChannel.open(addr); 06 start(); 07 ByteBufferbuf = ByteBuffer.allocate(1024); 08 while (true) { 09 buf.clear(); 10 channel.read(buf); 11 System.out.println("(緩衝區有效資料個數:" 12 + buf.position() + ")"); 13 if (buf.position() == 0)
10-3 物件導向設計TelnetClientNio類別 14 break; 15 buf.flip(); 16 while (buf.hasRemaining()) { 17 byte b = buf.get(); 18 int data = b & 0xFF; // 將byte轉為整數 19 if (data == IAC) { 20 handleCommand(buf); 21 } else { 22 System.out.print((char) data); 23 } 24 } 25 } 26 channel.close(); 27 System.exit(0); 28 } catch (IOException e) { 29 e.printStackTrace(); 30 } 31 }
10-3 物件導向設計TelnetClientNio類別 ▌程式碼說明 ‧第3-4行,產生InetSocketAddress物件addr,目的地是本機localhost的23埠。 ‧第5行,由SocketChannel的靜態方法open取得連線通道物件channel。 ‧第6行,啟動本執行緒,也就是執行run()方法,目的是持續接收使用者在鍵盤上輸入的資料。 ‧第7行,準備緩衝區物件buf,大小為1024個位元組。 ‧第8-25行,以while迴圈持續讀取伺服器端所傳來的資料,並進行命令判斷。 ‧第9行,將緩衝區歸零。 ‧第10行,讀取伺服器端傳來資料,並將資料放置於buf,若資料送達時才執行下一行。 ‧第11-12行,印出讀取到的位元組數量。 ‧第15行,將緩衝區讀取指標移動至第0個位置,等待後續處理。 ‧第16-23行,以while迴圈將緩衝區的位元組一一讀出判斷是否為命令(IAC)。 ‧第19-20行,若資料為IAC,代表後續仍有與命令相關的資料,呼叫專門處理命令的handleCommand方法。 ‧第21-22行,若為一般資料,則轉成字元char後印出。
10-3 物件導向設計TelnetClientNio類別 • 辨識命令與回應-handleCommand方法 • 當伺服器傳來命令時,全權由handleCommand方法處理 • 解讀IAC、DO/WILL語法與其後的選項(Option)
10-3 物件導向設計TelnetClientNio類別 辨識命令與回應-handleCommand方法
01 public void handleCommand(ByteBufferbuf) throws IOException { 02 ByteBufferoutBuf = ByteBuffer.allocate(1024); 03 int tone = buf.get() & 0xFF; 04 int option = buf.get() & 0xFF; 05 outBuf.clear(); 06 if (tone == DO) { 07 outBuf.put((byte) IAC); 08 outBuf.put((byte) WONT); 09 outBuf.put((byte) option); 10 outBuf.flip(); 11 channel.write(outBuf); 12 } else if (tone == WILL) { 13 outBuf.put((byte) IAC); 14 outBuf.put((byte) DONT); 15 outBuf.put((byte) option); 16 outBuf.flip(); 17 channel.write(outBuf); 18 } 19 }
▌程式碼說明 ‧第1行,handleCommand方法的定義,接送參數為緩衝區buf,此方法可能會拋IOException。 ‧第2行,準備送出資料專用的緩衝區物件outBuf。 ‧第3行,讀取下一個位元組,定義為語意(DO或WILL)tone。 ‧第4行,讀取下一個位元組,定義為選項option。 ‧第5行,將outBuf緩衝區初始化。 ‧第6-11行,當伺服器來是DO 選項時,一律先回絕伺服器的要求。 ‧第12-17行,當伺服器來是WILL 選項時,一律先否定伺服器的請求。
10-3 物件導向設計TelnetClientNio類別 • 接收使用者鍵盤輸入-run方法 • 執行時可以送出鍵盤上輸入的資料至伺服器 • 繼承了Thread,以執行緒方式設計 • 覆寫了Thread類別的方法run(),在run方法中利用while迴圈持續讀取鍵盤上輸入的資料
10-3 物件導向設計TelnetClientNio類別 接收使用者鍵盤輸入-run方法
01 public void run() { 02 ByteBufferoutBuf = ByteBuffer.allocate(1024); 03 BufferedReader in = new BufferedReader( 04 new InputStreamReader(System.in)); 05 while (true) { 06 try { 07 outBuf.clear(); 08 String line = in.readLine(); 09 line = line + "\n\r"; 10 outBuf.put(line.getBytes()); 11 outBuf.flip(); 12 channel.write(outBuf); 13 } catch (IOException e) { 14 e.printStackTrace(); 15 } 16 } 17 }
▌程式碼說明 ‧第1行,run是覆寫本類別繼承Thread的方法,當本執行緒被執行時(呼叫start()),會執行本方法。 ‧第2行,準備送出資料專用的緩衝區物件outBuf。 ‧第3-4行,與鍵盤建立輸入串流BufferedReader物件in。 ‧第5-16行,以while迴圈持續讀取使用者在鍵盤上輸入的資料。 ‧第7行,清理outBuf緩衝區。 y第8行,當使用者按下Enter鍵時,自鍵盤讀取該字串。 ‧第9行,為該字串最後加上跳行字元"\r\n"。 ‧第10-11行,將字串的位元組放入緩衝區物件outBuf。 ‧第12行,利用通道channel的write方法送出緩衝區資料至伺服器。
10-3 物件導向設計TelnetClientNio類別 • 實際執行與測試 • 啟動Windows內建的Telnet伺服器後可進行簡單的測試,執行TelnetClientNio後將連線至localhost本機的23埠,讀取伺服器傳來資料並將使用者輸入資料送至伺服器,執行過程如下:
(緩衝區有效資料個數:21) (緩衝區有效資料個數:38) Welcome to Microsoft Telnet Service (緩衝區有效資料個數:9) 伺服器傳來的歡迎訊息 由使用者輸入帳號tom login: tom (緩衝區有效資料個數:25) tom password: aaa123 伺服器傳來剛由用戶端傳送的資料tom 由使用者輸入的密碼
10-4Telnet伺服器 • 具有帳號登入與驗證帳號的TELNET伺服器 • 使用者帳戶資料,包括了帳號與密碼兩個資訊 • 使用者連線至伺服器時必需輸入帳號與密碼 • 當登入成功後顯示歡迎訊息
10-4Telnet伺服器 • 類別概況設計 • TelnetServerNio負責傾聽9980埠號並等待連線 • 「ClientHandler」服務每個連上線的用戶
10-4Telnet伺服器 • TelnetServerNio類別 • 負責傾聽埠號並接收用戶端的連線 • 使用while迴圈傾聽特定埠號 • 當用戶端連線並進入可寫出資料狀態時,即利用ClientHandler類別專門處理用戶端的資料處理 • TelnetServerNio類別就如同醫院的掛號台,當有病患上門時安排他至醫生處看診
10-4Telnet伺服器 01 package com.ch10; 02 03 import java.io.IOException; 04 import java.net.InetSocketAddress; 05 import java.net.ServerSocket; 06 import java.nio.channels.ClosedChannelException; 07 import java.nio.channels.SelectionKey; 08 import java.nio.channels.Selector; 09 import java.nio.channels.ServerSocketChannel; 10 import java.nio.channels.SocketChannel; 11 import java.util.Iterator; 12 import java.util.Set; 13 14 public class TelnetServerNio { 15 int port = 9980; 16 17 public TelnetServerNio(int port) { TelnetServerNio類別
10-4Telnet伺服器 18 this.port = port; 19 } 20 21 public void accept() { 22 try { 23 ServerSocketChannelserverChannel = ServerSocketChannel 24 .open(); 25 ServerSocketss = serverChannel.socket(); 26 ss.bind(new InetSocketAddress(port)); 27 serverChannel.configureBlocking(false); 28 Selector selector = Selector.open(); 29 serverChannel.register(selector, SelectionKey.OP_ACCEPT); 30 while (true) { 31 selector.select(); 32 Set keys = selector.selectedKeys(); 33 Iterator it = keys.iterator(); 34 int count = 0; 35 while (it.hasNext()) { 36 SelectionKey key = (SelectionKey) it.next();
10-4Telnet伺服器 37 it.remove(); 38 if (key.isAcceptable()) { 39 System.out.println("用戶連上線"); 40 ServerSocketChannel server = 41 (ServerSocketChannel) key.channel(); 42 SocketChannel client = server.accept(); 43 client.configureBlocking(false); 44 SelectionKeyclientKey = client.register(selector, 45 electionKey.OP_READ); 46 //指派ClientHandler物件,專門處理用戶端送達的資料 47 ClientHandler handler = 48 new ClientHandler(clientKey); 49 handler.welcomeMessage(); 50 // 將ClientHandler物件附加於clientKey 51 clientKey.attach(handler); 52 } else if (key.isReadable()) { 53 // 由key鍵值中取出附加物件,並轉型為ClientHandler 54 ClientHandler handler = 55 (ClientHandler) key.attachment();
10-4Telnet伺服器 56 // 處理用戶端送達的資料 57 handler.handleRead(key); 58 } 59 } 60 } 61 } catch (ClosedChannelException e) { 62 e.printStackTrace(); 63 } catch (IOException e) { 64 e.printStackTrace(); 65 } 66 } 67 68 public static void main(String[] args) { 69 new TelnetServerNio(9980).accept(); 70 } 71 }
10-4Telnet伺服器 ▌程式碼說明 ‧第21-66行,accept()方法,呼叫此方法後,伺服器將持續傾聽9980埠,若用戶端連線成功,則為該用戶端產生一個專屬協定處理物件ClientHandler,針對用戶端傳輸資料進行解讀與服務。 ‧第38-51行,若用戶端連線則為其準備協定處理物件。 ‧第47-48行,產生一個ClientHandler物件。 ‧第49行,呼叫ClientHandler的welcomeMessage()方法,傳送歡迎訊息給用戶端,並提示用戶端進行帳號登入,如下圖: Welcome login: ‧第51行,將用戶協定處理物件附掛於key鍵中。 ‧第52-58行,若用戶端傳來資料(已連線完成),則由ClientHandler的handleRead方法處理資料。 ‧第54-55行,取出key鍵中附掛的ClientHandler物件。 ‧第57行,由ClientHandler物件負責解讀用戶端傳來的資料。
10-4Telnet伺服器 • ClientHandler用戶協定處理類別 • 專門的協定處理類別 • 專注於用戶端傳入資料的事件(OP_READ) • 幾個重點方法說明 • initAccount():用來測試後續用戶端傳送帳號資料 • welcomeMessage():送出歡迎訊息 • handleCommand(ByteBuffer buf):當用戶端傳來資料為TELNET指令(IAC)時,解析指令並回應否定選項的方法 • handleRead(SelectionKey key):本類別最重要的方法,專門負責辨別與處理用戶端傳來資料
10-4Telnet伺服器 01 package com.ch10; 02 03 import java.io.IOException; 04 import java.nio.ByteBuffer; 05 import java.nio.channels.SelectionKey; 06 import java.nio.channels.SocketChannel; 07 import java.util.Map; 08 import java.util.TreeMap; 09 10 public class ClientHandler { 11 private Map<String, String> users; 12 SocketChannel channel; 13 ByteBuffer buff = ByteBuffer.allocate(100); 14 public static finalint IAC = 255; 15 public static finalint WILL = 251; 16 public static finalint WONT = 252; • ClientHandler用戶協定處理類別 • 用戶協定處埋ClientHandler類別的整體程式碼如下:
10-4Telnet伺服器 17 public static finalint DO = 253; 18 public static finalint DONT = 254; 19 public static finalint STAGE_USER = 0; 20 public static finalint STAGE_PASSWORD = 1; 21 public static finalint STAGE_LOGON = 2; 22 int stage = STAGE_USER; 23 String userid = ""; 24 String pw = ""; 25 26 public ClientHandler(SelectionKey key) { 27 channel = (SocketChannel) key.channel(); 28 initAccount(); 29 } 30 31 public void handleRead(SelectionKey key) { 32 try { 33 int count = channel.read(buff); 34 if (count > 0) { 35 buff.flip();
10-4Telnet伺服器 36 while (buff.position() < count) { 37 int data = buff.get() & 0xFF; 38 if (data == IAC) { 39 handleCommand(buff); 40 } else { 41 switch (stage) { 42 case STAGE_USER: 43 if (data == 10) { 44 send("\n\rpassword:"); 45 stage = STAGE_PASSWORD; 46 userid = userid.substring(0, 47 userid.length() - 1); 48 System.out.println("id="+userid); 49 } else { 50 userid = userid + (char) data; 51 } 52 break; 53 case STAGE_PASSWORD: 54 if (data == 10) {
10-4Telnet伺服器 55 pw =pw.substring(0,pw.length()-1); 56 System.out.println("pw=" + pw); 57 if (users.get(userid).equals(pw)){ 58 send("\n\rLogin successful"); 59 stage = STAGE_LOGON; 60 } else { 61 send("\n\rLogin failed"); 62 channel.close(); 63 } 64 } else { 65 pw = pw + (char) data; 66 } 67 break; 68 } 69 } 70 } 71 buff.clear(); 72 } 73 } catch (IOException e) {