Như vậy, sau khi hoàn thành phần 1 trong loạt bài hướng dẫn Java socket, chúng ta đã xây dựng một cấu trúc hoàn chỉnh cho ứng dụng.
Kỳ này, chúng ta sẽ xây dựng thêm một tính năng cơ bản mà hầu hết các ứng dụng Chat cần có, là gửi tin nhắn đến một user bất kỳ, kể cả gửi cho chính bản thân mình. Để tiết kiệm thời gian thì đây vẫn chỉ là ví dụ trên console, nếu các bạn thật sự có tâm thì có làm Client trên giao diện với JavaFX và JavaSwing, đây là 2 nền tảng mà Java cung cấp để chúng ta xây dựng ứng dụng trên Desktop.
Cấu trúc dự án
Commoms Project
Để thêm một tính năng nào đó, trước tiên chúng ta cần update các thành phần cơ bản nhất được đặt ở trong commoms.
Thêm một SEND_MESSAGE_TO_USER_SPECIFIC
cho Action giữa client và server trong lúc giao tiếp với nhau để hiểu là User đang muốn gửi tin nhắn đến cho một User khác.
public enum Action { GET_USERS_ONLINE, SEND_MESSAGE_TO_USER_SPECIFIC, DISCONNECT }
Để gửi một tin nhắn đến User khác, Client bắt buộc phải cung cấp 2 thông tin cơ bản là UserID muốn nhắn tin đến, và 1 Message. Vì vậy chúng ta sẽ tạo một RequestMessage mới chứa đầy đủ các thông tin trên.
package com.chat.socket.commoms.request; import com.chat.socket.commoms.enums.Action; import lombok.Builder; import lombok.Getter; import lombok.NonNull; @Getter public class MessageRequest extends Request { private String message; private String uid; @Builder public MessageRequest(@NonNull Action action, String message, String uid) { super(action); this.message = message; this.uid = uid; } }
Đi kèm với RequestMessage chúng ta sẽ có một ResponseMesage tương ứng mà Server sử dụng để chuyển tin nhắn đến 1 User được chỉ định. ResponseMessage sẽ phải chứa các thông cơ bản: Nội dung tin nhắn, và người gửi là ai?
package com.chat.socket.commoms.response; import com.chat.socket.commoms.enums.StatusCode; import lombok.Builder; import lombok.Getter; @Getter public class MessageResponse extends Response { private String message; private String senderId; @Builder public MessageResponse(String message, String senderId, StatusCode statusCode) { this.message = message; this.senderId = senderId; this.statusCode = statusCode; } }
Server Project
Sau khi các thành phần cơ bản đã được xây dựng xong. Công việc của Server sẽ là handle thêm một SEND_MESSAGE_TO_USER_SPECIFIC Action enum.
Nếu UserID trong RequestMesage gửi lên hợp lệ, chúng ta sẽ tìm thấy ClientHandler instance chịu trách nhiệm giao tiếp với User đó, khi này, chúng ta chỉ cần lấy ClientHanlder instance để response một ResponseMesage với Message được lấy trong RequestMessage. Ngược lại, chúng ta response BAD_REQUEST cho User thực hiện request.
case SEND_MESSAGE_TO_USER_SPECIFIC:{ ClientHandler clientHandler = clientHandlers.get(((MessageRequest)(request)).getUid()); if (clientHandler == null) { this.response(MessageResponse.builder() .statusCode(StatusCode.BAD_REQUEST) .build()); } else { clientHandler.response(MessageResponse.builder() .message(((MessageRequest)(request)).getMessage()) .senderId(this.getUid()) .statusCode(StatusCode.OK) .build()); } break; }
Mã nguồn đầy đủ của Server class
package com.chat.socket.server; import com.chat.socket.commoms.enums.StatusCode; 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.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 SEND_MESSAGE_TO_USER_SPECIFIC:{ ClientHandler clientHandler = clientHandlers.get(((MessageRequest)(request)).getUid()); if (clientHandler == null) { this.response(MessageResponse.builder() .statusCode(StatusCode.BAD_REQUEST) .build()); } else { clientHandler.response(MessageResponse.builder() .message(((MessageRequest)(request)).getMessage()) .senderId(this.getUid()) .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(); } } } } }
Client Project
Cuối cùng, Client sẽ thêm một control cho phép User chọn chức năng gửi tin nhắn cho User khác, với 2 thông tin cơ bản là UserID và nội dung tin nhắn.
case "2": { System.out.println("User onlines: "); if (this.userOnlines.isEmpty()) { System.out.println("No user online"); break; } for (int i = 0; i < this.userOnlines.size(); i++) { System.out.println("User " + (i + 1) + ": " + this.userOnlines.get(i)); } System.out.println("Please select number from 1 to " + this.userOnlines.size() + " to send the message"); int choise = scanner.nextInt(); System.out.println(); System.out.print("Enter message: "); scanner.nextLine(); String message = scanner.nextLine(); sendRequest(MessageRequest.builder() .action(Action.SEND_MESSAGE_TO_USER_SPECIFIC) .uid(this.userOnlines.get(choise - 1)) .message(message) .build()); break; }
Để nhận tin nhắn gửi về từ Server, chúng ta cần thêm xử lý trong ResponseProcess class khi nhận được response là MessageResponse.
private class ResponseProcess extends Thread { @Override public void run() { try { while (true) { Object object = in.readObject(); // ..... if (object instanceof MessageResponse) { MessageResponse messageResponse = (MessageResponse) object; System.out.println("You is received: from " + messageResponse.getSenderId()); System.out.println("Message " + messageResponse.getMessage()); } } } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
Dưới đây là Client phiên bản đầy đủ để các bạn tiện theo dõi.
package com.chat.socket.client; 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; private Scanner scanner; 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(); } } private void getUserOnlines() throws IOException { sendRequest(InformationRequest.builder().action(Action.GET_USERS_ONLINE).build()); } 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()); scanner = new Scanner(System.in); userOnlines = new ArrayList<>(); new ResponseProcess().start(); while (true) { System.out.println("Chose your options"); System.out.println("1: GET ALL USER ONLINE"); System.out.println("2: SEND MESSAGE"); System.out.println("-1: ESC"); String ch = scanner.next(); switch (ch) { case "1": { getUserOnlines(); break; } case "2": { System.out.println("User onlines: "); if (this.userOnlines.isEmpty()) { System.out.println("No user online"); break; } for (int i = 0; i < this.userOnlines.size(); i++) { System.out.println("User " + (i + 1) + ": " + this.userOnlines.get(i)); } System.out.println("Please select number from 1 to " + this.userOnlines.size() + " to send the message"); int choise = scanner.nextInt(); System.out.println(); System.out.print("Enter message: "); scanner.nextLine(); String message = scanner.nextLine(); sendRequest(MessageRequest.builder() .action(Action.SEND_MESSAGE_TO_USER_SPECIFIC) .uid(this.userOnlines.get(choise - 1)) .message(message) .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!!!"); } } if (object instanceof MessageResponse) { MessageResponse messageResponse = (MessageResponse) object; System.out.println("You is received: from " + messageResponse.getSenderId()); System.out.println("Message " + messageResponse.getMessage()); } } } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } }
Là trích đoạn xử lý cho User gửi tin nhắn, Danh sách các User đang online đã được lấy từ trước với chức năng GET_USERS_ONLINE. Nếu không thực hiện chức năng này thì chương trình sẽ không tìm thấy bất cứ một User nào đang online cho phép chúng ta gửi tin nhắn. Nếu các bạn muốn tối ưu hơn thì có thể lấy danh sách các user online khi chương trình vừa khởi chạy.
Tóm lược
Như vậy chúng ta đã xây dựng xong chức năng gửi tin nhắn đến User được chỉ định. Đến đây chúng ta có thể thấy được dựa trên cấu trúc cũ mà chúng ta xây dựng tính năng mới khá nhanh. Nên các bạn nếu chưa đọc hoặc chưa nhớ phần 1 thì có thể quay lại xem nhé.
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.