Loading Now

Đường Đến Tim Crush Bằng Java – Socket (Part 2)

Tiếp tục phần 1, lần này, mình sẽ tiếp tục con đường xui dại anh em bằng cách tạo một Project hoàn chỉnh. Trước khi vào, mình khuyến nghị các bạn nên dùng Netbean vì nó dễ tạo giao diện bằng cách kéo thả và dễ makeup giao diện hơn.

Nếu chưa đọc phần 1, các bạn hãy click vào link này nhé: https://codelearn.io/sharing/cham-tim-crush-bang-java-socket-part-1

1. Muốn crush thấy bạn đẹp trai, trước hết bạn nên có một tâm hồn đẹp

Gọi nhan sắc của bạn là biến final vì tôi và bạn đều biết, nó không thể thay đổi, gọi tôi là ngh nhân makeup vì tôi có th làm cho tâm hồn bạn trở nên đẹp như những con thiên nga. Nhưng làm sao để crush biết tâm hồn của bạn rất đẹp, đẹp như những con thiên nga của Tchaikovsky? Hãy để vẻ đẹp đó thể hiện qua những dòng code và giao diện của app.

Làm theo tôi, bạn hãy tạo hai project: Server và Client, theo cấu trúc như sau : 

                            

Tôi chia project thành 3 package: Data , Gui và Controller. Package Data chứa những class giành cho việc truyền dữ liệu qua socket. Package Controller chứa những class xử lý data như: gửi file, đọc object, ….Và cuối cùng class Gui: chứa những class hiển thị giao diện cho người dùng.

Hãy coi cả project là một cái máy làm xúc xích, Gui là những gì bạn nhìn thấy ở cái máy: đó là những nút bấm, những công tắc lên xuống, màu sắc, hình dáng. Controller là những chi tiết, linh kiện bên trong để giúp cái máy hoạt động. Còn Data: nôm na là con bò, là nguyên liệu để cái máy hoạt động. Bạn nhét con bò vào máy, cái máy sẽ cho bạn xúc xích, thế thôi .

Về việc phân chia project thế này, nó không giúp tâm hồn của bạn đến được với crush, nhưng tôi cam đoan rằng, code của bạn sẽ gọn gàng hơn rất nhiều và tiết kiệm được hàng đống thời gian không cần thiết để tìm xem đâu là class mình  muốn viết thêm chức năng. Thay vì tốn thời gian vào việc như vậy, hãy tập trung makeup cho cái giao diện của bạn.

2. Thiết kế một tâm hồn đẹp

Dưới đây, tôi đã có hai tâm hồn khá đẹp.

Server 

                               

Client :      

                                                  

Nhìn lại, giao diện của tôi có vẻ không “nạnh nùng” và “kun ngầu” như kì vọng, nhưng bạn ôi, tôi có người yêu rồi, nên mấy thứ như giao diện, tôi đếch quan tâm cho lắm :v. Nhưng các bạn thì phải cần, hãy chăm sóc thật tốt cho giao diện của bạn, đừng chăm chút nó như chăm chút nhan sắc.

Nhớ hãy đặt tên cho các Button, các TextField, Text Area, Label để dễ dàng quản lý.

3. Những con bò – Nguyên liệu tạo nên cây xúc xích

Phần này tôi sẽ hướng đẫn anh em viết ra những con bò – nguyên liệu tạo nên cây xúc xích 88% từ thịt.

Và đây là những thứ có vai trò như con bò nằm trong package Data : 

public abstract class DataTransfer implements Serializable{
    private int type;

    public DataTransfer(int type) {
        this.type = type;
    }

    public int getType() {
        return type;
    }
}
public class FileTransfer extends DataTransfer {
    private byte[] byteArr;
    private String name;

    public FileTransfer(byte[] byteArr, String name, int type) {
        super(type);
        this.byteArr = byteArr;
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public byte[] getByteArr() {
        return byteArr;
    }
}
public class MessageTransfer extends DataTransfer {

