Lập trình mạng: Xây dựng ứng dụng Client- Server ở chế độ TCP (Có kết nối) Java

Đăng bởi: Admin | Lượt xem: 9338 | Chuyên mục: Java

Socket là phương tiện hiệu quả để xây dựng các ứng dụng theo kiến trúc Client-Server. Trong bài viết này, chúng ta sẽ tìm hiểu các bước cơ bản trong việc xây dựng các ứng dụng Client-Server sử dụng Socket làm phương tiện giao tiếp theo chế độ có nối kết (TCP – Transmission Control Protocol)


1. Mô hình Client-Server sử dụng Socket ở chế độ có nối kết (TCP)

Có thể phân thành 4 giai đoạn như sau:

  • Giai đoạn 1: Server tạo Socket, gán số hiệu cổng và lắng nghe yêu cầu nối kết. Server sẵn sàng phục vụ Client.socket(): Server yêu cầu tạo một socket để có thể sử dụng các dịch vụ của tầng vận chuyển.
    • bind(): Server yêu cầu gán số hiệu cổng (port) cho socket.
    • listen(): Server lắng nghe các yêu cầu nối kết từ các client trên cổng đã được gán.
  • Giai đoạn 2: Client tạo Socket, yêu cầu thiết lập một nối kết với Server.
    • socket(): Client yêu cầu tạo một socket để có thể sử dụng các dịch vụ của tầng vận chuyển, thông thường hệ thống tự động gán một số hiệu cổng còn rảnh cho socket của Client.
    • connect(): Client gởi yêu cầu nối kết đến server có địa chỉ IP và Port xác định.
    • accept(): Server chấp nhận nối kết của client, khi đó một kênh giao tiếp ảo được hình thành, Client và server có thể trao đổi thông tin với nhau thông qua kênh ảo này.
  • Giai đoạn 3: Trao đổi thông tin giữa Client và Server.
    • Sau khi chấp nhận yêu cầu nối kết, thông thường server thực hiện lệnh read() và nghẽn cho đến khi có thông điệp yêu cầu (Request Message) từ client gởi đến.
    • Server phân tích và thực thi yêu cầu. Kết quả sẽ được gởi về client bằng lệnh write().
    • Sau khi gởi yêu cầu bằng lệnh write(), client chờ nhận thông điệp kết quả (ReplyMessage) từ server bằng lệnh read().
  • Giai đoạn 4: Kết thúc phiên làm việc.
    • Các câu lệnh read(), write() có thể được thưc hiện nhiều lần (ký hiệu bằng hình ellipse).
    • Kênh ảo sẽ bị xóa khi Server hoặc Client đóng socket bằng lệnh close().

2. Xây dựng ứng dụng Client-Server với Socket trong Java

Thông qua các lớp trong gói java.net, các chương trình Java có thể sử dụng TCP hoặc UDP để giao tiếp qua Internet.

  • Lớp IntetAddress:  Lớp này quản lý địa chỉ Internet bao gồm địa chỉ IP và tên máy tính.
  • Lớp 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.
  • Lớp 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.
  • Lớp 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.
  • Lớp 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.

3. Xây dựng chương trình Client – Server ở chế độ có nối kết (TCP)

Client sẽ gởi lần lượt các số từ 0 đến 9 tới Server. Server lần lượt sẽ gởi các số nhận được về Client.

Các bước tổng quát xây dựng một chương trình Client – Server ở chế độ có nối kết như sau:

  • 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.
  • Lấy InputStream và OutputStream gán với Socket.
  • Tham khảo Protocol của dịch vụ để định dạng đúng dữ liệu trao đổi với Server.
  • Trao đổi dữ liệu với Server nhờ vào các InputStream và OutputStream.
  • Đóng Socket trước khi kết thúc chương trình.

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

Một số 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:

  • public Socket(String HostName, int PortNumber) : 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
  • 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ề.
  • 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.
  • 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.

Code của chương trình Client như sau:


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
 
public class EchoChatClient {
    public final static String SERVER_IP = "127.0.0.1";
    public final static int SERVER_PORT = 7;
 
    public static void main(String[] args) throws IOException, InterruptedException {
        Socket socket = null;
        try {
            socket = new Socket(SERVER_IP, SERVER_PORT); // Connect to server
            System.out.println("Connected: " + socket);
 
            InputStream is = socket.getInputStream();
            OutputStream os = socket.getOutputStream();
            for (int i = '0'; i <= '9'; i++) {
                os.write(i); // Send each number to the server
                int ch = is.read(); // Waiting for results from server
                System.out.print((char) ch + " "); // Display the results received from the server
                Thread.sleep(200);
            }
        } catch (IOException ie) {
            System.out.println("Can't connect to server");
        } finally {
            if (socket != null) {
                socket.close();
            }
        }
    }
}

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

