Giáo trình

Lập trình truyền thông

Science and Technology

Socket dưới ngôn ngữ Java

Tác giả: unknown

Socket dưới ngôn ngữ Java

Java hỗ trợ lập trình mạng thông qua các lớp trong gói java.net. Một số lớp tiêu biểu được dùng cho lập trình Client-Server sử dụng socket làm phương tiện giao tiếp như:

  • InetAddress:  Lớp này quản lý địa chỉ Internet bao gồm địa chỉ IP và tên máy tính.
  • Socket: Hỗ trợ các phương thức liên quan đến Socket cho chương trình Client ở chế độ có nối kết.
  • ServerSocket: Hỗ trợ các phương thức liên quan đến Socket cho chương trình Server ở chế độ có nối kết.
  • DatagramSocket: Hỗ trợ các phương thức liên quan đến Socket ở chế độ không nối kết cho cả Client và Server.
  • DatagramPacket: Lớp cài đặt gói tin dạng thư tín người dùng (Datagram Packet) trong giao tiếp giữa Client và Server ở chế độ không nối kết.

Xây dựng chương trình Client ở chế độ có nối kết 

Các bước tổng quát:

  1. Mở một socket nối kết đến server đã biết địa chỉ IP (hay tên miền) và số hiệu cổng.
  2. Lấy InputStream  và OutputStream gán với Socket.
  3. Tham khảo Protocol của dịch vụ để định dạng đúng dữ liệu trao đổi với Server.
  4. Trao đổi dữ liệu với Server nhờ vào các InputStream  và OutputStream.
  5.  Đóng Socket trước khi kết thúc chương trình.

Lớp java.net.Socket

Lớp Socket hỗ trợ các phương thức cần thiết để xây dựng các chương trình client sử dụng socket ở chế độ có nối kết. Dưới đây là một số phương thức thường dùng để xây dựng Client: 

public Socket(String HostName, int PortNumber) throws IOException

Phương thức này dùng để nối kết đến một server có tên là HostName, cổng là PortNumber. Nếu nối kết thành công, một kênh ảo sẽ được hình thành giữa Client và Server.

  • HostName: Địa chỉ IP hoặc tên logic theo dạng tên miền.
  • PortNumber: có giả trị từ 0 ..65535

Ví dụ: Mở socket và nối kết đến Web Server của khoa Công Nghệ Thông Tin, Đại Học Cần Thơ:

Socket s = new Socket(“www.cit.ctu.edu.vn”,80);Hoặc: Socket s = new Socket(“203.162.36.149”,80);

public InputStream getInputStream()

Phương thức này trả về InputStream nối với Socket. Chương trình Client dùng InputStream này để nhận dữ liệu từ Server gởi về.

Ví dụ: Lấy InputStream của Socket s:

            InputStream is = s.getInputStream();

public OutputStream getOutputStream()

Phương thức này trả về OutputStream nối với Socket. Chương trình Client dùng OutputStream này để gởi dữ liệu cho Server.

Ví dụ: Lấy OutputStream của Socket s:

            OutputStream os = s.getOutputStream();

public close()

Phương thức này sẽ đóng  Socket lại, giải phóng kênh ảo, xóa nối kết giữa Client và Server.

Ví dụ: Đóng Socket s:

            s.close();

Chương trình TCPEchoClient

Trên hệ thống UNIX, Dịch vụ Echo được thiết kế theo kiến trúc Client-Server sử dụng Socket làm phương tiện giao tiếp. Cổng mặc định dành cho Echo Server là 7, bao gồm cả hai chế độ có nối kết và không nối kết.

Chương trình TCPEchoClient sẽ nối kết đến EchoServer ở chế độ có nối kết, lần lượt gởi đến Echo Server 10 ký tự từ ‘0’ đến '9', chờ nhận kết quả trả về và hiển thị chúng ra màn hình.

Hãy lưu chương trình sau vào tập tin TCPEchoClient.java

