วันอังคารที่ 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) สามารถโหลดได้จากลิงก์นี้ครับ  ซึ่งจะได้โปรแกรมทั้งฝั่งเซิร์ฟเวอร์และไคลเอนต์เลย แต่บล็อกเกี่ยวกับไคลเอนต์โปรแกรมจะอยู่ในตอนหน้านะครับ

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