    private String message;

    public MessageTransfer(String message, int type) {
        super(type);
        this.message = message;
    }
    public String getMessage() {
        return message;
    }
}
public class DefineDataType {
    public static int mode_File = 1;
    public static int mode_Message = 2;
}

Để hiểu được vì sao tôi lại làm như vậy, anh em cần có một chút kiến thức về tính đa hình trong OOP. Tôi muốn coi tất cả Object truyền đi trong stream là DataTransfer, và sau đó, bằng cách đọc biến type, chúng ta có thể biết được nó là object gửi file hay message

Anh em có nh phần code đơn giản về gửi message tôi viết ở phần 1 không? Tại sao không gọi hàm WriteUTF() để gửi luôn message, cho nó vào class làm gì ?

Vậy tại sao tôi lại phải wrap dữ liệu vào 1 class? Bạn thấy đấy, khi tặng tiền, người ta thường bỏ tiền vào phong bì, khi tặng quà, người ta thường dùng giấy bọc đẹp mắt. Trong hàng tá món quà mà crush bạn nhân được ngày sinh nhật, chắc gì crush đã biết quà nào của ông nào. Đấy! Vậy nên chúng ta phải bọc  (wrap) nó bằng giấy (Class DataTransfer) để khi tặng có thể kèm thêm vài thứ nho nhỏ xinh xinh nữa, tỉ dụ như tấm thiệp, một lời chúc cute có kèm tên người gửi chẳng hạn. 

Một đặc điểm khác của việc wrap này là bạn có thể gửi đi nhiều hơn là một tin nhắn. Có thể là cái Icon sau tin nhắn thì sao. Mà Icon là dạng gif -> mesage của bạn sẽ trở thành dạng kết hợp giữa image(file) và string, trong khi đó, WriteUTF() thì chỉ gửi string thôi, làm sao mà gửi được, đúng không? Nghe hợp lý quá còn gì. Nhờ vậy, bạn có khả năng mở rộng thêm chức năng, loại dữ liệu mà không phải lo nhầm lẫn hay không xử lý được.

Class FileTransfer bao gồm một dãy Byte[], tại sao lại như vậy? Vì khi bạn lưu file được gửi qua Socket, bạn phải chuyển nó qua dangj Byte[] để ghi vào FileOutputStream. Nếu không làm cách này, bạn bắt buộc phải gửi ba mảng byte []. Một mảng định nghĩa kiểu dữ liệu là gì? File hay string, mảng 2 gửi kích thước file để bên Receiver nhận đúng số byte và mảng 3 mới là nội dung của file đã chuyển qua byte[]. Vậy nên, tội gì không làm cách dễ dàng? 

4. Bên ngoài đẹp trai nhưng vẫn phải có những thằng đệ làm nền 

(Lưu Ý : hai Project Client và Server có những class giống nhau về cả tên và nội dung, vậy nên chỉ cần viết ở một Project và coppy những class đó sang project còn lại )

Đúng vậy, đối với cưa gái, đẹp trai không là chưa đủ, có rất nhiều thằng đẹp trai như bạn nhưng nó lại nhiều tiền hơn bạn và kết cục cho bạn là những chiếc xừng nho nhỏ xinh xinh trên đầu. Vậy nên, hãy gọi thêm mấy thằng em giúp sức, sẵn sàng đấm bất cứ thằng nào tăm tia crush của bạn.     

4.1 Thằng Sen ( Sender )

Như cấu trúc trên, tôi sẽ giải thích một cách chi tiết công việc mà thằng em này phải làm, và tại sao nó lại được đứng trong hàng ngũ xử lý

Anh em hãy chiêm ngưỡng nhan sắc và tâm hồn của nó : 

public class Sender {
    private ObjectOutputStream obout;
      
    public Sender(ObjectOutputStream obout) {
        this.obout = obout;
    }

