วันศุกร์ที่ 10 กรกฎาคม พ.ศ. 2563

ปัญหาเบสคลาสแตกง่าย (Fragile Base Class Problem)

หลังจากไม่ได้อัพเดตบล็อกนี้มานานมาก ทั้ง ๆ ที่ควรจะทำ เพราะเป็นการขยายเรื่องที่สอนในห้องมาให้นักศึกษาอ่านทบทวนกัน เอาเป็นว่าตั้งแต่นี้จะเขียนให้มากขึ้น แล้วกันนะครับ

วันนี้มาเริ่มด้วยเรื่องทางด้าน object-oriented development ที่ผมได้สอนให้นักศึกษาฟังในวิชา  Component-Based Software Development มาหลายปีแล้ว ซึ่งเรื่องนี้เกี่ยวข้องกับคุณสมบัติที่ทำให้ object-oriented development เป็นที่นิยมก็คือคุณสมบัติ  inheritance และ polymorphism นั่นเอง 

โดยคุณสมบัตินี้ถ้าใช้อย่างไม่ระวังมันก็จะเกิดปัญหากับการพัฒนาโปรแกรมได้ และปัญหาหนึ่งที่มากับแนวคิดนี้ก็คือ ปัญหาที่ทำให้คลาสมีความขึ้นต่อกัน ซึ่งก็เป็นที่มาของปัญหา เบสคลาสแตกง่าย  (Fragile Base Class Problem) ซึ่งผมจะขอเรียกสั้น ๆ ว่า FBC

ปัญหา FBC เป็นปัญหาที่เกิดจากคำถามว่า  ถ้ามีการปรับปรุงเบสคลาส ซึ่งจะขอเรียกว่าคลาสแม่ แล้วจำเป็นต้องปรับปรุงดีไรว์คลาส (derived class) ซึ่งจะขอเรียกว่าคลาสลูก ของมันด้วยหรือไม่ ยกตัวอย่างเช่น สมมติ คลาส A เป็นคลาสแม่ มีคลาส B เป็นคลาสลูก และมี คลาส C เป็นลูกของ B อีกทีหนึ่ง สมมติคลาส C เรียกใช้เมท็อดที่อยู่ในคลาส B คราวนี้ลองสมมติต่อว่าถ้าในเวอร์ชันใหม่ของคลาสA กับ B มีการย้ายเมท็อดที่ C เรีกใช้จากคลาส B ขึ้นไปอยู่ในคลาส A  แน่นอนว่าคลาส A กับ B ต้องคอมไพล์ใหม่ แต่เราควรต้องมาคอมไพล์คลาส C ด้วยหรือไม่ ซึ่งคำตอบของคำถามนี้คือไม่ควร แต่ในภาษาเขียนโปรแกรมบางภาษาเราจำเป็นต้องคอมไพล์คลาส C ด้วย 

ถ้าปัญหาเป็นแบบนี้ เราอาจจะคิดว่าไม่เห็นจะเป็นอะไร ก็แค่คอมไพล์ใหม่ แต่จริง ๆ แล้วมันก็เสียเวลานะครับ ถ้าเป็นโปรแกรมใหญ่ แต่โอเคถ้ามองในแง่แค่คอมไพล์ใหม่ไม่เห็นเป็นอะไร ถ้าอย่างนั้นมามองดูสิ่งที่อาจเกิดขึ้นได้อีกแบบหนึ่ง จะเกิดอะไรขึ้นถ้าเราปรับปรุงคลาสแม่แล้วมีผลทำให้คลาสลูกทำงานเปลี่ยนไปจากเดิม โดยที่เราไม่ได้แก้โค้ดของคลาสลูกเลยแม้แต่บรรทัดเดียว ลองคิดดูนะครับถ้าเหตุการณ์นี้เกิดขึ้นมันจะยุ่งแค่ไหน เราอาจต้องไปไล่ดูโค้ดทั้งตัวคลาสแม่และคลาสลูก ซึ่งก็อาจใช้เวลามาก หรือจะเกิดอะไรขึ้น ถ้าเราไม่มีโค้ดของคลาสแม่เช่นเราอาจซื้อไลบรารีของคลาสแม่มา แล้วเรามาสร้างคลาสลูกขึ้นมาใช้งานเอง พอบริษัทที่เราซื้อคลาสแม่มาอัพเดตเวอร์ชันของคลาสแม่แล้วให้เราโหลดมาใช้ แต่ปรากฏว่าคลาสลูกของเราทำงานผิด

คงเข้าใจปัญหานี้กันแล้วนะครับ โดยสรุปแล้วปัญหา FBC แบ่งเป็น 2 ประเภทคือ
  • Syntatic FBC หรือเรียกอีกอย่างว่า Fragile Binary Interface Problem
  • Sematic FBC  