Một số 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.:

  • 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.
  • 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.
  • public InputStream getInputStream() : Phương thức này trả về InputStream nối với Socket. Chương trình Server dùng InputStream này để nhận dữ liệu từ Client gởi đến.
  • public OutputStream getOutputStream() :  Phương thức này trả về OutputStream nối với Socket. Chương trình Server dùng OutputStream này để trả dữ liệu cho Client.
  • 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.

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.

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

Các bước thực hiện như sau:

  • Tạo socket và gán số hiệu cổng cho server.
  • Lắng nghe yêu cầu nối kết.
  • 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.

Code của chương trình Server như sau:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
 
public class EchoChatSingleServer {
 
    public final static int SERVER_PORT = 7;
 
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = null;
        try {
            System.out.println("Binding to port " + SERVER_PORT + ", please wait  ...");
            serverSocket = new ServerSocket(SERVER_PORT);
            System.out.println("Server started: " + serverSocket);
            System.out.println("Waiting for a client ...");
            while (true) {
                try {
                    Socket socket = serverSocket.accept();
                    System.out.println("Client accepted: " + socket);
 
                    OutputStream os = socket.getOutputStream();
                    InputStream is = socket.getInputStream();
                    int ch = 0;
                    while (true) {
                        ch = is.read(); // Receive data from client
                        if (ch == -1) {
                            break;
                        }
                        os.write(ch); // Send the results to client
                    }
                    socket.close();
                } catch (IOException e) {
                    System.err.println(" Connection Error: " + e);
                }
            }
        } catch (IOException e1) {
            e1.printStackTrace();
        } finally {
            if (serverSocket != null) {
                serverSocket.close();
            }
        }
    }
 
}

Chạy chương trình Server, ta có kết quả sau:

Tiếp tục chạy chương trình Client, ta có kết quả sau:

Xem lại cửa sổ console của Server, ta thấy kết quả như sau:

Xây dựng chương trình Server phục vụ song song

Server phục vụ song song có thể chia thành 2 phần thực hiện song song nhau:

  • Phần 1 (Dispatcher Thread) : Xử lý các yêu cầu nối kết.  Lặp lại các công việc sau:
    • Lắng nghe yêu cầu nối kết của Client. 
    • 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 Client, tạo Phần 2 để xử lý các thông điệp yêu cầu của Client.
  • Phần 2 (Worker Thread) : Xử lý các thông điệp yêu cầu từ khách hàng. Lặp lại các công việc sau:
    •  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.

Code của chương trình Server như sau:

EchoChatMultiServer.java

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class EchoChatMultiServer {
 
    public static final int NUM_OF_THREAD = 4;
    public final static int SERVER_PORT = 7;
 
    public static void main(String[] args) throws IOException {
        ExecutorService executor = Executors.newFixedThreadPool(NUM_OF_THREAD);
        ServerSocket serverSocket = null;
        try {
            System.out.println("Binding to port " + SERVER_PORT + ", please wait  ...");
            serverSocket = new ServerSocket(SERVER_PORT);
            System.out.println("Server started: " + serverSocket);
            System.out.println("Waiting for a client ...");
            while (true) {
                try {
                    Socket socket = serverSocket.accept();
                    System.out.println("Client accepted: " + socket);
 
                    WorkerThread handler = new WorkerThread(socket);
                    executor.execute(handler);
                } catch (IOException e) {
                    System.err.println(" Connection Error: " + e);
                }
            }
        } catch (IOException e1) {
            e1.printStackTrace();
        } finally {
            if (serverSocket != null) {
                serverSocket.close();
            }
        }
    }
 
}

WorkerThread.java

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
 
public class WorkerThread extends Thread {
    private Socket socket;
 
    public WorkerThread(Socket socket) {
        this.socket = socket;
    }
 
    public void run() {
        System.out.println("Processing: " + socket);
        try {
            OutputStream os = socket.getOutputStream();
            InputStream is = socket.getInputStream();
            while (true) {
                int ch = is.read(); // Receive data from client
                if (ch == -1) {
                    break;
                }
                os.write(ch); // Send the results to client
            }
        } catch (IOException e) {
            System.err.println("Request Processing Error: " + e);
        }
        System.out.println("Complete processing: " + socket);
    }
}

Chạy chương trình Server, ta có kết quả sau:

Chạy liên tục 3 chương trình Client, ta có kết quả như sau:

Như bạn thấy, cả 3 chương trình Client đều được Server xử lý cùng lúc.

Ở bài sau, chúng ta cùng tìm hiểu cách tạo ứng dụng Client- Server sử dụng giao thức UDP

vncoder logo

Theo dõi VnCoder trên Facebook, để cập nhật những bài viết, tin tức và khoá học mới nhất!



Khóa học liên quan

Khóa học: Java

Tổng hợp Bài tập Java có lời giải
Số bài học:
Lượt xem: 17256
Đăng bởi: Admin
Chuyên mục: Java

Lập trình Java cơ bản
Số bài học:
Lượt xem: 39769
Đăng bởi: Admin
Chuyên mục: Java