วันอังคารที่ 28 เมษายน พ.ศ. 2558

Java RMI ตอนที่1: Java RMI คืออะไร

การเรียกใช้ฟังก์ชันที่อยู่บนคอมพิวเตอร์เครื่องอื่นในปัจจุบันกลายเป็นเรื่องที่เราใช้กันแทบจะเป็นปกติในปัจจุบันกันแล้ว เช่นเราเขียนโปรแกรมเรียกใช้ฟังก์ชันของ Google Maps  เพื่อนำแผนที่มาใช้กับโปรแกรมของเราโดยเราไม่ต้องรู้เลยว่า แผนที่นั้นจัดเก็บอย่างไร ฟังก์ชันที่เราเรียกใช้เขียนขึ้นมาด้วยภาษาอะไร และโปรแกรมแผนที่ทำงานอยู่บนระบบปฏิบัติการอะไร ซึ่งเทคโนโลยีที่เราใช้เป็นพื้นฐานของการทำงานดังกล่าวในปัจจุบันก็คือเทคโนโลยีที่เรียกว่าเว็บเซอร์วิส (web service) นั่นเอง ซึ่งผมก็หวังว่าจะหาเวลามาเขียนให้ได้อ่านกันสักวันหนึ่ง 

จริง ๆ แล้วการเรียกใช้ฟังก์ชันที่ทำงานอยู่บนเครื่องคอมพิวเตอร์เครื่องอื่นในเครือข่ายไม่ได้เพิ่งมาเกิดขึ้นเพราะเว็บเซอร์วิส แต่แนวคิดดังกล่าวมีมานานแล้ว และก็มีเทคโนโลยีต่าง ๆ ที่ถูกพัฒนาขึ้นเพื่อนำมาใช้ในงานดังกล่าว ซึ่งเว็บเซอร์วิสก็ถูกพัฒนาขึ้นมาโดยใช้เทคโนโลยีเหล่านี้เป็นฐาน เทคโนโลยีดังกล่าวเริ่มมาตั้งแต่ การเรียกโพรซีเยอร์จากระยะไกล (Remote Procedure Call) หรือเรียกย่อ ๆ ว่าอาร์พีซี (RPC)  การเรียกใช้เมท็อดจากระยะไกล (Remote Method Invocation) หรือเรียกย่อ ๆ ว่าอาร์เอ็มไอ (RMI) และ Common Object Request Broker Architecture  (CORBA) (อันนี้ขอไม่แปลแล้วกันนะครับ ยังคิดคำแปลที่ถูกใจไม่ได้:)) เป็นต้น ซึ่งผมหวังว่าจะมีโอกาสได้เขียนให้ได้อ่านกันต่อไป แต่ในวันนี้ผมจะมาพูดถึง อาร์เอ็มไอครับ 

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

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


RMI Architecture


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

ทั้งสตับและสเกเลตันยังมีหน้าที่ในการแปลงข้อมูลการเรียกใช้ และค่าที่ส่งกลับมาจากการทำงานของอ็อบเจกต์ให้อยู่ในรูปที่ส่งผ่านเครือข่ายได้ (ข้อมูลที่ส่งผ่านเครือข่ายจะต้องอยู่ในรูปของสายธารตัวอักษร (stream of byte)) และแปลงข้อมูลที่ได้รับจากเครือข่ายให้กลับมาในรูปแบบที่โปรแกรมเข้าใจ ซึ่งการทำงานดังกล่าวเรียกว่า มาร์แชล (Marshal) และ อันมาร์แชล (Unmarshal) ตามลำดับ  

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

obj.add(1, 2);  

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

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

