Lập trình phân tán – Lập trình Socket trong Java

1. Tổng quan

Lập trình Socket là cách viết chương trình thực trên nhiều thiết bị máy tính được kết nối với nhau sử dụng mạng.

Hai kiểu giao thức giao tiếp  có thể sử dụng lập trình cho socket: giao thức UDP (User Datagram Protocol) và giao thức TCP (Tranfer Control Protocol).

Sự khác nhau căn bản giữa hai kiểu giao thức đó là UDP  là không kết nối, có nghĩa là không có phiên (session) giữa client và server, trong khi TCP lại theo hướng kết nối, nghĩa là một kết nối riêng phải được thiết lập giữa client và server trước khi giao dịch được diễn ra.

Bài hướng dẫn này sẽ hướng dẫn lập trình Socket qua mạng TCP/IP và hướng dẫn cách viết ứng dụng client/server trong Java.

2. Cài đặt Project



Java cung cấp một tập các class và các interface để thực hiện giao tiếp cấp thấp giữa client và server.

Hầu hết nó nằm trong gói java.net, vì vậy chúng ta có thể import theo câu lệnh:


import java.net.*;

Chúng ta cũng cần gói java.io để chúng ta thực hiện vào ra (input/output)  để đọc và ghi khi giao tiếp:


import java.io.*;

Vì mục đích đơn giản, chúng ta sẽ chạy các chương trình client và server trên cùng một máy tính. Nếu chúng ta muốn thực thi chúng trên các máy tính nối mạng khác, thì chỉ cần thay đổi là địa chỉ IP, trong trường hợp này, chúng ví dụ này chúng ta sẽ sử dụng localhost với địa chỉ IP: 127.0.0.1.

3. Ví dụ đơn giản

Viết một ứng dụng đơn giản client/server, thực hiênj giao tiếp giữa client và server, client gửi lời chào tới server và server sẽ phản hồi lại.

3.1. Code phía server


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class GreetServer {
    private ServerSocket serverSocket;
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void start(int port) {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        String greeting = in.readLine();
            if ("hello server".equals(greeting)) {
                out.println("hello client");
            }
            else {
                out.println("unrecognised greeting");
            }
    }

    public void stop() {
        in.close();
        out.close();
        clientSocket.close();
        serverSocket.close();
    }
    public static void main(String[] args) {
        GreetServer server=new GreetServer();
        server.start(6666);
    }
}

3.2. Code ở phía Client


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;

public class GreetClient {
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void startConnection(String ip, int port) {
        clientSocket = new Socket(ip, port);
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
    }

    public String sendMessage(String msg) {
        out.println(msg);
        String resp = in.readLine();
        return resp;
    }

    public void stopConnection() {
        in.close();
        out.close();
        clientSocket.close();
    }
    //Chạy chương trình client
    public static void main(String[] args) {
        GreetClient client = new GreetClient();
        client.startConnection("127.0.0.1", 6666);
        String response = client.sendMessage("hello server");
        assertEquals("hello client", response);
    }
}

Trong phần tới, chúng ta sẽ giải thích chi tiết từng phần giao tiếp sử dụng giao tiếp bằng Socket qua ví dụ này.

4. Cách làm việc của Socket

Theo định nghĩa, Socket là một điểm cuối (endpoint) của liên kết giao tiếp hai chiều giữa hai chương trình chạy trên các máy tính khác nhau trong mạng. Socket được liên kết với một cổng (port) để tầng Transport có thể xác định dữ liệu của ứng dụng này được gửi tới.

4.1. Server

Thông thường, server chạy trên một máy tính cố định trên mạng và có một socket được gắn với một cổng cố định. Trong ví dụ trên, cả client chạy trên cùng máy tính và kết nối tới cổng 6666.


ServerSocket serverSocket = new ServerSocket(6666);

Server sẽ đợi, lắng nghe ở socket để client tạo một yêu cầu kết nối. Điều này xảy ra ở dòng kế tiếp:


Socket clientSocket = serverSocket.accept();

Máy chủ gọi phương thức accept(), nó sẽ chặn cho đến khi client đưa ra yêu cầu kết nối với nó.

Nếu mọi thứ diễn ra tốt đẹp, Server chấp nhận kết nối. Sau khi chấp nhận, Server nhận một socket mới, clientSocket, được liên kết tới cùng cổng cục bộ là 6666 và cũng có endpoint truy cập từ xa tới địa chỉ và cổng của client.
Tại điểm này, một đối tượng Socket mới đặt server một kết nối trực tiếp với client, chúng ta có thể truy cập các luồng output và input để ghi và nhận message tới và từ client tương ứng.


PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

Từ đây server có khả năng trao đổi message với client liên tục cho đến khi socket đóng các luồng của nó.

Tuy nhiên, trong ví dụ này, server chỉ có thể gửi phản hồi lời chào trước khi đóng kết nối, điều này có nghĩa là nếu chúng ta chạy lại, kết nối sẽ bị từ chối.

Để cho phép giao tiếp liên tục, chúng ta sẽ phải đọc từ luồng đầu vào bên trong vòng lặp while và chỉ thoát ra khi máy khách gửi yêu cầu kết thúc, chúng ta sẽ hiệu chỉnh trong phần sau.

Với mỗi client mới, server cần một socket mới được trả về bởi lời gọi accept(). serverSocket được sử dụng để tiếp tục lắng nghe các yêu cầu kết nối khi các client kết nối tới.

