Giáo trình Lập trình mạng (Phần 2) - Nguyễn Duy Hiếu

pdf 38 trang Gia Huy 17/05/2022 1760
Bạn đang xem 20 trang mẫu của tài liệu "Giáo trình Lập trình mạng (Phần 2) - Nguyễn Duy Hiếu", để tải tài liệu gốc về máy bạn click vào nút DOWNLOAD ở trên

Tài liệu đính kèm:

  • pdfgiao_trinh_lap_trinh_mang_phan_2_nguyen_duy_hieu.pdf

Nội dung text: Giáo trình Lập trình mạng (Phần 2) - Nguyễn Duy Hiếu

  1. CHƯƠNG 5. LẬP TRÌNH VỚI GIAO THỨC TCP 5.1 Khái niệm chung Thuật ngữ lập trình mạng với Java đề cập đến việc viết các chương trình thực hiện trên nhiều thiết bị máy tính, trong đó các thiết bị được kết nối với nhau. Gói java.net của Java chứa một tập hợp các lớp và giao tiếp cung cấp giao thức truyền thông ở mức độ thấp. Gói java.net được cung cấp hỗ trợ cho hai giao thức mạng phổ biến sau: • TCP - Transmission Control Protocol: TCP thường được sử dụng qua giao thức Internet (Internet Protocol), được gọi là TCP/IP. Giao thức này cho phép giao tiếp tin cậy giữa hai ứng dụng. • UDP - User Datagram Protocol: một giao thức khác cho phép truyền dữ liệu giữa các ứng dụng. Giao thức này không kiểm tra đến việc gói tin đã được gửi hay chưa nên đây là giao tiếp không tin cậy giữa hai hoặc nhiều ứng dụng. Chúng ta sẽ tìm hiểu về lập trình với giao thức UDP ở chương sau. TCP và UDP là các giao thức cốt lõi của việc kết nối các thiết bị công nghệ với nhau. Các ứng dụng có thể dùng một trong hai hoặc cả hai giao thức này để trao đổi với các ứng dụng trên máy tính khác thông qua mạng máy tính. 5.2 Khái niệm cổng (port number) Để có thể thực hiện các cuộc giao tiếp, một trong hai quá trình phải công bố số hiệu cổng của socket mà mình sử dụng. Mỗi cổng giao tiếp thể hiện một địa chỉ xác định trong hệ thống. Khi quá trình được gán một số hiệu cổng, nó có thể nhận dữ liệu gửi đến cổng này từ các quá trình khác. Quá trình còn lại cũng được yêu cầu tạo ra một socket. Số hiệu cổng (port number) được sử dụng để xác định tính duy nhất của các ứng dụng khác nhau. Nó hoạt động như một điểm kết nối cuối trong giao tiếp giữa các ứng dụng. Số hiệu cổng gán cho Socket phải duy nhất trên phạm vi máy tính đó, có giá trị trong khoảng từ 0 đến 65535 (16 bit). Trong đó, giá trị cổng: • Từ 0-1023: là cổng hệ thống (common hay well-known port), được dành riêng cho các quá trình của hệ thống. 67
  2. • Từ 1024-49151: là cổng phải đăng ký (registered port). Các ứng dụng muốn sử dụng cổng này phải đăng ký với IANA (Internet Assigned Numbers Authority). • Từ 49152-65535: là cổng dùng riêng hay cổng động (dynamic hay private port). Người sử dụng có thể dùng cho các ứng dụng của mình, không cần phải đăng ký. Một số cổng thường được sử dụng: • 21: dịch vụ FTP • 23: dịch vụ Telnet • 25: dịch vụ Email (SMTP) • 80: dịch vụ Web (HTTP) • 110: dịch vụ Email (POP) • 143: dịch vụ Email (IMAP) • 443: dịch vụ SSL (HTTPS) • 1433/1434: cơ sở dữ liệu SQL Server • 3306: cơ sở dữ liệu MySQL 5.3 Lớp Socket Đơn vị truyền và nhận tin bằng phương thức TCP được gọi là Socket. Socket cho phép dữ liệu được trao đổi giữa các thiết bị trong môi trường mạng máy tính. Lớp java.net.Socket trong Java giúp quản lý quá trình truyền và nhận giữa các máy tính trong mạng máy tính bằng giao thức TCP. Một Socket đóng vai trò một đầu-cuối của một kết nối thực. Một Socket vừa có thể của Client để gửi yêu cầu kết nối tới Server vừa có thể được tạo bởi Server để xử lý yêu cầu trao đổi tin từ Client. Chúng ta cùng tìm hiểu các phương thức của lớp Socket. 5.3.1 Các phương thức tạo public Socket(String host, int port) throws IOException, UnknownHostException 68
  3. Constructor này cố gắng để kết nối với máy chủ được chỉ định tại cổng được chỉ định. Nếu constructor này không ném một ngoại lệ, kết nối thành công và máy khách được kết nối với máy chủ. public Socket(InetAddress host, int port)throws IOException, UnknownHostException Constructor này giống hệt với hàm tạo trước đó, ngoại trừ việc máy chủ được chỉ định bởi một đối tượng InetAddress. public Socket(String host, int port, InetAddress localAddress, int localPort)throws IOException Kết nối đến máy chủ và cổng được chỉ định, tạo một socket trên máy chủ cục bộ tại địa chỉ và cổng được chỉ định. public Socket(InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException Constructor này giống hệt với constructor trước đó, ngoại trừ máy chủ được chỉ định bởi một đối tượng InetAddress thay vì một String. public Socket() Tạo một socket không chỉ định trước kết nối. Sau này chúng ta sử dụng phương thức connect() để kết nối socket này với máy chủ. 5.3.2 Các phương thức kiểm soát vào-ra public InputStream getInputStream() throws IOException Trả về dòng đầu vào của socket. Input stream được kết nối với output stream của socket remote. public OutputStream getOutputStream() throws IOException Trả về dòng đầu ra của socket. Output stream được kết nối với input stream của socket remote. 5.3.3 Một số phương thức khác public void connect(SocketAddress host, int timeout) throws IOException Phương thức này kết nối socket với máy chủ được chỉ định. Phương thức này là cần thiết chỉ khi chúng ta khởi tạo Socket bằng cách sử dụng constructor không có đối số. 69
  4. public InetAddress getInetAddress() Phương thức này trả về địa chỉ mạng của máy chủ mà socket này được kết nối. public int getPort() Trả về cổng mà socket bị ràng buộc trên máy remote. public int getLocalPort() Trả về cổng mà socket bị ràng buộc trên máy local. public SocketAddress getRemoteSocketAddress() Trả về địa chỉ của socket từ xa. public synchronized void setSoTimeout(int timeout) throws SocketException Thiết lập thời gian tồn tại của socket. Nếu timeout khác 0, đây chính là khoảng thời gian (được tính bằng mili giây) mà socket còn hoạt động. Hết thời gian này chương trình socket sẽ tự hủy. public void close() throws IOException Đóng socket, làm cho đối tượng Socket này không còn có khả năng kết nối với bất kỳ máy chủ nào. 5.4 Lớp ServerSocket Lớp java.net.ServerSocket được sử dụng bởi các ứng dụng máy chủ để tạo ra một một ứng dụng tại một cổng và lắng nghe các yêu cầu của máy khách. Một đối tượng của lớp ServerSocket được tạo trên phía máy chủ và lắng nghe kết nối từ các máy khách. Đối tượng này luôn tồn tại trong một chương trình ứng dụng mạng đang chạy bằng giao thức TCP phía máy chủ. 5.4.1 Các phương thức tạo public ServerSocket(int port) throws IOException Cố gắng tạo một ServerSocket bị ràng buộc vào port được chỉ định. Một ngoại lệ xảy ra nếu port đã bị ràng buộc bởi một ứng dụng khác. public ServerSocket(int port, int backlog) throws IOException Tương tự như hàm tạo trước đó, tham số backlog xác định có bao nhiêu máy khách đến để lưu trữ trong một hàng đợi. 70
  5. public ServerSocket(int port, int backlog, InetAddress address) throws IOException Tương tự như constructor trước đó, tham số InetAddress chỉ định địa chỉ IP cục bộ để ràng buộc. InetAddress được sử dụng cho các máy chủ có thể có nhiều địa chỉ IP, cho phép máy chủ xác định địa chỉ IP nào để chấp nhận yêu cầu của máy khách. public ServerSocket() throws IOException Tạo ra một ServerSocket không kết nối. Khi sử dụng constructor này, sử dụng phương thức bind() khi chúng ta muốn ràng buộc socket tới máy chủ. 5.4.2 Các phương thức khác public Socket accept() throws IOException Chờ cho một máy khách kết nối đến. Phương thức này ngăn chặn cho đến khi một máy trạm kết nối đến máy chủ trên cổng được chỉ định hoặc socket hết hạn, giả sử rằng giá trị thời gian đã được thiết lập bằng phương thức setSoTimeout(). Nếu không, phương thức này sẽ khóa lại vô thời hạn. public int getLocalPort() Trả về cổng mà socket của máy chủ lắng nghe. Phương thức này rất hữu ích nếu chúng ta truyền 0 như là số cổng trong một constructor và để cho máy chủ tìm thấy một cổng cho chúng ta. public void setSoTimeout(int timeout) Thiết lập giá trị thời gian chờ cho bao lâu socket của máy chủ chờ khách hàng trong suốt quá trình chấp nhận. public void bind(SocketAddress host, int backlog) Liên kết socket tới máy chủ và cổng được chỉ định trong đối tượng SocketAddress. Sử dụng phương thức này nếu chúng ta đã tạo ra các ServerSocket bằng cách sử dụng constructor không có đối số. public void close() Đóng ServerSocket, ngừng phục vụ. Chúng ta ít khi sử dụng phương thức này vì ServerSocket thường luôn phục vụ phía máy chủ. Khi ServerSocket gọi accept(), phương thức này sẽ không return cho đến khi một máy khách kết nối đến. Sau khi máy khách kết nối, ServerSocket tạo một 71
  6. Socket mới trên một cổng không xác định và trả về một tham chiếu đến Socket mới này và thực hiện kết nối TCP giữa máy khách và máy chủ để có thể truyền tin. 5.5 Lập trình TCP bằng mô hình Client/Server Trong mô hình lập trình TCP Client/Server với Java chúng ta sử dụng hai lớp ServerSocket và Socket. Lớp ServerSocket chỉ sử dụng ở phía Server trong khi lớp Socket sử dụng đồng thời ở phía Client và Server để trao đổi dữ liệu. Hình 5.1: Mô hình Client/Server theo kỹ thuật lập trình với giao thức TCP Quan sát hình trên chúng ta thấy rằng để tạo một ứng dụng mạng chạy bằng giao thức TCP chúng ta cần thiết lập hai ứng dụng riêng biệt: một ứng dụng Server và một ứng dụng cho Client. Theo trình tự thời gian, ứng dụng phía máy chủ sẽ chạy trước và tạo ra một ServerSocket trên cổng x để lắng nghe các kết nối từ phía máy khách. Máy khách sẽ tạo ra một Socket để kết nối tới máy chủ hostid qua cổng x. Khi có yêu cầu kết nối, máy chủ sẽ chấp nhận kết nối bằng cách tạo ra một Socket qua phương thức accept(). Sau khi thiết lập kết nối máy chủ và máy khách có thể trao đổi dữ liệu thông qua các phương thức kiểm soát vào-ra của lớp Socket. Kết nối này sẽ tồn tại đến khi nào một trong hai bên hủy bỏ kết nối bằng cách đóng kết nối qua phương thức close() hoặc có sự cố về mạng. 72
  7. 5.6 Xử lý ngoại lệ trong lập trình mạng Trong lập trình mạng nói chung, chúng ta thường xuyên gặp phải một số lỗi nhất định khi chạy chương trình. Có thể kể ra như lỗi xung đột cổng giữa các ứng dụng trên máy chủ, lỗi không kết nối được giữa các máy tính, lỗi không gửi/nhận dữ liệu được qua mạng Java định nghĩa một số ngoại lệ (Exception) để xử lý các sự cố này như: IOException, UnknownHostException Để xử lý các ngoại lệ này chúng ta có hai cách: • Sử dụng cú pháp try-catch: chúng ta nên dùng cách này để chủ động trong việc xử lý lỗi. Khi có lỗi xảy ra, chúng ta có thể có biện pháp khắc phục hợp lý hoặc thông báo lỗi cho người dùng biết. • Sử dụng cú pháp throws cho các phương thức: sử dụng cách này là chúng ta giao cho các lớp xử lý ngoại lệ của Java xử lý giúp. Cách này chỉ nên dùng với những lỗi đơn giản hoặc ít gặp phải. 5.7 Một số ví dụ Ví dụ 5-1. Viết chương trình kiểm tra một cổng trên máy chủ có đang hoạt động hay không. Cổng đang hoạt động được hiểu là cổng đang có một ứng dụng chạy trên đó. Có nghĩa là nó đang đóng vai trò là máy phục vụ trên cổng đó. Việc thiết kế giao diện người dùng trong ví dụ này và các ví dụ sau được thực hiện trong phần mềm NetBeans. Chúng ta có thể sử dụng các công cụ khác để thiết kế hoặc tự sinh các đối tượng đồ họa bằng thư viện Swing và AWT. Dùng Swing hoặc AWT thiết kế giao diện như sau, tên biến của các đối tượng đồ họa được chú thích ở cuối mũi tên. Hình 5.2: Thiết kế giao diện kiểm tra cổng mạng Xử lý sự kiện khi người dùng bấm nút Kiểm tra như sau: private void btCheckActionPerformed(java.awt.event.ActionEvent evt) { String host = tfHost.getText(); //Máy chủ (host) 73
  8. int port = Integer.parseInt(tfPort.getText()); //Cổng (port) Socket sk = new Socket(); //Tạo socket try { sk.connect(new InetSocketAddress(host, port), 1000); JOptionPane.showMessageDialog(null, "Cổng "+port+" đang hoạt động", "Trạng thái", 1); sk.close(); } catch (IOException ex) { JOptionPane.showMessageDialog(null, "Cổng "+port+" đang không hoạt động", "Trạng thái", 1); } } Kết quả chúng ta nhận được như sau: Hình 5.3: Kết quả kiểm tra cổng mạng Ví dụ 5-2. Viết chương trình quét cổng trên máy chủ. Chương trình sẽ quét trong một phạm vi cổng nhất định trên máy chủ xem cổng nào đang hoạt động, cổng nào không hoạt động. Thiết kế giao diện như hình dưới: Hình 5.4: Thiết kế giao diện quét cổng mạng Xử lý sự kiện khi người dùng bấm vào nút btScan như sau: 74
  9. public void btScanActionPerformed(java.awt.event.ActionEvent evt) { String host = tfHost.getText(); int min = Integer.parseInt(tfMin.getText()); int max = Integer.parseInt(tfMax.getText()); taResult.setText(""); for (int i = min; i <= max; i++) { if (isActive(host,i,1000)) { taResult.append("Cổng "+i+" đang hoạt động\n"); } else { taResult.append("Cổng "+i+" đang không hoạt động\n"); } } } public boolean isActive(String host, int port, int timeout) { Socket sk = new Socket(); try { sk.connect(new InetSocketAddress(host, port), timeout); sk.close(); return true; } catch (IOException ex) { return false; } } Trong đoạn mã trên, phương thức isActive(host, port, timeout) dùng để kiểm tra xem cổng port trên máy chủ host có đang hoạt động hay không. Phương thức này sẽ trả về giá trị true nếu chương trình kết nối được với host qua cổng port trong thời gian chờ timeout, ngược lại nó sẽ trả về giá trị false. Hình 5.5: Kết quả quét kiểm tra cổng mạng Ví dụ 5-3. Viết chương trình theo mô hình Client/Server. Client gửi một xâu lên Server. Server chuyển xâu thành chữ in hoa rồi gửi trả lại cho Client. Bước 1: Lập trình phía Server. - Tạo một project đặt tên là TCP_Server. 75
  10. - Trong project này tạo một class và đặt tên là Server. - Viết mã lệnh cho Server như sau: public class Server { private final int PORT = 3210; public static void main(String[] args) { new Server().run(); } public void run() { try { ServerSocket server = new ServerSocket(PORT); System.out.println("Máy chủ đang chạy "); while (true) { Socket sk = server.accept(); Scanner in = new Scanner(sk.getInputStream()); PrintWriter out = new PrintWriter(sk.getOutputStream(),true); if (in.hasNextLine()) { String inSt = in.nextLine(); String outSt = inSt.toUpperCase(); out.println(outSt); } sk.close(); } } catch (IOException ex) { System.out.println("Không thể khởi chạy máy chủ!!!");; } } } Bước 2: Lập trình phía Client - Tạo một project khác đặt tên là TCP_Client. - Trong project này tạo một JFrame Form đặt tên là Client. - Thiết kế giao diện như hình dưới: Hình 5.6: Thiết kế giao diện Client xử lý xâu - Xử lý sự kiện khi người dùng nhấn vào nút Gửi: 76
  11. private void btSendActionPerformed(java.awt.event.ActionEvent evt) { String host = tfHost.getText(); int port = Integer.parseInt(tfPort.getText()); String inputStr = tfInput.getText(); try { Socket sk = new Socket(host, port); Scanner in = new Scanner(sk.getInputStream()); PrintWriter out = new PrintWriter(sk.getOutputStream(), true); out.println(inputStr); String serverStr = in.nextLine(); tfResult.setText(serverStr); sk.close(); } catch (IOException ex) { JOptionPane.showMessageDialog(null, "Không thể kết nối tới máy chủ!!!", "Lỗi", 0); } } Bước 3: Chạy chương trình - Chạy Server trước. - Chạy Client sau, nhập dữ liệu và nhấn nút “Gửi”. - Kết quả như hình dưới. Hình 5.7: Kết quả xử lý xâu bằng máy chủ TCP Trên đây là một ví dụ đơn giản về kỹ thuật lập trình với giao thức TCP để xử lý xâu. Chúng ta thấy rằng việc xử lý xâu hoàn toàn ở phía Server, còn Client chỉ có nhiệm vụ gửi xâu và đợi kết quả. Việc xử lý xâu như thế nào là hoàn toàn do Server quyết định và một Server có thể xử lý yêu cầu của nhiều Client khác nhau. Trong chương trình phía Server chúng ta thấy có vòng lặp while(true), vòng lặp này không bao giờ dừng trừ khi chúng ta tự tắt chương trình. Việc có vòng lặp này đảm bảo rằng phía Server luôn chạy, nếu không có while(true)chương trình sẽ chỉ phục vụ một lần. Server có thể xử lý yêu cầu từ nhiều Client cùng một lúc. 77
  12. Tuy đơn giản, nhưng ví dụ trên đã minh họa đầy đủ các bước thực hiện một giao tiếp mạng bằng giao thức TCP. Chúng ta có thể xử lý các yêu cầu phức tạp hơn ở phía Server hay gửi nhiều yêu cầu hơn ở phía Client. Đó là những kiến thức thuộc kỹ thuật lập trình, hoàn toàn có thể thực hiện được. Và để gửi nhiều dữ liệu hơn ở phía Client, chúng ta cùng xem xét ví dụ phía dưới. Ví dụ 5-4. Viết chương trình theo mô hình Client/Server. Client gửi lên Server hai số thực và một trong bốn phép toán: cộng, trừ, nhân, chia. Server xử lý tính toán theo yêu cầu và gửi trả kết quả. Bước 1: Lập trình phía Client - Tạo project đặt tên là TCP_Calculator_Client. - Trong project này tạo một JFrame Form đặt tên là Client. - Thiết kế giao diện cho Client như sau: Hình 5.8: Thiết kế giao diện Client xử lý số - Xử lý sự kiện người dùng bấm vào nút Tính toán như sau: private void btCalculateActionPerformed(java.awt.event.ActionEvent evt) { try { String host = tfHost.getText(); int port = Integer.parseInt(tfPort.getText()); double n1 = Double.parseDouble(tfNumber1.getText()); double n2 = Double.parseDouble(tfNumber2.getText()); String operator = cbOperator.getSelectedItem().toString(); Socket sk = new Socket(host,port); Scanner in = new Scanner(sk.getInputStream()); PrintWriter out = new PrintWriter(sk.getOutputStream(),true); out.println(n1); out.println(n2); out.println(operator); tfResult.setText(in.nextLine()); sk.close(); } catch (IOException ex) { 78
  13. JOptionPane.showMessageDialog(null, "Không thể kết nối tới máy chủ!!!", "Lỗi", 0); } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(null, "Vui lòng nhập dữ liệu hợp lệ!!!", "Lỗi", 0); } } Bước 2: Lập trình phía Server - Tạo một project đặt tên là TCP_Calculator_Server. - Trong project này tạo một class và đặt tên là Server. - Viết mã lệnh cho Server như sau: public class Server { private final int PORT = 3210; public static void main(String[] args) { new Server().run(); } public void run() { try { ServerSocket server = new ServerSocket(PORT); System.out.println("Máy chủ đang chạy "); while (true) { Socket sk = server.accept(); Scanner in = new Scanner(sk.getInputStream()); PrintWriter out = new PrintWriter(sk.getOutputStream(),true); double n1 = in.nextDouble(); double n2 = in.nextDouble(); String operator = in.next(); String result = ""; switch (operator) { case "+": result = (n1+n2) + ""; break; case "-": result = (n1-n2) + ""; break; case "*": result = (n1*n2) + ""; break; case "/": DecimalFormat f = new DecimalFormat("#.##"); result = f.format(n1/n2) + ""; break; } out.println(result); sk.close(); } } catch (IOException ex) { System.out.println("Không thể khởi chạy máy chủ!!!"); } 79
  14. } } Bước 3: Chạy chương trình - Chạy Server trước. - Chạy Client sau, nhập dữ liệu và nhấn nút Tính toán. - Kết quả như hình dưới. Hình 5.9: Kết quả xử lý số bằng máy chủ TCP Trong đoạn mã phía Client, chúng ta thấy việc gửi dữ liệu (n1, n2, operator) lên Server được thực hiện nối tiếp nhau bằng ba câu lệnh println(). Như là một giao ước, phía bên Server cũng sẽ phải nhận liên tiếp ba giá trị này. Trong kỹ thuật lập trình mạng, sau khi thiết lập kết nối TCP các máy tính có thể thực hiện nhiều lần việc gửi và nhận dữ liệu. Tuy nhiên, chúng ta cần thiết lập quy tắc trao đổi dữ liệu để sao cho một bên gửi thì bên kia nhận dữ liệu. Việc gửi- nhận này cần phải nhịp nhàng, chính xác. Thêm vào đó, thay vì phải gửi liên tiếp ba lần cho ba dữ liệu, chúng ta có thể nối 3 dữ liệu này thành một xâu duy nhất và gửi đi một lần. Phía Server chia tách xâu và xác định lại các dữ liệu cần thiết. Cấu trúc của xâu ghép phải được Client và Server thống nhất với nhau. Giả sử, ở ví dụ trên thay vì thực hiện ghi ba lần liên tiếp lên dòng ra các dữ liệu n1, n2, operator ta chỉ cần nối chúng thành một xâu ngăn cách bởi kí tự đặc biệt và gửi tới máy chủ xâu đó: “n1@n2@operator”. CÂU HỎI, BÀI TẬP VẬN DỤNG: 1. Lớp Socket dùng để làm gì? Liệt kê các phương thức của lớp Socket? 2. Lớp ServerSocket dùng để làm gì? Liệt kê các phương thức của lớp ServerSocket? 80
  15. 3. Trình bày kỹ thuật lập trình với giao thức TCP bằng mô hình Client/Server? 4. Sử dụng kỹ thuật lập trình với giao thức TCP, viết chương trình Java theo mô hình Client/Server xử lý xâu như sau: • Tính số từ của xâu. • Tìm xâu đảo ngược của xâu. Ví dụ: “ABCD” à “DCBA”. 5. Sử dụng kỹ thuật lập trình với giao thức TCP, viết chương trình Java theo mô hình Client/Server thực hiện tính toán như sau: • Chuyển một số sang hệ Hexa-Decimal (hệ thập lục phân). • Kiểm tra xem số đã cho có phải số hoàn hảo không? (N là số hoàn hảo nếu N có tổng các ước số bằng chính nó - trừ ước là N) 6. Sử dụng kỹ thuật lập trình với giao thức, viết chương trình Java theo mô hình Client/Server thực hiện tính toán như sau: • Tìm ước số chung lớn nhất của A và B. • Tìm xem có số nào trong hai số là số nguyên tố không? 7. Viết chương trình chat đơn giản trong mạng LAN sử dụng giao thức TCP? 8. Viết chương trình trò chơi TicTacToe giữa 2 người trên 2 máy tính khác nhau sử dụng giao thức TCP? 9. Viết chương trình trò chơi đuổi hình bắt chữ bằng giao thức TCP với hình ảnh và dữ liệu câu hỏi nằm trên máy chủ? 10. Viết chương trình thi trắc nghiệm với bộ câu hỏi nằm trong cơ sở dữ liệu trên máy chủ sử dụng giao thức TCP? 81
  16. CHƯƠNG 6. LẬP TRÌNH VỚI GIAO THỨC UDP 6.1 Khái niệm chung UDP là viết tắt của cụm từ User Datagram Protocol. UDP là một phần của bộ giao thức Internet được sử dụng bởi các chương trình chạy trên các máy tính khác nhau trên mạng. Không giống như TCP, UDP được sử dụng để gửi các gói tin ngắn gọi là datagram, cho phép truyền nhanh hơn. Tuy nhiên, UDP không cung cấp kiểm tra lỗi nên không đảm bảo toàn vẹn dữ liệu. Giao thức UDP hoạt động tương tự như TCP nhưng nó không tạo ra một kết nối giữa các máy tính và không cung cấp kiểm tra lỗi khi truyền gói tin. Khi một ứng dụng sử dụng UDP, các gói tin chỉ được gửi đến người nhận. Người gửi không đợi để đảm bảo người nhận có nhận được gói tin hay không, mà tiếp tục gửi các gói tiếp theo. Nếu người nhận bỏ lỡ một vài gói tin UDP, gói tin đó bị mất vì người gửi sẽ không gửi lại chúng. Điều này có nghĩa là các thiết bị có thể giao tiếp nhanh hơn. UDP được sử dụng khi tốc độ được ưu tiên và sửa lỗi là không cần thiết. UDP thường được sử dụng cho phát sóng trực tuyến và trò chơi trực tuyến. Ví dụ như khi xem một luồng video trực tiếp, thường được phát bằng UDP thay vì TCP. Máy chủ chỉ gửi một luồng UDP liên tục tới các máy tính đang xem. Nếu bị mất kết nối trong vài giây, video có thể bị ngưng hoặc lag trong chốc lát và sau đó phát tiếp phần hiện tại. Nếu bị mất gói tin nhỏ, video hoặc âm thanh có thể bị méo mó một chút khi video tiếp tục phát mà không có dữ liệu bị mất. Điều này hoạt động tương tự trong các trò chơi trực tuyến. Nếu chúng ta bỏ lỡ một số gói UDP, các nhân vật của người chơi có thể xuất hiện trên bản đồ ở vị trí khác khi nhận được các gói UDP mới hơn. Sự khác nhau giữa TCP và UDP thường được minh họa bằng sự khác nhau giữa hệ thống điện thoại và hệ thống bưu chính. TCP giống với hệ thống điện thoại. Khi chúng ta gọi một số điện thoại nào đó, người đó cần đồng ý kết nối bằng cách nhấc điện thoại (trả lời cuộc gọi). Nếu điện thoại bận hoặc không có ai nhấc máy, chúng ta không liên lạc được. UDP thì giống như hệ thống bưu chính. Bạn gửi thư tay tới một địa chỉ cố định thông qua hệ thống bưu chính. Thư có thể tới nơi hoặc không tới nơi nhưng bạn không thể biết chắc được điều này. Bạn có thể gửi nhiều thư đi cho người nhận, đánh số chúng và yêu cầu họ gửi thư lại xác nhận xem thư nào tới, thư nào chưa tới. Bạn và người nhận phải thực hiện điều này chứ hệ thống bưu chính không thực hiện cho bạn. Cả hệ thống điện thoại và hệ thống bưu chính hiện vẫn đang hoạt động song song cho những mục đích khác nhau. Cũng giống như vậy, TCP và UDP đều có mục 82
  17. đích sử dụng riêng. Chúng ta không thể nói rằng cái nào tốt hơn cái nào. Thay vào đó, với từng ứng dụng cụ thể hay nói chính xác hơn là với từng yêu cầu cụ thể chúng ta sử dụng TCP hay UDP. Trong Java, cả UDP Server và UDP Client đều sử dụng các đối tượng của lớp java.net.DatagramSocket để giao tiếp và java.net.DatagramPacket là lớp được sử dụng để đóng gói dữ liệu để gửi đi và nhận dữ liệu dưới dạng packet. Số lượng byte lớn nhất có thể gửi thông qua UDP là 65507 byte cho một lần. Mặc dù vậy, trên các ứng dụng thực chúng ta ít khi sử dụng hết độ dài này mà thường trong khoảng 8.192 bytes (8K). Và khi tiến hành gửi/nhận dữ liệu chúng ta cũng dùng kích thước bé hơn nhiều. Hình 6.1: Cấu tạo của DatagramPacket Để gửi dữ liệu, chúng ta cần chuyển nó về kiểu byte[], đưa dữ liệu vào DatagramPacket và gửi nó qua một DatagramSocket. Để nhận dữ liệu, ta nhận một DatagramPacket thông qua một DatagramSocket và tách lấy dữ liệu từ packet. Dữ liệu nhận được là kiểu byte[], do đó chúng ta cần chuyển nó về kiểu dữ liệu thích hợp để lấy được thông tin cần thiết. DatagramPacket cũng có các phương thức giúp đọc thông tin liên quan tới máy gửi như địa chỉ mạng, cổng, Việc sử dụng hai lớp DatagramPacket và DatagramSocket tương phản với TCP sử dụng lớp Socket và ServerSocket. Khác biệt thứ nhất giữa TCP và UDP là UDP không thực hiện một kết nối giữa hai máy tính khi gửi/nhận dữ liệu. Một socket có thể gửi và nhận dữ liệu từ nhiều máy tính khác nhau chứ không phụ thuộc vào một kết nối như TCP. Thứ hai, TCP socket sử dụng một kết nối để điều khiển các luồng dữ liệu. Với TCP, chúng ta gửi/nhận thông qua các dòng vào/dòng ra của socket. UDP thì không như vậy, chúng ta làm việc với các datagram packet riêng lẻ. 83
  18. Các packet không liên quan với nhau và khi nhận chúng ta không thể biết packet nào được gửi trước, packet nào được gửi sau. 6.2 Lớp DatagramSocket Không giống như TCP, đối với UDP thì cả bên nhận và bên gửi sẽ cùng sử dụng lớp DatagramSocket để giao tiếp với nhau. Một số phương thức chính: public DatagramSocket() throws SocketException Hàm khởi tạo UDP socket cho Client. Khởi tạo UDP socket và chưa chỉ định cổng cụ thể, dùng các cổng còn trống của hệ thống. public DatagramSocket(int port) throws SocketException Hàm khởi tạo UDP socket cho Server. Khởi tạo UDP socket và ràng buộc nó vào một port cụ thể được chỉ ra. public synchronized void setSoTimeout(int timeout) throws SocketException Phương thức thiết lập thời gian chờ cho DatagramSocket. Hết thời gian chờ này không có phản hồi từ phía máy gửi, socket sẽ tự hủy. public void send(DatagramPacket p) throws IOException Gửi DatagramPacket đến host nhận. DatagramPacket chứa dữ liệu cần gửi, độ dài dữ liệu, địa chỉ IP và số hiệu port của host sẽ nhận. public void receive(DatagramPacket p) throws IOException Thực hiện nhận về packet từ DatagramSocket. Khi phương thức này được gọi thành công, buf của DatagramPacket sẽ chứa nội dung dữ liệu nhận được. Đồng thời DatagramPacket còn chứa thông tin về địa chỉ IP và port của bên gửi. Phương thức này khi gọi sẽ bị block cho đến khi có 1 DatagramPacket được nhận. void bind(SocketAddress addr) throws SocketException Ràng buộc DatagramSocket vào một địa chỉ mạng cụ thể (IP và port). Phương thức này thường được dùng khi tạo DatagramSocket bằng phương thức thứ nhất, tức là không chỉ định rõ cổng cụ thể hoặc là muốn thay đổi địa chỉ mạng cho UDP socket. public void close() Đóng DatagramSocket. 84
  19. 6.3 Lớp DatagramPacket Lớp DatagramPacket trong Java dùng để đóng gói dữ liệu để gửi đi, đồng thời cũng dùng để nhận dữ liệu từ DatagramSocket. Một số phương thức chính: public DatagramPacket(byte buf[], int length) Khởi tạo một DatagramPacket dùng để nhận 1 packet có độ dài là length nhỏ hơn hoặc bằng buf.length; buf[] là vùng nhớ đệm dùng để lưu dữ liệu sắp nhận; length là số lượng byte lớn nhất được dùng để nhận dữ liệu. public DatagramPacket(byte buf[], int length, InetAddress address, int port) Khởi tạo một DatagramPacket để gửi 1 packet có độ dài là length đến cổng có số hiệu port trên host cụ thể được chỉ ra trong address; buf[] chính là dữ liệu muốn gửi. public InetAddress getAddress() Trả về địa chỉ IP của host đã gởi packet hoặc host sẽ nhận packet. public int getPort() Trả về giá trị port của host đã gởi packet hoặc host mà packet sẽ được gởi đến. public byte[] getData() Trả về nội dung dữ liệu trong DatagramPacket public int getLength() Trả về độ dài của dữ liệu trong DatagramPacket. public synchronized int getOffset() Trả về offset (độ lệch) của dữ liệu trong DatagramPacket. 6.4 Lập trình UDP theo mô hình Client/Server Trong mô hình lập trình UDP Client/Server đều sử dụng hai lớp DatagramSocket và DatagramPacket cho cả phía Client và Server. Tuy nhiên cách sử dụng hai lớp này ở phía Client và Server là khác nhau trong các constructor. Quan sát mô hình trên Hinh 6.2, ta thấy rằng để tạo một giao tiếp bằng UDP, phía máy chủ sẽ tạo ra một socket và ràng buộc trên một cổng nhất định nào đó. Muốn gửi dữ liệu (hoặc yêu cầu) thì phải biết trước các thông tin liên quan như địa chỉ mạng, cổng kết nối của máy nhận. Máy gửi sẽ tạo ra một socket dùng cho việc gửi dữ liệu. Máy gửi tạo ra một DatagramPacket với các thông tin như dữ liệu, 85
  20. chiều dài dữ liệu, địa chỉ mạng, cổng kết nối của máy nhận. Chúng ta sẽ gọi tới socket đã tạo và gửi packet đi. Hình 6.2: Mô hình Client/Server theo kỹ thuật lập trình với giao thức UDP Đoạn chương trình để gửi dữ liệu bằng giao thức UDP: DatagramSocket socket = new DatagramSocket(); byte[] data = inputStr.getBytes(); InetAddress host = InetAddress.getByName("localhost"); int port = 3210; DatagramPacket sPacket = new DatagramPacket(data,data.length,host,port); socket.send(sPacket); Phía máy nhận muốn nhận dữ liệu thì sẽ tạo ra một socket để gửi/nhận dữ liệu. Nếu đã có socket rồi thì có thể sử dụng lại. Sau đó, máy nhận tạo ra một packet với thông tin về biến nhớ đệm và độ dài dữ liệu. Sau đó, chúng ta sẽ dùng socket để nhận dữ liệu. Đoạn chương trình để gửi dữ liệu bằng giao thức UDP: DatagramSocket socket = new DatagramSocket(); byte[] buffer = new byte[65507]; DatagramPacket rPacket = new DatagramPacket(buffer,buffer.length); socket.receive(rPacket); 86
  21. Trong đoạn mã trên, 65507 được dùng làm kích thước của dữ liệu nhận. Đây là kích thước lớn nhất có thể dùng. Tuy nhiên, trên thực tế khi dùng ta có thể sử dụng số bé hơn. Để đọc thông tin của packet nhận được, ta có đoạn mã sau: InetAddress host = rPacket.getAddress(); //Địa chỉ mạng của máy gửi int port = rPacket.getPort(); //Cổng của máy gửi byte[] data = rPacket.getData(); //Dữ liệu nhận được //Tạo xâu từ dữ liệu nhận được String str = new String(data,rPacket.getOffset(),rPacket.getLength()); Thông tin đọc được từ phía máy gửi có thể dùng để gửi lại phản hồi sau này. Nói một cách chính xác thì thông tin địa chỉ mạng và cổng sẽ dùng để đóng gói thông tin qua một DatagramPacket dùng để gửi phản hồi cho máy vừa gửi thông tin. Điều này cũng giống như việc gửi thư, khi nhận được thư ta thường đọc thông tin của người gửi (nếu không biết trước) trên phong bì thư để gửi lại thư phản hồi. 6.5 Một số ví dụ Ví dụ 6-1. Viết chương trình UDP kiểm tra một xâu có phải là xâu đối xứng hay không. Client sẽ gửi lên Server một xâu. Server sẽ gửi trả kết quả kiểm tra tính đối xứng của xâu. Bước 1: Lập trình phía Server. - Tạo một project đặt tên là UDP_Server. - Trong project này tạo một class và đặt tên là Server. - Viết mã lệnh cho Server như sau: public class Server { private final int PORT = 3210; public static void main(String[] args) { new Server().run(); } public void run() { try { DatagramSocket server = new DatagramSocket(PORT); System.out.println("Máy chủ đang chạy "); while (true) { byte[] buffer = new byte[65507]; DatagramPacket rPacket = new DatagramPacket(buffer,buffer.length); server.receive(rPacket); InetAddress host = rPacket.getAddress(); int port = rPacket.getPort(); String inputStr = new String(rPacket.getData(),rPacket.getOffset(),rPacket.getLength());//Xau nhan duoc 87
  22. String reverseStr = ""; for (int i = inputStr.length()-1; i >= 0; i ) { reverseStr += inputStr.charAt(i); } String result = (reverseStr.equals(inputStr)) ? "1" : "0"; byte[] data = result.getBytes(); DatagramPacket sPacket = new DatagramPacket(data,data.length,host,port); server.send(sPacket); } } catch (SocketException ex) { System.out.println("Không thể khởi chạy máy chủ!!!"); } catch (IOException ex) { System.out.println("Không thể gửi/nhận dữ liệu!!!"); } } } Bước 2: Lập trình phía Client - Tạo một project khác đặt tên là UDP_Client. - Trong project này tạo một JFrame Form đặt tên là Client. - Thiết kế giao diện như hình dưới: Hình 6.3: Thiết kế giao diện xử lý xâu bằng UDP - Xử lý sự kiện khi người dùng nhấn vào nút Kiểm tra: private void btCheckActionPerformed(java.awt.event.ActionEvent evt) { try { InetAddress host = InetAddress.getByName(tfHost.getText()); int port = Integer.parseInt(tfPort.getText()); String inputStr = tfInput.getText(); DatagramSocket sk = new DatagramSocket(); byte[] data = inputStr.getBytes(); DatagramPacket sPacket = new DatagramPacket(data,data.length,host,port); sk.send(sPacket); byte[] buffer = new byte[65507]; DatagramPacket rPacket = new DatagramPacket(buffer,buffer.length); sk.receive(rPacket); 88
  23. String result = new String(rPacket.getData(),rPacket.getOffset(),rPacket.getLength()); if (result.equals("1")) { tfResult.setText("Xâu đã nhập là đối xứng"); } else { tfResult.setText("Xâu đã nhập không đối xứng"); } sk.close(); } catch (UnknownHostException ex) { JOptionPane.showMessageDialog(null, "Không kết nối được tới máy chủ!!!", "Lỗi", 0); } catch (SocketException ex) { JOptionPane.showMessageDialog(null, "Không thể tạo socket!!!", "Lỗi", 0); } catch (IOException ex) { JOptionPane.showMessageDialog(null, "Không thể gửi/nhận dữ liệu!!!", "Lỗi", 0); } } Bước 3: Chạy chương trình - Chạy Server trước. - Chạy Client sau, nhập dữ liệu và nhấn nút Kiểm tra. - Kết quả như hình dưới. Hình 6.4: Kết quả xử lý xâu bằng máy chủ UDP Trên đây là một ví dụ đơn giản về kỹ thuật lập trình với giao thức UDP để xử lý xâu. So với các ví dụ về TCP, rõ ràng chúng ta chương trình không tạo một kết nối giữa Server và Client. Việc giao tiếp giữa chúng được thực hiện thông qua các DatagramSocket. Dữ liệu được gửi/nhận bằng cách tạo ra các DatagramPacket với các phương thức tạo dùng để gửi và để nhận. Các DatagramPacket này được gửi/nhận thông qua các DatagramSocket. Chúng ta có thể làm lại các ví dụ ở chương trước (TCP) với kỹ thuật trao đổi dữ liệu Client/Server theo ví dụ trên. 89
  24. CÂU HỎI, BÀI TẬP VẬN DỤNG: 1. Lớp DatagramSocket dùng để làm gì? Liệt kê các phương thức của lớp DatagramSocket? 2. Lớp DatagramPacket dùng để làm gì? Liệt kê các phương thức của lớp DatagramPacket? 3. Trình bày kỹ thuật lập trình với giao thức UDP bằng mô hình Client/Server? 4. Phân biệt UDP với TCP và kỹ thuật lập trình với hai giao thức này? 5. Sử dụng kỹ thuật lập trình với giao thức UDP, viết chương trình Java theo mô hình Client/Server xử lý xâu như sau: • Tính số từ của xâu. • Tìm xâu đảo ngược của xâu. Ví dụ: “ABCD” à “DCBA”. 6. Sử dụng kỹ thuật lập trình với giao thức UDP, viết chương trình Java theo mô hình Client/Server thực hiện tính toán như sau: • Chuyển một số sang hệ Hexa-Decimal (hệ thập lục phân). • Kiểm tra xem số đã cho có phải số hoàn hảo không? (N là số hoàn hảo nếu N có tổng các ước số bằng chính nó - trừ ước là N) 7. Sử dụng kỹ thuật lập trình với giao thức UDP, viết chương trình Java theo mô hình Client/Server thực hiện tính toán như sau: • Tìm ước số chung lớn nhất của A và B. • Tìm xem có số nào trong hai số là số nguyên tố không? 8. Viết chương trình chat đơn giản trong mạng LAN sử dụng giao thức UDP? 9. Viết chương trình chuyển đổi mệnh giá các loại tiền tệ với tỉ giá được lưu trữ ở máy chủ, sử dụng giao thức UDP? 10. Viết chương trình cung cấp thông tin thời tiết cho một số thành phố tại Việt Nam với dữ liệu thời tiết được lưu trữ ở máy chủ, sử dụng giao thức UDP? 90
  25. CHƯƠNG 7. KỸ THUẬT LẬP TRÌNH PHÂN TÁN RMI 7.1 Khái niệm chung Lập trình đối tượng phân tán là một vấn đề hấp dẫn của công nghệ phân tán phần mềm ngày này. Java là ngôn ngữ đi tiên phong với RMI (Remote Method Invocation), một kỹ thuật cài đặt các đối tượng phân tán vô cùng hiệu quả và linh hoạt. Thông thường các chương trình của chúng ta được viết dưới dạng thủ tục - hàm và việc các hàm gọi lẫn nhau và truyền tham số chỉ xảy ra ở máy cục bộ. Kỹ thuật RMI - mang ý nghĩa là triệu gọi phương thức từ xa là cách thức giao tiếp giữa các đối tượng trong Java có mã lệnh cài đặt nằm ở trên các máy khác nhau có thể triệu gọi lẫn nhau. Hình 7.1: Mô hình RMI tổng quát 7.2 Kỹ thuật lập trình RMI theo mô hình Client/Server Để giải quyết một số vấn đề trong việc truyền thông giữa Client ó Server. RMI không gọi trực tiếp mà thông qua lớp trung gian. Lớp này tồn tại ở cả hai phía Client và Server: • Lớp ở Client gọi là Stub • Lớp ở máy Server gọi là Skel (Skeleton) Các đặc tính của RMI: • RMI là mô hình đối tượng phân tán của Java, nó giúp cho việc truyền thông giữa các đối tượng phân tán được dễ dàng hơn. • RMI là API bậc cao được xây dựng dựa trên lập trình socket. 91
  26. • RMI không những cho phép chúng ta truyền dữ liệu giữa các đối tượng trên các hệ thống máy tính khác nhau và còn gọi được các phương thức trong các đối tượng ở xa. • Việc truyền dữ liệu giữa các máy khác nhau được sử lý một cách trong suốt bởi máy ảo Java (Java Virtual Machine). • RMI cung cấp cơ chế callback, nó cho phép Server triệu gọi các phương thức ở Client. Hình 7.2: Kiến trúc cơ bản của RMI Kiến trúc của RMI: • Remote interface: Nên extend từ java.rmi.Remote. Nó khai báo tất cả các phương thức mà Client có thể triệu gọi. Tất cả các phương thức trong interface này nên throws RemoteException. • Remote implementation: Được thực thi từ Remote interface và mở rộng từ UnicastRemoteObject. Triển khai các phương thức được khai báo trong interface tại đây. Nó là một Remote Object thực sự. Phát sinh hai lớp trung gian Stub và Skeleton. • Server class bao gồm: o Các class được hiện thực trên Server. o RMI registry: Bộ đăng kí này sẽ đăng kí một Remote object với Naming Registry. Giúp các Remote object được chấp nhận khi gọi các phương thức từ xa. 92
  27. • Client class: Truy vấn trên tên Remote object trên RMI registry, thông qua Stub để gọi các phương thức trên Server. Truyền tin trong RMI: • RMI sử dụng lớp trung gian để truyền tin Skeleton và Stub. • Lớp Stub dùng ở Client. • Lớp Skeleton dùng ở Server. • Java tạo ra các lớp trung gian. • RMI sử dụng các TCP Socket. Cách thức hoạt động của RMI: • Server RMI phải đăng ký với 1 dịch vụ tra tìm và đăng ký tên miền. • Sau khi Server được đăng ký, nó sẽ chờ các yêu cầu của RMI client. • Các ClientRMI sẽ gửi thông điệp RMI để gọi một phương thức trên một đối tượng từ xa. • Ứng dụng Client yêu cầu một tên dịch vụ cụ thể và nhận một URL trỏ tới tài nguyên từ xa. Mô hình lập trình phân tán RMI: Tạo Hiện Viết Viết Tạo các Interface thực chương chương project dùng Interface trình cho trình cho chung ở Server Server Client Hình 7.3: Các bước lập trình theo kỹ thuật RMI • Bước 1: Tạo project cho Client và Server. • Bước 2: Tạo Interface dùng chung cho cả Client và Server. • Bước 3: Hiện thực Interface ở Server. • Bước 4: Viết chương trình phía Server. • Bước 5: Viết chương trình phía Client. 7.3 Một số ví dụ Ví dụ 7-1. Viết chương trình liệt kê các số nguyên tố từ 1 tới N, với N là một số nguyên dương, sử dụng kỹ thuật lập trình RMI. Phương thức kiểm tra số nguyên tố được triệu gọi từ xa. Bước 1: Tạo 2 project RMI_Prime_Client và RMI_Prime_Server. 93
  28. Bước 2: Trong project RMI_Prime_Server tạo một package đặt tên là Core. Trong package này tạo một interface đặt tên là PrimeInterface như sau: public interface PrimeInterface extends Remote{ public boolean isPrime(int x) throws RemoteException; } Chú ý rằng trong kỹ thuật lập trình RMI các Interface phải kế thừa lớp Remote, các phương thức của nó phải throws RemoteException. Phương thức isPrime(int x) dùng để kiểm tra một số x có phải là số nguyên tố hay không. Phương thức này chưa được hiện thực mà mới chỉ khai báo. Sao chép package Core sang project RMI_Prime_Client (bao gồm cả PrimeInterface). Bước 3: Hiện thực PrimeInterface phía Server. Trong project RMI_Prime_Server tạo một package mới đặt tên là RMI. Trong package này tạo một lớp mới đặt tên là Prime. Hiện thực hóa interface trong lớp này như sau: public class Prime extends UnicastRemoteObject implements PrimeInterface{ //Constructor public Prime() throws RemoteException { } @Override public boolean isPrime(int x) throws RemoteException { for (int i = 2; i <= Math.sqrt(x); i++) { if (x%i == 0) { return false; } } return true; } } Bước 4: Lập trình cho Server. Trong package RMI tạo class đặt tên là Server. Chúng ta tạo một Registry trên cổng bất kỳ (chẳng hạn 3210) rồi ràng buộc (bind) một PrimeService cho một đối tượng thuộc lớp Prime trên đó. public class Server { private final int PORT = 3210; public static void main(String[] args) { new Server().run(); } public void run() { try { Registry reg = LocateRegistry.createRegistry(PORT); reg.rebind("PrimeService", new Prime()); System.out.println("Máy chủ đang chạy "); } catch (RemoteException ex) { 94
  29. System.out.println("Không thể khởi chạy máy chủ!!!"); } } } Bước 5: Lập trình cho Client. Trong project RMI_Prime_Client tạo một package đặt tên là RMI. Trong package này tạo một JFrame Form đặt tên là Client. Thiết kế giao diện cho Client như sau: Hình 7.4: Thiết kế giao diện liệt kê số nguyên tố Lập trình cho sự kiện người dùng nhấp chuột vào nút Lấy kết quả như sau: private void btGetActionPerformed(java.awt.event.ActionEvent evt) { try { String host = tfHost.getText(); int port = Integer.parseInt(tfPort.getText()); int max = Integer.parseInt(tfMax.getText()); taResults.setText(""); Registry reg = LocateRegistry.getRegistry(host, port); NumberInterface prime = (NumberInterface)reg.lookup("PrimeService"); int count = 0; for (int i = 2; i <= max; i++) { if (prime.isPrime(i)) { taResults.append(i + " "); count++; } if (count == 10) { taResults.append("\n"); count = 0; } } } catch (RemoteException ex) { JOptionPane.showMessageDialog(null, "Không kết nối được tới máy chủ!!!", "Lỗi", 0); 95
  30. } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(null, "Giá trị lớn nhất phải là số nguyên!!!", "Lỗi", 0); } catch (NotBoundException ex) { JOptionPane.showMessageDialog(null, "Không tìm thấy dịch vụ!!!", "Lỗi", 0); } } Chạy Server trước, sau đó chạy Client. Trong form xuất hiện, nhập giá trị lớn nhất bất kỳ, giả sử là 500. Nhấn nút Lấy kết quả, chúng ta nhận được như hình bên dưới: Hình 7.5: Kết quả liệt kê số nguyên tố với máy chủ RMI Ví dụ 7-2. Viết chương trình tìm ước số chung lớn nhất và kiểm tra tính nguyên tố của hai số nguyên dương bất kỳ, sử dụng kỹ thuật lập trình RMI. Phương thức kiểm tra số nguyên tố và phương thức tính ước số chung lớn nhất của hai số được triệu gọi từ xa. Bước 1: Tạo 2 project RMI_XuLySo_Client và RMI_XuLySo_Server. Bước 2: Trong project RMI_XuLySo_Server tạo một package đặt tên là Core. Trong package này tạo một interface đặt tên là NumberInterface với nội dung như sau: public interface NumberInterface extends Remote{ public int ucln(int a, int b) throws RemoteException; public boolean isPrime(int x) throws RemoteException; } 96
  31. Phương thức ucln(int a, int b) dùng để dùng để tìm ước số chung lớn nhất của hai số nguyên a và b. Phương thức isPrime(int x) dùng để kiểm tra một số x có phải là số nguyên tố hay không. Sao chép package Core sang project RMI_XuLySo_Client. Bước 3: Hiện thực NumberInterface. Trong project RMI_XuLySo_Server tạo một package mới đặt tên là RMI. Trong package này tạo một lớp mới đặt tên là NumberClass. Hiện thực hóa interface trong lớp này như sau: public class NumberClass extends UnicastRemoteObject implements NumberInterface{ //Constructor public NumberClass() throws RemoteException { } @Override public int ucln(int a, int b) throws RemoteException { while (a != b) { if (a>b) { a = a-b; } else { b = b-a; } } return a; } @Override public boolean isPrime(int x) throws RemoteException { for (int i = 2; i <= Math.sqrt(x); i++) { if (x%i == 0) { return false; } } return true; } } Bước 4: Lập trình cho Server. Trong package RMI tạo class đặt tên là Server. Chúng ta tạo một Registry trên cổng bất kỳ (chẳng hạn 3210) rồi ràng buộc (bind) một tên là NumberService cho một đối tượng thuộc lớp NumberClass trên đó. public class Server { private final int PORT = 3210; public static void main(String[] args) { new Server().run(); } public void run() { try { 97
  32. Registry reg = LocateRegistry.createRegistry(PORT); reg.rebind("NumberService", new NumberClass()); System.out.println("Máy chủ đang chạy "); } catch (RemoteException ex) { System.out.println("Không thể khởi chạy máy chủ!!!"); } } } Bước 5: Lập trình cho Client. Trong project RMI_XuLySo_Client tạo một package đặt tên là RMI. Trong package này tạo một JFrame Form đặt tên là Client. Thiết kế giao diện cho Client như sau: Hình 7.6: Thiết kế giao diện xử lý số RMI Lập trình cho sự kiện người dùng nhấp chuột vào nút Thực hiện như sau: private void btThucHienActionPerformed(java.awt.event.ActionEvent evt) { try { String host = tfHost.getText(); int port = Integer.parseInt(tfCong.getText()); int n1 = Integer.parseInt(tfSo1.getText()); int n2 = Integer.parseInt(tfSo2.getText()); Registry reg = LocateRegistry.getRegistry(host, port); NumberInterface nService = (NumberInterface)reg.lookup("NumberService"); if (rdUCLN.isSelected()) { JOptionPane.showMessageDialog(null, "Ước chung lớn nhất là " + nService.ucln(n1, n2), "Kết quả", 1); } else { if (nService.isNguyenTo(n1) && !nService.isNguyenTo(n2)) { JOptionPane.showMessageDialog(null, "Số thứ nhất là số nguyên tố.", "Kết quả", 1); } if (!nService.isNguyenTo(n1) && nService.isNguyenTo(n2)) { JOptionPane.showMessageDialog(null, "Số thứ hai là số nguyên tố.", "Kết quả", 1); } if (nService.isNguyenTo(n1) && nService.isNguyenTo(n2)) { JOptionPane.showMessageDialog(null, "Cả hai đều là số nguyên tố.", "Kết quả", 1); 98
  33. } if (!nService.isNguyenTo(n1) && !nService.isNguyenTo(n2)) { JOptionPane.showMessageDialog(null, "Cả hai đều không là số nguyên tố.", "Kết quả", 1); } } } catch (NumberFormatException e) { JOptionPane.showMessageDialog(null, "Dữ liệu không hợp lệ!!!", "Lỗi", 0); } catch (RemoteException ex) { JOptionPane.showMessageDialog(null, "Không thể kết nối tới máy chủ!!!", "Lỗi", 0); } catch (NotBoundException ex) { JOptionPane.showMessageDialog(null, "Không tìm thấy dịch vụ!!!", "Lỗi", 0); } } Chạy Server trước, sau đó chạy Client. Trong form xuất hiện, nhập giá trị thích hợp. Nhấn nút Thực hiện, chúng ta nhận được như hình bên dưới: Hình 7.7: Kết quả tìm ước chung lớn nhất của hai số Hình 7.8: Kết quả kiểm tra tính nguyên tố của hai số 99
  34. Ví dụ 7-3. Viết chương trình xử lý xâu ký tự sử dụng kỹ thuật lập trình RMI. Các chức năng xử lý xâu gồm có: chuyển thành xâu in thường, chuyển thành xâu in hoa, chuẩn hóa xâu (xóa các dấu cách thừa), đếm số từ của xâu. Các phương thức xử lý xâu được triệu gọi từ xa. Bước 1: Tạo 2 project RMI_XuLyXau_Client và RMI_XuLyXau_Server. Bước 2: Trong project RMI_XuLyXau_Server tạo một package đặt tên là Core. Trong package này tạo một interface đặt tên là StringInterface như sau: public interface StringInterface extends Remote{ public String lower(String st) throws RemoteException; public String upper(String st) throws RemoteException; public String standardize(String st) throws RemoteException; public int countWord(String st) throws RemoteException; } Sao chép package Core sang project RMI_XuLyXau_Client. Bước 3: Hiện thực StringInterface. Trong project RMI_XuLyXau_Server tạo một package mới đặt tên là RMI. Trong package này tạo một lớp mới đặt tên là StringClass. Hiện thực hóa interface trong lớp này như sau: public class StringClass extends UnicastRemoteObject implements StringInterface{ //Constructor public StringClass() throws RemoteException { } @Override public String upper(String st) throws RemoteException { return st.toUpperCase(); } @Override public String lower(String st) throws RemoteException { return st.toLowerCase(); } @Override public String standardize(String st) throws RemoteException { String result = st.trim(); while (result.contains(" ")) { result = result.replace(" ", " "); } return result; } @Override public int countWord(String st) throws RemoteException { String str = this.standardize(st); int count=0; if (str.equals("")) { return count; } else { 100
  35. count = 1; } for (int i = 0; i < str.length(); i++) { if (str.charAt(i) == ' ') { count++; } } return count; } } Bước 4: Lập trình cho Server. Trong package RMI tạo class đặt tên là Server. Chúng ta tạo một Registry trên cổng bất kỳ (chẳng hạn 3210) rồi ràng buộc (bind) một StringService cho một đối tượng thuộc lớp StringClass trên đó. public class Server { private final int PORT = 3210; public static void main(String[] args) { new Server().run(); } public void run() { try { Registry reg = LocateRegistry.createRegistry(PORT); reg.rebind("StringService", new StringClass()); System.out.println("Máy chủ đang chạy "); } catch (RemoteException ex) { System.out.println("Không thể khởi chạy máy chủ!!!"); } } } Bước 5: Lập trình cho Client. Trong project RMI_XuLyXau_Client tạo một package đặt tên là RMI. Trong package này tạo một JFrame Form đặt tên là Client. Thiết kế giao diện cho Client như sau: Hình 7.9: Thiết kế form xử lý xâu theo kỹ thuật RMI Lập trình cho sự kiện người dùng nhấp chuột vào nút Xử lý như sau: private void btProcessActionPerformed(java.awt.event.ActionEvent evt) { String host = tfHost.getText(); int port = Integer.parseInt(tfPort.getText()); String st = tfInput.getText(); try { Registry reg = LocateRegistry.getRegistry(host, port); StrInterface StrObj = (StrInterface)reg.lookup("StringService"); 101
  36. String result; switch (cbFunction.getSelectedIndex()) { case 0: result = StrObj.lower(st); JOptionPane.showMessageDialog(null, result, "Kết quả", 1); break; case 1: result = StrObj.upper(st); JOptionPane.showMessageDialog(null, result, "Kết quả", 1); break; case 2: result = StrObj.standardize(st); JOptionPane.showMessageDialog(null, result, "Kết quả", 1); break; case 3: result = StrObj.countWord(st)+""; JOptionPane.showMessageDialog(null, result, "Kết quả", 1); break; } } catch (RemoteException ex) { JOptionPane.showMessageDialog(null, "Không thể kết nối tới máy chủ!", "Lỗi", 0); } catch (NotBoundException ex) { JOptionPane.showMessageDialog(null, "Không tìm thấy dịch vụ!", "Lỗi", 0); } } Chạy Server trước, sau đó chạy Client. Trong form xuất hiện, nhập một xâu bất kỳ, chọn chức năng cần xử lý và nhấn nút Xử lý, chúng ta nhận được kết quả như hình bên dưới: Hình 7.10: Kết quả chuyển xâu thành in hoa Hình 7.11: Kết quả đếm số từ của xâu 102
  37. CÂU HỎI, BÀI TẬP VẬN DỤNG: 1. Trình bày khái niệm về kỹ thuật lập trình RMI? 2. Trình bày quy trình tạo một ứng dụng mạng sử dụng kỹ thuật lập trình RMI? 3. Sử dụng kỹ thuật lập trình RMI, viết chương trình Java theo mô hình Client/Server xử lý xâu như sau: • Tính số từ của xâu. • Tìm xâu đảo ngược của xâu. Ví dụ: “ABCD” à “DCBA”. 4. Sử dụng kỹ thuật lập trình RMI, viết chương trình Java theo mô hình Client/Server thực hiện tính toán như sau: • Chuyển một số sang hệ Hexa-Decimal (hệ thập lục phân). • Kiểm tra xem số đã cho có phải số hoàn hảo không? (N là số hoàn hảo nếu N có tổng các ước số bằng chính nó - trừ ước là N) 5. Sử dụng kỹ thuật lập trình RMI, viết chương trình Java theo mô hình Client/Server thực hiện tính toán như sau: • Tìm ước số chung lớn nhất của A và B. • Tìm xem có số nào trong hai số là số nguyên tố không? 6. Viết chương trình trò chơi TicTacToe giữa 2 người trên 2 máy tính khác nhau sử dụng kỹ thuật lập trình RMI? 7. Viết chương trình trò chơi đuổi hình bắt chữ bằng kỹ thuật lập trình RMI với hình ảnh và dữ liệu câu hỏi nằm trên máy chủ? 8. Viết chương trình thi trắc nghiệm với bộ câu hỏi nằm trong cơ sở dữ liệu trên máy chủ sử dụng kỹ thuật lập trình RMI? 9. Viết chương trình chuyển đổi mệnh giá các loại tiền tệ với tỉ giá được lưu trữ ở máy chủ, sử dụng kỹ thuật lập trình RMI? 10. Viết chương trình cung cấp thông tin thời tiết cho một số thành phố tại Việt Nam với dữ liệu thời tiết được lưu trữ ở máy chủ, sử dụng kỹ thuật lập trình RMI? 103
  38. TÀI LIỆU THAM KHẢO 1. Elliotte Rusty Harold, Java Network Programming, 4th Edition, O’Reilly, USA, 2014. 2. Kenneth L. Calvert, Micheal J. Donahoo, TCP/IP Sockets in Java: Practical guide for programmers, 2nd Edition, Elsevier, USA, 2008. 3. Richard M. Reese, Learning Network Programming with Java, Packt, UK, 2015. 4. William Stallings, Data and Computer Communications, 10th Edition, Prentice Hall, USA, 2013. 5. James F. Kurose, Keith W. Ross, Computer Networking: A Top-Down Approach, 7th Edition, Addison-Wesley, USA, 2017. 6. Cay S. Horstmann, Core Java, Volume I - Fundamentals, 10th Edition, Prentice Hall, USA, 2016. 7. Cay S. Horstmann, Core Java, Volume II - Advanced Features, 10th Edition, Prentice Hall, USA, 2017. 104