ถึงตรงนี้ถ้ายังงงอยู่ไม่เป็นไรครับ ผมคิดว่าน่าจะเห็นภาพชัดเจนขึ้นเมื่อเราได้เขียนโปรแกรมกันจริง ๆ ในตอนต่อไป แต่ตอนนี้ผมขอตอบคำถามที่หลายคนอาจจะสงสัยกันนะครับว่า สรุปแล้วการเขียนโปรแกรมจาวาอาร์เอ็มไอนี่เราต้องเขียนโปรแกรมตรงไหนบ้าง เราต้องเขียนโปรแกรมสตับและสเกเลตันเองหรือเปล่า อาร์เอ็มไอรีจิสตรีจะไปหามาจากไหน คำตอบคืออย่างนี้ครับ โปรแกรมอาร์เอ็มไอรีจิสตรีจะมากับจาวาเจดีเค  (Java JDK) ที่เราโหลดมาใช้ในการพัฒนาโปรแกรมด้วยภาษาจาวาอยู่แล้ว ส่วนสตับและสเกเลตันเราก็ไม่ต้องเขียนเองครับ ถ้าเราใช้จาวาก่อนเวอร์ชัน 5 เราจะใช้โปรแกรมที่ชื่อว่าอาร์เอ็มไอซี (rmic) ช่วยสร้างให้เรา ซึ่งโปรแกรมนี้ก็มากับจาวาเจดีเคอีกเช่นกัน แต่ตั้งแต่จาวาเวอร์ชัน 5.0 เป็นต้นไปเราไม่ต้องใช้ rmic แล้วนะครับเพราะสตับและสเกเลตันจะถูกจัดการโดนจาวารันไทม์ (Java Runtime)

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

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


วันพุธที่ 25 มีนาคม พ.ศ. 2558

การใช้งาน make เบื้องต้น

สำหรับวันนี้ผมจะมาแนะนำโปรแกรมอรรถประโยชน์ (utiliies program) ตัวหนึ่ง ที่ชื่อว่า make ครับ make เป็นโปรแกรมที่จะช่วยให้เราคอมไพล์โปรแกรมที่ประกอบด้วยไฟล์โปรแกรมหลาย ๆ ไฟล์ได้ง่าย และมีประสิทธิภาพมากขึ้น โดยเราจะระบุรายชื่อไฟล์ต้นฉบับและเงื่อนไขการคอมไพล์โปรแกรมไว้ใน make file ซึ่งโครงสร้างแบบง่ายที่สุดของ make file จะเป็นดังนี้ครับ 


target: dependencies


[tab] command

ในการอธิบายให้เข้าใจถึงโครงสร้างของ make file จะขอใช้ตัวอย่าง make file ต่อไปนี้ครับ 

 all: testPeterson

testPeterson: testPeterson.o peterson.o
            gcc -o testPeterson testPeterson.o peterson.o

testPeterson.o: testPeterson.c
            gcc -c testPeterson.c  

peterson.o: peterson.c
            gcc -c peterson.c

clean:
            rm -rf *.o testPeterson


จากตัวอย่างนี้จะเห็นว่ามี target อยู่ 5 ตัวคือ 
all:,  testPeterson:,  testPeterson.o, peterson.o และ clean: 

โดยปกติแล้ว target จะใช้เป็นแบ่งการคอมไพล์โปรแกรมออกเป็นส่วน ๆ  โดยเราจะระบุ  target ที่สัมพันธ์กันผ่านทาง dependencies จากตัวอย่างจะเห็นว่า เราแบ่งการคอมไพล์โปรแกรมของเราออกเป็น 3 ส่วนตาม target สามตัวคือ 
testPeterson:, testPeterson.o และ peterson.o 

ให้สังเกตว่า target ทั้งสามตัวนี้คือชื่อไฟล์ โดย testPeterson คือชื่อไฟล์โปรแกรมที่เราจะได้ ส่วน testPeterson.o และ peterson.o จะเป็นชื่อไฟล์ของ object code (ซึ่งความสำคัญของเรื่องนี้จะได้กล่าวถึงต่อไป) 

target all: เป็นชื่อโดยปริยาย (default) ที่จะถูกเรียกทำงานในกรณีที่เราเรียกใช้ make file โดยไม่ระบุ  target ส่วน target clean: เป็น target ที่ไม่มี dependencies มีแต่คำสั่ง ซึ่งจุดประสงค์ของ target clean: นี้ไม่เกี่ยวกับการคอมไพล์โปรแกรม แต่เป็นการสั่งให้ลบไฟล์ .o ทั้งหมดและไฟล์ testPeterson ทิ้งไป เราจะเรียกให้ target clean: ทำงานในกรณีที่เราต้องการ จะลบ object code และโปรแกรมทิ้งไปเพื่อเริ่มต้นสร้างใหม่ทั้งหมด