    public void sendMessage(String mess) {
        MessageTransfer object_transfer = new MessageTransfer(mess, DefineDataType.mode_Message);
        try {
            obout.writeObject(object_transfer);
        } catch (Exception e) {
            JOptionPane.showMessageDialog(null, "can not Send Message !!!");
        }
    }

    public void sendFile(File file) {
        byte[] byte_arr = transferFileToByteArray(file);
        FileTransfer file_trans = new  FileTransfer(byte_arr,file.getName(), DefineDataType.mode_File);
        try {
            obout.writeObject(file_trans);
        } catch (IOException ex) {
            JOptionPane.showMessageDialog(null, "Can not send File !!!");
        }
    }

    private byte[] transferFileToByteArray(File file) {
        FileInputStream fIn = null;
        BufferedInputStream buffer = null;

        int size = (int) file.length();
        byte[] result = new byte[size];

        try {
            fIn = new FileInputStream(file);
            buffer = new BufferedInputStream(fIn);
            int count = buffer.read(result, 0, size);
        } catch (Exception e) {
        } finally {
            try {
                buffer.close();
                fIn.close();
            } catch (IOException ex) {
                JOptionPane.showMessageDialog(null, "Can not close BufferredInputStream or FileInputStream !!!");
            }
        }
        return result;
    }
}

Trái tim của thằng đệ Sender là ObjectOutputStream – Thứ giúp chương trình của bạn ghi data vào Socket để truyền đi. Lần này, chúng ta không dùng trực tiếp thằng InputStream như ở phần 1 mà ban cho nó một Class, một thế giới riêng, và nó sẽ là trái tim, là chủ đạo trong class đó.

Với yêu cầu bài toán của chúng ta là gửi Message và tài liệu học tập (dạng File) nên chúng ta sẽ viết hai hàm cho thằng em Sender này là SendFile()SendMessage().

Với cả hai hàm, công việc của chúng ta là là gửi đi một trong hai Object kế thừa object DataTransfer. Trước hết, với việc gửi Message, mọi việc trở nên cực kì dễ dàng bằng cách tạo một đối tượng MessageTransfer và gửi nó đi bằng ObjectOutputStream. 

Đối với File, yêu cầu khó hơn một chút, chúng ta cần chuyển File thành byte[] trước khi đóng gói vào class. Hàm TransferFileToByteArray()  sẽ giúp bạn làm điều này.

Muốn có byte[], trước hết , ta phải có File. “Thịt chó phải có mắm tôm, File không tồn tại  thì transfer kiểu gì ?”. Hàm sendFIle() với input là 1 File – File lấy ở đâu tôi không biết, nhưng phải là 1 file khác null ( tức là file đó phải tồn tại ). Công việc tiếp theo là lấy độ dài của File bằng hàm Length(), khởi tạo một mảng byte[] có kích cỡ bằng file để lưu nội dung File sau khi chuyển thành các bytes. Việc chuyển File thành byte[] đã có BufferedInputStream đảm nhận.

int size = (int) file.length();
byte[] result = new byte[size];

Sau khi có byte[], ta nhờ thằng BufferedInputStream để đọc file và ghi vào mảng byte vừa tạo. Xong bước này, chúng ta chỉ cần lấy tên file, tạo object FileTransfer và nhờ thằng ObjectOutputStream gửi đi thôi.

4.2 Thằng đệ Ri ship vơ (Receiver)  – Chào em, anh đứng đây từ chiều :v

Trước hết hãy chiêm ngưỡng nhan sắc của thằng em đã: 

public class Receiver extends Thread {

    private ObjectInputStream obIn;
    private JTextArea console;

    public Receiver(ObjectInputStream obIn, JTextArea console) {
        this.obIn = obIn;
        this.console = console;
    }

    @Override
    public synchronized void start() {
        DataTransfer data = null;
        while (true) {
            try {
                // Read data in Socket and execute
                Object object = obIn.readObject();
                data = (DataTransfer) object;
                execute(data);
            } catch (Exception ex) {
                JOptionPane.showMessageDialog(null, "Connection Closed !!" );
                try {
                    obIn.close();
                    break;
                } catch (IOException ex1) {
                    Logger.getLogger(Receiver.class.getName()).log(Level.SEVERE, null, ex1);
                }
            }
        }
    }

