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

pdf 76 trang Gia Huy 17/05/2022 3850
Bạn đang xem 20 trang mẫu của tài liệu "Giáo trình Lập trình mạng - 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_nguyen_duy_hieu.pdf

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

  1. BỘ GIÁO DỤC VÀ ĐÀO TẠO TRƯỜNG ĐẠI HỌC TÂY BẮC GIÁO TRÌNH LẬP TRÌNH MẠNG NGUYỄN DUY HIẾU MAI VĂN TÁM SƠN LA, NĂM 2019
  2. LỜI NÓI ĐẦU Trong những năm gần đây, lập trình mạng luôn là một trong những nội dung quan trọng trong lĩnh vực công nghệ phần mềm. Nhờ sự phát triển vượt bậc trong lĩnh vực mạng máy tính, những phần mềm máy tính dùng cho doanh nghiệp hiện nay sử dụng rất nhiều trên môi trường mạng đặc biệt là Internet. Mạng máy tính là nơi các kỹ thuật liên quan tới mô hình khách/chủ, mô hình phân tán hay mô hình hợp tác được triển khai. Các ứng dụng mạng xử lý tập trung hoặc phân tán, tận dụng tối đa sức mạnh của các hệ thống phần cứng để mang lại hiệu quả cao. Giáo trình này phục vụ giảng dạy và học tập học phần Lập trình mạng tại Trường Đại học Tây Bắc. Trong nội dung, ngoài những kiến thức cơ bản về mạng máy tính có liên quan, chúng tôi sẽ hướng dẫn cách thức làm việc với kỹ thuật lập trình socket với giao thức TCP, UDP và kỹ thuật lập trình phân tán RMI. Trong từng phần kiến thức, giáo trình cũng cung cấp các ví dụ minh họa. Đây là những ví dụ hết sức cơ bản, giúp bạn đọc hiểu được cách thức mà các ứng dụng mạng hoạt động. Từ những ví dụ này, độc giả có thể tự phát triển để tạo ra các ứng dụng phức tạp hơn, mạnh mẽ hơn. Chúng tôi lựa chọn ngôn ngữ lập trình Java để trình bày về các kỹ thuật liên quan tới lập trình mạng cũng như các ví dụ minh họa. Việc lựa chọn ngôn ngữ lập trình Java không chỉ bởi Java hiện nay luôn là một trong những ngôn ngữ hàng đầu để phát triển phần mềm mà còn bởi Java là ngôn ngữ vốn sinh ra để giải quyết các vấn đề liên quan tới các ứng dụng mạng. Giáo trình không tránh khỏi những sơ suất. Chúng tôi mong nhận được các ý kiến đóng góp quý báu của quý thầy cô và các bạn sinh viên để hoàn thiện giáo trình hơn nữa. Chúng tôi xin chân thành cảm ơn. NHÓM TÁC GIẢ
  3. MỤC LỤC CHƯƠNG 1. CÁC KHÁI NIỆM CƠ BẢN VỀ MẠNG MÁY TÍNH 1 1.1 Mạng máy tính 1 1.2. Các lớp của một mạng 2 1.2.1 Lớp máy tính-mạng 4 1.2.2 Lớp Internet 4 1.2.3 Lớp giao vận 5 1.2.4 Lớp ứng dụng 6 1.3 Giao thức IP, TCP và UDP 6 1.3.1 Khái quát về giao thức IP, TCP và UDP 6 1.3.2 Địa chỉ IP và tên miền 7 1.3.3 Các cổng 9 1.4 Mạng Internet 10 1.4.1 Các khối địa chỉ Internet 10 1.4.2 Dịch địa chỉ mạng 11 1.4.3 Tường lửa 11 1.4.4 Máy chủ proxy 11 1.4.5 Mô hình Client/Server 13 CHƯƠNG 2. CÁC DÒNG VÀO-RA (STREAM) 15 2.1 Các dòng ra (output stream) 15 2.2 Các dòng vào (input stream) 19 2.3 Các dòng filter stream 23 2.3.1 Gắn kết các filter stream 25 2.3.2 Các lớp BufferedInputStream và BufferedOutputStream 25 2.3.3 Lớp PrintStream 26 2.3.4 Các lớp DataInputStream và DataOutputStream 27 2.4 Các lớp Reader and Writer 29 2.4.1 Lớp Writer 29 2.4.2 Lớp OutputStreamWriter 30
  4. 2.4.3 Lớp Reader 30 2.4.4 Các lớp Filter Reader và Filter Writer 31 2.4.5 Lớp Scanner 32 2.4.6 Lớp PrintWriter 33 CHƯƠNG 3. LẬP TRÌNH ĐA LUỒNG TRONG JAVA 35 3.1 Giới thiệu về luồng (thread) 35 3.1.1 Thread là gì? Multi-thread là gì? 35 3.1.2 Đa nhiệm (multitasking) 35 3.1.3 Ưu điểm và nhược của đa luồng 36 3.2 Vòng đời của một luồng trong Java 36 3.3 Cách tạo luồng trong Java 37 3.3.1 Tạo luồng bằng cách kế thừa từ lớp Thread 38 3.3.2 Tạo luồng bằng cách hiện thực từ giao diện Runnable 38 3.4 Ví dụ minh họa sử dụng đa luồng 39 3.5 Các phương thức của lớp Thread thường hay sử dụng 43 3.6 Một số vấn đề liên quan đến luồng 44 3.6.1 Một số tham số của luồng 44 3.6.2 Sử dụng phương thức sleep() 46 3.6.3 Sử dụng join() và join(long millis) 47 3.6.4 Xử lý ngoại lệ cho luồng 49 CHƯƠNG 4. LỚP INETADDRESS 51 4.1 Khởi tạo đối tượng InetAddress 53 4.2 Nhớ đệm (caching) 55 4.3 Tìm kiếm bằng địa chỉ IP 56 4.4 Các phương thức Get 56 4.5 Kiểm tra loại địa chỉ 58 4.6 Kiểm tra khả năng kết nối (reachable) 62 4.7 Các phương thức của Object 62 4.8 Inet4Address và Inet6Address 63
  5. 4.9 Lớp NetworkInterface 64 CHƯƠNG 5. LẬP TRÌNH VỚI GIAO THỨC TCP 67 5.1 Khái niệm chung 67 5.2 Khái niệm cổng (port number) 67 5.3 Lớp Socket 68 5.3.1 Các phương thức tạo 68 5.3.2 Các phương thức kiểm soát vào-ra 69 5.3.3 Một số phương thức khác 69 5.4 Lớp ServerSocket 70 5.4.1 Các phương thức tạo 70 5.4.2 Các phương thức khác 71 5.5 Lập trình TCP bằng mô hình Client/Server 72 5.6 Xử lý ngoại lệ trong lập trình mạng 73 5.7 Một số ví dụ 73 CHƯƠNG 6. LẬP TRÌNH VỚI GIAO THỨC UDP 82 6.1 Khái niệm chung 82 6.2 Lớp DatagramSocket 84 6.3 Lớp DatagramPacket 85 6.4 Lập trình UDP theo mô hình Client/Server 85 6.5 Một số ví dụ 87 CHƯƠNG 7. KỸ THUẬT LẬP TRÌNH PHÂN TÁN RMI 91 7.1 Khái niệm chung 91 7.2 Kỹ thuật lập trình RMI theo mô hình Client/Server 91 7.3 Một số ví dụ 93
  6. DANH MỤC HÌNH ẢNH Hình 1.1: Các giao thức trong các lớp khác nhau của một mạng 2 Hình 1.2: Các lớp trong mô hình TCP/IP 3 Hình 1.3: Cấu trúc của một IPv4 datagram 5 Hình 1.4: Kết nối các lớp thông qua máy chủ proxy 12 Hình 1.5: Kết nối Client/Server 14 Hình 2.1: Dữ liệu có thể bị mất nếu không flush các luồng 18 Hình 2.2: Dòng dữ liệu qua một chuỗi các filter 24 Hình 3.1: Các trạng thái của luồng 37 Hình 3.2: Tạo luồng bằng cách extends từ lớp Thread 41 Hình 3.3: Tạo luồng bằng cách implements từ giao diện Runnable 43 Hình 5.1: Mô hình Client/Server theo kỹ thuật lập trình với giao thức TCP 72 Hình 5.2: Thiết kế giao diện kiểm tra cổng mạng 73 Hình 5.3: Kết quả kiểm tra cổng mạng 74 Hình 5.4: Thiết kế giao diện quét cổng mạng 74 Hình 5.5: Kết quả quét kiểm tra cổng mạng 75 Hình 5.6: Thiết kế giao diện Client xử lý xâu 76 Hình 5.7: Kết quả xử lý xâu bằng máy chủ TCP 77 Hình 5.8: Thiết kế giao diện Client xử lý số 78 Hình 5.9: Kết quả xử lý số bằng máy chủ TCP 80 Hình 6.1: Cấu tạo của DatagramPacket 83 Hình 6.2: Mô hình Client/Server theo kỹ thuật lập trình với giao thức UDP 86 Hình 6.3: Thiết kế giao diện xử lý xâu bằng UDP 88 Hình 6.4: Kết quả xử lý xâu bằng máy chủ UDP 89 Hình 7.1: Mô hình RMI tổng quát 91 Hình 7.2: Kiến trúc cơ bản của RMI 92 Hình 7.3: Các bước lập trình theo kỹ thuật RMI 93 Hình 7.4: Thiết kế giao diện liệt kê số nguyên tố 95 Hình 7.5: Kết quả liệt kê số nguyên tố với máy chủ RMI 96 Hình 7.6: Thiết kế giao diện xử lý số RMI 98 Hình 7.7: Kết quả tìm ước chung lớn nhất của hai số 99 Hình 7.8: Kết quả kiểm tra tính nguyên tố của hai số 99 Hình 7.9: Thiết kế form xử lý xâu theo kỹ thuật RMI 101 Hình 7.10: Kết quả chuyển xâu thành in hoa 102 Hình 7.11: Kết quả đếm số từ của xâu 102
  7. CHƯƠNG 1. CÁC KHÁI NIỆM CƠ BẢN VỀ MẠNG MÁY TÍNH 1.1 Mạng máy tính Một mạng máy tính là một tập hợp các máy tính và các thiết bị. Các máy tính và các thiết bị trên mạng có thể gửi và nhận dữ liệu với các máy tính và các thiết bị khác. Với mạng được kết nối bằng dây dẫn, các bit dữ liệu sẽ được biến đổi thành các sóng điện từ di chuyển dọc theo dây dẫn. Các mạng không dây truyền dữ liệu bằng cách sử dụng sóng vô tuyến. Và với những đường truyền có khoảng cách lớn, dữ liệu được truyền đi bằng cách sử dụng cáp quang. Cáp quang sử dụng các sóng ánh sáng với độ dài bước sóng khác nhau để truyền dữ liệu. Mỗi một thiết bị trên một mạng được gọi là một nút mạng (node). Hầu hết các nút là các máy tính, tuy nhiên các nút cũng có thể là các máy in, router, cầu nối, gateway (thiết bị nối ghép hai mạng cục bộ không cùng họ với nhau, hoặc mạng cục bộ với một mạng diện rộng, với một máy tính mini hay máy tính lớn ), các thiết bị cuối câm (thiết bị cuối không có bộ xử lý trung tâm và các ổ đĩa - dumb terminal). Ta sẽ dùng thuật ngữ nút để nói đến bất kỳ thiết bị nào trên mạng và dùng thuật ngữ host để nói đến một nút là một máy tính đa năng. Mỗi một nút mạng có một địa chỉ. Một địa chỉ là một chuỗi các byte xác định duy nhất một nút. Địa chỉ được gán cho từng nút mạng sẽ khác nhau đối với các mạng khác nhau. Các địa chỉ Ethernet được gắn vào phần cứng vật lý Ethernet. Các nhà sản xuất phần cứng Ethernet sử dụng mã nhà sản xuất được chỉ định trước để đảm bảo không có xung đột giữa địa chỉ trong phần cứng của họ và địa chỉ của phần cứng của nhà sản xuất khác. Mỗi nhà sản xuất chịu trách nhiệm đảm bảo rằng không có bất kỳ hai card mạng Ethernet nào có cùng địa chỉ. Các địa chỉ Internet thường được gán cho một máy tính bởi Nhà cung cấp dịch vụ Internet (Internet Service Provider - ISP). Các mạng máy tính hiện đại là các mạng chuyển mạch gói (packet-switched): dữ liệu truyền trên mạng được chia nhỏ thành các đoạn được gọi là các gói (packet). Mỗi một gói chứa thông tin về bên gửi và bên nhận. Một số ưu điểm của việc chia nhỏ dữ liệu thành các gói được gán địa chỉ là: + Các gói tin từ nhiều nguồn trao đổi thông tin khác nhau có thể được truyền đi trên cùng một đường dây và sẽ làm giảm giá thành cho việc xây dựng các mạng. Các máy tính có thể chia sẻ chung một đường truyền mà không sợ bị can nhiễu lẫn nhau. + Các mã kiểm tra tổng (checksum) có thể được sử dụng để phát hiện gói tin có bị hư hại trong quá trình truyền tin hay không. 1
  8. Để các mạng máy tính hoạt động được ta cần phải có những quy tắc hoạt động, đó chính là các giao thức (protocol). Một giao thức là một tập hợp chính xác các luật định nghĩa cách thức các máy tính giao tiếp với nhau: khuôn dạng của các địa chỉ, cách chia dữ liệu được thành các gói Ví dụ: + Giao thức HTTP (Hypertext Transfer Protocol) quy định cách thức trao đổi thông tin giữa các trình duyệt web (web browsers) với máy chủ dịch vụ web (webserver). + Chuẩn IEEE 802.3 quy định cách mã hóa các bit thành các tín hiệu điện trên từng loại dây dẫn cụ thể. Các chuẩn giao thức mở, đã được công bố cho phép phần mềm và thiết bị từ các nhà cung cấp khác nhau giao tiếp với nhau. Ví dụ một máy chủ web không cần phải quan tâm người dùng sẽ sử dụng hệ điều hành nào trên thiết bị của họ, chẳng hạnh như Windows, Unix, Android, iOS vì tất cả các thiết bị này đều sử dụng chung giao thức HTTP. 1.2. Các lớp của một mạng Truyền dữ liệu trên một mạng là một quá trình phức tạp. Để người phát triển ứng dụng và để người sử dụng không cần nhìn thấy sự phức tạp của quá trình truyền dữ liệu, các thành phần khác nhau của một mạng truyền thông được chia thành nhiều lớp. Mỗi một lớp biểu diễn một mức khác nhau của việc trừu tượng hóa giữa phần cứng vật lý (dây dẫn, điện ) và thông tin được truyền. Hình 1.1: Các giao thức trong các lớp khác nhau của một mạng 2
  9. Về mặt lý thuyết, mỗi một lớp chỉ trao đổi với các lớp kề ngay trên và kề ngay dưới. Việc tách một mạng thành các lớp cho phép ta sửa đổi thậm chí là thay thế phần mềm trong một lớp mà không ảnh hưởng đến các lớp khác miễn sao cho các giao diện giữa các lớp không thay đổi. Sơ đồ trong Hình 1.1 trình bày một mô hình phân tầng của các giao thức có thể có trong một mạng. Trong khi các giao thức các lớp ở giữa tương đối ổn định trong mạng Internet thì các giao thức trên đỉnh và dưới đáy thay đổi rất nhiều. Tồn tại nhiều mô hình chia lớp khác nhau như mô hình OSI, mô hình TCP/IP. Mỗi mô hình phù hợp với các yêu cầu của một kiểu mạng cụ thể. Trong tài liệu này ta sẽ sử dụng mô hình TCP/IP, đây là mô hình chuẩn bốn lớp phù hợp với mạng Internet. Sơ đồ sau sẽ trình bày cấu trúc của mô hình TCP/IP. Hình 1.2: Các lớp trong mô hình TCP/IP Các lớp trong mô hình TCP/IP bao gồm: + Lớp ứng dụng (Application Layer). + Lớp giao vận (Transport Layer) sử dụng các giao thức TCP hoặc UDP. + Lớp Internet (Internet Layer) sử dụng giao thức IP. + Lớp máy tính-mạng (Host-To-Network Layer) sử dụng các giao thức Ethernet, WiFi, LTE Một ứng dụng được chạy trong lớp ứng dụng và chỉ trao đổi thông tin với lớp giao vận. Lớp giao vận chỉ trao đổi thông tin với lớp ứng dụng và lớp Internet. Lớp Internet, đến lượt mình, chỉ trao đổi thông tin với lớp giao vận và lớp máy tính-mạng và không trao đổi trực tiếp với lớp ứng dụng. Lớp máy tính-mạng chuyển dữ liệu thông qua các dây dẫn, cáp quang hoặc các đường truyền vật lý khác tới lớp máy tính-mạng trên hệ thống từ xa khác trong mạng. Lớp máy tính-mạng trên hệ thống từ xa sau đó sẽ chuyển dữ liệu lên các lớp bên trên tới lớp ứng dụng trong hệ thống này. Ta sẽ tìm hiểu chức năng của mỗi lớp ở các phần dưới đây. 3
  10. 1.2.1 Lớp máy tính-mạng Lớp máy tính-mạng (lớp vật lý) quy định cách thức một giao diện mạng cụ thể. Chẳng hạn như một card Ethernet hoặc một ăng-ten WiFi gửi các IP datagram qua kết nối vật lý của nó tới mạng cục bộ và tới mạng diện rộng. Lớp máy tính-mạng được xây dựng bởi phần cứng kết nối các máy tính với nhau (dây dẫn, cáp quang, sóng vô tuyến) và đôi khi còn được gọi là lớp vật lý của một mạng. Các lập trình viên sử dụng ngôn ngữ Java không cần phải quan tâm tới lớp này trừ khi có các vấn đề về kỹ thuật cần phải khắc phục. Các lập trình viên chỉ cần quan tâm đến hiệu suất của mạng, ví dụ khách hành của chúng ta sử dụng một mạng cáp quang tốc độ cao ta sẽ cần phải thiết kế một giao thức và các ứng dụng phù hợp với tốc đô của mạng bên phía khách hàng. 1.2.2 Lớp Internet Lớp tiếp theo trong một mạng và là lớp đầu tiên ta cần phải quan tâm đến đó là lớp Internet. Trong mô hình OSI, lớp Internet có tên gọi là lớp mạng. Một giao thức cho lớp mạng quy định cách các bit và các byte dữ liệu được tổ chức thành một nhóm lớn hơn được gọi là các packet và mô hình đánh địa chỉ để các máy tính có thể tìm thấy nhau trong mạng. Giao thức Internet (Internet Protocol - IP) là giao thức được sử dụng rộng rãi nhất trên thế giới và Java hiểu được các giao thức của lớp này. Giao thức IP có hai phiên bản là IPv4 và IPv6. IPv4 sử dụng các địa chỉ 32 bit và IPv6 sử dụng các địa chỉ 128 bit. IPv6 bổ sung thêm một số tính năng kỹ thuật để trợ giúp quá trình định tuyến. Hiện nay IPv6 đã được sử dụng rộng rãi và có khả năng sẽ vượt qua IPv4 về số lượng người dùng. Cần phải có các gateway đặc biệt và các giao thức đường ống (tunnel) để hai giao thức IPv4 và IPv6 có thể cùng hoạt động trên một mạng. Cả hai giao thức IPv4 và IPv6 gửi dữ liệu qua lớp Internet trong các packet được gọi là datagram. Mỗi một datagram trong IPv4 chứa một phần đầu (header) có độ dài từ 20 byte đến 60 byte và một khối lượng dữ liệu (payload) lên đến 65.515 byte. Trong thực tế thì payload trong IPv4 nhỏ hơn nhiều, từ khoảng vài chục byte đến khoảng 8 kilobyte. Một datagram của IPv6 chứa một header lớn hơn và payload có thể lên đến 4 gigabyte. Sơ đồ trong Hình 1.3 trình bày cấu trúc của một IPv4 datagram. Trong sơ đồ này tất cả các bit và các byte được biểu diễn dưới dạng big-endian: MSB đến LSB tính từ trái qua phải. 4
  11. Hình 1.3: Cấu trúc của một IPv4 datagram Bên cạnh chức năng định tuyến và đánh địa chỉ, mục đích thứ hai của lớp Internet là cho phép các lớp máy tính đến mạng với nhiều kiểu khác nhau có thể trao đổi được với nhau. Các router Internet chuyển đổi các giao thức giữa WiFi và Ethernet, Ethernet và DSL, DSL và cáp quang. Nếu không có lớp Internet, mỗi máy tính chỉ có thể trao đổi với các máy tính khác trên cùng một kiểu mạng. Lớp Internet có nhiệm vụ kết nối các mạng không đồng nhất sử dụng các giao thức không đồng nhất với nhau. 1.2.3 Lớp giao vận Các datagram chưa được xử lý có nhiều hạn chế. Đáng chú ý nhất là không có một sự bảo đảm nào cho các datagram sẽ được chuyển giao tới bên nhận, thậm chí nếu được chuyển đi thì các datagram cũng có thể bị hư hỏng trong quá trình truyền. Phần kiểm tra tổng của phần header (header checksum) cũng chỉ có thể phát hiện những hư hỏng trong phần header và không thể phát hiện những hư hỏng trong phần dữ liệu của một datagram. Cuối cùng là các datagram có thể không bị hư hỏng trong quá trình gửi thì cũng có thể sẽ đến đích không theo đúng thứ tự mà các datagram đã được gửi đi vì các datagram có thể đi theo những tuyến đường khác nhau từ nguồn đến đích. Ví dụ: datagram A được gửi trước datagram B nhưng không có nghĩa là datagram A sẽ đến đích trước datagram B. 5
  12. Lớp giao vận có nhiệm vụ để đảm bảo các gói tin được nhận theo đúng thứ tự đã được gửi đi và dữ liệu không bị mất hay bị hư hỏng. Nếu một gói tin bị mất, lớp giao vận có thể yêu cầu bên gửi gửi lại gói tin đó. Các mạng IP cài đặt cơ chế này bằng cách bổ sung thêm một phần header vào mỗi datagram. Có hai giao thức quan trọng nhất tại lớp giao vận đó là giao thức TCP (Transmission Control Protocol) và giao thức UDP (User Datagram Protocol). Giao thức TCP là giao thức cho phép truyền lại các dữ liệu bị mất hay bị hư hỏng và chuyển giao các byte theo đúng thứ tự đã được gửi đi. TCP được gọi là giao thức đáng tin cậy. Giao thức UDP cho phép bên nhận phát hiện các gói tin bị hư hỏng nhưng không đảm bảo các gói tin được chuyển giao theo đúng thứ tự. UDP thường nhanh hơn rất nhiều so với TCP. Giao thức UDP là giao thức không tin cậy, tuy nhiên UDP vẫn được sử dụng nhiều trong các mạng Internet. 1.2.4 Lớp ứng dụng Lớp ứng dụng có trách nhiệm chuyển dữ liệu đến người sử dụng. Ba lớp bên dưới làm việc cùng với nhau để quy định cách dữ liệu được truyền từ một máy tính đến một máy tính khác. Lớp ứng dụng quyết định cần phải làm gì với dữ liệu sau khi dữ liệu đã được truyền tới lớp này. Tồn tại nhiều giao thức khác nhau trong lớp ứng dụng, ví dụ: các giao thức HTTP, HTTPS cho World Wide Web; các giao thức SMTP, POP, IMAP cho email; các giao thức FTP, FSP, TFTP cho truyền file; giao thức NFS cho truy nhập file; các giao thức Gnutella and BitTorrent cho chia sẻ file; các giao thức Session Initiation Protocol (SIP) and Skype cho truyền tiếng nói 1.3 Giao thức IP, TCP và UDP 1.3.1 Khái quát về giao thức IP, TCP và UDP Giao thức Internet - IP được phát triển trong thời chiến tranh lạnh giữa Liên Xô và Mỹ và được đỡ đầu bởi quân đội Mỹ. IP cần phải đáp ứng được các yêu cầu: Đầu tiên, giao thức IP phải đủ mạnh để toàn bộ hệ thống mạng không bị ngừng hoạt động nếu Liên Xô tấn công hạt nhân vào một vị trí nào đó, chẳng hạn ở Cleveland. Tất cả các thông báo vẫn được chuyển đến đích (ngoại trừ đến Cleveland). Do đó, giao thức IP được thiết kế để cho phép có nhiều tuyến đường giữa hai điểm bất kỳ và để định tuyến các gói tin không đi qua các router đã bị hỏng. Thứ hai, quân đội Mỹ có nhiều loại máy tính khác nhau và tất cả các máy tính này phải trao đổi được với nhau. Do đó, giao thức IP cần phải là một giao thức mở và không phụ thuộc vào hệ điều hành. Do có nhiều tuyến đường giữa hai điểm bất kỳ và đường đi nhanh nhất giữa hai điểm có thể thay đổi liên tục nên các gói tin của 6
  13. một luồng dữ liệu có thể không đi cùng một tuyến đường đến đích và có thể đến đích không theo đúng thứ tự đã được gửi. Để cải thiện mục tiêu trên, giao thức TCP đã được đặt bên trên IP để cho phép điểm cuối của một kết nối có khả năng phản hồi các gói tin đã nhận được và yêu cầu gửi lại các gói tin bị mất hoặc bị hư hỏng. TCP còn cho phép các gói tin được sắp xếp lại theo đúng thứ tự đã được gửi đi. Tuy nhiên, TCP lại chứa một phần không nhỏ thông tin phụ được gọi là overhead, phần này sẽ ảnh hưởng đến tốc độ truyền các gói tin. Do đó, nếu thứ tự của các gói tin không thật sự quan trọng và việc một vài gói tin bị hư hỏng hay bị mất không ảnh hưởng đến luồng dữ liệu thì các gói tin đôi khi được gửi đi mà không cần được đảm bảo và TCP cung cấp việc sử dụng giao thức UDP. Như đã trình bày ở phần Lớp giao vận, UDP là giao thức không tin cậy nên giao thức này có thể ảnh hưởng đến việc truyền file. Tuy nhiên giao thức này lại thích hợp cho các ứng dụng khi việc mất một phần dữ liệu sẽ không bị người dùng cuối phát hiện, ví dụ việc mất một vài bit từ một tín hiệu video hay audio sẽ không làm giảm đến chất lượng của dòng bit dữ liệu. Các mã sửa lỗi có thể được đưa vào trong các dòng dữ liệu sử dụng UDP tại mức ứng dụng để xử lý các thông tin bị mất. Một số lượng lớn các giao thức có thể chạy trên đỉnh của giao thức IP, phổ biến nhất là giao thức Internet Control Message Protocol - ICMP. ICMP sử dụng các datagram chưa được xử lý để chuyển tiếp các thông báo lỗi giữa các máy tính. Ping là chương trình được sử dụng phổ biến nhất trong giao thức ICMP. Java chỉ hỗ trợ TCP, UDP và các giao thức trong lớp ứng dụng, Java không hỗ trợ ICMP. Tất cả các giao thức khác của các lớp giao vận, lớp Internet và các lớp bên dưới chỉ có thể cài đặt trong Java bằng cách triển khai các mã gốc (native code). 1.3.2 Địa chỉ IP và tên miền Mỗi máy tính trên một mạng IPv4 được xác định bởi một địa chỉ IP duy nhất. Địa chỉ IP này gồm 4 byte, thường được viết dưới dạng bốn chữ số thập phân ngăn cách nhau bởi dấu chấm, ví dụ 192.168.1.1. Mỗi một chữ số thập phân được biểu diễn bằng một byte, do đó phạm vi của mỗi số thập phân sẽ từ 0 đến 255. Khi các gói tin được truyền đi trên mạng, địa chỉ của máy gửi và địa chỉ của máy nhận sẽ được chứa trong phần header của gói tin. Các router dọc theo đường truyền sẽ dựa vào địa chỉ IP của máy nhận để lựa chọn tuyến đường tốt nhất gửi các gói tin đến đích. Máy nhận sẽ sử dụng địa chỉ IP của máy gửi để biết ai đã gửi các gói tin đến mình. Có khoảng hơn bốn tỷ địa chỉ IPv4, tuy nhiên các địa chỉ này không được phân phối phù hợp giữa các vùng trên thế giới. Tháng 4 năm 2011, châu Á và Australia 7
  14. sử dụng hết các địa chỉ IP được phân phối. Tháng 9 năm 2012, châu Âu cũng sử dụng hết các địa chỉ IP được phân phối. Bắc Mỹ, Mỹ latinh và châu Phi vẫn còn một số khối địa chỉ IP chưa sử dụng, tuy nhiên các địa chỉ này cũng sẽ nhanh chóng được sử dụng hết. Có một sự dịch chuyển từ sử dụng IPv4 sang IPv6. IPv6 là các địa chỉ có độ dài 16 byte, đủ để cung cấp địa chỉ IP cho toàn bộ con người, toàn bộ máy tính và trong thực tế toàn bộ các thiết bị trên thế giới. Cách biểu diễn địa chỉ IPv6: + Thông thường Các địa chỉ IPv6 được viết thành tám khối, mỗi khối gồm bốn chữ số viết trong hệ thập lục phân, các khối được ngăn cách nhau bởi dấu :, ví dụ FEDC:BA98:7654:3210:FEDC:BA98:7654:3210. + Các chữ số không (0) ở đầu không cần phải viết. Nếu trong một địa chỉ IPv6 có nhiều khối gồm toàn bộ số 0 thì những khối này có thể được thay thế bằng cặp dấu ::, ví dụ, địa chỉ FEDC:0000:0000:0000:00DC:0000:7076:0010 có thể được viết ngắn gọn thành FEDC::DC:0:7076:10. Mỗi cặp :: chỉ được phép xuất hiện nhiều nhất một lần trong một địa chỉ IPv6 + Trong các mạng kết hợp cả IPv4 và IPv6, bốn byte cuối cùng của một địa chỉ IPv6 đôi khi được viết như là một địa chỉ IPv4, ví dụ địa chỉ FEDC:BA98:7654:3210:FEDC:BA98:7654:3210 có thể được viết thành FEDC:BA98:7654:3210:FEDC:BA98:118.84.50.16. Rất khó để con người nhớ được các địa chỉ IP viết dưới dạng các con số, đặc biệt là với địa chỉ IPv6. Do đó, hệ thống tên miền Domain Name System (DNS) đã được phát triển để chuyển các địa chỉ IP thành các tên máy (hostname) cho dễ nhớ, ví dụ địa chỉ 208.201.239.101 được chuyển thành www.oreilly.com. Khi các chương trình viết bằng Java truy nhập mạng, các chương trình này cần phải xử lý cả các địa chỉ viết bằng các số và cả các hostname tương ứng. Các phương thức để thực hiện công việc này được cung cấp bởi lớp java.net.InetAddress. Một vài máy tính, đặc biệt là các máy chủ, có địa chỉ cố định. Các máy tính khác, đặc biệt là các máy khách trên các mạng cục bộ và các kết nối không dây sẽ nhận các địa chỉ khác nhau mỗi khi các máy tính được khởi động và kết nối vào mạng. Các địa chỉ này được cung cấp bởi DHCP Server. DHCP là viết tắt của Dynamic Host Configuration Protocol - Giao thức cấu hình host động. Trong địa chỉ IPv4, một vài khối địa chỉ và khuân mẫu có dạng đặc biệt. Tất cả các khối địa chỉ bắt đầu bằng 10., bằng 172.16. đến 172.31. và bằng 192.168. là chưa được cấp cho bất kỳ máy tính nào trên mạng. Các khối địa chỉ này được gọi là các khối địa chỉ không thể định tuyến được (non-routable) và có thể được sử dụng trên 8
  15. các mạng cục bộ. Không một máy tính nào sử dụng địa chỉ trong các khối trên có thể truy nhập được vào Internet. Các địa chỉ không thể định tuyến được rất thích hợp cho việc xây dựng các mạng riêng, các mạng này sẽ không xuất hiện trên mạng Internet. Các địa chỉ IPv4 bắt đầu bằng 127 luôn có nghĩa là các địa chỉ loopback cục bộ (local loopback address), nghĩa là các địa chỉ này luôn chỉ tới máy tính cục bộ. Hostname cho địa chỉ này thường là localhost. Trong IPv6, địa chỉ 0:0:0:0:0:0:0:1 (cũng được viết tắt là ::1) là địa chỉ loopback. Địa chỉ 0.0.0.0 luôn được chỉ đến máy gửi nhưng cũng có thể được dùng như là địa chỉ nguồn mà không phải địa chỉ đích. Tương tự, trong IPv4, bất kỳ địa chỉ nào bắt đầu bằng 0. (tám bit không) là chỉ đến một máy tính trong cùng một mạng cục bộ. Trong IPv4 địa chỉ 255.255.255.255 được sử dụng làm địa chỉ quảng bá (broadcast address). Các gói tin được gửi đến địa chỉ này sẽ được tất cả các máy trong mạng cục bộ nhận và sẽ không được gửi ra bên ngoài mạng cục bộ. Địa chỉ quảng bá thường được sử dụng để phát hiện các máy trong cùng một mạng cục bộ, ví dụ: Khi một máy tính trong mạng cục bộ được khởi động, máy tính này sẽ gửi một thông báo đặc biệt đến địa chỉ 255.255.255.255 để tìm DHCP Server. Tất cả các máy tính trong mạng sẽ nhận được thông báo này nhưng chỉ có DHCP Server sẽ trả lời cho máy gửi. Thực tế, DHCP Server gửi sẽ các thông tin về cấu hình mạng cục bộ, bao gồm cả địa chỉ IP mà máy tính vừa khởi động sẽ sử dụng trong phiên làm việc và địa chỉ của DNS Server để máy tính này có thể sử dụng để phân giải tên miền. 1.3.3 Các cổng Các máy tính hiện đại thực hiện nhiều việc khác nhau cùng một lúc. Email cần phải được tách ra khỏi các yêu cầu FTP, các yêu cầu về FTP cần được tách biệt với lưu lượng truy nhập web. Điều này được thực hiện thông qua cổng (port). Mỗi máy tính với một địa chỉ IP có hàng nghìn cổng logic, chính xác là 65.535 cổng trên giao thức tầng giao vận. Các cổng này hoàn toàn là trừu tượng trong bộ nhớ của máy tính và không phải là các cổng vật lý như cổng USB. Mỗi một cổng được xác định bằng một con số nằm giữa 1 và 65.535. Mỗi một cổng có thể được cấp cho một dịch vụ cụ thể. Ví dụ, giao thức HTTP thường sử dụng cổng 80, chúng ta thường nói máy chủ web lắng nghe (listen) các kết nối đang đến trên cổng 80. Khi một máy tính với một địa chỉ IP cụ thể gửi dữ liệu (thông thường là các yêu cầu) đến máy chủ web thì máy tính cũng sẽ gửi dữ liệu tới cổng 80 trên máy này. Máy chủ web sẽ kiểm tra mỗi gói tin nhận được để phát hiện ra số hiệu của cổng bên máy gửi và sau đó sẽ gửi dữ liệu đến bất kỳ chương trình nào đang nghe trên cổng đó. Bảng sau sẽ liệt kê danh sách các cổng thường được sử dụng trong các ứng dụng. 9
  16. Giao Giao Cổng Mục đích thức thức Giao thức kiểm tra dùng để xác minh hai máy tính có thể kết nối với nhau bằng cách: một máy tính echo 7 TCP/UDP sẽ gửi lại thông tin được gửi từ máy tính phía bên kia. Cung cấp biểu diễn ASCII về thời gian hiện tại daytime 13 TCP/UDP trên Server. ftp data 20 TCP Được sử dụng để truyền các file. Được sử dụng để gửi các lệnh của FTP như put ftp data 21 TCP và get. ssh 22 TCP Được sử dụng để mã hóa đăng nhập an toàn từ xa Được sử dụng cho tương tác, các phiên làm việc telnet 23 TCP từ xa. Simple Mail Transfer Protocol - Giao thức gửi smtp 25 TCP thư giữa hai máy. Một dịch vụ về thư mục đơn giản cho các quản trị whois 43 TCP mạng Internet. Một dịch vụ trả về thông tin người dùng hay một finger 79 TCP nhóm người dùng trên hệ thống cục bộ. http 80 TCP Giao thức truyền siêu văn bản. Port Office Protocol Version 3 - Là giao thức pop3 110 TCP truyền các email được tích lũy cho một khách hàng thỉnh thoảng mới truy nhập mạng . 1.4 Mạng Internet 1.4.1 Các khối địa chỉ Internet Mỗi nhà cung cấp dịch vụ Internet (ISP) được cấp một số khối địa chỉ IPv4. Khi một tổ chức muốn thiết lập một mạng máy tính kết nối với Internet, ISP sẽ cấp cho tổ chức này một khối địa chỉ, mỗi một khối sẽ được cố định phần đầu của địa chỉ, ví dụ nếu phần đầu của địa chỉ là 216.245.85 thì tổ chức này có thể sử dụng các địa chỉ từ 216.245.85.0 đến 216.245.85.255 (lưu ý các địa chỉ 216.245.85.0 và 216.245.85.255 không được dùng để gán địa chỉ cho các máy tính). Vì phần cố định gồm 24 bit đầu tiên nên sẽ đực ký hiệu là /24. Nếu một khối có phần cố định là /23 thì sẽ còn 9 bit để gán địa chỉ cho các máy tính, do đó sẽ có 29-2 = 510 địa chỉ được gán cho các máy tính. Nếu một mạng có có phần cố định là /30 thì chỉ còn 2 bit để gán địa chỉ cho các máy tính và như vậy sẽ chỉ có tối đa 2 địa chỉ được dùng để gán cho các máy tính trong mạng. 10
  17. 1.4.2 Dịch địa chỉ mạng Do sự khan hiếm địa chỉ IPv4, ngày nay hầu hết các mạng sử dụng giao thức dịch địa chỉ mạng - Network Address Translation (NAT). Trong các mạng dựa trên NAT, hầu hết các nút trong mạng chỉ có địa chỉ cục bộ, không định tuyến được. Các địa chỉ này được lựa chọn tùy ý trong các khối địa chỉ 10.x.x.x, 172.16.x.x. đến 172.31.x.x. và 192.168.x.x. Các router kết nối các mạng cục bộ với ISP sẽ dịch các địa chỉ cục bộ này thành một tập nhỏ hơn các địa chỉ có thể định tuyến được. 1.4.3 Tường lửa Ngày nay các nguy cơ về an toàn và an ninh mạng đang trở nên hiện hữu, do đó cần phải thiết lập một điểm truy nhập tới mạng cục bộ để kiểm tra tất cả luồng dữ liệu ra và vào mạng tại điểm truy nhập này. Các thiết bị phần cứng và phần mềm được đặt giữa mạng Internet và mạng cục bộ để kiểm tra tất cả các dữ liệu đi vào mạng hay đi ra khỏi mạng cục bộ được gọi là tường lửa (firewall). Firewall sẽ kiểm tra các gói tin và chấp nhận cho các gói tin được đi qua hay sẽ bị hủy bỏ dựa trên một tập hợp các quy định đã được thiết lập. Firewall thường là một bộ phận của router nhưng cũng có thể là là một máy tính chuyên dụng. Các hệ điều hành hiện đại như macOS và Red Hat Linux thường có các firewall cá nhân gắn liền trông hệ điều hành để theo dõi các luồng dữ liệu được gửi đến máy tính này. Việc lọc các gói tin thường được dựa trên các địa chỉ mạng và các cổng, ví dụ, tất cả các gói tin đến từ lớp địa chỉ 192.28.25.x có thể bị firewall của một mạng loại bỏ do trước đó một máy tính sử dụng một trong các địa chỉ thuộc lớp này đã tấn công vào mạng. 1.4.4 Máy chủ proxy Máy chủ proxy liên quan đến firewall. Nếu một firewall ngăn cản không cho một máy tính trong mạng kết nối trực tiếp đến các mạng bên ngoài thì máy chủ proxy có thể hoạt động như là trung gian giữa máy tính trong mạng và firewall. Khi một máy tính trong mạng muốn tải một trang từ máy chủ web bên ngoài mạng, máy tính này có thể gửi yêu cầu kết nối đến máy chủ proxy. Máy chủ proxy sau đó có thể yêu cầu trang này từ máy chủ web bên ngoài và chuyển trang đến máy tính đã yêu cầu. Các proxy cũng có thể sử dụng cho các dịch vụ FTP và các kết nối khác. Một trong những ưu điểm về an toàn khi sử dụng máy chủ proxy đó là các máy tính bên ngoài mạng chỉ có thể tìm thấy máy chủ proxy mà không thể tìm được các tên và các địa chỉ IP của các máy bên trong mạng. Điều này sẽ làm cho hacker khó tấn công vào mạng. 11
  18. Firewall thường hoạt động ở lớp giao vận và lớp Internet, các máy chủ proxy thường hoạt động ở lớp ứng dụng. Một máy chủ proxy hiểu rõ một vài giao thức mức ứng dụng, chẳng hạn như HTTP và FTP. Tuy nhiên các máy chủ proxy SOCKS hoạt động tại lớp giao vận và có thể ủy quyền cho tất cả các kết nối TCP và UDP mà không cần quan tâm đến giao thức được sử dụng ở lớp ứng dụng. Các gói tin có thể được máy chủ proxy kiểm tra để đảm bảo rằng các gói tin này chứa các dữ liệu phù hợp với kiểu của các gói tin. Ví dụ, các gói tin của giao thức FTP chứa các dữ liệu Telnet có thể bị loại bỏ. Sơ đồ sau trình bày cách kết nối các lớp thông qua máy chủ proxy. Hình 1.4: Kết nối các lớp thông qua máy chủ proxy Máy chủ proxy cho phép vừa chuyển tiếp vừa kiểm soát chặt chẽ các truy nhập Internet. Một công ty có thể sử dụng máy chủ proxy để ngăn chặn các truy nhập của người sử dụng trong công ty đến các trang web có nội dung độc hại nhưng lại cho phép những truy nhập đến các trang web có nội dung tốt. Một vài công ty cho phép người sử dụng sử dụng giao thức FTP để nhận các file nhưng không cho phép sử dụng giao thức này để truyền các file ra ngoài công ty, do đó các dữ liệu có tính chất bí mật của công ty không dễ dàng bị mua bán bất hợp pháp. Các công ty có thể sử dụng máy chủ proxy để theo dõi việc truy nhập các trang web của các nhân viên để biết ai sử dụng Internet để hỗ trợ cho công việc, ai sử dụng Internet cho các mục đích khác Các máy chủ proxy cũng có thể được sử dụng để cài đặt các vùng nhớ cache cục bộ. Khi người sử dụng yêu cầu một file từ máy chủ web, máy chủ proxy đầu tiên kiểm tra xem file được yêu cầu có trong bộ nhớ cache hay không. Nếu file đã có trong bộ nhớ cache thì máy chủ proxy sẽ gửi file này đến người sử dụng mà không cần phải tìm kiếm trên Internet. Nếu file được yêu cầu không có trong bộ nhớ cache, máy chủ proxy sẽ tìm file này trên Internet và chuyển tiếp đến người sử dụng, đồng thời lưu trữ file đó vào bộ nhớ cache để phục vụ cho các yêu cầu về sau. Cơ chế hoạt 12
  19. động của bộ nhớ cache sẽ giảm đáng kể truy nhập Internet và cải thiện được thời gian đáp ứng yêu cầu. Một trong các ví dụ điển hình của việc sử dụng các máy chủ proxy như là bộ nhớ cache chính là America Online (AOL). Bên cạnh những ưu điểm thì máy chủ proxy cũng bộc lộ những hạn chế. Một trong những hạn chế lớn nhất của máy chủ proxy là không có khả năng xử lý với những giao thức mới. Các máy chủ proxy cho phép các giao thức phổ biến đã được thiết lập như HTTP, FTP và SMTP được truyền qua nhưng lại không cho phép các giao thức mới, ví dụ giao thức BitTorrent được truyền qua các máy chủ proxy. Với những thay đổi nhanh chóng của Internet, điều này đã trở thành một trong những hạn chế quan trọng. Trong Java, người lập trình rất dễ dàng tạo ra các giao thức mới để tối ưu hóa công việc. Những hạn chế của máy chủ proxy đã làm giảm tính hiệu quả của các giao thức do người dùng tạo ra vì máy chủ proxy không hiểu được các giao thức này. 1.4.5 Mô hình Client/Server Hầu hết các lập trình mạng hiện đại đều dựa trên mô hình khách/chủ (Client/Server). Một ứng dụng Client/Server thường lưu trữ các khối lượng lớn dữ liệu trên một hay một chuỗi (còn được gọi là đám mây) các máy chủ có công suất lớn và đắt tiền trong khi hầu hết các chương trình và các giao diện người sử dụng được quản lý bởi phần mềm bên phía Client. Phần mềm này chạy trên các máy tính cá nhân tương đối rẻ tiền. Trong hầu hết các trường hợp, một Server chủ yếu là gửi dữ liệu và Client chủ yếu là nhận dữ liệu đã được gửi đi từ Server, tuy nhiên rất ít chương trình được viết chỉ để gửi hoặc chỉ để nhận dữ liệu. Thông thường, một Client sẽ bắt đầu một cuộc hội thoại và một Server sẽ đợi để bắt đầu cuộc hội thoại với Client. Trong một số trường hợp, cùng một chương trình có thể vừa là Client vừa là Server. Không phải tất cả các ứng dụng đều thích hợp với mô hình Client/Server. Bên cạnh mô hình Client/Server ta còn có các mô hình kết nối khác chẳng hạn như kết nối ngang hàng (peer to peer). Hệ thống điện thoại là một ví dụ điển hình về mạng kết nối ngang hàng. Mỗi một điện thoại có thể gọi đến các điện thoại khác và cũng có thể nhận các cuộc gọi từ các điện thoại khác nữa. Ngoài ra, các mô hình lai (hybrid) giữa mô hình Client/Server và mô hình Peer-to-Peer cũng được sử dụng rộng rãi trong các ứng dụng mạng. 13
  20. Hình 1.5: Kết nối Client/Server Java mặc định không hỗ trợ kết nối ngang hàng trong API. Tuy nhiên, các ứng dụng có thể dễ dàng cung cấp các kết nối ngang hàng theo nhiều cách, phổ biến nhất là chúng hoạt động như là cả Server và cả Client. Một lựa chọn khác là các máy ngang hàng có thể trao đổi với nhau thông qua một chương trình Server trung gian, chương trình này chuyển tiếp các dữ liệu giữa các máy ngang hàng. CÂU HỎI, BÀI TẬP VẬN DỤNG: 1. Hãy trình bày hiểu biết cơ bản của anh/chị về mạng máy tính? 2. Mô hình TCP/IP có mấy lớp? Anh/chị hãy trình bày hiểu biết của mình về các lớp này? 3. Trình bày khái quát về các giao thức IP, TCP và UDP? 4. Hãy trình bày các khái niệm về địa chỉ IP, tên miền và cổng trong mạng máy tính? 5. Hãy phân biệt tường lửa (firewall) và máy chủ proxy? 6. Mô hình Client/Server là gì? Ngoài mô hình này các ứng dụng mạng còn có thể sử dụng các mô hình nào? 14
  21. CHƯƠNG 2. CÁC DÒNG VÀO-RA (STREAM) Các chương trình mạng thường thực hiện các công việc nhập, xuất (I/O) đơn giản đó là chuyển các byte từ một hệ thống này đến một hệ thống khác. Trong Java, I/O được tổ chức tương đối khác so với các ngôn ngữ lập trình khác, chẳng hạn như Fortran, C và C++. I/O trong Java được xây dựng trong các stream (các dòng). Input stream đọc dữ liệu và ouput stream ghi dữ liệu. Các lớp stream khác nhau như java.io.FileInputStream và sun.net.TelnetOutput đọc và ghi các nguồn dữ liệu cụ thể. Tuy nhiên tất cả các output stream cùng có các phương thức chung để ghi dữ liệu và tất cả các input stream sử dụng các phương thức chung để đọc dữ liệu. 2.1 Các dòng ra (output stream) Lớp output cơ bản của Java là java.io.OutputStream, với khai báo như sau: public abstract class OutputStream{ } Lớp này cung cấp các phương thức cơ bản để ghi dữ liệu, đó là: • public abstract void write(int b) throws IOException • public void write(byte[] data) throws IOException • public void write(byte[] data, int offset, int length) throws IOException • public void flush() throws IOException • public void close() throws IOException Các lớp con của OutputStream sử dụng các phương thức này để ghi dữ liệu lên một phương tiện cụ thể. Ví dụ, một FileOutputStream sử dụng các phương thức này để ghi dữ liệu vào một file. Một TelnetOutputStream sử dụng các phương thức để ghi dữ liệu vào một kết nối mạng. Một ByteArrayOutputStream dùng các phương thức để ghi dữ liệu vào một mảng các byte có thể mở rộng được. Phương thức cơ bản của OutputStream là write(int b). Phương thức này sử dụng một số nguyên có giá trị từ 0 đến 255 như là một đối số và ghi byte tương ứng vào output stream. Phương thức này được mô tả là trừu tượng (abstract) do các lớp con cần thay đổi phương thức này để kiểm soát phương tiện cụ thể. Một ByteArrayOutputStream có thể cài đặt phương thức với mã Java để sao chép byte vào mảng trong Java. Tuy nhiên, một FileOutputStream sẽ cần phải sử dụng mã riêng, mã này biết cách ghi dữ liệu trong các file trên nền của hệ điều hành. 15
  22. Mặc dù phương thức write(int b) sử dụng một số nguyên như là một đối số nhưng khi ghi sẽ ghi một byte không dấu (unsigned byte). Java không có kiểu dữ liệu byte không dấu nên phải sử dụng một số nguyên. Sự khác biệt giữa một byte không dấu và byte có dấu chính là cách diễn giải. Các byte không dấu và byte có dấu đều được tạo ra từ 8 bit. Khi ta ghi một số nguyên vào một kết nối mạng sử dụng write(int b) thì chỉ có 8 bit được đặt lên các đường truyền. Nếu một số nguyên ngoài phạm vi 0 - 255 được truyền cho phương thức write(int b), byte ít quan trọng hơn của số nguyên này sẽ được viết và 3 byte còn lại của số nguyên sẽ bị bỏ qua. Đây chính là hiệu ứng của việc chuyển một số nguyên thành byte. Ví dụ: Giao thức bộ sinh ký tự (character-generator protocol) định nghĩa một Server gửi ra các văn bản mã hóa bằng mã ASCII. Một trong các biến thể của giao thức này sẽ gửi các dòng, mỗi dòng chứa 72 ký tự mã hóa bằng mã ASCII có thể in ra được. Các ký tự này có số thứ tự từ 33 đến 126 trong bảng mã ASCII ngoại trừ các ký tự trắng và các ký tự điều khiển. • Dòng đầu chứa các ký tự từ 33 đến 104. • Dòng thứ hai chứa các ký tự từ 34 đến 105. • Dòng thứ ba chứa các ký tự từ 35 đến 106. • Dòng thứ 29 chứa các ký tự từ 55 đến 126. Đến đây, các ký tự được gói xoay vòng sao cho dòng 30 chứa các ký tự từ 56 đến 126 và sau đó lại là ký tự 33. Các dòng được kết thúc bằng dấu Enter (ASCII 13) và một mã xuống dòng (ASCII 10). Đoạn mã sau sẽ trình bày cách triển khai phương thức write(): public static void generateCharacters(OutputStream out) throws IOException { int firstPrintableCharacter = 33; int numberOfPrintableCharacters = 94; int numberOfCharactersPerLine = 72; int start = firstPrintableCharacter; while (true) { /* infinite loop */ for (int i = start; i < start + numberOfCharactersPerLine; i++) { out.write(((i - firstPrintableCharacter) % numberOfPrintableCharacters) + firstPrintableCharacter); } out.write('\r'); // carriage return out.write('\n'); // linefeed start = ((start + 1) - firstPrintableCharacter) % numberOfPrintableCharacters + firstPrintableCharacter; } } 16
  23. Kết quả của giao thức bộ sinh ký tự là: !"#$%&'()*+,-./0123456789:; ?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefgh "#$%&'()*+,-./0123456789:; ?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghi #$%&'()*+,-./0123456789:; ?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghij $%&'()*+,-./0123456789:; ?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijk %&'()*+,-./0123456789:; ?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijkl &'()*+,-./0123456789:; ?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklm '()*+,-./0123456789:; ?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmn Trong ví dụ trên, một OutputStream được truyền đến phương thức generateCharacters() trong đối số out. Dữ liệu được viết lần lượt từng byte trên out. Các byte này được cho dưới dạng các số nguyên trong một chuỗi xoay vòng từ 33 đến 126. Sau mỗi một chuỗi 72 ký tự được viết dấu Enter (ASCII 13) và một mã xuống dòng (ASCII 10) được viết lên output stream. Ký tự kế tiếp được tính toán và vòng lặp sẽ được lặp lại. Phương thức out sử dụng throw IOException. Điều này rất quan trọng do Server bộ sinh ký tự sẽ chỉ kết thúc khi bên phía Client kết thúc kết nối và mã Java sẽ nhận biết việc kết thúc này như là một IOException. Việc ghi mỗi lần một byte thường không hiệu quả. Ví dụ, mỗi TCP segment chứa ít nhất 40 byte của phần overhead dùng cho việc định tuyến và sửa lỗi. Nếu mỗi lần chỉ gửi một byte ta có thể làm cho mạng phải tải tổng cộng 41 byte. Do đó, hầu hết các cài đặt TCP/IP sử dụng vùng đệm dữ liệu có một kích thước nào đó. Các cài đặt này sẽ tích lũy dữ liệu cần gửi trong vùng đệm (buffer) và chỉ gửi các dữ liệu này đi khi lượng dữ liệu được tích lũy hay thời gian tích lũy đã vượt một ngưỡng cho trước. Sử dụng write(byte[] data) hoặc write(byte[] data, int offset, int length) thường nhanh hơn việc ghi lần lượt từng thành phần của một mảng dữ liệu. Sau đây là ví dụ của việc cài đặt phương thức generateCharacters(), phương thức này gửi mỗi lần một dòng bằng cách đóng gói toàn bộ một dòng trong một mảng các byte: public static void generateCharacters(OutputStream out) throws IOException { int firstPrintableCharacter = 33; int numberOfPrintableCharacters = 94; int numberOfCharactersPerLine = 72; int start = firstPrintableCharacter; byte[] line = new byte[numberOfCharactersPerLine + 2]; // the +2 is for the carriage return and linefeed while (true) { /* infinite loop */ for (int i = start; i < start + numberOfCharactersPerLine; i++) { line[i - start] = (byte) ((i - firstPrintableCharacter) % numberOfPrintableCharacters + firstPrintableCharacter); } line[72] = (byte) '\r'; // carriage return line[73] = (byte) '\n'; // line feed 17
  24. out.write(line); start = ((start + 1) - firstPrintableCharacter) % numberOfPrintableCharacters + firstPrintableCharacter; } } Các stream cũng có thể được đưa vào vùng đệm bằng phần mềm như mã Java cũng như là trong phần cứng của mạng. Thông thường, việc đưa dữ liệu vào vùng đệm được thực hiện bằng cách gắn một BufferedOutputStream hay một BufferedWriter vào một stream lớp dưới. Để đưa dữ liệu trong một vùng đệm lên một kết nối ta sử dụng phương thức flush(). Phương thức flush() sẽ yêu cầu dòng phải gửi dữ liệu đã được đưa vào vùng đệm lên kết nối, ngay cả khi vùng đệm chưa đầy. Ta cần phải đưa hết tất cả các dữ liệu trong vùng đệm của các dòng bằng phương thức flush() trước khi ta đóng các dòng này, nếu không thì khi đóng các dòng, dữ liệu trong các vùng đệm sẽ bị mất. Hình 2.1: Dữ liệu có thể bị mất nếu không flush các luồng Khi đã làm việc xong với một stream ta có thể đóng stream này bằng cách gọi phương thức close(). Phương thức close() sẽ giải phóng tất cả các tài nguyên được liên kết với stream, chẳng hạn như các đặc tả tập tin (file handle) hay các cổng. Nếu stream có được từ một kết nối mạng thì khi đóng stream sẽ hủy bỏ kết nối mạng. Khi một output stream đã bị đóng việc ghi thêm dữ liệu vào stream sẽ đưa ra IOException. Tuy nhiên một vài kiểu stream vẫn cho phép chúng ta tiếp tục làm việc với đối tượng khi đã đóng stream. Ví dụ, một ByteArrayOutputStream đã bị đóng vẫn có thể được chuyển đổi thành mảng các byte thật sự và một DigestOutputStream đã bị đóng vẫn có thể trả về giá trị digest của nó. 18
  25. Nếu không đóng một stream trong một chương trình lớn có thể làm tiết lộ các đặc tả tập tin, các cổng mạng và các tài nguyên khác. Do đó, với Java 6 và các phiên bản trước đó, ta nên có một khối có tên là finally để đóng các dòng, khối này là khối cuối cùng trong một chương trình. Thông thường, ta khai báo biến stream bên ngoài khối try nhưng nên khởi tạo dòng bên trong khối try. Để tránh gặp phải NullPointerExceptions ta cần kiểm tra xem biến stream có phải là null hay không trước khi ta đóng dòng này. Ví dụ sau minh họa cách viết sử dụng finally: OutputStream out = null; try { out = new FileOutputStream("/tmp/data.txt"); // work with the output stream } catch (IOException ex) { System.err.println(ex.getMessage()); } finally { if (out != null) { try { out.close(); } catch (IOException ex) { } } } Kỹ thuật này đôi khi được gọi là dispose pattern. Dispose pattern phổ biến cho các đối tượng cần phải được dọn dẹp trước khi các dữ liệu không còn sử dụng được thu dọn. Dispose pattern không chỉ được sử dụng cho các đối tượng và còn được sử dụng cho cả các sockets, channels, JDBC connections và các statements. Java 7 giới thiệu cách sử dụng khối try cùng với xây dựng các tài nguyên để thu dọn dữ liệu không còn sử dụng. Với cách này, dòng được khai báo trong một danh sách đối số bên trong khối try. Đoạn mã trên được viết lại như sau: try (OutputStream out = new FileOutputStream("/tmp/data.txt")) { // work with the output stream } catch (IOException ex) { System.err.println(ex.getMessage()); } Trong đoạn mã này ta nhận thấy khối finally đã không còn cần thiết. Java sẽ tự động gọi phương thức close() trên bất kỳ đối tượng có thuộc tính AutoCloseable bên trong danh sách đối số của khối try. 2.2 Các dòng vào (input stream) Lớp input cơ bản của Java là java.io.InputStream, với khai báo như sau: public abstract class InputStream{ } 19
  26. Lớp này cung cấp các phương thức cơ bản để đọc dữ liệu như là các byte chưa được xử lý còn được gọi là byte thô: • public abstract int read() throws IOException • public int read(byte[] input) throws IOException • public int read(byte[] input, int offset, int length) throws IOException • public long skip(long n) throws IOException • public int available() throws IOException • public void close() throws IOException Các lớp con cụ thể của InputStream sử dụng các phương thức này để đọc dữ liệu từ một phương tiện cụ thể. Ví dụ, một FileOutputStream sử dụng các phương thức này để đọc dữ liệu từ một file. Một TelnetOutputStream sử dụng các phương thức này để đọc dữ liệu từ một kết nối mạng. Một ByteArrayOutputStream sử dụng các phương thức này để đọc dữ liệu từ một mảng các byte. Phương thức cơ bản của InputStream là phương thức read() không có đối số. Phương thức read() đọc một byte dữ liệu từ nguồn của một input stream và trả lại một số nguyên nằm trong khoảng từ 0 đến 255. Kết thúc của một stream được báo hiệu bằng cách trả lại giá trị -1. Phương thức read() sẽ chờ và ngăn không cho thực thi bất kỳ đoạn mã nào sau phương thức này cho đến khi một byte dữ liệu sẵn sàng cho việc đọc. Việc đọc và ghi dữ liệu có thể chậm. Do đó nếu chương trình đang phải thực thi một đoạn mã quan trọng thì nên đặt các thao tác I/O trong một luồng (thread) riêng của thao tác I/O. Phương thức read() được mô tả là trừu tượng do các lớp con cần thay đổi phương thức này để quản lý các phương tiện cụ thể. Ví dụ, một ByteArrayInputStream có thể thực thi phương thức này bằng mã Java để sao chép byte từ một mảng. Tuy nhiên, một TelnetInputStream cần sử dụng một thư viện riêng để biết cách đọc dữ liệu từ một giao diện mạng trên nền của hệ điều hành. Đoạn mã sau đọc 10 byte từ InputStream có tên là in và lưu các byte trong một mảng byte có tên input. Trong quá trình đọc nếu phát hiện đã hết dữ liệu thì vòng lặp sẽ được kết thúc sớm hơn: byte[] input = new byte[10]; for (int i = 0; i < input.length; i++) { int b = in.read(); if (b == -1) break; input[i] = (byte) b; } 20
  27. Mặc dù read() chỉ đọc một byte, phương thức này trả lại một số nguyên, nên cần phải thực hiện chuyển đổi một số nguyên thành một byte bằng cast trước khi lưu trữ kết quả trong mảng byte. Vì read() trả lại một giá trị nguyên có phạm vi từ - 128 đến 127 thay vì 0 đến 255 nên ta cần phải chuyển đổi một byte có dấu thành byte không dấu như sau: int i = b >= 0 ? b : 256 + b; Cũng tương tự như việc ghi mỗi lần một byte trong OutputStream, việc đọc mỗi lần một byte sẽ là không hiệu quả. Có hai phương thức overloaded là read(byte[] input) và read(byte[] input, int offset, int length). Hai phương thức này sẽ làm đầy một mảng các byte từ một dòng. Phương thức thứ nhất sẽ cố gắng làm đầy một mảng các byte có tên input. Phương thức thứ hai sẽ cố gắng làm đầy một mảng con các byte của mảng input, bắt đầu từ vị trí offset và sẽ ghi length byte. Việc ghi vào một mảng đôi khi sẽ không thành công vì nhiều lý do khác nhau. Ví dụ, tại một thời điểm, ta cố gắng để đọc 1024 byte từ một kết nối mạng, khi đó ta có thể nhận được 512 byte từ Server còn 512 byte đang được chuyển đến. Để biết được số lượng byte đã được đọc ta sử dụng các phương thức đọc nhiều byte. Đoạn mã sau trình bày cách đọc nhiều byte: byte[] input = new byte[1024]; int bytesRead = in.read(input); Đoạn mã sẽ cố gắng đọc 1024 byte từ InputStream vào một mảng các byte có tên là input. Tuy nhiên, nếu chỉ có 512 byte để đọc thì bytesRead sẽ được thiết lập là 512. Để đảm bào đọc được tất cả các byte ta đặt thao tác đọc vào trong một vòng lặp cho đến khi mảng được làm đầy. Ví dụ: int bytesRead = 0; int bytesToRead = 1024; byte[] input = new byte[bytesToRead]; while (bytesRead < bytesToRead) { bytesRead += in.read(input, bytesRead, bytesToRead - bytesRead); } Tất cả các phương thức read() sẽ trả lại giá trị -1 để báo hiệu kết thúc một stream. Nếu một stream kết thúc trong khi vẫn còn dữ liệu chưa được đọc thì các phương thức đọc nhiều byte sẽ vẫn đọc dữ liệu cho đến khi vùng đệm trở nên trống. Khi đó việc đọc tiếp sẽ trả lại giá trị -1 và giá trị này sẽ không được ghi vào mảng. Do đó trong mảng chỉ chứa dữ liệu thực sự. Đoạn mã trên chưa cân nhắc đến trường hợp tất cả 1024 byte chưa đến được vùng đệm. Do đó, ta cần kiểm tra giá trị của read() trước khi bổ sung giá trị này vào bytesRead. Ví dụ: 21
  28. int bytesRead = 0; int bytesToRead = 1024; byte[] input = new byte[bytesToRead]; while (bytesRead < bytesToRead) { int result = in.read(input, bytesRead, bytesToRead - bytesRead); if (result == -1) break; // end of stream bytesRead += result; } Ta có thể sử dụng phương thức available() để xác định số lượng byte có thể đọc ngay mà không cần phải chờ. Phương thức này trả lại số lượng byte tối thiểu mà ta có thể đọc. Ví dụ: int bytesAvailable = in.available(); byte[] input = new byte[bytesAvailable]; int bytesRead = in.read(input, 0, bytesAvailable); // continue with rest of program immediately Trong một số ít trường hợp nếu muốn bỏ qua việc đọc dữ liệu, ta sử dụng phương thức skip(). Khi đã đọc xong dữ liệu từ một stream ta nên đóng stream bằng cách gọi phương thức close() của input stream. Phương thức này sẽ giải phóng tất cả các tài nguyên liên kết với dòng như các đặc tả tập tin hay các cổng. Khi một input strream đã bị đóng, các thao tác đọc dữ liệu tiếp theo sẽ đưa ra IOException. Tuy nhiên một vài kiểu stream có thể vẫn cho phép làm một số công việc với đối tượng. Ví dụ, ta sẽ không thể nhận được message digest từ một java.security.DigestInputStream cho đến khi dữ liệu đã được đọc và dòng bị đóng lại. Mark và Reset Lớp InputStream có ba phương thức ít được sử dụng, các phương thức này cho phép các chương trình sao chép dự phòng (backup) và đọc lại dữ liệu đã được đọc trước đó: • public void mark(int readAheadLimit) • public void reset() throws IOException • public boolean markSupported() Để đọc lại dữ liệu cần đánh dấu (mark) vị trí hiện thời trong dòng bằng phương thức mark(). Sử dụng phương thức reset() để thiết lập lại (reset) dòng tại điểm đã đánh dấu. Các thao tác đọc tiếp theo sẽ trả lại dữ liệu bắt đầu từ điểm đánh dấu. Số lượng các byte có thể đọc từ vị trí đánh dấu được xác định bởi đối số readAheadLimit trong mark(). Nếu ta quay trở lại vượt quá điểm đã đánh dấu thì chương trình sẽ đưa ra IOException. Trong một dòng luôn chỉ tồn tại duy nhất một đánh dấu. Đánh dấu vị trí thứ hai sẽ xóa bỏ đánh dấu thứ nhất. 22
  29. Đánh dấu và thiết lập lại thường được thực hiện bằng cách lưu trữ các byte được đọc kể từ vị trí đã được đánh dấu trong một vùng đệm bên trong chương trình. Tuy nhiên không phải tất cả các dòng đều hỗ trợ mark và reset. Để kiểm tra xem một dòng có hỗ trợ mark và reset không ta sử dụng phương thức markSupported(). Nếu phương thức này trả lại giá trị là true thì dòng có hỗ trợ mark và reset. Nếu phương thức này trả lại giá trị là false thì dòng không hỗ trợ mark và reset, khi đó mark() sẽ không làm gì và reset() sẽ đưa ra một IOException. Chỉ có các lớp BufferedInputStream và ByteArrayInputStream trong java.io là luôn hỗ trợ mark và reset. Tuy nhiên các input stream khác chẳng hạn như TelnetInputStream cũng có thể hỗ trợ mark nếu các lớp này lần đầu được gắn vào với một BufferedInputStream. 2.3 Các dòng filter stream InputStream và OutputStream là các lớp tương đối đơn giản. Các lớp này chỉ thực hiện việc đọc và ghi từng byte hay một nhóm các byte. Việc xác định ý nghĩa của các byte, chẳng hạn như các byte có phải là các số nguyên, là các số thực dấu phảy động IEEE 754 hay là văn bản được mã hóa bằng bảng mã Unicode hoàn toàn phụ thuộc vào người lập trình và mã chương trình. Tuy nhiên có một vài định dạng dữ liệu vô cùng phổ biến đã được cài đặt trong thư viện lớp. Ví dụ, rất nhiều các số nguyên được truyền đi như là các phần của các giao thức mạng là các số nguyên 32 bit big-endian; nhiều văn bản được truyền trên web được mã hóa hoặc là ASCII 7-bit, Latin-1 8-bit, hay UTF-8 multibyte; nhiều file được truyền đi bằng giao thức FTP được lưu trữ dưới định dạng ZIP. Java cung cấp một số lượng các lớp lọc (filter classes) có thể được gắn vào các dòng để dịch các byte thô thành các định dạnh khác. Các filter được chia thành hai kiểu: các filter stream reader và writer. Filter stream chủ yếu làm việc với các dữ liệu thô như các byte, chẳng hạn như nén dữ liệu hoặc diễn dịch dữ liệu là các số nhị phân. Reader và writer quản lý trường hợp đặc biệt của văn bản trong nhiều cách mã hóa khác nhau như UTF-8 và ISO 8859-1. Các filter được tổ chức trong một chuỗi (chain). Mỗi liên kết trong chuỗi nhận dữ liệu từ filter trước hoặc từ dòng và truyền dữ liệu đến liên kết kế tiếp trong chuỗi. Sơ đồ sau trình bày chuỗi các filter. 23
  30. Hình 2.2: Dòng dữ liệu qua một chuỗi các filter Trong ví dụ trên, một file văn bản đã được mã hóa và được nén được truyền đến thông qua giao diện mạng cục bộ. Mã riêng của chương trình chuyển file này đến TelnetInputStream. Một BufferedInputStream sẽ đưa file vào vùng đệm để tăng tốc toàn bộ quá trình. Một CipherInputStream giải mã dữ liệu và một GZIPInputStream sẽ giải nén dữ liệu. Một InputStreamReader chuyển đổi dữ liệu đã được giải nén thành file văn bản mã hóa bằng mã Unicode. Cuối cùng, file văn bản đã sẵn sảng cho ứng dụng trong lớp ứng dụng và được xử lý. Các filter output stream có cùng các phương thức như write(), close() và flush() như java.io.OutputStream. Các filter input stream có cùng các phương thức như read(), close() và available() như java.io.InputStream. 24
  31. 2.3.1 Gắn kết các filter stream Các filter được kết nối với các dòng bằng các constructor của filter. Đoạn mã sau sẽ đưa vào vùng đệm dữ liệu từ file data.txt. Đầu tiên, đối tượng fin của FileInputStream được tạo bằng cách truyền tên của file như là một đối số cho constructor của FileInputStream. Sau đó một đối tượng có tên bin của BufferedInputStream được tạo bằng cách truyền fin như là một đối số cho constructor của BufferedInputStream: FileInputStream fin = new FileInputStream("data.txt"); BufferedInputStream bin = new BufferedInputStream(fin); Từ đây ta có thể sử dụng phương thức read() của cả fin và bin để đọc dữ liệu từ file data.txt. Trong phần lớn thời gian, ta chỉ nên sử dụng filter cuối cùng trong chuỗi để thực hiện việc đọc và ghi thực sự. 2.3.2 Các lớp BufferedInputStream và BufferedOutputStream Lớp BufferedOutputStream lưu trữ dữ liệu đã được ghi trong một vùng đệm (một mảng byte được bảo vệ có tên là buf) cho đến khi vùng đệm đầy hoặc dòng được đẩy đi. Sau đó lớp này sẽ ghi một lần tất cả dữ liệu vào output stream lớp dưới. Với cùng một số byte cần ghi lên một kết nối mạng, việc ghi một lần nhiều byte sẽ nhanh hơn nhiều việc ghi mỗi lần một byte do mỗi TCP segment hay mỗi gói UDP sẽ phải mang thêm một phần overhead đến 40 byte. Lớp BufferedInputStream cũng có một mảng byte được bảo vệ có tên là buf, mảng này phục vụ như là một vùng đệm. Mỗi khi phương thức read() của dòng được gọi, đầu tiên phương thức này sẽ cố gắng để tìm đọc các dữ liệu đã được yêu cầu từ vùng đệm. Chỉ khi vùng đệm không còn dữ liệu thì dòng sẽ đọc dữ liệu từ các nguồn lớp dưới. Khi đó dòng sẽ đọc càng nhiều dữ liệu có thể được từ nguồn lớp dưới vào vùng đệm, cho dù dòng có thể cần hay không cần các dữ liệu này ngay tức thì. Các dữ liệu không được sử dụng ngay tức thì sẽ được sử dụng cho các lần gọi phương thức read() về sau. Sử dụng vùng đệm có thể cải thiện đáng kể hiệu suất. BufferedInputStream và BufferedOutputStream đều có hai constructor: • public BufferedInputStream(InputStream in) • public BufferedInputStream(InputStream in, int bufferSize) • public BufferedOutputStream(OutputStream out) • public BufferedOutputStream(OutputStream out, int bufferSize) 25
  32. Đối số thứ nhất là dòng lớp dưới mà từ dòng này các dữ liệu chưa được đưa vào vùng đệm sẽ được đọc hoặc các dữ liệu đã được đưa vào vùng đệm sẽ được ghi lên dòng lớp dưới này. Đối số thứ hai (nếu có) sẽ xác định số lượng byte trong vùng đệm, nếu không thì kích thước của vùng đệm sẽ được thiết lập bằng 2018 byte cho một input stream và bằng 512 byte cho một output stream. Kích thức lý tưởng cho một vùng đệm phụ thuộc vào kiểu của dòng mà ta đang đưa vào vùng đệm. BufferedInputStream không có các phương thức riêng mà chỉ ghi đè (override) lên các phương thức của InputStream. BufferedInputStream hỗ trợ mark và reset. BufferedOutputStream cũng không có các phương thức riêng. Ta có thể gọi các phương thức của BufferedOutputStream giống như cách ta gọi các phương thức của bất kỳ output stream nào. Điểu khác biệt ở đây là phương thức write() trong BufferedOutputStream sẽ đặt dữ liệu trong một vùng đệm thay vì ghi trực tiếp xuống output stream lớp dưới. Do đó ta cần phải thực hiện đẩy dữ liệu ra khỏi vùng đệm bằng phương thức flush() tại thời điểm mà dữ liệu cần phải gửi đi. 2.3.3 Lớp PrintStream Lớp PrintStream là filter output stream đầu tiên mà hầu hết các lập trình viên gặp do System.out là một PrintStream. Tuy nhiên nhiều output stream khác cũng được gắn vào print stream, sử dụng 2 constructor: • public PrintStream(OutputStream out) • public PrintStream(OutputStream out, boolean autoFlush) Mặc định, các print stream cần phải được đẩy ra kết nối, nếu đối số autoFlush là true thì dòng sẽ được đẩy ra kết nối mỗi khi một mảng byte hoặc ký hiệu linefeed được viết hoặc một phương thức println() được gọi. PrintStream có 9 phương thức overloaded print() và 10 phương thức overloaded println(): • public void print(boolean b) • public void print(char c) • public void print(int i) • public void print(long l) • public void print(float f) • public void print(double d) • public void print(char[] text) • public void print(String s) • public void print(Object o) 26
  33. • public void println() • public void println(boolean b) • public void println(char c) • public void println(int i) • public void println(long l) • public void println(float f) • public void println(double d) • public void println(char[] text) • public void println(String s) • public void println(Object o) Mỗi phương thức print() chuyển đổi đối số của nó thành một xâu và ghi xâu này trên output stream lớp dưới sử dụng cách mã hóa mặc định. Phương thức println() cũng có chung mục đích như phương thức print() nhưng có bổ sung ký hiệu ngăn cách dòng (ký hiệu này phụ thuộc vào hệ điều hành) vào cuối của dòng mà phương thức này sẽ ghi. Ký hiệu ngăn cách dòng trong Unix là \n, trong Mac OS 9 là \r, trong Windows là một cặp (\r\n). 2.3.4 Các lớp DataInputStream và DataOutputStream Các lớp DataInputStream và DataOutputStream cung cấp các phương thức cho việc đọc và ghi các kiểu dữ liệu cơ bản của Java và các xâu dưới dạng nhị phân. Các định dạng nhị phân được sử dụng cho các mục đích trao đổi dữ liệu giữa các chương trình Java khác nhau thông qua một kết nối mạng, một file dữ liệu, một đường ống (pipe line) hoặc các môi trường trung gian khác Lớp DataOutputStream cung cấp 11 phương thức để ghi các kiểu dữ liệu cụ thể của Java: • public final void writeBoolean(boolean b) throws IOException • public final void writeByte(int b) throws IOException • public final void writeShort(int s) throws IOException • public final void writeChar(int c) throws IOException • public final void writeInt(int i) throws IOException • public final void writeLong(long l) throws IOException • public final void writeFloat(float f) throws IOException • public final void writeDouble(double d) throws IOException • public final void writeChars(String s) throws IOException • public final void writeBytes(String s) throws IOException • public final void writeUTF(String s) throws IOException 27
  34. Tất cả các dữ liệu được viết dưới dạng big-endian. Các số nguyên được viết dưới dạng bù hai với số lượng byte ít nhất. Do đó, một số nguyên khai báo kiểu byte được viết như là 1 byte, khai báo kiểu short sẽ được biểu diễn bằng 2 byte, khai báo kiểu int sẽ được biểu diễn bằng 4 byte, khai báo kiểu long sẽ được biểu diễn bằng 8 byte. Các số thực khai báo kiểu float và double sẽ được viết dưới dạng IEEE 754 và được biểu diễn lần lượt là 4 và 8 byte. Các dữ liệu kiểu boolean sẽ được biểu diễn bằng 1 bit với giá trị 0 cho false và 1 cho true. Các ký tự được viết dưới dạng 2 byte không dấu. Cùng với các phương thức để viết các số nhị phân và các xâu, DataOutputStream vẫn có các phương thức thông thường như write(), flush(), và close() như bất kỳ lớp output stream nào. DataInputStream là lớp bổ sung cho lớp DataOutputStream. DataInputStream có thể đọc tất cả các định dạng mà lớp DataOutputStream ghi. Thêm vào đó, lớp DataInputStream cũng có các phương thức thông thường như read(), available(), skip() và close() cũng như các phương thức để đọc toàn bộ các mảng các byte và các dòng của văn bản. Có 9 phương thức để đọc đọc dữ liệu nhị phân tương thích với 11 phương thức trong DataOutputStream: • public final boolean readBoolean() throws IOException • public final byte readByte() throws IOException • public final char readChar() throws IOException • public final short readShort() throws IOException • public final int readInt() throws IOException • public final long readLong() throws IOException • public final float readFloat() throws IOException • public final double readDouble() throws IOException • public final String readUTF() throws IOException Lớp DataInputStream cung cấp hai phương thức để đọc các byte không dấu và các số nguyên khai báo kiểu short không dấu và trả về số nguyên tương ứng. Mặc dù Java không có hai kiểu dữ liệu là byte không dấu và số nguyên kiểu short không dấu nhưng người lập trình trong Java có thể gặp các kiểu dữ liệu này khi đọc dữ liệu nhị phân viết bằng ngôn ngữ C. Hai phương thức là: • public final int readUnsignedByte() throws IOException • public final int readUnsignedShort() throws IOException 28
  35. Lớp DataInputStream có hai phương thức thông thường để đọc dữ liệu vào môt mảng hoặc một mảng con và trả về số byte đã đọc. Lớp DataInputStream cũng có hai phương thức readFully(), hai phương thức này sẽ lặp lại việc đọc dữ liệu vào môt mảng từ input stream lớp dưới cho đến khi đã đọc đủ số byte đã yêu cầu. Nếu không đọc được đủ dữ liệu một IOException sẽ được đưa ra. Các phương thức là: • public final int read(byte[] input) throws IOException • public final int read(byte[] input, int offset, int length) throws IOException • public final void readFully(byte[] input) throws IOException • public final void readFully(byte[] input, int offset, int length) throws IOException DataInputStream cung cấp phương thức phổ biến readLine() để đọc một dòng trong văn bản và trả lại một dòng: • public final String readLine() throws IOException Tuy nhiên ta không nên sử dụng phương thức này vì phương thức này bị phản đối và có thể gây ra lỗi trong quá trình đọc. 2.4 Các lớp Reader and Writer 2.4.1 Lớp Writer Lớp Writer tương ứng với lớp java.io.OutputStream. Lớp Writer là abstract và có hai protected constructor. Cũng giống như OutputStream, lớp Writer không bao giờ được sử dụng trực tiếp, thay vào đó lớp này được sử dụng theo kiểu polymorphic thông qua một trong số các lớp con của nó. Lớp Writer có năm phương thức write() và các phương thức flush(), close(): • protected Writer() • protected Writer(Object lock) • public abstract void write(char[] text, int offset, int length) throws IOException • public void write(int c) throws IOException • public void write(char[] text) throws IOException • public void write(String s) throws IOException • public void write(String s, int offset, int length) throws IOException • public abstract void flush() throws IOException 29
  36. • public abstract void close() throws IOException Phương thức write(char[] text, int offset, int length) là phương thức cơ bản để cho bốn phương thức write() còn lại được triển khai. Một lớp con cần phải ít nhất override phương thức này cũng như là override các phương thức flush() và close(). 2.4.2 Lớp OutputStreamWriter OutputStreamWriter là lớp con cụ thể quan trọng nhất của lớp Writer. Một OutputStreamWriter nhận các ký tự từ một chương trình Java sau đó sẽ chuyển đổi thành các byte dựa trên một cách mã hóa cụ thể và ghi các byte này lên một output stream lớp dưới. Constructor của OutputStreamWriter xác định output stream để ghi vào và cách mã hóa được sử dụng: • public OutputStreamWriter(OutputStream out, String encoding) throws UnsupportedEncodingException Ngoài các constructor, OutputStreamWriter chỉ có các phương thức Writer thông thường được sử dụng giống như trong lớp Writer và một phương thức để trả về mã hóa của đối tượng: • public String getEncoding() 2.4.3 Lớp Reader Lớp Reader tương ứng với lớp java.io.InputStream. Lớp Reader là abstract và có hai protected constructor. Giống như InputStream và Writer, lớp Reader không bao giờ được dùng trực tiếp và chỉ được sử dụng thông qua một trong số các lớp con của nó. Lớp Reader có ba phương thức read() và các phương thức skip(), close(), ready(), mark(), reset() và markSupported(): • protected Reader() • protected Reader(Object lock) • public abstract int read(char[] text, int offset, int length) throws IOException • public int read() throws IOException • public int read(char[] text) throws IOException • public long skip(long n) throws IOException • public boolean ready() • public boolean markSupported() • public void mark(int readAheadLimit) throws IOException • public void reset() throws IOException • public abstract void close() throws IOException 30
  37. Phương thức read(char[] text, int offset, int length) là phương thức cơ bản qua đó hai phương thức read() còn lại được cài đặt. Một lớp con cần phải ít nhất override phương thức này cũng như là override phương thức close(). InputStreamReader là lớp con cụ thể quan trọng nhất của Reader. Một InputStreamReader đọc các byte từ một input stream lớp dưới, chẳng hạn như một FileInputStream hay TelnetInputStream sau đó chuyển đổi các byte này thành các ký tự dựa trên một cách mã hóa cụ thể và trả về các ký tự này. Constructor của InputStreamReader xác định input stream để đọc và cách mã hóa được sử dụng: • public InputStreamReader(InputStream in) • public InputStreamReader(InputStream in, String encoding) throws UnsupportedEncodingException 2.4.4 Các lớp Filter Reader và Filter Writer Các lớp InputStreamReader và OutputStreamWriter hoạt động trên đỉnh của các input stream và output stream. Các lớp này thay đổi giao diện từ một giao diện hướng byte (byte-oriented interface) thành một giao diện hướng ký tự (character-oriented interface). Sau khi đã thay đổi giao diện, các bộ lọc hướng ký tự (character-oriented filters) bổ sung sẽ được đặt trên đỉnh của reader và writer sử dụng các lớp java.io.FilterReader và java.io.FilterWriter. Cũng như các filter stream, có nhiều lớp con thực hiện các thao tác lọc cụ thể, bao gồm: • BufferedReader • BufferedWriter • LineNumberReader • PushbackReader • PrintWriter Các lớp BufferedReader và BufferedWriter là các lớp dựa trên ký tự (character-based) tương đương với các lớp hướng byte (byte-oriented) BufferedInputStream và BufferedOutputStream. Khi một chương trình đọc dữ liệu từ một BufferedReader, văn bản được lấy từ vùng đệm mà không phải được lấy trực tiếp từ input stream lớp dưới hay các nguồn khác. Khi vùng đệm không có dữ liệu, nó sẽ lại được làm đầy với số lượng ký tự càng nhiều càng tốt. Các ký tự này có thể sẽ không được sử dụng ngay mà có thể được sử dụng cho các lần đọc sau. Khi một chương trình ghi vào một BufferedWriter, văn bản sẽ được đưa vào vùng đệm. Văn bản chỉ được chuyển đến output stream lớp dưới hay các đích đến khác khi vùng đệm đã đầy hoặc khi vùng đệm được flush, điều này sẽ làm cho việc ghi trở nên nhanh hơn. 31
  38. BufferedReader và BufferedWriter có những phương thức thông dụng kết hợp với các reader và writer là read(), ready(), write() và close(). Mỗi lớp có hai constructor, các constructor gắn BufferedReader hoặc BufferedWriter với reader hoặc writer lớp dưới và thiết lập kích thước của vùng đệm. Nếu kích thước của vùng đệm không được thiết lập thì kích thước mặc định của vùng đệm sẽ là 8192 ký tự: • public BufferedReader(Reader in, int bufferSize) • public BufferedReader(Reader in) • public BufferedWriter(Writer out) • public BufferedWriter(Writer out, int bufferSize) Lớp BufferedReader cũng có một phương thức readLine() để đọc một dòng văn bản và trả về như một dòng: • public String readLine() throws IOException Phương thức này thay thế cho phương thức đã bị phản đối readLine() trong DataInputStream. Sự khác biệt giữa hai phương thức này khi gắn một BufferedReader vào một InputStreamReader ta có thể đọc chính xác các dòng trong một tập hợp các ký tự thay vì cách mã hóa mặc định của hệ thống. Lớp BufferedWriter bổ sung một phương thức mới không có trong lớp cha (superclass), được gọi là newLine(), phương thức này hướng đến việc ghi các dòng: • public void newLine() throws IOException 2.4.5 Lớp Scanner Lớp Scanner trong Java dùng để quét trên các dòng vào (bàn phím, socket, xâu kí tự ) để lấy được các giá trị mong muốn: xâu kí tự, số nguyên, số thực, Một số phương thức thường được sử dụng của lớp Scanner trong Java: Phương thức Mô tả public String next() Trả về một xâu kí tự trước khoảng trắng public String nextLine() Trả về kết quả nội dung của một dòng public byte nextByte() Trả về kiểu dữ liệu byte public short nextShort() Trả về kiểu dữ liệu short public int nextInt() Trả về kiểu dữ liệu int public long nextLong() Trả về kiểu dữ liệu long public float nextFloat() Trả về kiểu dữ liệu float public double nextDouble() Trả về kiểu dữ liệu double Đoạn mã sau cho phép nhập một số nguyên từ bàn phím và in ra số đó: 32
  39. import java.util.Scanner; public ViDuScanner { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.print("Vui lòng nhập một: "); int so = sc.nextInt(); System.out.print("Số bạn vừa nhập: " + so); } } 2.4.6 Lớp PrintWriter Lớp PrintWriter thay thế cho lớp PrintStream trong Java 1.0. Lớp này thích hợp cho việc kiểm soát các ký tự được mã hóa bằng nhiều byte và các văn bản quốc tế. Ngoài các constructor, lớp PrintWriter cũng có một tập hợp các phương thức tương tự như lớp PrintStream, đó là: • public PrintWriter(Writer out) • public PrintWriter(Writer out, boolean autoFlush) • public PrintWriter(OutputStream out) • public PrintWriter(OutputStream out, boolean autoFlush) • public void flush() • public void close() • public boolean checkError() • public void write(int c) • public void write(char[] text, int offset, int length) • public void write(char[] text) • public void write(String s, int offset, int length) • public void write(String s) • public void print(boolean b) • public void print(char c) • public void print(int i) • public void print(long l) • public void print(float f) • public void print(double d) • public void print(char[] text) • public void print(String s) • public void print(Object o) • public void println() • public void println(boolean b) • public void println(char c) • public void println(int i) • public void println(long l) • public void println(float f) • public void println(double d) • public void println(char[] text) 33
  40. • public void println(String s) • public void println(Object o) Hầu hết các phương thức trên hoạt động tương tự như các phương thức trong PrintStream ngoại trừ bốn phương thức write(). Bốn phương thức write() sẽ không ghi các byte mà ghi các ký tự. Trong lập trình mạng hiện nay, chúng ta thường dùng lớp Scanner làm input stream và PrintWriter làm output stream vì 2 lớp này hỗ trợ tốt việc đọc và ghi nhiều kiểu dữ liệu khác nhau với các phương thức linh hoạt, hiệu quả. CÂU HỎI, BÀI TẬP VẬN DỤNG: 1. Các lớp InputStream và OutputStream trong Java được khai báo như thế nào? Liệt kê các phương thức cơ bản của 2 lớp này? 2. Trình bày hiểu biết của anh (chị) về các lớp BufferedInputStream, BufferedOutputStream? 3. Trình bày cấu tạo của lớp PrintStream? 4. Các lớp DataInputStream và DataOutputStream có những đặc điểm gì cần chú ý? 5. Trình bày cấu tạo của lớp Reader, Writer và các lớp dẫn xuất của nó? 6. Lớp Scanner và PrintWriter dùng để làm gì? Nêu cấu tạo của chúng? Tại sao hai lớp này được dùng nhiều trong các ứng dụng mạng? 34
  41. CHƯƠNG 3. LẬP TRÌNH ĐA LUỒNG TRONG JAVA 3.1 Giới thiệu về luồng (thread) 3.1.1 Thread là gì? Multi-thread là gì? Thread (luồng) về cơ bản là một tiến trình con (sub-process). Một đơn vị xử lý nhỏ nhất của máy tính có thể thực hiện một công việc riêng biệt. Trong Java, các luồng được quản lý bởi máy ảo Java (JVM). Multi-thread (đa luồng) là một tiến trình thực hiện nhiều luồng đồng thời. Một ứng dụng Java ngoài luồng chính có thể có các luồng khác thực thi đồng thời làm ứng dụng chạy nhanh và hiệu quả hơn. Trình duyệt web hay các chương trình chơi nhạc là ví dụ điển hình về đa luồng: + Khi duyệt 1 trang web, có rất nhiều hình ảnh, mã CSS, mã JavaScript được tải đồng thời bởi các luồng khác nhau. + Khi đang chơi nhạc, chúng ta vẫn có thể tương tác được với nút điều khiển như: play, pause, next, back vì luồng phát nhạc là luồng riêng biệt với luồng tiếp nhận tương tác của người dùng. 3.1.2 Đa nhiệm (multitasking) Multitasking là khả năng chạy đồng thời một hoặc nhiều chương trình cùng một lúc trên một hệ điều hành. Hệ điều hành quản lý việc này và sắp xếp lịch phù hợp cho các chương trình đó. Ví dụ, trên hệ điều hành Windows chúng ta có làm việc đồng thời với các chương trình khác nhau như: Word, Excel, Chrome, Chúng ta sử dụng đa nhiệm để tận dụng tính năng của CPU. Đa nhiệm có thể đạt được bằng hai cách: a) Đa nhiệm dựa trên tiến trình (process) – Đa tiến trình (multiprocessing). • Mỗi tiến trình có địa chỉ riêng trong bộ nhớ, tức là mỗi tiến trình được phân bổ vùng nhớ riêng biệt. • Đa nhiệm dựa trên tiến trình là nặng. • Sự giao tiếp giữa các tiến trình có chi phí cao. • Chuyển đổi từ tiến trình này sang tiến trình khác đòi hỏi thời gian để đăng ký việc lưu và tải các bản đồ bộ nhớ, các danh sách cập nhật, b) Đa nhiệm dựa trên luồng (thread) – Đa luồng (multithreading). • Các luồng chia sẻ không gian địa chỉ ô nhớ giống nhau. • Đa nhiệm dựa trên luồng là nhẹ. 35
  42. • Sự giao tiếp giữa các luồng có chi phí thấp. Đa tiến trình (multiprocessing) và đa luồng (multithreading) đều được sử dụng để tạo ra hệ thống đa nhiệm (multitasking). Nhưng chúng ta sử dụng đa luồng nhiều hơn đa tiến trình bởi vì các luồng chia sẻ một vùng bộ nhớ chung. Chúng không phân bổ vùng bộ nhớ riêng biệt để tiết kiệm bộ nhớ, và chuyển đổi ngữ cảnh giữa các luồng mất ít thời gian hơn giữa các tiến trình. 3.1.3 Ưu điểm và nhược của đa luồng Ưu điểm: • Đa luồng không chặn người sử dụng vì các luồng là độc lập và chúng ta có thể thực hiện nhiều công việc cùng một lúc. • Mỗi luồng có thể dùng chung và chia sẻ nguồn tài nguyên trong quá trình chạy, nhưng có thể thực hiện một cách độc lập. • Luồng là độc lập vì vậy nó không ảnh hưởng đến luồng khác nếu ngoại lệ xảy ra trong một luồng duy nhất. • Chương trình có thể thực hiện nhiều hoạt động với nhau để tiết kiệm thời gian. Ví dụ một ứng dụng có thể được tách thành: luồng chính chạy giao diện người dùng và các luồng phụ gửi kết quả xử lý đến luồng chính. Nhược điểm: • Càng nhiều luồng thì xử lý càng phức tạp. • Xử lý vấn đề về tranh chấp bộ nhớ, đồng bộ dữ liệu khá phức tạp. • Cần phát hiện tránh các luồng chết (deadlock) - luồng chạy mà không làm gì trong ứng dụng cả. Deadlock xảy ra khi 2 tiến trình đợi nhau hoàn thành trước khi chạy, kết quả của quá trình là cả 2 tiến trình không bao giờ kết thúc. 3.2 Vòng đời của một luồng trong Java Vòng đời của luồng trong Java được kiểm soát bởi JVM. Java định nghĩa các trạng thái của luồng trong các thuộc tính static của lớp Thread.State: 36
  43. Hình 3.1: Các trạng thái của luồng NEW: Đây là trạng thái khi luồng vừa được khởi tạo bằng phương thức khởi tạo của lớp Thread nhưng chưa được start(). Ở trạng thái này, luồng được tạo ra nhưng chưa được cấp phát tài nguyên và cũng chưa chạy. Nếu luồng đang ở trạng thái này mà ta gọi các phương thức ép buộc stop(), resume(), suspend() sẽ là nguyên nhân xảy ra ngoại lệ IllegalThreadStateException . RUNNABLE: Sau khi gọi phương thức start() thì luồng đã được cấp phát tài nguyên và các lịch điều phối CPU cho luồng cũng bắt đầu có hiệu lực. Ở đây, chúng ta dùng trạng thái là Runnable chứ không phải Running, vì luồng không thực sự luôn chạy mà tùy vào hệ thống mà có sự điều phối CPU khác nhau. BLOCKED: Đây là 1 dạng của trạng thái Not Runnable. Luồng chờ được unlock mới hoạt động trở lại. TERMINATED: Một luồng ở trong trạng thái terminated hoặc dead khi phương thức run() của nó bị thoát. TIMED WAITING: Luồng chờ trong một thời gian nhất định, hoặc là có một luồng khác đánh thức nó. WAITING: Luồng chờ không giới hạn cho đến khi được một luồng khác đánh thức nó. 3.3 Cách tạo luồng trong Java Trong Java ta có thể tạo ra một luồng bằng một trong hai cách sau: tạo 1 đối tượng của lớp được kế thừa từ lớp Thread hoặc hiện thực từ giao diện Runnable. 37
  44. 3.3.1 Tạo luồng bằng cách kế thừa từ lớp Thread Để tạo luồng bằng cách tạo lớp kế thừa từ lớp Thread, ta phải làm các công việc sau : 1. Khai báo 1 lớp mới kế thừa từ lớp Thread. 2. Override lại phương thức run() ở lớp này, những gì trong phương thức run() sẽ được thực thi khi luồng bắt đầu chạy. Sau khi luồng chạy xong tất cả các câu lệnh trong phương thức run() thì luồng cũng tự hủy. 3. Tạo 1 instance (hay 1 đối tượng) của lớp vừa khai báo. 4. Sau đó gọi phương thức start() của đối tượng này để bắt đầu thực thi luồng. Ví dụ đơn giản về tạo luồng từ lớp Thread: package vn.tbit.simple; public class TheadSimple extends Thread { public void run() { System.out.println("Thread đang chạy "); } public static void main(String args[]) { TheadSimple t1 = new TheadSimple(); t1.start(); } } Lưu ý : • Tuy khai báo những công việc cần làm của luồng trong phương thức run() nhưng khi thực thi luồng ta phải gọi phương thức start(). Vì đây là phương thức đặc biệt mà Java xây dựng sẵn trong lớp Thread, phương thức này sẽ cấp phát tài nguyên cho luồng mới rồi chạy phương thức run() ở luồng này. Vì vậy, nếu ta gọi phương thức run() mà không gọi start() thì cũng tương đương với việc gọi một phương thức của một đối tượng bình thường và phương thức vẫn chạy trên luồng đã gọi phương thức chứ không chạy ở luồng mới tạo ra. Như vậy, vẫn chỉ có một luồng chính làm việc chứ ứng dụng vẫn không phải là đa luồng. • Sau khi gọi start() một luồng thì không bao giờ có thể gọi start() lại. Nếu làm như vậy, một ngoại lệ IllegalThreadStateException sẽ xảy ra. 3.3.2 Tạo luồng bằng cách hiện thực từ giao diện Runnable Để tạo luồng bằng cách hiện thực từ Interface Runnable, ta phải làm các công việc sau : 38
  45. 1. Khai báo 1 lớp mới implements từ Interface Runnable. 2. Hiện thực phương thức run() ở lớp này, những gì trong phương thức run() sẽ được thực thi khi luồng bắt đầu chạy. Sau khi luồng chạy xong tất cả các câu lệnh trong phương thức run() thì luồng cũng tự hủy. 3. Tạo 1 instance (hay 1 đối tượng) của lớp vừa khai báo, giả sử là r1. 4. Tạo 1 instance của lớp Thread bằng phương thức khởi tạo Thread(Runnable target) trong đó target là 1 đối tượng thuộc lớp được implements từ giao diện Runnable. Ví dụ: Thread t1 = new Thread(r1); 5. Gọi phương thức start() của đối tượng t1. Ví dụ đơn giản về tạo luồng từ giao diện Runnable: package vn.tbit.simple; public class RunnableSimple implements Runnable { public void run() { System.out.println("Thread đang chạy "); } public static void main(String args[]) { RunnableSimple runable = new RunnableSimple(); Thread t1 = new Thread(runable); t1.start(); } } Khi nào implements từ Interface Runnable? + Cách hay được sử dụng và được yêu thích là dùng Interface Runnable, bởi vì nó không yêu cầu phải tạo một lớp kế thừa từ lớp Thread. Trong trường hợp ứng dụng thiết kế yêu cầu sử dụng đa kế thừa, chỉ có interface mới có thể giúp giải quyết vấn đề. Ngoài ra nó cũng rất hiệu quả và được cài đặt, sử dụng rất đơn giản. + Trong trường hợp còn lại ta có thể kế thừa từ lớp Thread. 3.4 Ví dụ minh họa sử dụng đa luồng Ví dụ 3-1. Tạo luồng bằng cách extends từ lớp Thread. Tạo lớp extends từ Thread: package vn.tbit.flow; public class ThreadDemo extends Thread { private Thread t; private String threadName; 39
  46. ThreadDemo(String name) { threadName = name; System.out.println("Creating " + threadName); } @Override public void run() { System.out.println("Running " + threadName); try { for (int i = 4; i > 0; i ) { System.out.println("Thread: " + threadName + ", " + i); // Thread tạm nghỉ. Thread.sleep(50); } } catch (InterruptedException e) { System.out.println("Thread " + threadName + " interrupted."); } System.out.println("Thread " + threadName + " exiting."); } public void start() { System.out.println("Starting " + threadName); if (t == null) { t = new Thread(this, threadName); t.start(); } } } Chương trình sử dụng đa luồng: package vn.tbit.flow; public class ThreadDemoTest { public static void main(String args[]) { System.out.println("Main thread running "); ThreadDemo T1 = new ThreadDemo("Thread-1-HR-Database"); T1.start(); ThreadDemo T2 = new ThreadDemo("Thread-2-Send-Email"); T2.start(); System.out.println("==> Main thread stopped!!! "); } } Kết quả thực thi chương trình trên: Main thread running Creating Thread-1-HR-Database Starting Thread-1-HR-Database Creating Thread-2-Send-Email Starting Thread-2-Send-Email ==> Main thread stopped!!! 40
  47. Running Thread-1-HR-Database Running Thread-2-Send-Email Thread: Thread-2-Send-Email, 4 Thread: Thread-1-HR-Database, 4 Thread: Thread-1-HR-Database, 3 Thread: Thread-2-Send-Email, 3 Thread: Thread-2-Send-Email, 2 Thread: Thread-1-HR-Database, 2 Thread: Thread-2-Send-Email, 1 Thread: Thread-1-HR-Database, 1 Thread Thread-2-Send-Email exiting. Thread Thread-1-HR-Database exiting. Kết quả chương trình trên được giải thích thông qua hình bên dưới: Hình 3.2: Tạo luồng bằng cách extends từ lớp Thread Ví dụ 3-2. Tạo luồng bằng cách implements từ giao diện Runnable. Tạo lớp implements từ giao diện Runnable: package vn.tbit.flow; class RunnableDemo implements Runnable { private Thread t; private String threadName; RunnableDemo(String name) { threadName = name; System.out.println("Creating " + threadName); } @Override public void run() { System.out.println("Running " + threadName); try { for (int i = 4; i > 0; i ) { System.out.println("Thread: " + threadName + ", " + i); // Let the thread sleep for a while. 41
  48. Thread.sleep(50); } } catch (InterruptedException e) { System.out.println("Thread " + threadName + " interrupted."); } System.out.println("Thread " + threadName + " exiting."); } public void start() { System.out.println("Starting " + threadName); if (t == null) { t = new Thread(this, threadName); t.start(); } } } Chương trình sử dụng đa luồng: package vn.tbit.flow; public class RunnableDemoTest { public static void main(String args[]) { System.out.println("Main thread running "); RunnableDemo R1 = new RunnableDemo("Thread-1-HR-Database"); R1.start(); RunnableDemo R2 = new RunnableDemo("Thread-2-Send-Email"); R2.start(); System.out.println("==> Main thread stopped!!! "); } } Kết quả thực thi chương trình trên: Main thread running Creating Thread-1-HR-Database Starting Thread-1-HR-Database Creating Thread-2-Send-Email Starting Thread-2-Send-Email ==> Main thread stopped!!! Running Thread-1-HR-Database Running Thread-2-Send-Email Thread: Thread-1-HR-Database, 4 Thread: Thread-2-Send-Email, 4 Thread: Thread-1-HR-Database, 3 Thread: Thread-2-Send-Email, 3 Thread: Thread-1-HR-Database, 2 Thread: Thread-2-Send-Email, 2 Thread: Thread-1-HR-Database, 1 Thread: Thread-2-Send-Email, 1 Thread Thread-1-HR-Database exiting. Thread Thread-2-Send-Email exiting. 42
  49. Kết quả chương trình trên được giải thích thông qua hình bên dưới: Hình 3.3: Tạo luồng bằng cách implements từ giao diện Runnable 3.5 Các phương thức của lớp Thread thường hay sử dụng • suspend(): phương thức làm tạm dừng hoạt động của 1 luồng nào đó bằng các ngưng cung cấp CPU cho luồng này. Để cung cấp lại CPU cho luồng ta sử dụng phương thức resume(). Cần lưu ý là ta không thể dừng ngay hoạt động của luồng bằng phương thức này. Phương thức suspend() không dừng ngay tức thì hoạt động của luồng mà sau khi luồng này trả CPU về cho hệ điều hành thì không cấp CPU cho luồng nữa. • resume(): phương thức làm cho luồng chạy lại khi luồng bị dừng do phương thức suspend() bên trên. Phương thức này sẽ đưa luồng vào lại lịch điều phối CPU để luồng được cấp CPU chạy lại bình thường. • stop(): phương thức này sẽ kết thúc phương thức run() bằng cách ném ra 1 ngoại lệ ThreadDeath, điều này cũng sẽ làm luồng kết thúc 1 cách ép buộc. Nếu giả sử, trước khi gọi stop() mà luồng đang nắm giữa 1 đối tượng nào đó hoặc 1 tài nguyên nào đó mà luồng khác đang chờ thì có thể dẫn tới việc xảy ra deadlock. • destroy(): dừng hẳn luồng. • isAlive(): phương thức này kiểm tra xem luồng còn active hay không. Phương thức sẽ trả về true nếu luồng đã được start() và chưa rơi vào trạng thái dead. Nếu phương thức trả về false thì luồng đang ở trạng thái New Thread hoặc là đang ở trạng thái dead. • yeild() : hệ điều hành đa nhiệm sẽ phân phối CPU cho các tiến trình, các luồng theo vòng xoay. Mỗi luồng sẽ được cấp CPU trong 1 khoảng thời gian nhất 43
  50. định, sau đó trả lại CPU cho hệ điều hành, hệ điều hành sẽ cấp CPU cho luồng khác. Khi gọi phương thức này luồng sẽ bị ngừng cấp CPU và nhường cho luồng tiếp theo trong hàng chờ Ready. Luồng không phải ngưng cấp CPU như suspend() mà chỉ ngưng cấp trong lần nhận CPU đó mà thôi. • sleep(long): tạm dừng luồng trong một khoảng thời gian tính bằng mili giây. • join(): thông báo rằng hãy chờ luồng này hoàn thành rồi luồng cha mới được tiếp tục chạy. • join(long): luồng cha cần phải đợi sau mili giây mới được tiếp tục chạy, kể từ lúc gọi join(long). Nếu tham số bằng 0 nghĩa là đợi cho tới khi luồng này kết thúc. • getName(): trả về tên của luồng. • setName(String name): thay đổi tên của luồng. • getId(): trả về id của luồng. • getState(): trả về trạng thái của luồng. • currentThread(): trả về tham chiếu của luồngđang được thi hành. • getPriority(): trả về mức độ ưu tiên của luồng. • setPriority(int): thay đổi mức độ ưu tiên của luồng. • isDaemon(): kiểm tra nếu luồnglà một luồng Daemon. • setDaemon(boolean): thiết lập luồnglà một luồng Daemon hay không. • interrupt(): làm gián đoạn một luồng trong Java. Nếu luồng nằm trong trạng thái sleep hoặc wait, nghĩa là sleep() hoặc wait() được gọi ra. Việc gọi phương thức interrupt() trên luồng đó sẽ phá vỡ trạng thái sleep hoặc wait và ném ra ngoại lệ InterruptedException. Nếu luồng không ở trong trạng thái sleep hoặc wait, việc gọi phương thức interrupt() thực hiện hành vi bình thường và không làm gián đoạn luồng nhưng đặt cờ interrupt thành true. • isInterrupted(): kiểm tra luồng nào đó đã bị ngắt hay không. • interrupted(): kiểm tra xem luồng hiện tại đã bị ngắt hay không. 3.6 Một số vấn đề liên quan đến luồng 3.6.1 Một số tham số của luồng Định danh của luồng (ThreadId): ThreadId là định danh của luồng, được dùng để phân biệt với các luồng khác cùng tiến trình hoặc cùng tập luồng. Đây là thông số mà máy ảo Java tự tạo ra khi ta tạo luồng nên ta không thể sửa đổi cũng 44
  51. như áp đặt thông số này khi tạo luồng. Nhưng ta có thể lấy được ThreadId thông qua phương thức getId() của lớp Thread. Tên của luồng (ThreadName): ThreadName là tên của luồng, đây là thuộc tính mà ta có thể đặt hoặc không đặt cho luồng. Nếu ta không đặt cho luồng thì máy ảo Java sẽ tự đặt với quy tắc sau: “Thread-” + Thứ tự luồng được tạo ra, bắt đầu từ 0. Độ ưu tiên của luồng (Priority): Như đã nói ở phần trước, mỗi luồng có 1 độ ưu tiên nhất định. Đây sẽ là thông số quyết định mức ưu tiên khi cấp phát CPU cho các luồng. Trong Java, đế đặt độ ưu tiên cho 1 luồng ta dùng phương thức: void setPriority(int newPriority) • int newPriority: Mức độ ưu tiên từ 1 đến 10. Java có định nghĩa sẵn 3 mức ưu tiên chuẩn như sau: • Thread.MIN_PRIORITY (giá trị 01) • Thread.NORM_PRIORITY (giá trị 05) • Thread.MAX_PRIORITY (giá trị 10) Để lấy độ ưu tiên của 1 luồng, ta dùng phương thức: int getPriority(). Ví dụ 3-3. Chương trình xác định mức độ ưu tiên của luồng. WorkingThread.java package vn.tbit.info; public class WorkingThread extends Thread { public WorkingThread(String name) { super(name); } public void run() { for (int i = 0; i < 5; i++) { System.out.printf("Luồng: %s có độ ưu tiên là %d \n", getName(), getPriority()); } } } ThreadInfoExample.java package vn.tbit.info; public class ThreadInfoExample { public static void main(String[] args) { 45
  52. Thread t1 = new WorkingThread("Luồng 1"); Thread t2 = new WorkingThread("Luồng 2"); Thread t3 = new WorkingThread("Luồng 3"); System.out.println("ID luồng 1: " + t1.getId()); System.out.println("ID luồng 2: " + t2.getId()); System.out.println("ID luồng 3: " + t3.getId()); t1.setPriority(1); t2.setPriority(5); t3.setPriority(10); t1.start(); t2.start(); t3.start(); } } Kết quả thực thi chương trình trên: ID luồng 1: 10 ID luồng 2: 11 ID luồng 3: 12 Luồng: Luồng 2 có độ ưu tiên là 5 Luồng: Luồng 2 có độ ưu tiên là 5 Luồng: Luồng 2 có độ ưu tiên là 5 Luồng: Luồng 2 có độ ưu tiên là 5 Luồng: Luồng 2 có độ ưu tiên là 5 Luồng: Luồng 1 có độ ưu tiên là 1 Luồng: Luồng 3 có độ ưu tiên là 10 Luồng: Luồng 3 có độ ưu tiên là 10 Luồng: Luồng 3 có độ ưu tiên là 10 Luồng: Luồng 3 có độ ưu tiên là 10 Luồng: Luồng 3 có độ ưu tiên là 10 Luồng: Luồng 1 có độ ưu tiên là 1 Luồng: Luồng 1 có độ ưu tiên là 1 Luồng: Luồng 1 có độ ưu tiên là 1 Luồng: Luồng 1 có độ ưu tiên là 1 3.6.2 Sử dụng phương thức sleep() Phương thức sleep() của lớp Thread được sử dụng để tạm ngừng một luồng trong một khoảng thời gian nhất định. Ví dụ 3-4. Viết chương trình in ra số từ 1 đến 5, tạm ngừng 500ms trước khi in chữ số tiếp theo. package vn.tbit.sleep; public class SleepMethodExample extends Thread { public void run() { for (int i = 1; i <= 5; i++) { System.out.println(i); try { 46
  53. Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String args[]) { SleepMethodExample t1 = new SleepMethodExample(); t1.start(); } } 3.6.3 Sử dụng join() và join(long millis) Phương thức join() dùng để thông báo rằng hãy chờ luồng này hoàn thành rồi luồng cha mới được tiếp tục chạy. Ví dụ 3-5. Chương trình minh họa sửa dụng phương thức join() của luồng. package vn.tbit.join; public class UsingJoinMethod extends Thread { public UsingJoinMethod(String name) { super(name); } @Override public void run() { System.out.println(getName()); for (int i = 1; i <= 5; i++) { try { System.out.print(i + " "); Thread.sleep(300); } catch (InterruptedException ie) { System.out.println(ie.toString()); } } System.out.println(); } public static void main(String[] args) throws InterruptedException { UsingJoinMethod t1 = new UsingJoinMethod("Thread 1"); UsingJoinMethod t2 = new UsingJoinMethod("Thread 2"); t1.start(); t1.join(); t2.start(); System.out.println("Main Thread Finished"); } } Thực thi chương trình trên: Thread 1 47
  54. 1 2 3 4 5 Main Thread Finished Thread 2 1 2 3 4 5 Phương thức join(long millis) dùng để thông báo luồng cha cần phải đợi sau millis ( tính bằng mili giây) mới được tiếp tục chạy, kể từ lúc gọi join(long millis). Nếu tham số bằng 0 nghĩa là đợi cho tới khi luồng này kết thúc. Ví dụ 3-6. Chương trình minh họa sử dụng phương thức join(long millis). package vn.tbit.join; public class UsingJoinMethod2 extends Thread { public UsingJoinMethod2(String name) { super(name); } @Override public void run() { System.out.println(getName()); for (int i = 1; i <= 5; i++) { try { System.out.print(i + " "); Thread.sleep(300); } catch (InterruptedException ie) { System.out.println(ie.toString()); } } System.out.println(); } public static void main(String[] args) throws InterruptedException { UsingJoinMethod2 t1 = new UsingJoinMethod2("Thread 1"); UsingJoinMethod2 t2 = new UsingJoinMethod2("Thread 2"); t1.start(); // Main Thread phải chờ 450ms mới được tiếp tục chạy. // Không nhất thiết phải chờ Thread t1 kết thúc t1.join(450); t2.start(); System.out.println("Main Thread Finished"); } } Thực thi chương trình trên: Thread 1 1 2 Main Thread Finished Thread 2 1 3 2 4 3 5 4 5 48
  55. 3.6.4 Xử lý ngoại lệ cho luồng Phương thức Thread.setDefaultUncaughtExceptionHandler() thiết lập mặc định xử lý khi luồng đột ngột chấm dứt do một ngoại lệ xảy ra mà không có xử lý khác đã được xác định cho luồng đó. Ví dụ 3-7. Chương trình chưa xử lý ngoại lệ. package vn.tbit.exception; import java.util.Random; public class WorkingThread implements Runnable { @Override public void run() { while (true) { processSomething(); } } private void processSomething() { try { System.out.println("Processing working thread"); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } Random r = new Random(); int i = r.nextInt(100); if (i > 70) { throw new RuntimeException("Simulate an exception was not handled in the thread"); } } } Ví dụ 3-8. Chương trình minh họa xử lý ngoại lệ trong luồng. package vn.tbit.exception; public class ThreadExceptionDemo { public static void main(String[] args) { System.out.println("==> Main thread running "); Thread thread = new Thread(new WorkingThread()); Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("#Thread: " + t); 49
  56. System.out.println("#Thread exception message: " + e.getMessage()); } }); thread.start(); System.out.println("==> Main thread end!!!"); } } Thực thi chương trình trên: ==> Main thread running ==> Main thread end!!! Processing working thread Processing working thread Processing working thread Processing working thread Processing working thread Processing working thread #Thread: Thread[Thread-0,5,main] #Thread exception message: Have a problem CÂU HỎI, BÀI TẬP VẬN DỤNG: 1. Luồng (thread) là gì? Lập trình đa luồng (multithread) có đặc điểm gì? 2. Đa nhiệm là gì? Các cách để đạt được đa nhiệm và phân biệt các cách đó? 3. Nêu ưu điểm và nhược điểm của lập trình đa luồng? 4. Trình bày các cách tạo luồng trong lập trình Java? 5. Viết một chương trình đa luồng dùng lớp Thread? 6. Viết một chương trình đa luồng dùng giao diện Runnable? 50
  57. CHƯƠNG 4. LỚP INETADDRESS Các thiết bị kết nối tới Internet được gọi là các nút mạng (node). Nếu nút là máy tính chúng ta gọi là host. Mỗi nút hoặc host được phân biệt với nhau bởi các địa chỉ mạng, chúng ta thường gọi là địa chỉ IP. Hầu hết địa chỉ IP hiện nay là địa chỉ IPv4 (địa chỉ IP phiên bản 4) có độ dài 4 byte. Mặc dù vậy, nhiều tổ chức và cá nhân đang dần chuyển sang sử dụng địa chỉ IPv6 (địa chỉ IP phiên bản 6) có độ dài 16 byte. Cả địa chỉ IPv4 và IPv6 đều bao gồm các byte được sắp thứ tự nhất định (có thể coi đó là một mảng các byte) nhưng chúng thực tế không phải là số. Một địa chỉ IPv4 gồm 4 byte, mỗi byte thường được kí hiệu bằng một số nguyên dương có giá trị nằm trong khoảng từ 0 tới 255. Các byte được ngăn các bởi các dấu chấm để cho chúng ta dễ nhận ra chúng. Ví dụ, địa chỉ IP của trang utb.edu.vn là 117.6.86.168. Cách kí hiệu này thuật ngữ tiếng Anh gọi là định dạng dotted quad. Một địa chỉ IPv6 thường được kí hiệu bởi 8 nhóm cách nhau bởi dấu hai chấm, trong đó mỗi nhóm gồm 4 số thập lục phân. Ví dụ, địa chỉ IP của trang www.hamiltonweather.tk là 2400:cb00:2048:0001:0000:0000:6ca2:c665. Những số 0 ở đầu mỗi nhóm có thể bỏ đi, do đó địa chỉ trên đây có thể viết là 2400:cb00:2048:1:0:0:6ca2:c665. Nếu trong địa chỉ IPv6 có một dãy các nhóm gồm toàn con số 0, chúng ta có thể dùng hai dấu hai chấm để kí hiệu thay thế. Ví dụ, địa chỉ 2001:4860:4860:0000:0000:0000:0000:8888 có thể được viết ngắn gọn là 2001:4860:4860::8888. Một cách viết địa chỉ IPv6 khác là kết hợp với cách viết địa chỉ IPv4 bằng cách viết 4 byte cuối của địa chỉ IPv6 dưới dạng địa chỉ IPv4. Ví dụ, địa chỉ FEDC:BA98:7654:3210:FEDC:BA98:7654:3210 có thể được viết là FEDC:BA98:7654:3210:FEDC:BA98:118.84.50.16. Địa chỉ IP rất tiện lợi cho máy tính nhưng gây khó khăn cho con người trong việc ghi nhớ chúng. Vào những năm 50 của thế kỉ XX, G. A. Miller đã khám phá ra hầu hết mọi người có thể ghi nhớ một số có bảy chữ số; một vài người có thể nhớ nhiều hơn chín chữ số và một số người không thể nhớ quá năm chữ số. Chi tiết bạn đọc có thể tìm hiểu bài viết “The Magical Number Seven, Plus or Minus Two: Some Limits on Our Capacity for Processing Information” trong cuốn Psychological Review, tập 63, trang 81-97. Điều đó giải thích tại sao số điện thoại chúng ta thường chia thành các nhóm ba hoặc bốn số và bổ sung thêm mã vùng để giảm bớt các con số cần phải ghi nhớ trong mỗi nhóm. Do đó, một địa chỉ IPv4 có thể có tới mười hai chữ số là quá khó khăn cho hầu hết chúng ta để ghi nhớ chúng. Để giúp chúng ta dễ dàng hơn trong việc ghi nhớ các địa chỉ mạng, các nhà thiết kế Internet đã phát minh ra các hệ thống DNS - Domain Name System (hệ thống máy chủ tên miền). DNS giúp chúng ta thay thế các kí hiệu theo dãy số bằng 51
  58. những chuỗi kí tự dễ nhớ hơn với con người, chúng ta gọi chúng là hostname. Ví dụ, thay vì phải nhớ địa chỉ 117.6.86.168 chúng ta có thể nhớ địa chỉ utb.edu.vn. Mỗi một máy chủ phải có ít nhất một hostname. Máy khách cũng thường có một hostname nhưng không có một địa chỉ IP cố định nếu IP này được cấp phát lại sau mỗi lần khởi động. Một số thiết bị có thể có nhiều tên. Ví dụ, cả tbit.vn và tuhoctin.net đều nằm trên một máy chủ Linux. Cái tên tbit.vn thực tế chỉ tới một website chứ không phải là tên một máy chủ cụ thể nào đó. Trong quá khứ, khi một website được chuyển từ một máy chủ tới một máy chủ khác thì tên của nó sẽ được gán lại trên máy chủ mới để luôn trỏ về website trên máy chủ hiện tại. Có những trường hợp một tên có thể tương ứng với nhiều địa chỉ IP. Khi đó, máy chủ nào được lựa chọn để phản hồi yêu cầu từ người dùng sẽ được máy chủ DNS lựa chọn ngẫu nhiên. Tính năng này thường được sử dụng với những website có lượng truy cập rất lớn nên cần mở rộng hệ thống theo chiều ngang để phân chia lượng người dùng tới nhiều hệ thống máy chủ khác nhau. Ví dụ, tên miền google.com.vn thường trỏ tới nhiều máy chủ có địa chỉ khác nhau và thay đổi theo từng thời điểm. Tất cả máy tính kết nối tới Internet đều truy cập một thiết bị gọi là máy chủ dịch vụ tên miền (máy chủ DNS). Đây là máy chủ thực hiện ánh xạ giữa tên miền và địa chỉ IP. Hầu hết máy chủ DNS chỉ biết những địa chỉ của những máy tính trong mạng cục bộ của mình và một số ít địa chỉ trên một mạng khác. Nếu máy khách yêu cầu địa chỉ của một máy nằm bên ngoài mạng cục bộ, máy chủ DNS của mạng cục bộ sẽ gửi yêu cầu tới một máy chủ DNS tại mạng khác đợi trả lời để phản hồi lại yêu cầu đó. Hầu như mọi lúc chúng ta có thể sử dụng hostname và đợi máy chủ DNS xử lý trả lại địa chỉ IP. Khi nào kết nối tới máy chủ DNS, chúng ta không cần lo lắng về việc làm thế nào để có thể ánh xạ giữa tên và địa chỉ IP trên máy chủ DNS cục bộ hay các bộ phận khác trên Internet. Mặc dù vậy, chúng ta cần truy cập ít nhất một máy chủ DNS nếu muốn thực hành một số ví dụ trong giáo trình này. Một số ví dụ cần phải kết nối Internet chứ không làm việc trên một máy độc lập được. Lớp java.net.InetAddress là một sự biểu diễn bậc cao của địa chỉ IP, bao gồm cả IPv4 và IPv6. Lớp này được dùng trong hầu hết các lớp khác như Socket, ServerSocket, URL, DatagramSocket, DatagramPacket InetAddress cũng bao gồm các thông tin cả về hostname và địa chỉ IP. 52