โดย Syntactic ก็คือไวยากรณ์ ตัวอย่างของคลาส A B C เป็นตัวอย่างของกลุ่มนี้ ส่วน Semantic ก็คือความหมาย ปัญหาของการแก้ไขโค้ดในคลาสแม่แล้ว ทำให้พฤติกรรมของคลาสลูก เปลี่ยนไปเป็นปัญหาในกลุ่มนี้ 

ถึงตรงนี้คงเห็นแล้วนะครับว่าปัญหา FBC แสดงให้เห็นว่าการใช้งาน inheritance และ polymorphism มีปัญหาคือทำให้คลาสขึ้นแก่กัน และจะเป็นปัญหาในการดูแล และบำรุงรักษาซอฟต์แวร์ซึ่งมีขนาดใหญ่ และนี่เป็นเหตุผลหลักเหตุผลหนึ่งที่ทำให้การพัฒนาโปรแกรมแบบคอมโพเนนต์หลีกเลี่ยงที่จะใช้ inheritance กับ polymorphism และมาใช้การประกอบ (compose) เป็นหลัก

สำหรับบล็อกนี้ขอจบแค่นี้นะครับ บล็อกต่อ ๆ ไปจะมาแสดงตัวอย่างของ Syntactic และ Semantic เพื่อให้เห็นภาพชัดเจนขึ้นครับ

วันศุกร์ที่ 1 กุมภาพันธ์ พ.ศ. 2562

การเขียนโปรแกรมเครือข่ายด้วยซ็อกเก็ต (Socket) ตอนที่ 10: ทดสอบการทำงานของ UDP Server & Client

หลังจากที่เราได้เขียนโปรแกรมทั้งฝั่งเซิร์ฟเวอร์และฝั่งไคลเอนต์เสร็จแล้ว ก็ได้เวลาทดสอบโปรแกรมของเรากันสักทีนะครับ การรันโปรแกรมก็ใช้วิธีเหมือนกับตอนที่เรารันโปรแกรมในเวอร์ชันทีซีพี (TCP) ครับ เริ่มจากรันเซิฟร์เวอร์ด้วยคำสั่ง start java UDPServer ซึ่งจะได้ผลลัพ์ดังนี้



 จากนั้นรันไคลเอนต์ด้วยคำสั่ง java UDPClient จะได้ผลัพธ์ดังนี้



เพื่อให้เห็นภาพว่ายูดีพีนั้นมีการทำงานแบบไม่มีการสร้างการเชื่อมต่อ ให้ทดลองโดยจบการทำงานของเซิฟร์เวอร์ จากนั้นลองรันไคลเอนต์ ซึ่งก็จะได้ผลลัพธ์ดังนี้ 



จะเห็นว่าโปรแกรมไคลเอนต์จะทำงานโดยให้ผู้ใช้ป้อนข้อมูลเข้าไป จากนั้นรอการตอบสนองจากเซิร์ฟเวอร์ ทั้ง ๆ ที่เซิร์ฟเวอร์ยังไม่ได้ทำงาน ซึ่งเหตุการณ์นี้จะไม่เกิดขึ้นกับโปรแกรมที่ใช้ทีซีพี เพราะถ้าโปรแกรมสร้างการติดต่อกับเซิร์ฟเวอร์ไม่ได้ จะแจ้งข้อผิดพลาดให้ทราบทันที 

จะเกิดอะไรขึ้นถ้ารันโปรแกรมเซิร์ฟเวอร์หลังจากที่มีไคลเอนต์นี้รอรับบริการอยู่ ลองมาดูกันครับ


จะเห็นว่าตอนนี้โปรแกรมเซิร์ฟเวอร์รันแล้ว แต่มันก็ไม่ได้ไปให้บริการตัวไคลเอนต์ที่รอการบริการอยู่ (หน้าต่างสีดำหลังสุด) แต่ถ้าลองรันไคลเอนต์ตัวใหม่ คราวนี้ตัวเซิร์ฟเวอร์จะให้บริการได้ดังรูปถัดไป 



จะเห็นว่าไคลเอนต์จะไม่ได้รับบริการ ถ้ามันติดต่อเข้ามาก่อนที่โปรแกรมเซิร์ฟเวอร์จะทำงาน 

ก็หวังว่าจะเข้าใจเรื่องยูดีพีซ็อกเก็ตกันมากขึ้นนะครับ สำหรับใครที่ยังไม่เข้าใจว่าจะรันโปรแกรมยังไง หรือยังไม่เข้าใจสิ่งที่อธิบายไปแล้ว สามารถดูได้จากวีดีโอต่อไปนี้ครับ