    public void execute(DataTransfer data) throws IOException {
        int mode = data.getType();
        if (mode == DefineDataType.mode_File) {
            FileTransfer file_trans = (FileTransfer) data;
            if (acceptFile(file_trans.getName())) {
                console.append("\nReceiving File from server ......");
                String path = getPath();
                writeFile(path + "\\" + file_trans.getName(), data);
            }
        } else if (mode == DefineDataType.mode_Message) {
            MessageTransfer mess = (MessageTransfer) data;
            console.append("\nServer Say : " + mess.getMessage());
        }
    }

    private boolean acceptFile(String name) {
        int accept = JOptionPane.showConfirmDialog(null, "Accept File : " + name + " From server ?");
        boolean result = accept == 0 ? true : false;
        return result;
    }

    private void writeFile(String path, DataTransfer data) {
        FileTransfer f_trans = (FileTransfer) data;
        File file = new File(path);
        FileOutputStream fIn = null;
        try {
            fIn = new FileOutputStream(file);
            fIn.write(f_trans.getByteArr());
            JOptionPane.showMessageDialog(null, "Write File Successful !!");
        } catch (FileNotFoundException ex) {

        } catch (IOException ex) {
            JOptionPane.showMessageDialog(null, "Write File Error !!");
        }
    }

    private String getPath() {
        String path = "";
        JFileChooser file_chooser = new JFileChooser();
        file_chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        if (file_chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
            path = file_chooser.getCurrentDirectory().getPath();
        }
        return path;
    }

}

Thằng em Receiver là một thằng em vất vả, đồng thời nó cũng là một thằng lụy tình không hơn không kém. Bắt đầu sinh ra khi kết nối giữa hai socket hình thành, thằng em cứ đơn phương, mòn mỏi chờ tín hiệu của em Sender xinh gái bên kia. Thằng em Receiver sẽ làm nhiều việc hơn là cô em Sender, nhận data, xử lý data và thông báo lên màn hình. Do đó, để khởi tạo thằng em, chúng ta cần truyền cho nó một ObjectInputStream và cái TextArea ở Gui.

Vốn mạng thân phận là một thằng kế thừa ý chí của vua hải tặc – à tôi nhầm, kế thừa class Thread, thằng em receiver mang trong mình một nhiệm vụ cao cả là hoạt động độc lập với main Thread.

Sau khi khởi tạo, trong hàm run() , ta quăng cho thằng em Receiver một cái vòng lặp vô tận, Receiver sẽ bắt đầu đứng từ đây, chờ data được gửi đến để làm nhiệm vụ phân thích dữ liệu và xử lý chúng. Với những object đọc được, Receiver sẽ chuyển nó thành đối tượng DataTransfer và truyền nó vào hàm Execute() để sử lý.

Sau khi đọc biến type, nếu DataTransfer là message, đơn giản là chuyển nó thành MessageTransfer, đọc nội dung tin nhắn bằng hàm getMessage() và in lên màn hình thôi. 

Nhưng nếu nó là file thì sao ? Bước đầu cần phải xác nhận xem, file có được chấp nhận lưu vào máy hay không? JOptionpane.ShowConfirmDialog()  sẽ giải quyết được vấn đề này. Nếu chấp nhận, Receiver sẽ gọi hàm getPath() để xác định thư mục cần lưu file xuống, tiếp sau đó là gọi hàm writeFile() để thực hiện lưu File.

5. Server, Client và xử lý sự kiện trên GUI

Như đã làm ở trên, chúng ta đã tạo hai Gui cho client và Server, công việc bây giờ là viết những hàm xử lý sự kiện 

Để làm được những điều này, các bạn nên tìm hiểu qua về Event trong Java Swing. Ở đây, tôi không viết code xử lý trên Gui, tôi xử lý trong class Server và Client, điều khiển và lấy giá trị các TextFiled, TextArea, label qua Getter của Gui.

Vậy nên , hãy nhớ thêm Getter cho tất cả các Element ( button, label , …) trên Gui.

5.1 Server

public class Server {
    private Gui screen;
    private Sender sender;
    private Receiver receiver;
    private ServerSocket server;
    private Socket socket;