dependencies จะเป็นส่วนที่ระบุ ว่า target นี้ขึ้นอยู่กับ target อะไรบ้าง จากตัวอย่างจะเห็นว่า all: ขึ้นอยู่กับ testPeterson นั่นคือเมื่อเราเรียก make file มาทำงานโดยเริ่มที่ target all: มันจะไปทำที่ target testPeterson:เพราะ all: ระบุ dependencies คือ testPeterson และเมื่อ make มาทำที่ target testPeterson: จะพบว่ามันมี dependencies สองตัวคือ testPeterson.o และ peterson.o มันก็จะไปทำงานที่  target ทั้งสองตามลำดับ ซึ่งการไปทำงานที่ว่านี้ก็คือการไปเรียก คำสั่ง (command) ที่ถูกระบุใน target นั้น เช่นคำสั่งของ peterson.o ก็คือ gcc –c petertson.c  นั่นคือถ้าเราเรียก make file นี้ทำงานโดยให้เริ่มทำงานที่  target all: ก็เท่ากับว่าเราได้เรียกใช้คำสั่งตามลำดับดังนี้ 

gcc –c testPeterson.c     (จาก target testPeterson.o)
gcc -c peterson.c   (จาก target peterson.o)
gcc -o testPeterson testPeterson.o peterson.o (จาก  target testPeterson)

หมายเหตุ: 
1. ในการเขียนคำสั่งให้กับแต่ละ target จะต้องกดแป้น Tab บนแป้นพิมพ์ เพื่อให้คำสั่งเยื้องเข้าไป ใช้การเคาะ Space Bar  หลาย ๆ ครั้งแทนไม่ได้
2. ในกรณีที่ชื่อของ target เป็นชื่อไฟล์ make จะทำงานโดยเปรียบเทียบวันเวลาที่สร้างไฟล์ที่เป็น target กับวันเวลาที่สร้างไฟล์ที่เป็น depencencies ถ้ามันพบว่าไฟล์ที่อยู่บนฝั่ง target ใหม่กว่า มันก็จะไม่ทำ target นั้น เช่น target peterson.o  มี dependencies คือ peterson.c ถ้า peterson.o ใหม่กว่า peterson.c ก็หมายความว่าหลังจากที่ได้คอมไพล์โปรแกรม peterson.c ไปในครั้งล่าสุดแล้วไม่ได้มีการแก้ไข อีกเลย peterson.c  จึงไม่ต้องเสียเวลาคอมไพล์ใหม่ 

คำสั่งในการเรียก  make file มีรูปแบบดังนี้ 

make [–f  ชื่อ make file] [target] 
เช่น
 make –f  peterson.mk 
จะเป็นการเรียก make file ชือ peterson.mk ให้มาทำงาน โดยจะทำงานที่ target all:
make –f  peterson.mk peterson.o 
จะเป็นการเรียก make file ชือ peterson.mk ให้มาทำงาน โดยจะทำงานที่ target peterson.o:
make –f  peterson.mk clean 
จะเป็นการเรียก make file ชือ peterson.mk ให้มาทำงาน โดยจะทำงานที่ target clean:

หมายเหตุ: ถ้าเราตั้งชื่อ make file ว่า makefile เราสามารถใช้คำสั่ง make โดยไม่ต้องระบุชื่อ make file เลยก็ได้ 
เช่น make clean จะหมายถึงให้ไปทำงานใน target clean: ของ make file ที่ชื่อ makefile

เอาล่ะครับ คิดว่าบทความนี้จะเป็นการเกริ่นนำให้รู้จักกับ make และสามารถนำไปใช้งานกันได้นะครับ แต่จริง ๆ แล้ว make มีความสามารถมากกว่านี้มากครับ ซึ่งใครที่สนใจก็ขอแนะนำให้ลองค้นคว้าเพิ่มเติมดูนะครับ ซึ่งลิงก์หนึ่งที่ผมแนะนำก็คือ http://www.gnu.org/software/make/manual/make.html  ขอให้สนุกกับการเริ่มใช้ make นะครับ ...

