Đồ án Ứng dụng nâng cao Client - Server nhỏ mô phỏng một chương trình FTP

FTP (File transfer Protocol) được biết đến như một giao thức truyền file nổi tiếng trong các mạng TCP/IP. Với các chương trình có cùng tên kèm theo trong các hệ điều hành có hỗ trợ TCP/IP như Linux, Unix, Windows 2000/XP người sử dụng có thể lấy các file trên máy chủ ở xa, cũng như upload file lên máy chủ một cách dễ dàng. Khi sử dụng FTP, phần mềm FTP trên máy cục bộ phải được kết nối đến phần mềm FTP trên máy ở xa, nơi bạn muốn lấy file về hay tải lên đó. Thông thường thì người dùng phải được cấp một tài khoản mới truy cập được vào máy ở xa. Nhưng cũng trong đa số các trường hợp người dùng không cần phải được cấp tải khoản trước mà có thể dùng tài khoản vô danh (Anonymous Account), với các hạn chế về truy cập tài nguyên.

doc18 trang | Chia sẻ: tuandn | Ngày: 24/05/2013 | Lượt xem: 2005 | Lượt tải: 6download
Bạn đang xem nội dung tài liệu Đồ án Ứng dụng nâng cao Client - Server nhỏ mô phỏng một chương trình FTP, để tải tài liệu về máy bạn click vào nút DOWNLOAD ở trên
TRƯỜNG ĐẠI HỌC QUY NHƠN KHOA TIN HỌC Môn : Thực hành nâng cao Giáo viên : Lê Văn Tuấn Nhóm : 3 Nguyễn Thanh Bình Lê Ngọc Hùng Phạm Giang Trường Quốc Phan Thanh Quý Trần Thị Hà Tuyên Quy Nhơn 06/2005 MỤC LỤC Giới thiệu 3 Giao diện chương trình 3 Các tính năng chính của chương trình MytinyFTP 5 Giao thức cài đặt và các bước hoạt động của chương trình 6 Các khó khăn kỹ thuật đã giải quyết 7 Cài đặt các lớp và các phương thức trong chương trình 8 Lớp information Lớp packageData Lớp Client Lớp Service_Client Lớp Server Lớp dataResume Lớp myCellRenderer Lớp myTree Lớp myVector Lớp Tree_server Cách sử dụng chương trình 17 Thành viên nhóm 18 Giới thiệu FTP (File transfer Protocol) được biết đến như một giao thức truyền file nổi tiếng trong các mạng TCP/IP. Với các chương trình có cùng tên kèm theo trong các hệ điều hành có hỗ trợ TCP/IP như Linux, Unix, Windows 2000/XP… người sử dụng có thể lấy các file trên máy chủ ở xa, cũng như upload file lên máy chủ một cách dễ dàng. Khi sử dụng FTP, phần mềm FTP trên máy cục bộ phải được kết nối đến phần mềm FTP trên máy ở xa, nơi bạn muốn lấy file về hay tải lên đó. Thông thường thì người dùng phải được cấp một tài khoản mới truy cập được vào máy ở xa. Nhưng cũng trong đa số các trường hợp người dùng không cần phải được cấp tải khoản trước mà có thể dùng tài khoản vô danh (Anonymous Account), với các hạn chế về truy cập tài nguyên. Trong đồ án kết thúc môn học “Thực hành nâng cao” này, nhóm 3 đã viết một ứng dụng client/server nhỏ mô phỏng một chương trình FTP với một số chức năng đơn giản: MyTinyFTP. Chương trình được cài đặt bằng ngôn ngữ Java. Giao diện của chương trình Chương trình khi chưa kết nối với Server sẽ có giao diện như trên. Vì chưa kết nối với Server nên bên phía cửa số Client vẫn là các file trên máy cục bộ. Khi client kêt nối với server, cửa sổ bên server sẽ hiển thị danh sách file của máy chủ: Người dùng có thể chuyển chế độ hiện thị dạng cây thư mục bên cửa sổ server: Các tính năng chính của chương trình MyTinyFTP Chương trình MyTinyFTP gồm có những chức năng chính sau: Cho phép gửi/nhận một/nhiều file hoặc thư mục từ máy trạm đến máy chủ ở xa (thông qua mạng cục bộ) Cho phép nhiều Client cùng kết nối đến một Server trong quá trình vận hành chương trình. Hỗ trợ chức năng resume trong trường hợp bị ngắt kết nối bất thường khi đang truyền một file từ Client lên Server và ngược lại. Với hai danh sách chọn file tương ứng với hai phía Client và Server người dùng có thể thao tác với nhiều file/thư mục ở nhiều nơi khác nhau để tải về Client hoặc chép lên Server. Với chức năng Tree bên phía Server, người dùng có thể dễ dàng hình dung được tổng thể toàn bộ cấu trúc file cũng như thư mục bên phía Server mà mình đuợc phép truy nhập, và cũng có thể dễ dàng chọn file/thư mục từ đây. Cho phép thiết lập linh động thư mục gốc (FTPRoot) bên Server mà người dùng được phép truy cập vào. Ngoài ra còn cho phép người dùng linh động với nhiều tuỳ chọn trong lúc khởi động chương trình. Giao thức được cài đặt và các bước hoạt động của chương trình Một hệ thống client/server luôn phải cài đặt một giao thức bên trong nó, với chương trình này giao thức được cài đặt là TCP. Với đặc tính là một giao thức hướng liên kết (connection-oriented), ổn định và chính xác nên TCP rất thích hợp cho việc truyền tải file qua mạng, tuy nhiên chính ưu điểm này khiến cho nó tiêu tốn khá nhiều tài nguyên mạng. Cơ chế làm việc của chương trình là từ tầng ứng dụng (nơi chương trình được cài đặt và làm việc), chương trình sẽ truyền các gói tin được chia nhỏ từ một file, rồi từ đó các tầng mạng tiếp tục xử lý các gói tin đó để chuyền qua mạng cho máy nhận. Hoạt động của chương trình như sau: Server sẽ hoạt động và lắng nghe trên một cổng mặc định (hoặc do người dùng quy định), và để Client có thể kết nối được với Server thì cần phải cung cấp cho Client biết tên của máy chủ cũng như cổng mà máy chủ làm việc. Khi Server được khởi động, nó sẽ tạo ra một file LOG (nếu chưa có) để ghi lại các lỗi có thể xảy ra trong quá trình làm việc. Sau đó server sẽ chạy vào một vòng lặp vô tận chờ Client gửi các yêu cầu (hợp lệ) lên. Trong chương trình, các yêu cầu sau là yêu cầu hợp lệ: Yêu cầu Ý nghĩa Send Nhận file từ Client gửi lên Get Load file từ server về máy Client Ngat Ngắt kết nối Chuyenthumuc chuyển thư mục hiện hành Yeucauresume Yêu cầu resume trong trường hợp upload Yeucauloadtiep Yêu cầu resume trong trường hợp download layJTree_JTable Cập nhật khung duyệt và cây layJTree Cập nhật cây layJTable Cập nhật khung duyệt Khi Client kết nối thành công với Server, trước tiên nó sẽ gửi yêu cầu layJTree_JTable đến Server lấy danh sách file trong FTPRoot để hiển thị lên giao diện bên Client. Sau đó Client sẽ tạo file LOG bên Client nếu nó chưa tồn tại, còn nếu đã có file Log rồi thì Client sẽ kiểm tra xem có cần Resume không (xét cả hai trường hợp upload và download) bằng cách kiểm tra các file log. Nếu trong lần kết nối trước đó, chương trình bị ngắt kết nối bất thường trong khi truyền chưa xong một file nào đó thì chương trình sẽ hỏi người dùng xem có muốn tiếp tục truyền hoàn thành file đó không; nếu câu trả lời nhận được là có thì chương trình sẽ tiến hành Resume. Quá trình truyền một file diễn ra như sau: đầu tiên chương trình sẽ lấy thông tin của file cần truyền, sau đó tình toán số lượng cũng như kích thước các gói sẽ được chia nhỏ ra để truyền qua mạng. Tên file cùng với các thông tin phụ sẽ được gửi đi trước, bên nhận sẽ kiểm tra sự tồn tại của file sắp được chuyển có có trong thư mục hiện tại hay không. Sau quá trình kiểm tra và xác nhận của người dùng (nếu cần thiết), chương trình sẽ tiến hành gửi các gói dữ liệu đi. Trong quá trình nhận các gói tin, nếu có lỗi xảy ra, ngay lập tức file mới được đóng lại và thông tin của lần truyền file bị lỗi này sẽ được lưu vào file log để phục vụ cho quá trình resume trong lần kết nối sau. Tất cả các lỗi cũng như các thông báo của chương trình trong quá trình vận hành được hiển thị trong khung Status. Riêng những lỗi trong lúc khởi động sẽ hiển thị trên màn hình Console. Các khó khăn kỹ thuật đã giải quyết Trong qua trình cài đặt chương trình, một vấn đề khó khăn nổi lên là làm sao nhận biết được ranh giới giữa các gói tin cũng như các thông điệp trong luồng nhập cũng như luồng xuất. Và khi tiến hành truyền nhiều file hoặc truyền một thư mục (không rỗng) thì vấn đề là làm sao biết được ranh giới dữ liệu giữa các file với nhau trong luồng. Với cách làm thông thường xử lý từng byte một thì việc này đòi hỏi cần phải có một sự kiểm soát luồng cực kỳ chặt chẽ (chặt chẽ đền từng byte một) thì mới có thể đảm bảo cho chương trình vận hành ổn định. Một giải pháp khác là ta không tiến hành gửi/nhận các byte mà ta sẽ gửi/nhận một đối tượng. Như vậy thì tất cả mọi thông tin muốn gửi qua mạng ta chỉ cần “đóng gói” chúng vào một đối tượng và chuyển chúng vào luồng. Ở đầu bên kia của luồng, ta lại đọc ra nguyên một đối tượng với đầy đủ dữ liệu cũng như các phương thức được xây dựng sẵn của đối tượng đó, rất thuận tiện cho việc thao tác dữ liệu. Hơn nữa một “gói” (đối tượng) có thể chứa nhiều cả về loại lẫn số lượng các dữ liệu nên ta có thể gộp nhiều thông tin vào một gói và gửi đi một lần, dễ dàng hơn trong việc kiểm soát luồng. Để có thể truyền được các đối tượng qua mạng thì các đối tượng này phải được cài đặt (implements) giao diện Serializable. Các đối tượng loại này trong chương trình gồm có: packageData : gói chứa dữ liệu được chia nhỏ từ các file cùng một số thông tin liên quan. Information : gói chứa thông tin của file sẽ được truyền đi myTree : gói tạo cây Tree_server : gói phục vụ quá trình tạo cây myVector : gói chứa danh sách file bên Server gửi qua cho Client dataResume : gói phục vụ cho quá trình resume Một khó khăn nữa là việc hiển thị danh sách các file ở Server trên máy Client. Vì việc sử dụng đối tượng JFileChooser trở nên phức tạp trong trường hợp này, nên việc lựa chọn đối tượng sử dụng cũng như hình thức thể hiện gặp nhiều khó khăn, vướng mắc. Mỗi một thành phần (component) có một ưu thế cũng như nhược điểm riêng trong việc thể hiện danh sách file của Server. Giải pháp được đưa ra là tích hợp nhiều thành phần để cho phép người sử dụng tuỳ chọn trong quá trình sử dụng. Trong chương trình thì hai thành phần đã được sử dụng là: JTree và JList. Để có thể load được các icon trong JList, đối tượng JList phải được setCellRenderer với đối số của phương thức này là một đối tượng kiểu CellRenderer (và cụ thể trong chương trình này là lớp myCellRenderer được kế thừa từ lớp ListCellRenderer). Việc lựa chọn loại của luồng (nhập/xuất), cũng như các phương thức đọc/ghi cũng gặp nhiều vấn đề. Bởi vì nếu sử dụng không đúng thì dữ liệu sau khi chuyển qua mạng sẽ bị hỏng cấu trúc, không sử dụng được. Vấn đề này được giải quyết chỉ bằng kinh nghiệm và sự thử nghiệm các luồng cũng như các phương thức để kiểm tra. Cài đặt các lớp và phương thức trong chương trình Chương trình bao gồm 11 lớp với các chức năng chính của từng lớp được tóm tắt trong bảng sau: Tên lớp Chức năng Client là lớp chính bên phía máy khách, thực hiện các chức năng truyền tải file cũng như hiển thị giao diện. Server là lớp thực thi bên phía máy chủ, có chức năng tạo ra các luồng cho mỗi Client nối vào. Service_Client là lớp chính bên phía máy chủ, thực hiện các chức năng truyền tải file. packageData là gói chứa thông tin và dữ liệu (được chia nhỏ ra từ các file) để truyền qua mạng. dataResume là gói chứa thông tin của file cần được resume. information chứa thông tin của file sẽ được truyền qua mạng myCellRenderer lớp phục vụ cho việc hiện thị các icon trong đối tượng JList myTree lớp phục vụ cho việc truyền đối tượng JTree qua mạng myVector lớp phục vụ cho việc truyền đối tượng Vector qua mạng Tree_server lớp dùng để tạo đối tượng JTree Vector_server lớp phục vụ cho việc trình bày các file và thư mục của Server bên Client (sắp xếp trình bày theo loại) Lớp information Lớp này dùng để chứa các thông tin của một file/thư mục chuẩn bị được truyền qua mạng. Do phải truyền qua mạng nên lớp này phải implements lớp Serializable. Lớp bao gồm có 4 thuộc tính cùng với các phương thức set/get cho mỗi thuộc tính này. Ý nghĩa của các thuộc tính: String ten :chứa tên của file/thư mục chuẩn bị được truyền qua String loai :xác định thông tin gửi là một file hay là thư mục int soluong :xác định số lượng file/thư mục con trong thư mục được gửi qua (trong trường hợp đối tượng gửi qua là thư mục) String path :chứa vị trí của đối tượng (file/thư mục) trên máy gửi (dùng cho chức năng Resume) Lớp này được dùng trong các phương thức gửi/nhận file của hai lớp Client và Service_Client. Lớp packageData Lớp này dùng để chứa dữ liệu cùng với các thông tin liên quan để chuyển qua mạng. Dữ liệu chứa trong các gói chính là dữ liệu của các file được “cắt” nhỏ ra để chuyển qua mạng. Lớp bao gồm có 4 thuộc tính cùng với các phương thức set/get cho mỗi thuộc tính này. Ý nghĩa của các thuộc tính: byte[] dulieu : mảng chứa dữ liệu int kichthuoc : kích thước thật sự của mảng dữ liệu boolean goicuoi : xác định gói được chuyển có phải là gói cuối cùng của một file hay không int goiso : thứ tự của gói được chuyển Phương thức khởi tạo có một đối số là: kích thước dữ liệu tối đa mà một gói có thể chứa trong một lần gửi đi. Lớp này được dùng trong các phương thức gửi file của các lớp Client và Service_Client. Lớp dataResume Lớp này dùng để chứa các thông tin phục vụ cho việc Resume. Khi một client kết nối với server, nếu có yêu cầu cần Resume thì các thông tin Resume sẽ được chuyển qua lại giữa các máy bằng gói này. Lớp bao gồm có 2 thuộc tính cùng với các phương thức get cho mỗi thuộc tính này. Phương thức khởi tạo của lớp có 2 đối số, chính là các giá trị khởi tạo cho các thuộc tính này. Ý nghĩa của các thuộc tính: String path : đường dẫn của file trên máy gửi int goiso : là gói truyền bị lỗi, nếu resume thì cần gửi bắt đầu từ gói này trở đi Lớp myCellRenderer Lớp này là lớp “chức năng” phục vụ cho việc load icon (để phân biệt đâu là thư mục, đâu là file) trong thành phần JList. Lớp này kế thừa (extends) lớp JLabel và “thực thi” (implements) lớp ListCellRenderer, bởi vì ta dùng cả hình ảnh (icon) lẫn văn bản để hiển thị trong mỗi ô của JList. ListCellRenderer là một giao diện đơn giản chỉ có một phương thức: public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) Phương thức này được gọi mỗi khi đối tượng JList muốn hiển thị ô trong thành phần danh sách. Ta quan tâm đến đối số thứ 2 để xác định ô đó thể hiện file hay thư mục để load icon tương ứng. File f = (File)value; if(f.isDirectory()) setIcon(new ImageIcon("images"+File.separator+"folder_icon.gif")); else setIcon(new ImageIcon("images"+File.separator+"file_icon.gif")); Sau khi có được lớp này rồi, để JList có thể load được các icon, ta cần gọi phương thức: setCellRenderer với đối số của phương thức này là một instance của lớp myCellRenderer: JList_File_Server.setCellRenderer(new myCellRenderer()); Lớp Server Lớp này là lớp thực thi bên phía Server, các đối số nó nhận vào từ dòng lệnh (nếu có) sẽ quy định việc khởi động Server hoạt động như thế nào. Nếu tham số nhập vào sai, hoặc không nhập vào thì các thông số mặc định sẽ được sử dụng, đó là làm việc trên cổng 1905 và thư mục làm việc (FTPRoot) là thư mục hiện hành. Khi Server chạy, nó sẽ đi vào một vòng lặp vô tận để có thể đáp ứng được nhiều Client cùng kết nối vào. Khi có một client kết nối vào, một instance của lớp Service_Client sẽ được tạo ra để thực hiện các “giao dịch”. while (true){ try{ Socket s = ss.accept(); new Service_Client(s,FTPRoot); }catch (Exception e2) {} } Lớp Client Lớp này là lớp thực thi bên phía Client, thực hiện hầu hết các chức năng truyền tải file cũng như hiển thị giao diện. Phương thức khởi tạo: Client(String host, int port) Nếu các đối số truyền nào chính xác thì chương trình sau khi hiển thì giao diện sẽ kết nối với Server luôn, ngược lại thì chỉ hiển thị giao diện thôi. Trong trường hợp kết nối luôn thì chương trình sẽ kiểm tra xem có cần resume không bằng cách gọi 2 phương thức kiểm tra là: this.kiemtraResume_Client(); kiểm tra resume theo chiều Client - Server this.resume(); kiểm tra resume theo chiều Server – Client Phương thức void showgui() Dùng để hiển thị giao diện của chương trình Phương thức void setGui(boolean ok) Dùng để thiết lập trạng thái (kích hoạt hoặc không kích hoạt) cho các JButton và các JTextField trong các trường hợp hoạt động cụ thể của chương trình. Phương thức boolean connection(String host, int port) Phương thức này dừng để kết nối Client với Server ở địa chỉ host và trên cổng port. Sau khi kết nối thành công, nó thiết lập 2 luồng nhập và xuất để gửi thông tin: s = new Socket(host,port); out = new ObjectOutputStream(s.getOutputStream()); in = new ObjectInputStream(s.getInputStream()); Phương thức void disconnect() Phương thức dùng để ngắt kết nối giữa Client và Server, đầu tiên nó gửi một yêu cầu ngắt kết nối sang Server, sau đó tiến hành đóng 2 luồng nhập xuất cũng như đóng kết nối. Và thiết lập lại giao diện cho thích hợp: try{ out.writeObject("ngat"); in.close(); out.close(); s.close(); }catch (Exception e){} // **** thiết lập lại giao diện **** ……………………… Phương thức boolean send(File filename) Phương thức này dùng để gửi một file/thư mục qua Server. Đầu tiên một gói information chứa thông tin về đối tượng sẽ được gửi qua Server trước, bao gồm tên, đường dẫn, loại (“dir” nếu là thư mục và “file” nếu là tập tin). Server sẽ kiểm tra sự tồn tại của đối tượng đó trên máy chủ và gửi yêu cầu xác nhận (“datontai” hoặc “ok”). Nếu đối tượng đó đã tồn tại thì một ConfirmDialog sẽ hiện ra để người dùng trả lời là có ghi đè không. Xét trong trường hợp có ghi đè hoặc đối tượng đó chưa tồn tại trên server: + Nếu là thư mục: gọi tiếp phương thức gửi thư mục sendDir(filename) để gửi toàn bộ nội dung bên trong (nếu có) của thư mục đó sang Server. + Nếu là file: tiến hành gửi dữ liệu qua. Đầu tiên tính toán xem bao nhiêu gói sẽ được chuyển qua. Sau đó dùng vòng lặp để chuyển các gói đi, mỗi gói chứa dữ liệu và số thứ tự của gói (số thứ tự này được dùng trong việc resume trong lần kết nối sau trong trường hợp bị lỗi trong lúc gửi file). Riêng gói cuối cùng thì thuộc tính goicuoi sẽ được thiết lập là true để báo cho server biết là đã truyền hết file. int i=0; for (i=0;i<solan-1;i++) { input.read(dulieu); data = new packageData(max); data.setDulieu(dulieu,max); data.setGoiso(i); out.writeObject(data); } input.read(dulieu); data = new packageData(max); data.setGoicuoi(true); data.setDulieu(dulieu,conlai); data.setGoiso(i+1); out.writeObject(data); Phương thức void sendDir(File f) Phương thức này dùng để gửi nội dung bên trong của một thư mục. Phương thức này chỉ đơn giản lấy các FILE có trong thư mục và gọi đệ quy phương thức send(File filename) để gửi đi. File[] files = f.listFiles(); for (int i=0;i<files.length;i++){ send(files[i]); } Phương thức void nhan(String pathCurrentDir) Phương thức này dùng để nhận file/thư mục download từ Server về. Đầu tiên nó sẽ nhận gói information từ bên Server gửi qua để biết được thông tin về “đối tượng“ sắp được nhận bao gồm: tên, loại,… Sau đó tiến hành kiểm tra xem “đối tượng” đã tồn tại trên máy cục bộ chưa (nếu đã tồn tại thì hiển thị thông báo cho người dùng là có chép đè lên không). Xét trường hợp “đối tượng” chưa tồn tại hoặc cho phép chép đè: + Nếu là thư mục: tiến hành tạo thư mục, sau đó gọi đệ quy lại phương thức nhan để tiếp tục nhận các file/thư mục con của thư mục này (nếu cần thiết). + Nếu là tập tin: tạo file mới với tên nhận được trong gói information, sau đó dùng vòng lặp để nhận các gói dữ liệu từ Server gửi qua. Khi đã gặp gói cuối thì đóng file lại. do{ dulieu = (packageData)in.readObject(); goiso = dulieu.getGoiso(); output2file.write(dulieu.getDulieu()); }while (!dulieu.getGoicuoi()); Nếu trong quá trình truyền file bị lỗi ở một gói nào đó thì file sẽ được đóng lại, và một dòng thông báo lỗi sẽ được ghi vào file log. Nội dung của dòng thông báo trong file log bao gồm: [địa_chỉ_IP_Server]*[đường_dẫn_file_nguồn]* [đường_dẫn_file_đích]*[thứ_tự_của_gói_bị_lỗi]*[trạng_thái_lỗi]. Các thông tin được ngăn cách nhau bằng dấu * , thông tin cuối cùng dùng để xác định xem lỗi này đã được khắc phục hay chưa. Phương thức void xoathumuc(File f) Phương thức dùng để xoá một thư mục khi có nhu cầu chép đè lên. Phương thức này được gọi đệ quy để có thể xoá toàn bộ thư mục (kể cả thư mục con). Phương thức void actionPerformed(ActionEvent ac) Phương thức xử lý các sự kiện của chương trình. Với nút SEND, chương trình sẽ lấy lần lượt các FILE có trong danh sách chọn, sau đó gọi phương thức send(File filename) cho mỗi FILE này. Việc xử lý đối với nút GET cũng hoàn toàn tương tự… Phương thức void mouseClicked(MouseEvent e) Phương thức xử lý sự kiện nhấp chuột của chương trình. Phương thức này chủ yếu là xử lý sự kiện nhấp đúp chuột trên danh sách file bên Server để chuyển thư mục. Khi sự kiện này xảy ra, chương trình sẽ gửi yêu cầu chuyenthumuc sang Server để Server thiết lập lại thư mục hiện hành. Sau đó, chương trình refresh lại khung hiển thị danh sách các FILE bên server bằng cách gọi phương thức getInformation(). Phương thức void kiemtraResume_Client() Phương thức này dùng để kiểm tra xem có cần tiến hành “resume download” (tức là theo hướng từ Server -> Client) không? Phương thức này được gọi ngay sau khi Client kết nối với một Server (trong phương thức khởi tạo và trong xử lý sự kiện nút Connect). Trước tiên chương trình sẽ mở file log trên client, tìm dòng lỗi chưa được xử lý tương ứng (tức là dòng có IP_Server trùng với IP của Server vừa mới kết nối vào và không có chữ [ok] cuối dòng). Nếu tìm thấy nó sẽ hỏi người dùng là có muốn resume không, nếu người dùng đồng ý thì chương trình sẽ “cắt” dòng lỗi đó ra thành từng thành phần (các phần ngăn cách nhau bởi dấu * ) để
Luận văn liên quan