Java Spring – Tạo một socket

1. Khái quát chung

Thuật ngữ lập trình socket đề cập đến chương trình viết rằng thực hiện trên nhiều máy tính trong đó các thiết bị đều được kết nối với nhau bằng một mạng lưới.

Có hai giao thức truyền thông mà người ta có thể sử dụng cho lập trình socket: Giao thức Datagram người dùng (UDP) và Giao thức điều khiển truyền (TCP) .

Sự khác biệt chính giữa hai là UDP không kết nối, nghĩa là không có phiên giữa máy khách và máy chủ trong khi TCP là hướng kết nối, nghĩa là kết nối độc quyền trước tiên phải được thiết lập giữa máy khách và máy chủ để giao tiếp diễn ra.

Hướng dẫn này sẽ giới thiệu về lập trình socket trên các mạng TCP/IP và hướng dẫn cách viết các ứng dụng server/client trong Java. UDP không phải là một giao thức chính thống và do đó có thể không thường gặp phải.

2. Thiết lập dự án

Java cung cấp một bộ sưu tập các lớp và các giao diện xử lý các chi tiết giao tiếp mức thấp giữa máy khách và máy chủ.

Đây là những chủ yếu chứa trong gói java.net , vì vậy tôi cần phải thực hiện nhập khẩu sau đây:

import java.net.*;

Tôi cũng cần gói java.io cung cấp cho tôi luồng đầu vào và đầu ra để ghi và đọc từ khi giao tiếp:

import java.io.*;

Để đơn giản, chúng ta sẽ chạy các chương trình máy khách và máy chủ trên cùng một máy tính. Nếu chúng ta thực thi chúng trên các máy tính nối mạng khác nhau, điều duy nhất có thể thay đổi là địa chỉ IP, trong trường hợp này, chúng ta sẽ sử dụng localhost trên 127.0.0.1 .

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

Hãy nhúng chàm với các ví dụ cơ bản nhất liên quan đến một máy khách và một máy chủ . Nó sẽ là một ứng dụng truyền thông hai chiều, nơi máy khách sẽ chào đón máy chủ và máy chủ phản hồi.

Hãy tạo ứng dụng máy chủ trong một lớp có tên là GreetServer.java với đoạn mã sau.

Tôi bao gồm các phương thức Main và các biến toàn cục để thu hút sự chú ý đến cách chúng ta sẽ chạy tất cả các máy chủ trong bài viết này. Trong phần còn lại của các ví dụ trong bài viết, chúng ta sẽ bỏ qua loại mã lặp lại nhiều hơn:

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);
    }
}

Hãy tạo một ứng dụng khách gọi là GreetClient.java với mã này:

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();
    }
}

Hãy bắt đầu máy chủ; trong IDE của bạn, bạn làm điều này bằng cách đơn giản chạy nó như một ứng dụng Java.

Và bây giờ chúng ta hãy gửi lời chào đến máy chủ bằng cách sử dụng một bài kiểm tra đơn vị, xác nhận rằng máy chủ thực sự gửi một lời chào đáp ứng:

@Test
public void givenGreetingClient_whenServerRespondsWhenStarted_thenCorrect() {
    GreetClient client = new GreetClient();
    client.startConnection("127.0.0.1", 6666);
    String response = client.sendMessage("hello server");
    assertEquals("hello client", response);
}

Đừng lo lắng nếu bạn không hoàn toàn hiểu những gì đang xảy ra ở đây, như ví dụ này có nghĩa là để cho chúng ta một cảm giác về những gì mong đợi sau này trong bài viết.

Trong các phần sau, chúng ta sẽ phân tích giao tiếp socket bằng cách sử dụng ví dụ đơn giản này và đi sâu hơn vào các chi tiết có nhiều ví dụ hơn.

4. Làm thế nào ổ cắm làm việc

Chúng ta sẽ sử dụng ví dụ trên để thực hiện các phần khác nhau của phần này.

Theo định nghĩa, một socket là một điểm cuối của liên kết giao tiếp hai chiều giữa hai chương trình đang chạy trên các máy tính khác nhau trên mạng. Một socket được gắn với một số cổng để lớp vận chuyển có thể xác định ứng dụng mà dữ liệu được gửi đến.

4.1. Máy chủ

Thông thường, một máy chủ chạy trên một máy tính cụ thể trên mạng và có một socket được gắn với một số cổng cụ thể. Trong trường hợp của chúng ta, chúng ta sử dụng cùng một máy tính với máy khách và khởi động máy chủ trên cổng 6666 :

1
ServerSocket serverSocket = new ServerSocket(6666);

Máy chủ chỉ đợi, nghe ổ cắm cho máy khách để thực hiện yêu cầu kết nối. Điều này xảy ra trong bước tiếp theo:

1
Socket clientSocket = serverSocket.accept();