วันศุกร์ที่ 17 มกราคม พ.ศ. 2557

การสร้าง Administered Object สำหรับ JMS Application บน GlassFish Server

ก่อนที่เราจะเขียนโปรแกรมในพาราไดม์ที่เป็น Messaging System ด้วย Java หรือที่เราจะเรียกว่า JMS Application สิ่งที่ต้องทำก่อนก็คือการสร้าง Administered Object ซึ่งอ็อบเจกต์เหล่านี้ไม่ได้ถูกสร้างในโปรแกรมที่เราเขียนขึ้น แต่จะต้องถูกสร้างผ่านเครื่องมือที่โปรแกรมเว็บเซิร์ฟเวอร์ที่รองรับ JMS Application เตียมไว้ให้ และอ็อบเจกต์เหล่านี้จะต้องถูกสร้างก่อนที่เราจะเขียนโปรแกรม ในบทความนี้จะแสดงให้เห็นขั้นตอนการสร้างอ็อบเจกต์ดังกล่าว ซึ่งอ็อบเจกต์ดังกล่าวแบ่งเป็นสองกลุ่มคือ connection factories และ destination มาดูขั้นตอนการสร้างอ็อบเจกต์เหล่านี้บน GlassFish Server กันครับ

1. เข้าสู่หน้า Admin Console ของ Glass Fish แล้วคลิกที่เมนู Resources


2. สร้าง Connection Factory Object โดยคลิกที่ Connection Factories


3. คลิกที่ปุ่ม new เพื่อสร้าง Connection Factory Object


4. ป้อนชื่อ JNDI (JNDI Name) และประเภททรัพยากร (Resource Type) สำหรับชื่อเราจะใส่อะไรก็ได้ แต่
รูปแบบที่เป็นที่นิยมกันคือใช้ jms/ชื่อที่ต้องการ ส่วนประเภททรัพยากรเราเลือกเป็น ConnectionFactory ถ้าเราต้องการจะใช้อ็อบเจกต์นี้เพื่อสร้าง destination ที่เป็นได้ทั้ง topic และ queue จากนั้นคลิกที่ปุ่ม OK


5. สร้าง Destination อ็อบเจกต์ โดยกลับไปที่ JMS Resources และเลือก Destination Resources

6. สร้าง Destination Object โดยคลิกที่ปุ่ม New



 7. ป้อนข้อมูลเกี่ยวกับ Destination อ็อบเจกต์ที่ต้องการ โดยเลือกประเภทของทรัพยากรให้ตรงตามที่ต้องการว่าจะให้เป็น topic หรือ queue จากนั้นคลิกที่ปุ่ม OK


ก็เป็นอันว่าเรียบร้อยครับตอนนี้เราก็ได้ Administered Object เพื่อใช้กับ JMS Application เรียบร้อยแล้ว ตอนนี้ก็สามารถลงมือเขียน JMS Application กันได้แล้ว

วันอังคารที่ 24 ธันวาคม พ.ศ. 2556

การเขียนโปรแกรมเครือข่ายด้วย Socket ตอนที่ 6 : TCP Concurrent Server

จากโปรแกรมเซิร์ฟเวอร์ในตอนที่ 3 จัดเป็นเซิร์ฟเวอร์แบบ iterative นั่นคือให้บริการไคลเอนต์ได้ครั้งละหนึ่งตัว ถ้ามีมากกว่าหนึ่งตัวตัวที่เข้ามาทีหลังจะต้องเข้าคิวรอจนกว่าจะถึงคิวตัวเอง ซึ่งเซิร์ฟเวอร์แบบนี้ไม่เหมาะสมกับงานที่ไคลเอนต์และเซิร์ฟเวอร์ต้องติดต่อกันเป็นเวลานาน ลองนึกภาพว่าถ้า Gmail ให้บริการแบบนี้ดูนะครับว่ามันจะเป็นยังไง ดังนั้นงานแบบนี้จะต้องเขียนเซิร์ฟเวอร์ที่ทำงานแบบพร้อมกัน (Concurrent) ครับ