import java.io.*;import java.net.Socket;public class TCPEchoClient{    public static void main(String args[]){        try {            Socket s = new Socket(args[0],7);            // Nối kết đến Server            InputStream is = s.getInputStream();         // Lấy InputStream            OutputStream os = s.getOutputStream(); // Lấy OutputStream            for (int i='0'; i<='9';i++){     // Gui ‘0’ ->’9’ den EchoServer                os.write(i);                               // Gởi 1 ký tự sang Server                 int ch = is.read();                    // Chờ nhận 1 ký tự từ Server                System.out.print((char)ch);   // In ký tự nhận được ra màn hình            }        }     //try        catch(IOException ie){            System.out.println("Loi: Khong tao duoc socket");        }     //catch    }     //main}

Biên dịch và thực thi chương trình như sau:

Kết quả biên dịch chương trình TCPEchoClient.java

Chương trình này nhận một đối số là địa chỉ IP hay tên miền của máy tính mà ở đó Echo Server đang chạy. Trong hệ thống mạng TCP/IP mỗi máy tính được gán một địa chỉ IP cục bộ là 127.0.0.1 hay có tên là localhost. Trong ví dụ trên, chương trình Client  nối kết đến Echo Server trên cùng máy với nó.

Xây dựng chương trình Server ở chế độ có nối kết 

Lớp java.net.ServerSocket

Lớp ServerSocket hỗ trợ các phương thức cần thiết để xây dụng các chương trình Server sử dụng socket ở chế độ có nối kết. Dưới đây là một số phương thức thường dùng để xây dựng Server: 

public ServerSocket(int PortNumber);

Phương thức này tạo một Socket với số hiệu cổng là PortNumber mà sau đó Server sẽ lắng nghe trên cổng này.

Ví dụ: Tạo socket cho Server với số hiệu cổng là 7:             ServerSocket ss = new ServerSocket(7);

public Socket accept()

Phương thức này lắng nghe yêu cầu nối kết của các Client. Đây là một phương thức hoạt động ở chế độ nghẽn. Nó sẽ bị nghẽn cho đến khi có một yêu cầu nối kết của client gởi đến. 

Khi có yêu cầu nối kết của Client gởi đến, nó sẽ chấp nhận yêu cầu  nối kết, trả về một Socket là một đầu của kênh giao tiếp ảo giữa Server và Client yêu cầu nối kết.

Ví dụ: Socket ss chờ nhận yêu cầu nối kết:

            Socket s = ss.accept(); 

Server sau đó sẽ lấy InputStream và OutputStream của Socket mới s để giao tiếp với Client.

Xây dựng chương trình Server phục vụ tuần tự 

Một Server có thể được cài đặt để phục vụ các Client theo hai cách: phục vụ tuần tự hoặc phục vụ song song.

Trong chế độ phục vụ tuần tự, tại một thời điểm Server chỉ chấp nhận một yêu cầu nối kết. Các yêu cầu nối kết của các Client khác đều không được đáp ứng (đưa vào hàng đợi).

Ngược lại trong chế độ phục vụ song song, tại một thời điểm Server chấp nhận nhiều yêu cầu nối kết và phục vụ nhiều Client cùng lúc. 

Các bước tổng quát của một Server phục vụ tuần tự

  1. Tạo socket và gán số hiệu cổng cho server.
  2. Lắng nghe yêu cầu nối kết.
  3. Với một yêu cầu nối kết được chấp nhận thực hiện các bước sau:
    • Lấy InputStream và OutputStream gắn với Socket của kênh ảo vừa được hình thành.
    • Lặp lại công việc sau:
      • Chờ nhận các yêu cầu (công việc).
      • Phân tích và thực hiện yêu cầu.
      • Tạo thông điệp trả lời.
      • Gởi thông điệp trả lời về Client.
      • Nếu không còn yêu cầu hoặc Client kết thúc, đóng Socket và quay lại bước 2.

Chương trình STCPEchoServer

STCPEchoServer cài đặt một Echo Server phục vụ tuần tự ở chế độ có nối kết. Server lắng nghe trên cổng mặc định số 7.

Hãy lưu chương trình sau vào tập tin STCPEchoServer.java

import java.net.*;import java.io.*;public class STCPEchoServer {        public final static int defaultPort = 7;    public static void main(String[] args) {        try {            ServerSocket ss = new ServerSocket(defaultPort);                while (true) {                    try {                         Socket s = ss.accept();                         OutputStream os = s.getOutputStream();                        InputStream is = s.getInputStream();                        int ch=0;                        while(true) {                             ch = is.read();                            if(ch == -1) break;                            os.write(ch);                        }                         s.close();                     }                     catch (IOException e) {                        System.err.println("  Connection Error: "+e);                    }                 }          }         catch (IOException e) {             System.err.println(" Server Creation Error:"+e);        }    } } 

Biên dịch và thực thi chương trình theo cách sau:

Kết quả biên dịch chương trình STCPEchoServer.java

Mở một cửa số DOS khác và thực thi chương trình TCPEchoClient ta có kết quả như sau:

Kết quả biên dịch chương trình TCPEchoClient.java

Hai chương trình này có thể nằm trên hai máy khác nhau. Trong trường hợp đó khi thực hiện chương trình TCPEchoClient phải chú ý nhập đúng địa chỉ IP của máy tính đang chạy chương trình STCPEchoServer.

Xem địa chỉ IP của một máy tính Windows bằng lệnh ipconfig.

Server phục vụ song song 

Các bước tổng quát của một Server phục vụ song song

Server phục vụ song song gồm 2 phần thực hiện song song nhau:

  • Phần 1: Xử lý các yêu cầu nối kết.
  • Phần 2: Xử lý các thông điệp yêu cầu từ khách hàng.

Có cấu trúc như hình sau,  trong đó Phần 1 là (Dispatcher Thread), Phần 2 là các (Worker Thread) 

Phần 1: Lặp lại các công việc sau:

  • Lắng nghe yêu cầu nối kết của khách hàng.
  • Chấp nhận một yêu cầu nối kết.
    • Tạo kênh giao tiếp ảo mới với khách hàng.
    • Tạo Phần 2 để xử lý các thông điệp yêu cầu của khách hàng.

Phần 2: Lặp lại các công việc sau:

Server ở chế độ song song
  • Chờ nhận thông điệp yêu cầu của khách hàng.
  • Phân tích và xử lý yêu cầu.
  • Gởi thông điệp trả lời cho khách hàng.

Phần 2 sẽ kết thúc khi kênh ảo bị xóa đi.

Với mỗi Client, trên Server sẽ có một Phần 2 để xử lý yêu cầu của khách hàng. Như vậy tại một thời điểm bất kỳ luôn tồn tại 1 Phần 1 và 0 hoặc nhiều Phần 2.

Do Phần 2 thực thi song song với Phần 1 cho nên nó được thiết kế là một Thread.

Chương trình PTCPEchoServer

PTCPEchoServer cài đặt một Echo Server phục vụ song song ở chế độ có nối kết. Server lắng nghe trên cổng mặc định là 7. Chương trình này gồm 2 lớp:

  • Lớp TCPEchoServer, cài đặt các chức năng của Phần 1 - xử lý các yêu cầu nối kết của TCPEchoClient.
  • Lớp RequestProcessing, là một Thread cài đặt các chức năng của Phần 2 - Xử lý các thông điệp yêu cầu.

Hãy lưu chương trình sau vào tập tin PTCPEchoServer.java

import java.net.*;import java.io.*;public class PTCPEchoServer {    public final static int defaultPort = 7; // Cổng mặc định    public static void main(String[] args) {        try {            ServerSocket ss = new ServerSocket(defaultPort); //Tạo socket cho server            while (true) {                try {                    Socket s = ss.accept(); // Lắng nghe các yêu cầu nối kết                    RequestProcessing rp = new RequestProcessing(s); // Tạo phần xử lý                     rp.start(); // Khởi động phần xử lý cho Client hiện tại                }                 catch (IOException e) {                    System.out.println("Connection Error: "+e);                }             }        }        catch (IOException e) {            System.err.println("Create Socket Error: "+e);        }     } }

class RequestProcessing extends Thread {    Socket channel; //Socket của kênh ảo nối với Client hiện tại    public RequestProcessing(Socket s){        channel = s; // Nhận socket của kênh ảo nối với Client    }    public void run() {        try {            OutputStream os = channel.getOutputStream();            InputStream is = channel.getInputStream();            while (true) {                int n = is.read();        // Nhận ký tự từ Client                if (n == -1) break;    // Thoát nếu kênh ảo bị xóa                os.write(n);              //  Gởi ký tự nhận được về Client            }        }        catch (IOException e) {            System.err.println("Request Processing Error: "+e);        }    }}

Biên dịch và thực thi chương trình như sau:

Kết quả biên dịch chương trình PTCPEchoServer.java

Sau đó mở thêm 2 của sổ DOS khác để thực thi chương trình TCPEchoClient nối kết tới PTCPEchoServer. Ta sẽ nhận thấy rằng PTCPEchoServer có khả năng phục vụ đồng thời nhiều Client.

Xây dựng chương trình Client - Server ở chế độ không nối kết 

Khi sử dụng socket, ta có thể chọn giao thức UDP cho lớp vận chuyển. UDP viết tắt của User Datagram Protocol, cung cấp cơ chế vận chuyển không bảo đảm và không nối kết trên mạng IP, ngược với giao thức vận chuyển tin cậy, có nối kết TCP. 

Cả giao thức TCP và UDP đều phân dữ liệu ra thành các gói tin. Tuy nhiên TCP có thêm vào những tiêu đề (Header) vào trong gói tin để cho phép truyền lại những gói tin thất lạc và tập hợp các gói tin lại theo thứ tự đúng đắn. UDP không cung cấp tính năng này, nếu một gói tin bị thất lạc hoặc bị lỗi, nó sẽ không được truyền lại, và thứ tự đến đích của các gói tin cũng không giống như thứ tự lúc nó được gởi đi. 

Tuy nhiên, về tốc độ, UDP sẽ truyền nhanh gấp 3 lần TCP. Cho nên chúng thường được dùng trong các ứng dụng đòi hỏi thời gian truyền tải ngắn và không cần tính chính xác cao, ví dụ truyền âm thanh, hình ảnh . . .

Mô hình client - server sử dụng lớp ServerSocket và Socket ở trên sử dụng giao thức TCP. Nếu muốn sử dụng mô hình client - server với giao thức UDP, ta sử dụng hai lớp java.net.DatagramSocket và java.net.DatagramPacket.

DatagramSocket được sử dụng để truyền và nhận các DatagramPacket. Dữ liệu được truyền đi là một mảng những byte, chúng được gói vào trong lớp DatagramPacket. Chiều dài của dữ liệu tối đa có thể đưa vào DatagramPacket là khoảng 60.000 byte (phụ thuộc vào dạng đường truyền). Ngoài ra DatagramPacket còn chứa địa chỉ IP và cổng của quá trình gởi và nhận dữ liệu.

Cổng trong giao thức TCP và UDP có thể trùng nhau. Trên cùng một máy tính, bạn có thể gán cổng 20 cho socket dùng giao thức TCP và cổng 20 cho socket sử dụng giao thức UDP. 

Lớp DatagramPacket

Lớp này dùng để đóng gói dữ liệu gởi đi. Dưới đây là các phương thức thường sử dụng để thao tác trên dữ liệu truyền / nhận qua DatagramSocket.

public DatagramPacket(byte[] b, int n)

  • Là phương thức khởi tạo, cho phép tạo ra một DatagramPacket chứa n bytes dữ liệu đầu tiên của mảng b. (n phải nhỏ hơn chiều dài của mảng b)
  • Phương thức trả về một đối tượng thuộc lớp DatagramePacket

Ví dụ: Tạo DatagramPacket để nhận dữ liệu:

        byte buff[] = new byte[60000];    // Nơi chứa dữ liệu nhận được        DatagramPacket inPacket = new Datagrampacket(buff, buff.lenth);

public DatagramPacket(byte[] b, int n, InternetAddress ia, int port)

  • Phương thức này cho phép tạo một DatagramPacket chứa dữ liệu và cả địa chỉ của máy nhận dữ liệu.
  • Phương thức trả về một đối tượng thuộc lớp DatagramePacket

Ví dụ: Tạo DatagramPacket chứa chuỗi "My second UDP Packet", với địa chỉ máy nhận là www.cit.ctu.edu.vn,  cổng của quá trình nhận là 19:

try {//Địa chỉ Internet của máy nhận             InetAddress ia = InetAddess.getByName("www.cit.ctu.edu.vn");             int port = 19; //    Cổng của socket nhận             String s = "My second UDP Packet";    // Dữ liệu gởi đi             byte[] b = s.getBytes();    // Đổi chuỗi thành mảng bytes

// Tạo gói tin gởi đi

DatagramPacket outPacket = new DatagramPacket(b, b.length, ia, port);         }        catch (UnknownHostException e) {             System.err.println(e);         }

Các phương thức lấy thông tin trên một DatagramPacket nhận được

Khi nhận được một DatagramPacket từ một quá trình khác gởi đến, ta có thể lấy thông tin trên DatagramPacket này bằng các phương thức sau:

  • public synchronized() InternetAddress getAddress() : Địa chỉ máy gởi
  • public synchronized() int getPort() : Cổng của quá trình gởi
  • public synchronized() byte[] getData() : Dữ liệu từ gói tin
  • public synchronized() int getLength() : Chiều dài của dữ liệu trong gói tin

Các phương thức đặt thông tin cho gói tin gởi

Trước khi gởi một DatagramPacket đi, ta có thể đặt thông tin trên DatagramPacket này bằng các phương thức sau:

  • public synchronized() void setAddress(IntermetAddress dis) : Đặt địa chỉ máy nhận.
  • public synchronized() void setPort(int port) : Đặt cổng quá trình nhận
  • public synchronized() void setData(byte buffer[]) : Đặt dữ liệu gởi
  • public synchronized() void setLength(int len) : Đặt chiều dài dữ liệu gởi

Lớp DatagramSocket

Lớp này hỗ trợ các phương thức sau để gởi / nhận các DatagramPacket

public DatagramSocket() throws SocketException

  • Tạo Socket kiểu không nối kết cho Client. Hệ thống tự động gán số hiệu cổng chưa sử dụng cho socket.

Ví dụ:  Tạo một socket không nối kết cho Client:

try{             DatagramSocket ds = new DatagramSocket();         } catch(SocketException se) {             System.out.print("Create DatagramSocket Error: "+se);         }

public DatagramSocket(int port) throws SocketException

  • Tạo Socket kiểu không nối kết cho Server với số hiệu cổng được xác định trong tham số (port).

Ví dụ:  Tạo một socket không nối kết cho Server với số hiệu cổng là 7:

try{             DatagramSocket dp = new DatagramSocket(7);         } catch(SocketException se) {             System.out.print("Create DatagramSocket Error: "+se);         }

public void send(DatagramPacket dp) throws IOException

  • Dùng để gởi một DatagramPacket đi.

Ví dụ: Gởi chuỗi "My second UDP Packet", cho quá trình ở địa  chỉ www.cit.ctu.edu.vn,  cổng nhận là 19:

        try {            DatagramSocket ds = new DatagramSocket(); //Tạo Socket

//Địa chỉ Internet của máy nhận             InetAddress ia = InetAddess.getByName("www.cit.ctu.edu.vn");             int port = 19; //    Cổng của quá trình  nhận             String s = "My second UDP Packet";     // Dữ liệu cần gởi             byte[] b = s.getBytes();     // Đổi sang mảng bytes

// Tạo gói tin           DatagramPacket outPacket = new DatagramPacket(b, b.length, ia, port);           ds.send(outPacket); // Gởi gói tin đi         }         catch (IOException e) {             System.err.println(e);         }

public synchronized void receive(Datagrampacket dp)  throws IOException

  • Chờ nhận một DatagramPacket. Quá trình sẽ bị nghẽn cho đến khi có dữ liệu đến.

Ví dụ: 

try {             DatagramSocket ds = new DatagramSocket(); //Tạo Socket              byte[] b = new byte[60000];    // Nơi chứa dữ liệu nhận được             DatagramPacket inPacket = new DatagramPacket(b, b.length);   // Tạo gói tin             ds.receive(inPacket); // Chờ nhận gói tin         }         catch (IOException e) {             System.err.println(e);         }

Chương trình UDPEchoServer

Chương trình UDPEchoServer cài đặt Echo Server ở chế độ không nối kết, cổng mặc định là 7. Chương trình chờ nhận từng gói tin, lấy dữ liệu ra khỏi gói tin nhận được và gởi ngược dữ liệu đó về Client.

Lưu chương trình sau vào tập tin UDPEchoServer.java

import java.net.*;import java.io.*;public class UDPEchoServer {     public final static int port = 7; // Cổng mặc định của Server     public static void main(String[] args) {         try {            DatagramSocket ds = new DatagramSocket(port); // Tạo Socket với cổng là 7             byte[] buffer = new byte[6000]; // Vùng đệm chứa dữ liệu cho gói tin nhận              while(true) { // Tạo gói tin nhận                 DatagramPacket incoming = new DatagramPacket(buffer,buffer.length);                 ds.receive(incoming); // Chờ nhận gói tin gởi đến // Lấy dữ liệu khỏi gói tin nhận                 String theString = new String(incoming.getData(),0,incoming.getLength());                    // Tạo gói tin gởi chứa dữ liệu vừa nhận được                 DatagramPacket outsending = new DatagramPacket(theString.getBytes(),                  incoming.getLength(),incoming.getAddress(), incoming.getPort());                  ds.send(outsending);             }         }         catch (IOException e) {             System.err.println(e);         }      } } 

Biên dịch và thực thi chương trình như sau

 

Kết quả biên dịch chương trình UDPEchoServer.java

Chương trình UDPEchoClient

Chương trình này cho phép người sử dụng nhận các chuỗi từ bàn phím, gởi chuỗi sang EchoServer ở chế độ không nối kết ở cổng số 7, chờ nhận và in dữ liệu từ Server gởi về ra màn hình.

Lưu chương trình sau vào tập tin UDPEchoClient.java

import java.net.*;import java.io.*;public class UDPEchoClient extends Object{        public final static int serverPort = 7; // Cổng mặc định của Echo Server    public static void main(String[] args) {        try {            if (args.length ==0) { // Kiểm tra tham số, là địa chỉ của Server  System.out.print("Syntax: java UDPClient HostName");              return;            }             DatagramSocket ds = new DatagramSocket();     // Tạo DatagramSocket             InetAddress server = InetAddress.getByName(args[0]);   // Địa chỉ Server             while(true) {                 InputStreamReader isr = new InputStreamReader(System.in);  // Nhập                     BufferedReader br = new BufferedReader(isr);    // một chuỗi                 String theString = br.readLine();        // từ bàn phím byte[] data = theString.getBytes();     // Đổi chuỗi ra mảng bytes // Tạo gói tin gởi                 DatagramPacket dp = new DatagramPacket(data,data.length,server, serverPort);                 ds.send(dp); // Send gói tin sang Echo Server                  byte[] buffer = new byte[6000];     // Vùng đệm cho dữ liệu nhận // Gói tin nhận                 DatagramPacket incoming = new DatagramPacket(buffer, buffer.length); ds.receive(incoming); // Chờ nhận dữ liệu từ EchoServer gởi về                 // Đổi dữ liệu nhận được dạng mảng bytes ra chuỗi và in ra màn hình                 System.out.println(new String(incoming.getData(), 0, incoming.getLength()));              }         }         catch (IOException e) {             System.err.println(e);         }     } } 

Biên dịch và thực thi chương trình như sau:

Kết quả biên dịch chương trình UDPEchoClient.java

Chú ý, khi thực hiện chương trình UDPEchoClient phải đưa vào đối số là địa chỉ của máy tính đang thực thi chương trình UDPEchoServer. Trong ví dụ trên, Server và Client cùng chạy trên một máy nên địa chỉ của UDPEchoServer là localhost (hay 127.0.0.1). Nếu UDPEchoServer chạy trên máy tính khác thì khi thực thi, ta phải biết được địa chỉ IP của máy tính đó và cung cấp vào đối số của chương trình. Chẳng hạn, khi UDPEchoServer đang phục vụ trên máy tính ở địa chỉ 172.18.250.211, ta sẽ thực thi UDPEchoClient theo cú pháp sau:

java UDPEchoClient 172.18.250.211

Mục lục
Đánh giá:
0 dựa trên 0 đánh giá
Nội dung cùng tác giả
 
Nội dung tương tự