    private  File f = null;
    
    public Server() {
        prepareGui();
    }

    // gắn Event vào các button
    public void prepareGui() {
        screen = new Gui();
        screen.getBtn_send_mss().setActionCommand("btn_sendMessage");
        screen.getBtn_send_file().setActionCommand("btn_send_file");
        screen.getBtn_choose_file().setActionCommand("btn_choose_file");
        screen.getBtn_send_mss().addActionListener(new ButtonClickHandle());
        screen.getBtn_send_file().addActionListener(new ButtonClickHandle());
        screen.getBtn_choose_file().addActionListener(new ButtonClickHandle());
    }

    public void startServer() {
        try {
            screen.setVisible(true);
            server = new ServerSocket(9999);
            screen.getLb_port().setText("Port : " + server.getLocalPort());
            screen.getLb_address().setText("IP : " + "192.168.43.117");
            screen.getTa_console().append("\nStart Server ....");
            screen.getTa_console().append("\nWaitting client ...");
            socket = server.accept();
            screen.getTa_console().append("\nSocket Connected at port : " + socket.getPort() + "\nIP : " + socket.getInetAddress());
            sender = new Sender(new ObjectOutputStream(socket.getOutputStream()));
            screen.getTa_console().append("\nCreated Sender ...");
            receiver = new Receiver(new ObjectInputStream(socket.getInputStream()), screen.getTa_console());
            screen.getTa_console().append("\nCreated Receiver ...");
            receiver.start();
        } catch (IOException ex) {
        }
    }

    public File chooseFile() {
        JFileChooser fileChooser = new JFileChooser();
        File fi = null;
        if (fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
            try {
                fi = fileChooser.getSelectedFile();
            } catch (Exception e) {
            }
        }
        if (fi != null) {
            screen.getTf_file_name().setText(fi.getName());
        }
        return fi;
    }
    
    // xử lý sự kiện
    class ButtonClickHandle implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent ae) {
            String command = ae.getActionCommand();
            if (command.equals("btn_sendMessage")) {
                String mess = screen.getTf_mess().getText().trim();
                sender.sendMessage(mess);
                screen.getTa_console().append("\nMe : " + mess);
                screen.getTf_mess().setText("");
            } else if (command.equals("btn_send_file")) {

                if (f == null) {
                    JOptionPane.showMessageDialog(null, "No file to Send !!");
                } else {
                    JOptionPane.showMessageDialog(null, "Send File : " + f.getName());
                    sender.sendFile(f);
                    screen.getTf_file_name().setText("");
                    f = null;
                }
            } else if (command.equals("btn_choose_file")) {
                try {
                    f = chooseFile();
                    screen.getTf_file_name().setText(f.getAbsolutePath());
                } catch (Exception e) {
                }
            }
        }

    }
}

Ở class Server, chúng ta viết hàm prepareGui() để gán cấc event cho Gui, đồng thời, tạo class ButtonClick để bắt và xử lý những sự kiện trên Gui. PrepareGui() sẽ được gọi ngay khi Server chạy. 

Hàm StartServer() để khởi tạo một ServerSocket và gọi hàm accept() để nhận socket từ Client kết nối đến. Tiếp  theo đó, dùng Socket đã kết nối đến, tạo hai thằng em Sender và Receive, truyền vào cho nó những thứ cần thiết và Start() chúng.

5.2 Client

public class Client {    
    private File f = null;
    private Socket socket;
    private Gui screen;
    private Receiver receiver;
    private Sender sender;