ซึ่งการเขียนก็ไม่ได้ยากอะไร แนวคิดที่ผ่านมาในตอนต่าง ๆ ยังใช้ได้หมด แต่สิ่งที่เราจะเพิ่มเข้ามาคือเราจะนำเธรด (Thread) มาใช้ครับ หลักการก็คือหลังจากที่เซิร์ฟเวอร์รับการติดต่อจากไคลเอนต์เข้ามาแล้วแทนที่จะไปให้บริการไคลเอนต์เองเหมือนที่ผ่านมา ก็จะสร้างเธรด ขึ้นมาให้บริการไคลเอนต์แต่ละตัว ไปลองดูโค้ดกันครับ
  1. //TCPConcurrentServer.java
  2. import java.io.*; 
  3. import java.net.*; 
  4. import java.util.*;
  5. public class TCPConcurrentServer { 
  6.    public static void main(String argv[])  { 
  7.       String clientSentence; 
  8.       String capitalizedSentence; 
  9.       ServerSocket welcomeSocket = null;
  10.       try {
  11.          welcomeSocket = new ServerSocket(6789);
  12.       }
  13.       catch (IOException e) {
  14.          System.out.println("Cannot create a welcome socket");
  15.          System.exit(1);
  16.       }
  17.       while(true) {
  18.          try {  
  19.             System.out.println("The server is waiting ");
  20.             Socket connectionSocket = welcomeSocket.accept(); 
  21.    EchoThread echoThread = new EchoThread(connectionSocket);
  22.             echoThread.start();
  23.          }
  24.          catch (IOException e) {
  25.             System.out.println("Cannot create this connection");
  26.          }
  27.       }
  28.    } 
  29. //EchoThread.java
  30. import java.io.*; 
  31. import java.net.*; 
  32. import java.util.*;
  33. public class EchoThread extends Thread {
  34.     private Socket connectionSocket;
  35.     public EchoThread(Socket connectionSocket) {
  36.         this.connectionSocket = connectionSocket;
  37.     }
  38.     public void run() {
  39.          Scanner inFromClient = null;
  40.          DataOutputStream outToClient = null;
  41.          try {
  42.             inFromClient = new Scanner(connectionSocket.getInputStream());
  43.    outToClient = 
  44.               new DataOutputStream(connectionSocket.getOutputStream()); 
  45.    String clientSentence = inFromClient.nextLine(); 
  46.          String capitalizedSentence = clientSentence.toUpperCase() + '\n'; 
  47.          outToClient.writeBytes(capitalizedSentence);         
  48.             
  49.    }
  50.         catch (IOException e) {
  51.             System.err.println("Closing Socket connection");
  52.         }
  53.         finally {
  54.             try {
  55.                if (inFromClient != null)
  56.                   inFromClient.close();
  57.                if (outToClient != null)
  58.                   outToClient.close();
  59.                if (connectionSocket != null)
  60.                   connectionSocket.close();
  61.                }
  62.             catch (IOException e) {
  63.                e.printStackTrace();
  64.             }
  65.         }
  66.     }
  67. }

สิ่งที่แตกต่างจากโปรแกรม iterative server ก็คือ คลาส EchoThread ในบรรทัดที่ 30-68 ซึ่งเป็นคลาสที่เขียนขึ้นเพื่อเป็นส่วนของการให้บริการไคลเอนต์ในการแปลงตัวอักษรตัวเล็กเป็นตัวใหญ่ จะเห็นว่างานบริการไคลเอนต์ที่เคยอยู่ในตัวโปรแกรม TCPServer จะถูกนำมาเขียนในคลาสนี้ บรรทัดที่ 21 และ 22 ในคลาส TCPConcurrent  จะสร้างออบเจกต์ของ EchoThread และสั่งให้เทรดเริ่มทำงาน ให้สังเกตว่ามีการส่งซ็อกเก็ตเชื่อมต่อที่สร้างขึ้นมาให้ออบเจกต์ของคลาส EchoThread ผ่านทางคอนสตรักเตอร์ หลังจากสร้างออบเจกต์ และสั่งให้เธรดเริ่มทำงานในการบริการไคลเอนต์แล้ว โปรแกรมเซิร์ฟเวอร์ก็สามารถกลับไปรับการติดต่อจากไคลเอนต์ตัวถัดไปได้ โดยตอนนี้การบริการไคลเอนต์จะเป็นหน้าที่ของเธรดที่ถูกสร้างขึ้น และถ้ามีการติดต่อเข้ามาก็ทำแบบเดียวกันคือสร้างเธรดอีกตัวหนึ่งไปให้บริการไคลเอนต์

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

สำหรับใครที่ไม่อยากพิมพ์โค้ดเองสามารถดาวน์โหลดได้จาก github ครับ

สำหรับใครที่ยังไม่เข้าใจว่าจะรันโปรแกรมยังไงดูได้จากวีดีโอนี้ครับ



สุดท้ายสำหรับวันนี้ก็ขอสุขสันต์วันคริสต์มาสมายังทุกคนครับ...

วันเสาร์ที่ 7 ธันวาคม พ.ศ. 2556

เขียนโปรแกรมเครือข่ายด้วยซ็อกเก็ต (Socket) ตอนที่ 5: การรันโปรแกรมเพื่อทดสอบการทำงาน

หลังจากที่เราเขียนโปรแกรมเซิร์ฟเวอร์และไคลเอนต์เรียบร้อยแล้ว ในตอนนี้เราจะมาลองรันโปรแกรมเพื่อทดสอบการทำงานกันดู  โดยจะขอยกตัวอย่างการรันโปรแกรมบนระบบปฏิบัติการไมโครซอฟท์วินโดวส์ นะครับ โดยเราจะรันทั้งสองโปรแกรมบนเครื่องเดียวกัน หลังจากคอมไฟล์ทั้งโปรแกรมเซิร์ฟเวอร์และไคลเอนต์แล้ว เราจะเริ่มจากรันโปรแกรมเซิร์ฟเวอร์ดังนี้ start java TCPServer

เราใช้คำสั่ง start นำหน้าคำสั่งที่ใช้รันโปรแกรมเซิร์ฟเวอร์เพราะเราต้องการจะเปิดหน้าจอ command prompt ชึ้นมาใหม่เพื่อใช้รันโปรแกรมเซิร์ฟเวอร์ โดนหน้าจอ command prompt เดิมจะได้ใช้สำหรับรันโปรแกรมไคลเอนต์ต่อไป

การรันไคลเอนต์ใช้คำสั่งดังนี้ java TCPClient

หลังจากนั้นเราสามารถป้อนข้อความอะไรก็ได้ จากนั้นโปรแกรมฝั่งเซิร์ฟเวอร์จะส่งข้อความที่เราป้อนเข้าไปกลับมาเป็นตัวอักษรภาษาอังกฤษตัวใหญ่ หลังจากรันโปรแกรมแล้วโปรแกรมฝั่งไคลเอนต์จะจบการทำงาน ส่วนโปรแกรมฝั่งเซิร์ฟเวอร์จะรอรับการติดต่อจากไคลเอนต์ต่อไป นั่นคือเราสามารถรันโปรแกรมฝั่งไคลเอนต์เพื่อติดต่อเข้าไปอีกกี่รอบก็ได้ ถ้าต้องการจบการทำงานของโปรแกรมฝั่งเซิร์ฟเวอร์ให้ไปที่หน้าต่างของโปรแกรมเซิร์ฟเวอร์และกด Ctrl-C

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

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



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


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

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


  1. import java.io.*; 
  2. import java.net.*;
  3. import java.util.*; 
  4. class TCPClient { 
  5.     public static void main(String argv[]) throws Exception 
  6.     { 
  7.          String sentence; 
  8.          String modifiedSentence;
  9.          Scanner inFromUser = null;
  10.          Socket clientSocket = null;
  11.          DataOutputStream outToServer = null;
  12.          Scanner inFromServer = null;
  13.          try { 
  14.             inFromUser = new Scanner(System.in);
  15.             clientSocket = new Socket("localhost", 6789); 
  16.             outToServer = 
  17.                new DataOutputStream(clientSocket.getOutputStream()); 
  18.            inFromServer = new Scanner(clientSocket.getInputStream());
  19.            System.out.print("Please enter words: ");
  20.            sentence = inFromUser.nextLine(); 
  21.            outToServer.writeBytes(sentence + '\n');
  22.            modifiedSentence = inFromServer.nextLine(); 
  23.            System.out.println("FROM SERVER: " + modifiedSentence);
  24.          }
  25.          catch (IOException e) {
  26.              System.out.println("Error occurred: Closing the connection");
  27.          }
  28.          finally {
  29.             try {
  30.                 if (inFromServer != null)
  31.                     inFromServer.close();
  32.                 if (outToServer != null)
  33.                     outToServer.close();
  34.                 if (clientSocket != null)
  35.                     clientSocket.close();
  36.             }
  37.             catch (IOException e) {
  38.                e.printStackTrace();
  39.             }
  40.          } 
  41.     } 


บรรทัดที่ 15 เป็นการสร้างซ็อกเก็ตเชื่อมต่อ (Connection Socket) ให้สังเกตว่าเราใช้ชื่อโฮสต์คือ localhost เพราะโปรแกรมตัวอย่างของเราเราจะรันโปรแกรมเซิร์ฟเวอร์และไคลเอนต์บนเครื่องเดียวกัน ในกรณีที่รันโปรแกรมเซิร์ฟเวอร์บนเครื่องอื่น ให้ระบุชื่อหรือหมายเลขไอพีของเครื่องดังกล่าว หมายเลขพอร์ตใช้หมายเลขที่โปรแกรมเซิร์ฟเวอร์รอรับการติดต่ออยู่ ในที่นี้ใช้ 6789 เพราะโปรแกรมเซิร์ฟเวอร์ที่เราเขียนขึ้นในบทความที่แล้วใช้หมายเลขพอร์ตนี้  บรรทัดที่ 16-17 สร้างสตรีมผลลัพธ์สำหรับเขียนข้อมูลลงซ็อกเก็ต บรรทัดที่ 18 สร้างสตรีมเพื่อรับข้อมูลจากซ็อกเก็ต บรรทัดที่ 21 เขียนข้อมูลลงซ็อกเก็ตเพื่อส่งให้เซิร์ฟเวอร์ บรรทัดที่ 22 รอรับข้อมูลที่เซิร์ฟเวอร์จะส่งกลับมา finally clause บรรทัดที่ 28-35 ปิดสตรีมทุกตัว และซ็อกเก็ตเชื่อมต่อ

สำหรับโปรแกรมต้นฉบับ (source code) สามารถโหลดได้จากลิงก์นี้ครับ ถ้าใครโหลดจาก gitlab ไปเมื่อบทความที่แล้ว ก็จะได้โค้ดโปรแกรมไปแล้วนะครับ ไม่ต้องโหลดใหม่ครับ

วันอังคารที่ 3 ธันวาคม พ.ศ. 2556

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

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

สำหรับแนวคิดของการอ่านและเขียนข้อมูลกับซ็อกเก็ตนั้นจะใช้แนวคิดของสตรีม (Stream) ซึ่งก็คือแนวคิดที่จาวา (Java) ใช้ในการรับส่งข้อมูลระหว่างโปรแกรมที่เขียนด้วยภาษาจาวา กับอุปกรณ์รอบข้าง หรือกับไฟล์ ตัวอย่างเช่น เวลาจะพิมพ์ข้อมูลออกมาทางจอภาพเราจะใช้คำสั่ง System.out.println("string"); ประโยคนี้ out คือสตรีมผลลัพธ์ (output stream) ซึ่งอ้างถึงตัวแสดงผลลัพธ์มาตรฐาน ซึ่งก็คือจอภาพนั่นเอง การเขียนโปรแกรมซ็อกเก็ตของภาษาจาวา (Java) ก็จะใช้แนวคิดนี้ เพียงแต่สตรีมที่เราสร้างขึ้น จะเชื่อมโยงกับซ็อกเก็ตแทนที่จะเป็นอุปกรณ์หรือไฟล์

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


  1. import java.io.*; 
  2. import java.net.*; 
  3. import java.util.*;
  4. class TCPServer { 
  5.    public static void main(String argv[])  { 
  6.       String clientSentence; 
  7.       String capitalizedSentence; 
  8.       ServerSocket welcomeSocket = null;
  9.       Socket connectionSocket = null;
  10.       Scanner inFromClient = null;
  11.    DataOutputStream outToClient = null;
  12.       try {
  13.          welcomeSocket = new ServerSocket(6789);
  14.       }
  15.       catch (IOException e) {
  16.          System.out.println("Cannot create a welcome socket");
  17.          System.exit(1);
  18.       }
  19.       while(true) {
  20.          try {
  21.             System.out.println("The server is waiting ");
  22.              connectionSocket = welcomeSocket.accept(); 
  23.              inFromClient = new Scanner(connectionSocket.getInputStream());
  24.              outToClient = 
  25.               new DataOutputStream(connectionSocket.getOutputStream()); 
  26.              clientSentence = inFromClient.nextLine(); 
  27.             capitalizedSentence = clientSentence.toUpperCase() + '\n'; 
  28.             outToClient.writeBytes(capitalizedSentence);
  29.          }
  30.          catch (IOException e) {
  31.             System.out.println("Error cannot create this connection");
  32.          }
  33.          finally {
  34.             try {
  35.                if (inFromClient != null)
  36.                   inFromClient.close();
  37.                if (outToClient != null)
  38.                   outToClient.close();
  39.                if (connectionSocket != null)
  40.                   connectionSocket.close();
  41.             }
  42.             catch (IOException e) {
  43.                e.printStackTrace();
  44.             }
  45.          }
  46.       }
  47.    } 

สำหรับโปรแกรมนี้ก็มีส่วนที่จะอธิบายดังนี้ครับ บรรทัดที่ 13 เป็นการสร้างเซิร์ฟเวอร์ซ็อกเก็ตโดยใช้หมายเลขพอร์ต 6789 ในการรอรับการติดต่อกับไคลเอนต์  บรรทัดที่ 22 จะเห็นว่ามีการเรียกใช้เมท็อด accept() เมท็อดนี้จะมีผลทำให้โปรแกรมเซิร์ฟเวอร์หยุดรอรับการติดต่อจากไคลเอนต์ เมื่อไคลเอนต์ติดต่อเข้ามาก็จะสร้างซ็อกเก็ตเชื่อมต่อเพื่อใช้ในการแลกเปลี่ยนข้อมูล บรรทัดที่ 23 สร้างสตรีมข้อมูลเข้า (input stream) เพื่ออ่านข้อมูลจากซ็อกเก็ต บรรทัดที่ 24-25 สร้างสตรีมผลลัพธ์ (output stream) เพื่อเขียนข้อมูลลงซ็อกเก็ต บรรทัดที่ 26 หยุดเพื่อรออ่านข้อมูลที่ไคลเอนต์จะส่งผ่านซ็อกเก็ตมา บรรทัดที่ 27 แปลงสตริงที่รับมาให้เป็นตัวอักษรตัวใหญ่ และบรรทัดที่ 28 เขียนข้อมูลผ่านซ็อกเก็ตกลับไปให้ไคลเอนต์ ใน finally clause จะปิด stream ที่สร้างขึ้นมาทั้งหมด และซ็อกเก็ตเชื่อมต่อตัวนี้ เนื่องจากในตัวอย่างนี้เซิฟร์เวอร์จะแปลงสตริงที่ไคลเอนต์ส่งเข้ามา ส่งสตริงที่แปลงกลับไป และตัดการเชื่อมต่อกับไคลเอนต์ทันที  และข้อสังเกตที่น่าสนใจก็คือจะเห็นว่าเราให้โปรแกรมนี้ทำงานอยู่ในลูปไม่รู้จบ ซึ่งเป็นธรรมชาติของโปรแกรมเซิร์ฟเวอร์ที่จะทำงานไปเรื่อย ๆ

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

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

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