Khi mã máy chủ gặp phương thức chấp nhận , nó sẽ chặn cho đến khi một máy khách tạo một yêu cầu kết nối đến nó.

Nếu mọi thứ suôn sẻ, máy chủ sẽ chấp nhận kết nối. Sau khi chấp nhận, máy chủ nhận được một socket mới, clientSocket , gắn với cùng một port local là: 6666 , và cũng có thiết bị đầu cuối từ xa được thiết lập đến địa chỉ và cổng của máy khách.

Tại thời điểm này, đối tượng Socket mới đặt máy chủ kết nối trực tiếp với máy khách, sau đó chúng ta có thể truy cập vào các luồng đầu ra và đầu vào để ghi và nhận các thông báo đến và từ máy khách tương ứng:

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
Từ bây giờ, máy chủ có khả năng trao đổi tin nhắn với khách hàng vô tận cho đến khi socket được đóng lại với các luồng của nó.

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

Để cho phép liên tục trong giao tiếp, 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 khi client gửi yêu cầu chấm dứt, chúng ta sẽ thấy điều này trong hành động trong phần sau.

Đối với mỗi client mới, server cần một socket mới được trả về bởi cuộc gọi chấp nhận. Các ServerSocket được sử dụng để tiếp tục lắng nghe các yêu cầu kết nối trong khi chăm sóc cho các nhu cầu của khách hàng được kết nối. Ví dụ đầu tiên của chưa thực hiện việc này.

4.2. Khách hàng

Client phải biết tên server hoặc IP của máy mà máy chủ đang chạy và số cổng mà máy chủ đang nghe.

Để thực hiện một yêu cầu kết nối, máy khách sẽ cố gắng hẹn gặp với máy chủ trên máy và cổng của máy chủ:

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

Máy khách cũng cần xác định chính nó đến máy chủ để nó liên kết với một số cổng cục bộ, được gán bởi hệ thống, mà nó sẽ sử dụng trong kết nối này. Chúng ta không giải quyết chuyện này.

Hàm khởi tạo trên chỉ tạo ra một socket mới khi máy chủ chấp nhận kết nối, nếu không, chúng ta sẽ nhận được một kết nối bị từ chối ngoại lệ. Khi tạo thành công, chúng ta có thể lấy các luồng đầu vào và đầu ra từ nó để giao tiếp với máy chủ:

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
Luồng đầu vào của máy khách được kết nối với luồng đầu ra của máy chủ, giống như luồng đầu vào của máy chủ được kết nối với luồng đầu ra của máy khách.

5. Truyền thông liên tục

Máy chủ hiện tại của tôi chặn cho đến khi một máy khách kết nối với nó và sau đó chặn lại một lần nữa để nghe một tin nhắn từ máy khách, sau một tin nhắn duy nhất, nó đóng kết nối bởi vì chúng ta chưa xử lý liên tục.

Vì vậy, nó chỉ hữu ích trong các yêu cầu ping, nhưng hãy tưởng tượng tôi muốn thực hiện một máy chủ trò chuyện, liên tục qua lại truyền thông giữa máy chủ và khách hàng chắc chắn sẽ được yêu cầu.

Chúng ta sẽ phải tạo một vòng lặp while để liên tục quan sát luồng đầu vào của máy chủ cho các tin nhắn gửi đến.

Hãy tạo một máy chủ mới có tên EchoServer.java với mục đích duy nhất là phản hồi lại bất kỳ thông điệp nào nhận được từ các máy khách:

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);
    }
}

Lưu ý rằng tôi đã thêm điều kiện chấm dứt trong đó vòng lặp while thoát khi tôi nhận được ký tự dấu chấm.

Tôi sẽ bắt đầu EchoServer bằng cách sử dụng phương pháp chính giống như tôi đã làm cho GreetServer . Lần này, chúng ta bắt đầu nó trên một cổng khác như 4444 để tránh nhầm lẫn.

Các EchoClient cũng tương tự như GreetClient , vì vậy tôi có thể lặp lại trong các mã. Tôi đang tách chúng cho rõ ràng.

Trong một lớp thử nghiệm khác, chúng ta sẽ tạo một thử nghiệm để cho thấy rằng nhiều yêu cầu tới EchoServer sẽ được phục vụ mà không có máy chủ đóng socket. Điều này đúng với điều kiện tôi gửi yêu cầu từ cùng một khách hàng.

Trường hợp với nhiều khách hàng là một trường hợp khác, mà chúng ta sẽ thấy trong phần tiếp theo.

Hãy tạo một phương thức thiết lập để khởi tạo kết nối với máy chủ:

@Before
public void setup() {
    client = new EchoClient();
    client.startConnection("127.0.0.1", 4444);
}

Chúng ta sẽ tạo ra một phương thức tearDown để giải phóng tất cả các tài nguyên của chúng ta, đây là phương pháp hay nhất cho mọi trường hợp chúng ta sử dụng tài nguyên mạng:

@After
public void tearDown() {
    client.stopConnection();
}

Sau đó hãy kiểm tra máy chủ echo của tôi với một vài yêu cầu:

@Test
public void givenClient_whenServerEchosMessage_thenCorrect() {
    String resp1 = client.sendMessage("hello");
    String resp2 = client.sendMessage("world");
    String resp3 = client.sendMessage("!");
    String resp4 = client.sendMessage(".");
    
    assertEquals("hello", resp1);
    assertEquals("world", resp2);
    assertEquals("!", resp3);
    assertEquals("good bye", resp4);
}

Đây là một cải tiến so với ví dụ ban đầu, nơi tôi sẽ chỉ giao tiếp một lần trước khi máy chủ đóng kết nối của tôi; bây giờ tôi gửi tín hiệu chấm dứt để thông báo cho máy chủ khi tôi hoàn tất phiên .

6. Máy chủ với nhiều khách hàng

Giống như ví dụ trước là một sự cải tiến so với ví dụ đầu tiên, nó vẫn không phải là một giải pháp tuyệt vời. Một máy chủ phải có khả năng phục vụ nhiều khách hàng và nhiều yêu cầu cùng một lúc.

Xử lý nhiều khách hàng là những gì tôi sẽ giới thiệu trong phần này.

Một tính năng khác mà chúng ta sẽ thấy ở đây là cùng một máy khách có thể ngắt kết nối và kết nối lại, mà không nhận được kết nối bị từ chối ngoại lệ hoặc thiết lập lại kết nối trên máy chủ. Trước đây  tôi không thể làm điều này.

Điều này có nghĩa là máy chủ của tôi sẽ mạnh mẽ và linh hoạt hơn trên nhiều yêu cầu từ nhiều khách hàng.

Làm thế nào tôi sẽ làm điều này là tạo ra một ổ cắm mới cho mỗi khách hàng mới và dịch vụ mà yêu cầu của khách hàng trên một chủ đề khác nhau. Số lượng khách hàng được phân phát đồng thời sẽ bằng số lượng chủ đề đang chạy.

Chủ đề chính sẽ chạy một vòng lặp while khi nó lắng nghe các kết nối mới.

Đủ nói chuyện, hãy tạo một máy chủ khác gọi là EchoMultiServer.java. Bên trong nó, chúng ta sẽ tạo ra một lớp trình xử lý thread để quản lý các giao tiếp của mỗi máy khách trên socket của nó:

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 là chấp nhận trong một thời gian vòng lặp. Mỗi khi vòng lặp while được thực hiện, nó chặn cuộc gọi chấp nhận cho đến khi một máy khách mới kết nối, sau đó trình xử lý, EchoClientHandler , được tạo cho máy khách này.

Điều gì xảy ra bên trong luồng là những gì tôi đã làm trước đây trong EchoServer , nơi tôi chỉ xử lý một máy khách duy nhất. Vì vậy, EchoMultiServer ủy nhiệm công việc này cho EchoClientHandler để nó có thể tiếp tục lắng nghe cho nhiều khách hàng hơn trong vòng lặp while.

Tôi vẫn sẽ sử dụng EchoClient để kiểm tra máy chủ, lần này tôi sẽ tạo nhiều khách hàng mỗi lần gửi và nhận nhiều tin nhắn từ máy chủ.

Hãy bắt đầu máy chủ của tôi bằng cách sử dụng phương thức chính của nó trên cổng 5555 .

Để rõ ràng, tôi sẽ vẫn đưa các bài kiểm tra vào một bộ mới:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void givenClient1_whenServerResponds_thenCorrect() {
    EchoClient client1 = new EchoClient();
    client1.startConnection("127.0.0.1", 5555);
    String msg1 = client1.sendMessage("hello");
    String msg2 = client1.sendMessage("world");
    String terminate = client1.sendMessage(".");
    
    assertEquals(msg1, "hello");
    assertEquals(msg2, "world");
    assertEquals(terminate, "bye");
}
@Test
public void givenClient2_whenServerResponds_thenCorrect() {
    EchoClient client2 = new EchoClient();
    client2.startConnection("127.0.0.1", 5555);
    String msg1 = client2.sendMessage("hello");
    String msg2 = client2.sendMessage("world");
    String terminate = client2.sendMessage(".");
    
    assertEquals(msg1, "hello");
    assertEquals(msg2, "world");
    assertEquals(terminate, "bye");
}

Có thể tạo bao nhiêu trường hợp thử nghiệm, mỗi lần sinh ra một khách hàng mới và máy chủ sẽ phục vụ tất cả chúng.

7. Kết luận

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

Có thể tìm thấy mã nguồn đầy đủ của bài viết – như thường lệ – trong dự án GitHub .

Nguồn: http://www.baeldung.com/a-guide-to-java-sockets

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 *