4.2. Client

Client phải biết tên máy chủ hoặc IP của máy chủ đang chạy và cổng (port) mà máy chủ đang lắng nghe.

Để thực hiện yêu cầu kết nối, client sẽ cố gắng kết nối máy chủ và cổng của mở trên máy chủ:


Socket clientSocket = new Socket("127.0.0.1", 6666);

Đoạn code trên sẽ tạo một socket mới khi server đã chấp nhận kết nối, nếu không, sẽ nhận được exception bị từ chối kết nối. Khi được tạo thành công, chúng có thể lấy các luồng input và output để giao tiếp với server:


PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

Luồng input của client được kết nối với luồng output của server, giống như luồng input của server được kết nối với luồng output của client.

5. Giao tiếp liên tục

Chúng ta sẽ cập nhật code để server lắng nghe liên tục các client tới, giống như chúng ta thực thi một chương trình chat server.
Chúng ta sẽ tạo một vòng lặp while tiếp tục giữ luồng input server cho các message tới.
Chúng ta sẽ tạo một lớp EchoServer.java nhiệm vụ sẽ gửi lại bất kỳ message nào nó nhận được từ client:


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class EchoServer {
    public void start(int port) {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        
        String inputLine;
        while ((inputLine = in.readLine()) != null) {
        if (".".equals(inputLine)) {
            out.println("good bye");
            break;
         }
         out.println(inputLine);
    }
}

Server trong trường hợp này kết thúc lắng nghe khi nhận được ký tự dấu chấm (“.”).
Các phương thức khác của EchoServer giống như GreetServer.
Các bạn hoàn thiện chương trình EchoClient tương tự như GreetClient và chạy thử chương trình.

6. Server với nhiều Client

Cải tiến code trên để server có thể phục vụ nhiều client và nhiều yêu cầu đồng thời cùng lúc.


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class EchoMultiServer {
    private ServerSocket serverSocket;

    public void start(int port) {
        serverSocket = new ServerSocket(port);
        while (true)
            new EchoClientHandler(serverSocket.accept()).start();
    }

    public void stop() {
        serverSocket.close();
    }

    private static class EchoClientHandler extends Thread {
        private Socket clientSocket;
        private PrintWriter out;
        private BufferedReader in;

        public EchoClientHandler(Socket socket) {
            this.clientSocket = socket;
        }

        public void run() {
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            in = new BufferedReader(
              new InputStreamReader(clientSocket.getInputStream()));
            
            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                if (".".equals(inputLine)) {
                    out.println("bye");
                    break;
                }
                out.println(inputLine);
            }

            in.close();
            out.close();
            clientSocket.close();
    }
}

Chú ý rằng chúng ta gọi phương thức accept bên trong vòng lặp while. Mỗi khi vùng lặp while thực thi, nó sẽ khoá lời gọi accept cho tới khi một client mới được kết nối, rồi một luồng handler thực hiện. EchoClientHandler được tạo cho client này.
Những gì xảy ra bên trong thread này giống như chúng ta đã làm trong EchoServer ở ví dụ trước mà chúng ta chỉ thực hiện với một client.
EchoMultiServer ủy quyền công việc này cho EchoClientHandler để nó có thể tiếp tục lắng nghe nhiều client hơn trong vòng lặp while.
Chúng ta vẫn sử dụng EchoClient để chạy thử kết nối tới server. Chúng ta sẽ tạo nhiều client để gửi và nhận nhiều message từ server.
Code tham khảo EchoClient:


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;


public class EchoClient {
	
	Socket clientSocket;
	PrintWriter out;
	BufferedReader in;
	public void startConnection(String ip, int port) throws UnknownHostException, IOException
	{
		clientSocket = new Socket(ip, port);
		out = new PrintWriter(clientSocket.getOutputStream(),true);
		in= new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
	}
	
	public String sendMessage(String msg) throws IOException
	{
		out.println(msg);
		String resp = in.readLine();
		return resp;
	}
	
	public void stopConnection() throws IOException
	{
		in.close();
		out.close();
		clientSocket.close();
	}

	public static void main(String[] args) throws UnknownHostException, IOException {
		// TODO Auto-generated method stub
		EchoClient client = new EchoClient();
		client.startConnection("127.0.0.1", 6666);
		String inputLine;
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); 
		while ((inputLine = br.readLine()) != null) {
			System.err.println("Client gửi:" + inputLine);
			String resp = client.sendMessage(inputLine);
			System.out.println(resp);
		}
	}

}

7. Bài tập

Bài tập 1

Viết chương trình Chat-Room:

  • Server sẽ lắng nghe tại một địa chỉ x.x.x.x và cổng 3000.
  • Các client kết nối đến server và có thể gửi message, thông tin message gửi sẽ bao gồm: tên client + nội dung message + thời gian gửi.
  • Khi một client gửi message thì các client khác cũng sẽ nhận được.
  • Khi một client thoát khỏi phòng chat hoặc join vào phòng chat, sẽ có thông báo gửi đến các client khác.

8. Kết luận

Trong hướng dẫn này, chúng tôi đã tập trung vào phần giới thiệu về lập trình socket qua TCP/IP và viết một ứng dụng Client/Server đơn giản bằng Java.

Có thể bạn sẽ thích…

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

EnglishVietnamese