    public Client() {

    }
    // thêm sự kiện cho các buttom
    public void prepareGui() {
        screen = new Gui();
        screen.getBtn_chooseFile().setActionCommand("choose_file");
        screen.getBtn_sendFile().setActionCommand("btn_sendFile");
        screen.getBtn_connect().setActionCommand("btn_connect");
        screen.getBtn_send_mess().setActionCommand("btn_sendMessage");
        screen.getBtn_sendFile().addActionListener(new ButtonClickAction());
        screen.getBtn_send_mess().addActionListener(new ButtonClickAction());
        screen.getBtn_connect().addActionListener(new ButtonClickAction());
        screen.getBtn_chooseFile().addActionListener(new ButtonClickAction());
        screen.setVisible(true);
    }

    public void connectServer(String IP, int port) throws IOException {
        socket = new Socket(IP, port);
        screen.getTa_console().append("\nConnect to server ....\nAt port : " + port);
        sender = new Sender(new ObjectOutputStream(socket.getOutputStream()));
        screen.getTa_console().append("\nSender is Created !");
        receiver = new Receiver(new ObjectInputStream(socket.getInputStream()), screen.getTa_console());
        SwingWorker worker = new SwingWorker() {
            @Override
            protected Object doInBackground() throws Exception {
                receiver.start();
                return null;
            }
        };
        worker.execute();
    }

    public File chooseFile() {
        JFileChooser fileChooser = new JFileChooser();
        File f = null;
        if (fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {

            f = fileChooser.getSelectedFile();
        }
        if (f != null) {
            screen.getTf_fileName().setText(f.getName());
        }
        return f;
    }

    // Xử lý sự kiện
    class ButtonClickAction implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent ae) {
            String command = ae.getActionCommand();
            if (command.equals("btn_sendMessage")) {
                String mess = screen.getTf_mess().getText().trim();
                sender.sendMessage(mess);
                screen.getTa_console().append("\nMe : " + mess);
                screen.getTf_mess().setText("");
            } else if (command.equals("btn_sendFile")) {

                if (f != null) {
                    sender.sendFile(f);
                    screen.getTf_fileName().setText("");
                    f = null;
                } else {
                    JOptionPane.showMessageDialog(null, "Choose your File !!!");
                }
            } else if (command.equals("btn_connect")) {
                String Ip = screen.getTf_Ip().getText().trim();
                int port = Integer.parseInt(screen.getTf_port().getText());
                try {
                    connectServer(Ip, port);
                } catch (IOException ex) {
                }
            } else if (command.equals("choose_file")) {
                try {
                    f = chooseFile();
                    screen.getTf_fileName().setText(f.getPath());
                } catch (Exception e) {
                }
            }
        }

    }
}

Cũng giống như Server, Client cũng làm những công việc tương tự. Khác ở  chỗ, chúng ta cần truyền cho nó địa chỉ IPv4 của ServerSocket và Port để Socket có thể kết nối được.

5.3 Hoàn thiện những bước cuối cùng 

Đến đây, anh em mở class Entry ở hai Project ra, viết vài dòng như thế này và bắt đầu chạy thôi

public class Entry {
     public static void main(String[] args) {
        Client client = new Client();
       client.prepareGui();
    }
}
public class Entry {
    public static void main(String[] args) throws IOException {
        Server server = new Server();
        server.startServer();
    }
}

Nhớ phải chạy App từ Class Entry nhé, chạy từ Gui là toang đấy :v

5.4 Thành Quả 

     

 Anh em tự test phần gửi File nhé @@

Lời kết 

Hi vọng sau bài viết vừa rồi, anh em có thể có được những kiến thức căn bản về Socket cũng như gửi File bằng Socket. Anh em có thể phát triển lên thành Multi chat hoặc design giao diện cho đẹp hơn :v. Cám ơn anh em đã giành thời gian đọc bài, nếu thấy chỗ nào không hợp lý hoặc thắc mắc, hãy comment phía dưới để mình có thể giải đáp và cải thiện những bài viết sau này. 

Post Comment

Contact