Mục lục
Ở bài trước, chúng ta đã tìm hiểu về Java Socket và làm ứng dụng Chat với chức năng đơn giản là client sẽ kết nối với nhau thông qua socket, sau khi kết nối thành công client sẽ gửi cho server 1 tin nhắn, server nhận được và phản hồi lại cho client.
Trong bài viết này, chúng ta sẽ dựa theo đó mà phát triển ứng dụng tiếp, mục tiêu của kỳ này là chúng ta sẽ xây dựng các thành phần cơ bản của ứng dụng sao cho hợp lý, cũng như cách tổ chức cấu trúc của dự án.
Các công cụ cơ bản
Để xây dựng ứng dụng hiệu quả, mình sẽ áp dụng các dụng các công cụ, thư viện, etc hỗ trợ nhầm giảm thời gian phát triển:
- Maven: đây là công cụ quản lý dự án hiệu quả, với rất nhiều các tính năng như biên dịch project, xây dựng và chia sẽ dependency, documentation etc.
- Lombok: là thư viện sinh code tự động theo bản mẫu nhất định ít khi thay đổi như các hàm getter, setter, constructor.
- Stream API: được giới thiệu trong Java 8, với cú pháp nhanh gọn và tích hợp nhiều tính năng hữu ích nhầm đơn giản hoá các Collection như filter, map, reduce etc.
- Thread: nếu xử lý cho tất cả các kết nối của client trong một thread thì sẽ giảm hiệu suất chương trình đáng kể, vậy ngại gì mà không xử lý đa luồng, để server có thể xử lý các yêu cầu của client nhanh hơn đúng không nào!
- Commoms Lang 3: là thư viện khá nỗi tiếng với rất nhiều các lớp tiện ích như StringUtils, ObjectUtils etc giảm các thao tác cơ bản như kiểm tra null, cắt chuỗi, chuyển ký tự hoa sang thường etc.
Trên đây là những thứ sẽ hỗ trợ chúng ta trong quá trình viết code, cá nhân mình thấy đây là những thứ căn bản mà các Java Dev nên biết. Chúng là các công cụ tuyệt vời, mà hầu hết các dự án Java lớn nhỏ đều sử dụng. Nên nếu các chưa biết một trong số chúng, hãy thử đọc trước theo đường link kèm theo đã nhé!
Cấu trúc dự án
Dự án gồm 3 project nhỏ:
- Commoms: định nghĩa các thành phần ở mức cơ bản được sử dụng chung ở cả client và server như Request, Response, etc.
- Server: Ứng dụng phía máy chủ, chạy trên một máy tính cụ thể.
- Client: Ứng dụng triển khai ở phía client, triển khai trên nhiều máy. Nó sẽ kết nối đến Server thông qua socket và sử dụng các dịch vụ mà Server cung cấp.
Commoms Project
Chúng ta sẽ bắt đầu với Commoms, vì đây là nơi định nghĩa những thành phần cơ bản nhất được sử dụng ở cả Server và Client.
Bên trong dự án commoms, Root package là com.chat.socket.commoms nhầm phân biệt với các dự án còn lại. Nó sẽ chứa các package con:
- enums: định nghĩa các enums chung cho cả client và server, dựa vào những enums này, mà client và server có thể phía bên kia đang muốn gì.
- request: định nghĩa các request object mà client sẽ sử dụng để gửi cho server trong các trường hợp cụ thể.
- response: là các response server dùng để trả về tương ứng với các request mà client gửi đến.
Pom.xml
File pom.xml trong Project Maven là nơi định nghĩa các thông cơ bản của dự án, như tên, phiên bản của dự án, các thư viện ngoài etc.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.chat.socket</groupId> <artifactId>commoms</artifactId> <version>1.0-SNAPSHOT</version> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> <dependencies> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> <scope>provided</scope> </dependency> </dependencies> </project>
Enums package
Trong enums package chứa các enum mà client và server sẽ sử dụng chung nhầm giao tiếp với nhau trong các trường hợp cụ thể.
Ví dụ Action chứa các hành động mà client muốn server thực hiện như GET_USERS_ONLINE thì server sẽ hiểu là client đang muốn lấy danh sách tất các client đang online.
package com.chat.socket.commoms.enums; public enum Action { GET_USERS_ONLINE, DISCONNECT }
Hay StatusCode đính kèm trong các response mà server sẽ trả về cho client, OK nghĩa là request hợp lệ, trong khi BAD_REQUEST có nghĩa là request không hợp lệ.
package com.chat.socket.commoms.enums; public enum StatusCode { OK, BAD_REQUEST }
Request & Reponse package
Trong request package, sẽ chứa các Request class mà client sử dụng để gửi đến server, server dựa vào các request này mà gửi các response tương ứng.
Định nghĩa một Request abstract class định nghĩa các điểm chung của tất cả các loại request cụ thể khác.
package com.chat.socket.commoms.request;Getter @Setter @RequiredArgsConstructor public abstract class Request implements Serializable { @NonNull protected Action action; }
InformationRequest là một subclass điển hình của Request abstract class trong bài viết này. Nó sẽ không có thêm một thông tin gì khác abstract class, thế nhưng cũng đủ cho server biết là client đang muốn lấy các thông tin gì đó sẽ được định nghĩa trong thuộc tính Action.
package com.chat.socket.commoms.request; @Getter public class InformationRequest extends Request { @Builder public InformationRequest(@NonNull Action action) { super(action); } }
Note: @Builder annotation của lombok cho phép chúng ta triển khai Builder Pattern trên InformationRequest class, cho phép chúng ta khởi tạo đối tượng theo từng bước cụ thể.
Các Response tương ứng như với InformationRequest kết hợp với GET_USERS_ONLINE thì server sẽ trả về UserOnlineResponse đính kèm danh sách các UserID trong đó.
package com.chat.socket.commoms.response; @Getter public abstract class Response implements Serializable { protected StatusCode statusCode; }
package com.chat.socket.commoms.response; @Getter public class UserOnlineResponse extends Response { private List<String> userIds; @Builder public UserOnlineResponse(List<String> userIds, StatusCode statusCode) { this.userIds = userIds; this.statusCode = statusCode; } }
Note: Request và Response class phải implement Serializable Interface thì mới có thể truyền đi trong các kết nối socket.
Server Project
Server ở thời điểm hiện tại chỉ chứa 1 Server class dùng để tạo socket cho phép các client kết nối đến và xử lý một vài request cơ bản mà client yêu cầu.
Pom.xml
Server project ngoài sử dụng các thư viện ngoài như lombok, commoms lang 3 thì nó cũng cần import commoms project được xây dựng nội bộ trong ứng dụng.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.chat.socket</groupId> <artifactId>server</artifactId> <version>1.0-SNAPSHOT</version> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>com.chat.socket</groupId> <artifactId>commoms</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.9</version> </dependency> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> <scope>provided</scope> </dependency> </dependencies> </project>
Server class
Server class được đặt ở root package com.chat.socket.server. Với các đặc điểm thay đổi hơn so với kỳ trước, mỗi client sẽ được tạo một ID khi kết nối thành công, ID này là duy nhất cho mỗi client sử dụng UUID để generate.
package com.chat.socket.server; import com.chat.socket.commoms.enums.StatusCode; import com.chat.socket.commoms.request.Request; import com.chat.socket.commoms.response.Response; import com.chat.socket.commoms.response.UserOnlineResponse; import lombok.Getter; import lombok.Setter; import org.apache.commons.lang3.ObjectUtils; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.*; import java.util.stream.Collectors; public class Server { private ServerSocket serverSocket; private Map<String, ClientHandler> clientHandlers; public Server() { this.clientHandlers = new HashMap<>(); } public void start(int port) { System.out.println("Server starting!!!"); try { serverSocket = new ServerSocket(port); System.out.println(serverSocket.getInetAddress().getHostName()); System.out.println(serverSocket.getLocalPort()); while (true) { ClientHandler clientHandler = new ClientHandler(serverSocket.accept()); clientHandler.start(); this.clientHandlers.put(clientHandler.getUid(), clientHandler); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (serverSocket != null) { serverSocket.close(); } } catch (IOException e) { e.printStackTrace(); } } } private List<String> getUserIdOnline() { return this.clientHandlers.values().stream() .map(ClientHandler::getUid) .collect(Collectors.toList()); } @Getter @Setter private class ClientHandler extends Thread { private Socket clientSocket; private ObjectInputStream in; private ObjectOutputStream out; private String uid; public ClientHandler(Socket socket) throws IOException { this.clientSocket = socket; out = new ObjectOutputStream(clientSocket.getOutputStream()); in = new ObjectInputStream(clientSocket.getInputStream()); this.uid = UUID.randomUUID().toString(); } private void response(Response response) throws IOException { this.out.writeObject(response); this.out.flush(); } @Override public void run() { try { while (true) { Object input = in.readObject(); if (ObjectUtils.isNotEmpty(input)) { Request request = (Request)input; switch (request.getAction()) { case GET_USERS_ONLINE: { this.response(UserOnlineResponse.builder() .userIds(getUserIdOnline()) .statusCode(StatusCode.OK) .build()); break; } case DISCONNECT: { clientHandlers.remove(this.getUid()); break; } default: break; } } } } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } finally { try { if (in != null) { in.close(); } if (out != null) { out.close(); } if (clientSocket != null) { clientSocket.close(); } } catch (Exception e) { e.printStackTrace(); } } } } }
Khi có một request gửi đến Server, Server ngay lập tức nhận và xử lý đối với các request cụ thể. Trong bài viết này, mình chỉ xử lý một request lấy về danh sách các user đang online, và ngắt kết nối khi user tắt chương trình.
Kế đến, tiến hành chạy Server ở port 8080 để lắng nghe các kết nối của client.
package com.chat.socket.server; public class Main { public static void main(String[] agrs) { new Server().start(8080); } }
Client Project
Client là project được xây dựng cuối cùng, vì nó cần sử dụng các dịch vụ của Server, thế nên mình code Server trước. Trong thực tế, Server và Client có thể làm song song, chúng ta chỉ cần thiết lập của quy chuẩn cho chúng và thực hiện theo.
Khá đơn giản như Server project, Client class cũng được đặt tại root package com.chat.socket.client chứa 1 Client class cho phép các User yêu cầu dịch vụ từ Server thông qua màn hình console.
Pom.xml
Tương tự Server project, Client cũng sử dung lombok, commoms lang 3, và commoms.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.chat.socket</groupId> <artifactId>client</artifactId> <version>1.0-SNAPSHOT</version> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>com.chat.socket</groupId> <artifactId>commoms</artifactId> <version>1.0-SNAPSHOT</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.9</version> </dependency> </dependencies> </project>
Client class
Đây là nơi chứa mã cho phép user thao tác qua màn hình console để yêu cầu các dịch vụ từ Server. Trong bài viết này, mình chỉ cho phép user thực hiện 1 hành động chính là lấy danh sách các user đang online, và ngắt kết nối khi user tắt chương trình.
import com.chat.socket.commoms.enums.Action; import com.chat.socket.commoms.enums.StatusCode; import com.chat.socket.commoms.request.InformationRequest; import com.chat.socket.commoms.request.MessageRequest; import com.chat.socket.commoms.request.Request; import com.chat.socket.commoms.response.MessageResponse; import com.chat.socket.commoms.response.UserOnlineResponse; import org.apache.commons.lang3.ObjectUtils; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.Socket; import java.util.ArrayList; import java.util.List; import java.util.Scanner; public class Client { private Socket clientSocket; private ObjectInputStream in; private ObjectOutputStream out; List<String> userOnlines; private void sendRequest(Request req) throws IOException { this.out.writeObject(req); this.out.flush(); } private void close() throws IOException { if (this.in != null) { this.in.close(); } if (this.out != null) { this.out.close(); } if (this.clientSocket != null) { this.clientSocket.close(); } } public void startConnection(String ip, int port) { try { clientSocket = new Socket(ip, port); this.out = new ObjectOutputStream(clientSocket.getOutputStream()); this.in = new ObjectInputStream(clientSocket.getInputStream()); userOnlines = new ArrayList<>(); while (true) { System.out.println("Chose your options"); System.out.println("1: GET ALL USER ONLINE"); System.out.println("-1: ESC"); switch (ch) { case "1": { sendRequest(InformationRequest.builder().action(Action.GET_USERS_ONLINE).build()); break; } case "-1": { sendRequest(InformationRequest.builder().action(Action.DISCONNECT).build()); close(); return; } default: break; } } } catch (IOException e) { e.printStackTrace(); } finally { try { if (in != null) { in.close(); } if (out != null) { out.close(); } if (clientSocket != null) { clientSocket.close(); } } catch (IOException e) { e.printStackTrace(); } } } private class ResponseProcess extends Thread { @Override public void run() { try { while (true) { Object object = in.readObject(); if (ObjectUtils.isEmpty(object)) { continue; } if (object instanceof UserOnlineResponse) { UserOnlineResponse userOnlineResponse = (UserOnlineResponse) object; if (StatusCode.OK.equals(userOnlineResponse.getStatusCode())) { userOnlineResponse.getUserIds().forEach(s -> System.out.println(s)); userOnlines = userOnlineResponse.getUserIds(); } else { System.out.println("Request failed!!!"); } } } } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } }
ResponseProcess là 1 inner class của Client, chịu trách nhiệm xử lý các response từ Server gửi về. Đặc điểm nổi bật của ResponseProcess là nó sẽ thực thi trên một thread khác để lắng nghe tất cả các response mà không làm gián đoạn các thao tác sử dụng các lệnh từ cmd của user.
Song song với đó, startConnection() method vẫn sẽ thực thi trên main thread và cho phép user sử dụng màn hình console để yêu cầu các dịch vụ từ client như lấy danh sách các user online, ngắt kết nối.
Vì mình chạy ứng dụng Server và Client trên cùng một máy, nên IP sẽ là “localhost” và port là 8080.
package com.chat.socket.client; public class Main { public static void main(String[] agrs) { Client client = new Client(); client.startConnection("localhost", 8080); } }
Tóm lược
Như vậy là chúng ta đã xây dựng các thành phần cơ bản và cấu trúc của ứng dụng. Ở kỳ sau, chúng ta sẽ xây dựng thêm các tính năng dựa trên cấu trúc này.
Nếu gặp rắc rối trong quá trình triển khai và kiểm thử, các bạn có thể tham khảo source code tại github.