การเขียนโปรแกรมเครือข่ายด้วยซ็อกเก็ต (Socket) ตอนที่ 9: UDP Client ด้วยภาษาจาวา (Java)

หลังจากสร้างโปรแกรมเซิร์ฟเวอร์ไปแล้วจากตอนที่ 8 ตอนนี้มาดูโปรแกรมฝั่งไคลเอนต์กันครับ มาดูโค้ดกันเลยครับ


  1. import java.io.*; 
  2. import java.net.*;
  3. import java.util.*; 
  4. class UDPClient { 
  5.    public static void main(String args[]) throws Exception { 
  6.       Scanner inFromUser = new Scanner(System.in);
  7.       DatagramSocket clientSocket = new DatagramSocket(); 
  8.       InetAddress IPAddress = InetAddress.getByName("localhost"); 
  9.       byte[] sendData = new byte[1024]; 
  10.       byte[] receiveData = new byte[1024]; 
  11.       System.out.print("Please enter words: ");
  12.       String sentence = inFromUser.nextLine();
  13.       sendData = sentence.getBytes();  
  14.       DatagramPacket sendPacket = 
  15.          new DatagramPacket(sendData, sendData.length, IPAddress, 9876); 
  16.       clientSocket.send(sendPacket); 
  17.       DatagramPacket receivePacket = 
  18.          new DatagramPacket(receiveData, receiveData.length); 
  19.       clientSocket.receive(receivePacket); 
  20.       String modifiedSentence = new String(receivePacket.getData()); 
  21.       System.out.println("FROM SERVER:" + modifiedSentence.trim()); 
  22.    clientSocket.close(); 
  23.    } 

บรรทัดที่ 7 สร้างซ็อกเก็ตเพื่อใช้ในการแลกเปลี่ยนข้อมูลกับเซิร์ฟเวอร์ บรรทัดที่ 8 แปลงชื่อโดเมนของเซิร์ฟเวอร์เป็นหมายเลขไอพี ในที่นี้เรารันโปรแกรมไคลเอนต์และเซิร์ฟเวอร์บนเครื่องเดียวกันชื่อโดเมนจึงเป็น localhost บรรทัดที่ 14-15 เตรียมแพ็กเก็ตที่ใช้ส่งข้อมูล โดยนอกจากข้อมูลที่จะส่งไปแล้ว ก็ต้องระบุหมายเลขไอพี และหมายเลขพอร์ตของโปรแกรมเซิร์ฟเวอร์ และถึงแม้เราจะไม่ได้ระบุหมายเลขไอพี และหมายเลขพอร์ตของไคลเอนต์ที่เป็นผู้ส่ง ข้อมูลนี้จะถูกใส่ลงไปในแพ็กเก็ตโดยอัตโนมัติ บรรทัดที่ 19 ส่งข้อมูลผ่านซ็อกเก็ตไปให้เซิร์ฟเวอร์ บรรทัดที่ 20 รอรับข้อมูลที่เซิร์ฟเวอร์จะตอบกลับมา

โดยโค้ดโปรแกรมสามารถดาวน์โหลดได้จากลิงก์นี้ ครับ ส่วนถ้าใครที่โหลดโค้ดจาก gitlab ไปแล้ว ก็ไม่ต้องโหลดซ้ำแล้วนะครับ เพราะคุณจะได้โค้ดตัวไคลเอนต์ไปเรียบร้อยแล้ว

การเขียนโปรแกรมเครือข่ายด้วยซ็อกเก็ต (Socket) ตอนที่ 8: UDP Server ด้วยภาษาจาวา (Java)

จากตอนที่ 7  ก็คงเข้าใจความแตกต่างระหว่างทีซีพี (TCP) กับยูดีพี (UDP) กันแล้วนะครับ คราวนี้เราจะมาเขียนโปรแกรมฝั่งเซิร์ฟเวอร์กัน โดยโปรแกรมเซิร์ฟเวอร์นี้จะทำงานเหมือน TCPServer คือรับข้อมูลสตริงจากไคลเอนต์ เอามาแปลงเป็นตัวอักษรตัวใหญ่ แล้วส่งกลับไปให้ไคลเอนต์ มาดูโค้ดกันเลยครับ


  1. import java.io.*; 
  2. import java.net.*; 
  3. class UDPServer { 
  4.   public static void main(String args[]) throws Exception 
  5.     { 
  6.     DatagramSocket serverSocket = new DatagramSocket(9876); 
  7.       byte[] receiveData = new byte[1024]; 
  8.       byte[] sendData  = new byte[1024];
  9.       while(true) 
  10.       { 
  11.             System.out.println("The server is waiting ");
  12.             DatagramPacket receivePacket = 
  13.              new DatagramPacket(receiveData, receiveData.length); 
  14.             serverSocket.receive(receivePacket);
  15.             String sentence = new String(receivePacket.getData());
  16.             InetAddress IPAddress = receivePacket.getAddress(); 
  17.             int port = receivePacket.getPort(); 
  18.             String capitalizedSentence = sentence.toUpperCase();
  19.             sendData = capitalizedSentence.getBytes(); 
  20.             DatagramPacket sendPacket = 
  21.                   new DatagramPacket(sendData, sendData.length, IPAddress, port); 
  22.               serverSocket.send(sendPacket); 
  23.         } 
  24.     } 
  25. }  
การสร้างซ็อกเก็ตจะใช้คลาส DatagramSocket ตามที่เขียนไว้ในบรรทัดที่ 6 ในที่นี้โปรแกรมเซิร์ฟเวอร์จะรอรับการติดต่ออยู่ที่พอร์ตหมายเลข 9876  บรรทัดที่ึ 7 และ 8 จะสร้างบัฟเฟอร์สำหรับรับและส่งข้อมูลตามลำดับ โดยในโปรแกรมนี้ระบุขนาดของการรับส่งข้อมูลไว้ที่ไม่เกิน 1024 ไบต์ บรรทัดที่ 12-13 สร้างแพ็กเก็ตรับข้อมูล โดยระบุว่าให้เอาข้อมูลมาเก็บไว้ในบัฟเฟอร์คือ receiveData ด้วยความยาวตามขนาดของบัฟเฟอร์ที่ประกาศไว้ ในตัวอย่างนี้คือ 1024 ไบต์ บรรทัดที่ 4 รออ่านข้อมูลที่ไคลเอนต์จะส่งมา โดยข้อมูลจะถูกเก็บลงแพ็กเก็ตที่ประกาศไว้ในบรรทัดที่ 12 

บรรทัดที่ 15-17 ดึงข้อมูลออกมาจากแพ็กเก็ต โดยบรรทัดที่ 15 เป็นการดึงตัวข้อมูลคือ บรรทัดที่ 16 และ 17 ดึงไอพีแอสเดรสและหมายเลขพอร์ตของไคลเอนต์ที่ส่งข้อมูลมาตามลำดับ สองบรรทัดนี้มีความจำเป็นเพราะการใช้โปรโตคอลยูดีพีไม่มีการสร้างการติดต่อไว้ก่อน ข้อมูลของผู้ส่งอย่างไอพีแอสเดรสและหมายเลขพอร์ตจะถูกส่งมาเป็นส่วนหนึ่งของแพกเก็ตด้วย

บรรทัดที่ 20 สร้างแพ็กเก็ตสำหรับส่งข้อมูลกลับ โดยระบุว่าจะส่งกลับไปที่ไหนโดยใช้ค่าหมายเลขไอพี และหมายเลขพอร์ตที่ดึงมาจากบรรทัดที่ 16 และ 17 ข้อสังเกตก็คือถึงแม้เราจะไม่ได้ระบุหมายเลขไอพีและหมายเลขพอร์ตของเซิร์ฟเวอร์ ซึ่งในจุดนี้คือผู้ส่ง แต่ข้อมูลทั้งสองนี้จะถูกใส่เข้าไปในแพ็กเก็ตโดยอัตโนมัติ บรรทัดที่ 22 เป็นการส่งแพ็กเก็ตผ่านซ็อกเก็ตกลับไปให้ไคลเอนต์ 

สำหรับโค้ดของโปรแกรมนี้สามารถดาวน์โหลดได้ที่ลิงก์นี้ครับ  หรือจาก gitlab ครับ ซึ่งจะได้ทั้งโปรแกรมเซิร์ฟเวอร์และไคลเอนต์ 

การเขียนโปรแกรมเครือข่ายด้วยซ็อกเก็ต (Socket) ตอนที่ 7: โปรโตคอลทีซีพี (TCP) กับ ยูดีพี (UDP)

กลับมาอีกครั้งหลังจากหยุดไปนาน จากบทความที่ผ่านมาผมได้แนะนำการเขียนโปรแกรมเครือข่ายโดยใช้ซ็อกเก็ตซึ่งทำงานกับโปรโตคอลทีซีพี (TCP) แต่จริง ๆ แล้วโปรโตคอลบนระดับชั้นทรานสปอร์ต (transport layer) ยังมีโปรโตคอลอีกตัวหนึ่งคือยูดีพี (UDP) ซึ่งเราก็สามารถเขียนโปรแกรมซ็อกเก็ตที่ทำงานกับโปรโตคอลยูดีพีได้ด้วย แต่ก่อนจะไปเขียนโปรแกรมกันในบทความนี้จะขอพูดถึงความแตกต่างระหว่างโปรโตคอลทั้งสองตัวอย่างคร่าว ๆให้ฟังกันก่อนนะครับ

เริ่มจากโปรโตคอลทีซีพีก่อนแล้วกันนะครับ โปรโตคอลนี้เป็นโปรโตคอลที่เรียกว่าเป็นแบบต้องมีการเชื่อมต่อ (connection-oriented) นั่นคือก่อนที่โปรแกรมจะพูดคุยกันได้ โปรแกรมที่จะพูดคุยกันต้องสร้างการสื่อสารระหว่างกันให้ได้เสียก่อน เปรียบเหมือนการพูดคุยกันผ่านทางโทรศัพท์ ซึ่งต้องมีการโทรหากันให้ติดและอีกฝ่ายรับโทรศัพท์ก่อนจึงจะพูดคุยกันได้ อีกประการหนึ่งคือโปรโตคอลทีซีพีเป็นโปรโตคอลที่มีความน่าเชื่อถือในการสื่อสาร กล่าวโดยคร่าว ๆ คือมันจะช่วยดูแลให้การรับส่งข้อมูลระหว่างโปรแกรมที่กำลังติดต่อกันเป็นได้อย่างถูกต้อง ซึ่งรายละเอียดว่ามันทำอย่างไรนั้นก็ขอให้ไปอ่านเพิ่มเติมเอานะครับในเรื่องการสื่อสารข้อมูลและระบบเครือข่าย 

ส่วนยูดีพีนั้นเป็นโปรโตคอลแบบที่ไม่ต้องมีการเชื่อมต่อก่อน (conectionless) คือก่อนที่โปรแกรมทั้งสองฝ่ายจะพูดคุยกันไม่จำเป็นต้องสร้างการเชื่อมต่อกันให้ได้ก่อนเหมือนกับที่ทีซีพีต้องทำ ถ้าจะเปรียบการสื่อสารประเภทนี้กับชีวิตประจำวันของเราก็คือการส่งจดหมายทางไปรษณีย์ครับ ถ้าคุณจะส่งจดหมายถึงเพื่อนคุณ คุณก็ลงมือเขียนได้เลยและเอาไปหยอดตู้ไปรษณีย์โดยคุณไม่ต้องติดต่อบอกเพื่อนคุณก่อนว่าคุณจะส่งจดหมายไปหา โปรโตคอลยูดีพีจัดเป็นโปรโตคอลที่ไม่น่าเชื่อถือเมื่อเทียบกับทีซีพี กล่าวคือมันไม่รับประกันความถูกต้องในการรับส่งข้อมูล อาจมีข้อมูลหายได้ในระหว่างการรับส่ง หรือแม้แต่ข้อมูลอาจถูกส่งมาในลำดับที่ไม่ถูกต้อง

ถ้าจะเปรียบเทียบให้เห็นขัดเจนก็ขอให้นึกถึงว่าการพูดคุยผ่านโทรศัพท์มีความน่าเชื่อถือกว่าการส่งจดหมาย เพราะมันรับประกันว่าผู้ที่พูดคุยกับเราจะได้รับข้อความจากเราแน่ หรือในกรณีที่เราได้ยินสิ่งที่เขาพูดไม่ชัดเราสามารถบอกให้เขาพูดใหม่ได้ แต่การส่งจดหมายมีโอกาสที่จดหมายจะส่งไปไม่ถึงผู้รีบ หรือผู้รับอาจเข้าใจผิดจากข้อความที่เราเขียนได้

พูดมาถึงตรงนี้หลายคนอาจบอกว่างั้นไม่ต้องสนใจไม่ต้องใช้ยูดีพีแล้วกัน มันจะมีไปทำไมเนี่ย คำตอบก็คือโปรโตคอลยูดีพีทำงานเร็วกว่าทีซีพีครับ เพราะมันไม่ต้องตรวจสอบอะไรมากมายเหมือนทีซีพี ดังนั้นยูดีพีจะเหมาะกับงานที่ไม่ต้องส่งข้อมูลมากนัก เพราะข้อมูลที่มีขนาดไม่ใหญ่โอกาสที่จะผิดพลาดก็มีน้อย หรือแม้แต่งานที่ต้องส่งข้อมูลขนาดใหญ่แต่ต้องการความรวดเร็วอย่างวีดีโอสตรีมมิง ก็ใช้โปรโตคอลยูดีพีนี้ในการรับส่งข้อมูลเช่นกัน เพราะมันต้องการความรวดเร็วซึ่งทีซีพีไม่สามารถทำได้ แต่เรื่องความน่าเชื่อถือนั้นจะมีการใช้โปรโตคอลตัวอื่นมาทำงานร่วมกับยูดีพี

โอเคครับ ถึงตรงนี้ก็คงเข้าใจความแตกต่างระหว่างทีซีพีและยูดีพีอย่างคร่าว กันแล้วนะครับ บทความต่อไปจะได้พูดถึงการเขียนโปรแกรมกันนะครับ 

วันพฤหัสบดีที่ 30 เมษายน พ.ศ. 2558

Java RMI ตอนที่ 3: การเขียนโปรแกรมฝั่งไคลเอนต์และการรันโปรแกรม

มาถึงตอนสุดท้ายซะที ส่วนใครที่ยังไม่ได้อ่านตอน 1 และตอน 2 ก็เชิญไปอ่านกันก่อนนะครับ ในตอนนี้เราจะมาเขียนโปรแกรมฝั่งไคลเอนต์และลองรันโปรแกรมกันครับ มาดูโปรแกรมฝั่งไคลเอนต์กันก่อนครับ

 1 import java.rmi.*;
 2 import java.net.*;
 3 import java.rmi.registry.LocateRegistry;
 4 import java.rmi.registry.Registry;
 5
 6
public class MyMathClient
 7 {
 8    public static void main(String[] args)
 9    {
10       if (args.length == 0)
11       {
12          System.err.println("Usage java MyMathClient <server-hostname>");
13       }
14       try
15       {
16          Registry registry = LocateRegistry.getRegistry(args[0]);
17          MyMath stub = (MyMath) registry.lookup("MyMathServer");
18          System.out.println(stub.add(3,2));

19         
20       }
21       catch (Exception e)
22       {
23          System.err.println("System Exception" + e);
24       }
25    }
26 }

จากโปรแกรมในบรรทัดที่ 16  เป็นการค้นหาอาร์เอ็มไอรีจิสตรี (RMI registry) ในเครื่องที่อ็อบเจกต์เซิร์ฟเวอร์รันอยู่ เราจะรันโปรแกรมนี้โดยระบุชื่อเครื่องเซิร์ฟเวอร์มาเป็นพารามิเตอร์บรรทัดคำสั่ง (command line parameter) และส่งชื่อเครื่องเซิร์ฟเวอร์นี้ให้กับเมท็อด getRegistry() บรรทัดที่ 17 ค้นหาอ็อบเจกต์โดยระบุชื่ออ็อบเจกต์เป็นชื่อเดียวกันกับที่ฝั่งเซิร์ฟเวอร์ลงทะเบียนไว้ ในที่นี้คือ MyMathServer ซึ่งที่เราจะได้กลับมาจากการเรียกเมท็อด lookup() ในบรรทัดนี้ก็คืออ็อบเจ็กต์เรฟเฟอร์เรนซ์ (object reference) ไปยังอ็อบเจกต์บนฝั่งเซิร์ฟเวอร์ ให้สังเกตว่าเราใช้ชื่อของอินเทอร์เฟซ (interface) คือ MyMath ไม่ใช่ชื่อของคลาสผู้รับใช้ (servant class) MyMathImpl หลังจากนี้เราก็สามารถเรียกใช้เมท็อดของอ็อบเจกต์บนฝั่งเซิร์ฟเวอร์ได้ราวกับอ็อบเจกต์นั้นอยู่บนเครื่องเดียวกับไคลเอนต์ ดังแสดงในบรรทัดที่ 18 ซึ่งเป็นการเรียกใช้เมท็อด add()

คราวนี้ก็มารันโปรแกรมกันครับ เริ่มจากรัน rmiregistry ก่อน วันนี้ขอแสดงการรันบน Mac นะครับ คำสั่งที่ใช้ก็คือ rmiregistry & เพื่อให้โปรแกรม rmiregistry ไปทำงานอยู่หลังฉาก ดังแสดงในรูปต่อไปนี้ครับ



ถ้าใช้ Windows ก็เปิดคอมมานด์พรอมพท์ของวินโดวส์แล้วก็ใช้ start rmiregistry ได้ครับ 

หลังจากนั้นก็รันโปรแกรมเซิร์ฟเวอร์ก็เหมือนการรันโปรแกรมปกติครับคือ 

java MyMathServer

ก็จะได้ผลลัพธ์ตามรูป





และจากนั้นก็รันไคลเอนต์ java MyMathClient ซึ่งก็จะได้ผลลัพธ์ตามรูปครับ 



เอาล่ะครับจบแล้ว ก็หวังว่าจะเป็นความรู้เบื้องต้นให้ทำตามแล้วนำไปต่อยอดกันต่อได้นะครับ...

สำหรับใครที่ไม่อยากป้อนโปรแกรมเองก็สามารถดาวน์โหลดได้จาก GitLab ครับ ถ้าใครโหลดไปแล้วจากตอนที่แล้วก็ไม่ต้องโหลดแล้วนะครับ 









วันพุธที่ 29 เมษายน พ.ศ. 2558

Java RMI ตอนที่ 2: การเขียนโปรแกรมฝั่งเซิร์ฟเวอร์

หลังจากเข้าใจว่าจาวาอาร์เอ็มไอ (Java RMI) คืออะไรแล้วจากตอนที่ 1 ในตอนนี้เราจะเริ่มมาพัฒนาโปรแกรมกันครับ โดยเราจะเริ่มจากฝั่งเซิร์ฟเวอร์กันก่อน โดยเราจะต้องคิดว่าอ็อบเจกต์ที่เราจะพัฒนาขึ้นจะให้บริการอะไรบ้าง โดยเราจะต้องเขียนรายการแอ็บแสตรกท์เมท็อด (abstract method) ที่จะให้บริการข้ามเครื่องในรูปแบบอินเทอร์เฟซ (interface) และไม่ใช่อินเทอร์เฟซธรรมดาจะต้องเป็นอินเทอร์เฟซระยะไกล (remote interface) ด้วย การประกาศอินเทอร์เฟซระยะไกลทำได้ง่ายมากด้วยการใช้รูปแบบการประกาศอินเทอร์เฟซของจาวาตามปกติ แต่ให้อินเทอร์เฟซนั้นเอ็กซ์เทนด์ (extend) อินเทอร์เฟซที่จาวาเตรียมไว้ให้แล้วที่ชื่อว่า Remote สรุปก็คือรูปแบบการประกาศอินเทอร์เฟซระยะไกลมีรูปแบบดังนี้

interface <interface name> extends Remote {...}

ไม่เอาดีกว่าอธิบายแบบนี้อาจไม่เข้าใจ มาดูตัวอย่างกันดีกว่า สมมติเราจะสร้างอ็อบเจกต์ที่จะให้บริการขึ้นมาสักตัวหนึ่ง เราจะสร้างอะไรกันดีครับ เอา Hello Wolrd ดีไหม... ไม่ดีนะครับน่าเบื่อ เอาอย่างนี้แล้วกันสร้างอ็อบเจกต์ที่ให้บริการบวกลบเลขดีไหมครับ น่าเบื่อน้อยลงหน่อย เอาตามนี้นะครับ มาสร้างกันเลย จากอ็อบเจกต์ที่เราต้องการสามารถสร้างอินเทอร์เฟซระยะไกลได้ดังนี้

1 import java.rmi.Remote;
2 import java.rmi.RemoteException;
3 interface MyMath extends Remote {
  public int add(int num1, int num2) throws RemoteException;
  public int subtract(int num1, int num2) throws RemoteException;
6 }


จากโค้ดตัวอย่างก็คือเราสร้างอินเทอร์เฟซระยะไกลชื่อ MyMath ซึ่งมีแอ็บสแตรกท์เมท็อดสองตัวคือ add() และ subtract() ให้สังเกตว่าแต่ละเมท็อดที่เราจะให้ถูกเรียกใช้จากระยะไกลได้จะต้องโทรว์ (throw) เอ็กซ์เซปชัน (Exception) ที่ชื่อว่า RemoteException

ตอนนี้เราจะเริ่มสร้างคลาสกันครับ ซึ่งคลาสที่เราจะสร้างบนฝั่งเซิร์ฟเวอร์นี้เรามักจะสร้างแบ่งเป็นสองคลาสครับ คือคลาสที่เรียกว่าคลาสผู้รับใช้ (servant class) และคลาสผู้บริการ (server class) คลาสผู้รับใช้เป็นคลาสที่เราอิมพลีเมนต์อินเทอร์เฟซ หรือพูดง่าย ๆ ก็คือมันเป็นคลาสที่สร้างอ็อบเจกต์ที่จะถูกเรียกใช้จากไคลเอนต์ นี่คือเหตุผลที่เราเรียกมันว่าคลาสผู้รับใช้ นอกจากนี้สตับและสเกเลตันจะถูกสร้างจากคลาสนี้เช่นกัน ส่วนคลาสผู้บริการจะเป็นคลาสที่ใช้ในการลงทะเบียนคลาสผู้รับใช้เข้ากับอาร์เอ็มไอรีจีสตรี (RMI Registry) เอาละครับมาเขียนคลาสผู้รับใช้กันดีกว่า

 1 import java.rmi.RemoteException;
 2 import java.rmi.server.UnicastRemoteObject;
 3 public class MyMathImpl extends UnicastRemoteObject implements MyMath  {
 4    public MyMathImpl() throws RemoteException {
 5      super();
 6    }
 7    public int add(int num1, int num2) throws RemoteException  {
 8       return num1+num2;
 9    }
10    public int subtract(int num1, int num2)throws RemoteException {
11       return num1-num2;
12    }
13
14
}

ข้อสังเกตสำหรับคลาสนี้มีสามเรื่องครับ เรื่องแรกก็คือชื่อของคลาส (บรรทัดที่ 3) นักเขียนโปรแกรมที่พัฒนาจาวาอาร์เอ็มไอนิยมตั้งกันคือคลาสจะมีชื่อเดียวกับอินเทอร์เฟซตามด้วย Impl จากโปรแกรมที่เรากำลังพัฒนาอินเทอร์เฟซของเรามีชื่อว่า MyMath ดังนั้นเราจึงตั้งชื่อคลาสนี้ว่า MyMathImpl เรื่องที่สองก็คือคลาสนี้สืบทอดคุณสมบัติมาจากคลาส UnicastRemoteObject ซึ่งเป็นคลาสที่จาวาเตรียมเอาไว้เพื่อสนับสนุนการเรียกอ็อบเจกต์ข้ามเครื่อง เรื่องที่สามก็คือเราต้องมีคอนสตรักเตอร์ (constructor) ที่โทรว์ RemoteException และเรียกคอนสตรักเตอร์ของคลาส UnicastRemoteObject เอาละครับเรียบร้อยครับ เห็นไหมครับว่าง่ายมาก

[หลังจากเราเขียนคลาสผู้รับใช้เสร็จแล้วเราก็คอมไพล์เพื่อให้ได้ MyMathImpl.class ครับ ถ้าเราใช้จาวาก่อนเวอร์ชัน 5 เราก็ใช้คำสั่งนี้ในการสร้างสตับและสเกเลตันได้เลยครับ

rmic MyMathImpl

เราจะได้ไฟล์ชื่อ MyMathImpl_Stub.class ซึ่งไฟล์นี้จะทำหน้าที่เป็นทั้งสตับและสเกเลตัน 

ซึ่งผมว่าตอนนี้เราคงไม่ใช้ภาษาจาวาเก่าแบบนั้นแล้วนะครับ ดังนั้นถ้าใครใช้ Java 5  ขึ้นมาก็ข้ามไปได้เลยนะครับ เพราะในจาวารุ่นใหม่จาวารันไทม์ (Java Runtime) จะจัดการเรื่อง Stub-Skeleton ให้

]

คราวนี้มาดูคลาสผู้ให้บริการกัน

 1 import java.net.*;
 2 import java.rmi.*;
 3 import java.rmi.registry.Registry;
 4 import java.rmi.registry.LocateRegistry;
 5 public class MyMathServer 
 6 {
 7    public static void main(String[] args)
 8    {
 9      
10       try
11       {
12          MyMathImpl myMathObj = new MyMathImpl();
13          Registry registry = LocateRegistry.getRegistry();
14          registry.bind("MyMathServer", myMathObj);
15          System.out.println("MyMath Server ready");
16       }
17       catch (Exception re) {
18          System.out.println("Execption in MyMathServer: " + re);
19       }
20    }
21 }

จากโปรแกรมจะเห็นว่าเราสร้างอ็อบเจกต์เรฟเฟอร์เรนซ์ (object referennce) ของคลาส MyMathImpl ในบรรทัดที่ 12 จากนั้นบรรทัดที่ 13 ประโยค

Registry registry = LocateRegistry.getRegistry();

เป็นการค้นหาอาร์เอ็มไอรีจิสตรีที่รันอยู่ในระบบ นั่นหมายความว่าก่อนจะรันโปรแกรมเซิร์ฟเวอร์เราต้องรันอาร์เอ็มไอรีจิสตรีก่อนซึ่งจะรันยังไงนั้นจะอยู่ในตอนต่อไปครับ บรรทัดที่ 14 ประโยค

 registry.bind("MyMathServer", myMathObj);

เป็นการลงทะะเบียนอ็อบเจกต์เรฟเฟอร์เรนซ์ดังกล่าวในชื่อ MyMathServer ซึ่งโปรแกรมฝั่งไคลเอนต์ที่เราจะเขียนในตอนต่อไปจะค้นหาอ็อบเจกต์นี้โดยใช้ชื่อนี้

เอาล่ะครับตอนนี้เราก็เสร็จงานด้านเซิร์ฟเวอร์แล้ว ตอนต่อไปเราจะไปเขียนไคลเอนต์ แล้วก็ลองรันกันดูครับ โปรดอดใจสักนิดหนึ่งนะครับ ขอพักหน่อย ถ้าใครไม่อยากพิมพ์โปรแกรมเองสามารถดาวน์โหลดโค้ดได้จาก GitLab ได้ครับ