Bài tập Java: lập trình mạng và cú pháp hiện đại
Luyện hai mảng của Java nâng cao: giao tiếp TCP qua socket (Chương 5) và các tính năng ngôn ngữ mới như record, sealed, switch biểu thức, Optional, text block (Chương 6). Tự viết code trước, bí thì mở Gợi ý. Mỗi khối code là một class công khai, biên dịch được.
Bài 1: khách nối tới server và nhận phản hồi
Viết chương trình khách (client) kết nối tới server tại localhost cổng 5000, gửi một
dòng chữ do người dùng nhập, rồi in ra dòng phản hồi mà server trả về.
Gợi ý
Mô hình khách chủ: khách chủ động mở kết nối, server thụ động chờ. Dùng Socket với địa
chỉ và cổng, lấy luồng ra để gửi và luồng vào để nhận.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class EchoClient {
public static void main(String[] args) throws Exception {
// try-with-resources closes the socket automatically
try (Socket socket = new Socket("localhost", 5000)) {
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
BufferedReader keyboard = new BufferedReader(
new InputStreamReader(System.in));
String line = keyboard.readLine();
out.println(line); // send one framed line
System.out.println("Server: " + in.readLine());
}
}
}
Xem lại bài Mô hình client-server.
Bài 2: echo server TCP một khách
Viết server mở ServerSocket ở cổng 5000, chờ một khách kết nối, đọc từng dòng và gửi
lại đúng dòng đó (echo) cho tới khi khách đóng kết nối.
Gợi ý
accept() chặn (blocking) cho tới khi có khách, rồi trả về một Socket đại diện cho kết
nối đó. Server một luồng như dưới đây chỉ phục vụ được một khách tại một thời điểm; khách
sau phải chờ.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer {
public static void main(String[] args) throws Exception {
try (ServerSocket server = new ServerSocket(5000)) {
System.out.println("Listening on 5000");
Socket client = server.accept(); // blocks until a client arrives
try (client) {
BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream(), true);
String line;
while ((line = in.readLine()) != null) {
out.println(line); // echo the same line back
}
}
}
}
}
Xem lại bài Lập trình Socket.
Bài 3: đọc ghi luồng và tự phân khung bản tin
TCP là dòng byte tin cậy, có thứ tự, nhưng không bảo toàn ranh giới bản tin: hai lần gửi có thể tới thành một lần đọc, hoặc một lần gửi bị tách làm nhiều lần đọc. Viết đoạn code nhận nhiều bản tin liên tiếp và tách đúng từng bản tin bằng ký tự xuống dòng làm dấu phân khung.
Gợi ý
Đừng giả định mỗi read là một bản tin. Chọn một quy ước phân khung: ở đây mỗi bản tin
kết thúc bằng \n, nên readLine() gom byte cho tới khi gặp dấu xuống dòng rồi trả về
đúng một bản tin.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.Socket;
public class FramedReader {
// Read newline-delimited messages; each readLine returns exactly one frame
public static void readAll(Socket socket) throws Exception {
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String message;
while ((message = in.readLine()) != null) {
System.out.println("One message: " + message);
}
}
}
Xem lại bài Lập trình Socket.
Bài 4: server đa luồng bằng thread pool
Nâng cấp echo server để phục vụ nhiều khách cùng lúc. Với mỗi kết nối accept() trả về,
giao việc xử lý cho một luồng lấy từ thread pool, còn luồng chính quay lại chờ khách kế.
Gợi ý
Dùng ExecutorService cỡ cố định thay cho việc tạo mới một Thread cho mỗi khách; pool
tái dùng luồng và giới hạn số luồng đồng thời. Vòng lặp chính chỉ accept() rồi submit.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PoolEchoServer {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(8);
try (ServerSocket server = new ServerSocket(5000)) {
while (true) {
Socket client = server.accept(); // accept then hand off
pool.submit(() -> handle(client));
}
}
}
private static void handle(Socket client) {
try (client) {
BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream(), true);
String line;
while ((line = in.readLine()) != null) {
out.println(line);
}
} catch (Exception e) {
System.err.println("Client error: " + e.getMessage());
}
}
}
Xem lại bài Lập trình Socket.
Bài 5: tuần tự hóa với serialVersionUID và transient
Định nghĩa lớp UserSession cài Serializable để ghi ra tệp và đọc lại. Khai báo tường
minh serialVersionUID, và đánh dấu trường mật khẩu là transient để không bị ghi ra.
Gợi ý
Nên khai serialVersionUID tường minh để kiểm soát tương thích phiên bản, tránh để trình
biên dịch tự sinh. Trường transient bị bỏ qua khi ghi và nhận giá trị mặc định (null
với tham chiếu, 0 với số) khi đọc lại. Cảnh báo an toàn: đọc dữ liệu tuần tự hóa từ nguồn
không tin cậy có thể dẫn tới thực thi mã tùy ý, nên chỉ giải tuần tự hóa dữ liệu đáng tin.
import java.io.Serializable;
public class UserSession implements Serializable {
// Explicit version id keeps deserialization compatible across builds
private static final long serialVersionUID = 1L;
private final String userName;
private final transient String password; // skipped when serialized
public UserSession(String userName, String password) {
this.userName = userName;
this.password = password;
}
public String getUserName() {
return userName;
}
public String getPassword() {
// After reading back, a transient field is null
return password;
}
}
Xem lại bài Tuần tự hóa.
Bài 6: record cho dữ liệu bất biến và switch biểu thức
Thay lớp dữ liệu nhiều mã lặp bằng record (Java 16) mang tên Point. Sau đó viết hàm
phân loại quadrant trả về góc phần tư, refactor chuỗi if / else if dài thành switch
biểu thức (Java 14).
Gợi ý
record tự sinh hàm khởi tạo, các accessor, equals, hashCode, toString, và các
trường là bất biến. Switch biểu thức dùng dạng -> và trả về giá trị trực tiếp, không cần
break, thường an toàn hơn chuỗi if dài.
public class Geometry {
// record: immutable data carrier, Java 16
public record Point(int x, int y) {}
// switch expression form ( -> ), Java 14
public static String quadrant(Point p) {
int sx = Integer.signum(p.x());
int sy = Integer.signum(p.y());
return switch (sx * 3 + sy) {
case 4 -> "I"; // x > 0, y > 0
case -2 -> "II"; // x < 0, y > 0
case -4 -> "III"; // x < 0, y < 0
case 2 -> "IV"; // x > 0, y < 0
default -> "truc"; // on an axis
};
}
}
Xem lại bài Cú pháp hiện đại.
Bài 7: sealed, Optional trả về và text block
Mô hình hóa hình phẳng bằng phân cấp sealed (Java 17) gồm Circle và Square. Viết
hàm findByName trả về Optional thay vì trả null, và một hàm dựng mô tả nhiều dòng
bằng text block (Java 15).
Gợi ý
sealed giới hạn tập lớp con được phép kế thừa, giúp switch phủ hết trường hợp. Optional
chỉ nên dùng làm giá trị trả về để buộc người gọi xử lý trường hợp rỗng; không dùng làm
trường hay tham số. Text block """...""" giữ nguyên xuống dòng, gọn hơn nối chuỗi.
import java.util.List;
import java.util.Optional;
public class Shapes {
// sealed hierarchy, Java 17: only these two subclasses are permitted
public sealed interface Shape permits Circle, Square {
String name();
}
public record Circle(String name) implements Shape {}
public record Square(String name) implements Shape {}
// Optional as a return value instead of returning null
public static Optional<Shape> findByName(List<Shape> shapes, String target) {
return shapes.stream()
.filter(s -> s.name().equals(target))
.findFirst();
}
// text block, Java 15
public static String describe(Shape shape) {
return """
Shape report
name: %s
kind: %s
""".formatted(shape.name(), shape.getClass().getSimpleName());
}
}
Xem lại bài Optional và xử lý ngoại lệ.
Bài 8: gửi gói tin bằng UDP DatagramSocket
Viết chương trình gửi một gói tin UDP tới localhost cổng 5000 bằng DatagramSocket và DatagramPacket. So sánh ngắn gọn với TCP về mặt kết nối và bảo đảm.
Gợi ý
UDP không kết nối và không bảo đảm giao nhận: không bắt tay, không truyền lại. Bù lại mỗi DatagramPacket là một gói độc lập nên ranh giới bản tin được giữ nguyên, khác với dòng byte của TCP.
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UdpSender {
public static void main(String[] args) throws Exception {
// UDP is connectionless: no handshake, no delivery guarantee
try (DatagramSocket socket = new DatagramSocket()) {
byte[] data = "xin chao".getBytes();
InetAddress addr = InetAddress.getByName("localhost");
DatagramPacket packet =
new DatagramPacket(data, data.length, addr, 5000);
socket.send(packet); // one datagram keeps its message boundary
System.out.println("Sent " + data.length + " bytes");
}
}
}
Xem lại bài Lập trình Socket.
Bài 9: ghi và đọc đối tượng qua ObjectStream
Ghi một đối tượng ra tệp bằng ObjectOutputStream rồi đọc lại bằng ObjectInputStream. Kiểm chứng đối tượng đọc lên bằng đúng đối tượng gốc.
Gợi ý
ObjectOutputStream.writeObject tuần tự hoá cả đồ thị đối tượng, readObject dựng lại. Lớp phải cài Serializable. Nhắc lại cảnh báo: chỉ giải tuần tự hoá dữ liệu từ nguồn tin cậy vì dữ liệu độc hại có thể dẫn tới thực thi mã tuỳ ý.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class ObjectStreamDemo {
record Diem(int x, int y) implements Serializable {}
public static void main(String[] args) throws Exception {
Diem goc = new Diem(3, 4);
try (ObjectOutputStream out =
new ObjectOutputStream(new FileOutputStream("diem.ser"))) {
out.writeObject(goc); // serialize the object graph
}
try (ObjectInputStream in =
new ObjectInputStream(new FileInputStream("diem.ser"))) {
Diem doc = (Diem) in.readObject(); // deserialize back
System.out.println(doc); // Diem[x=3, y=4]
}
}
}
Xem lại bài Tuần tự hóa.
Bài 10: record với compact constructor để kiểm hợp lệ
Định nghĩa record Tuoi chỉ chấp nhận số không âm. Dùng compact constructor để chặn giá trị âm ngay khi tạo, ném IllegalArgumentException nếu vi phạm.
Gợi ý
Compact constructor của record không có danh sách tham số và chạy trước khi các trường được gán, đúng chỗ để kiểm tra hoặc chuẩn hoá đầu vào. Không cần tự gán trường, trình biên dịch gán giúp sau đó.
public class CompactCtorDemo {
// compact constructor validates before fields are assigned
public record Tuoi(int nam) {
public Tuoi {
if (nam < 0) throw new IllegalArgumentException("nam am");
}
}
public static void main(String[] args) {
Tuoi t = new Tuoi(20);
System.out.println(t.nam()); // 20
try {
new Tuoi(-1); // triggers validation
} catch (IllegalArgumentException e) {
System.out.println("bi chan: " + e.getMessage());
}
}
}
Xem lại bài Cú pháp hiện đại.
Bài 11: sealed cùng switch mẫu
Mô hình hoá hình phẳng bằng phân cấp sealed gồm Circle và Square, rồi viết hàm tính diện tích bằng switch mẫu (pattern matching) không cần nhánh default.
Gợi ý
Vì kiểu là sealed, trình biên dịch biết trọn tập lớp con nên switch mẫu phủ hết mọi nhánh mà không cần default. Mỗi nhánh case gắn biến rồi truy cập thành phần của record ngay.
public class PatternSwitchDemo {
sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double canh) implements Shape {}
// pattern matching switch covers all permitted subtypes, no default
static double dienTich(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Square sq -> sq.canh() * sq.canh();
};
}
public static void main(String[] args) {
System.out.println(dienTich(new Square(3))); // 9.0
}
}
Xem lại bài Optional và xử lý ngoại lệ.
Bài 12: dựng JSON bằng text block
Viết hàm nhận tên và tuổi, trả về một chuỗi JSON nhiều dòng bằng text block. Dùng formatted để chèn giá trị mà không phải thoát dấu nháy kép.
Gợi ý
Text block """...""" giữ nguyên xuống dòng và cho phép viết dấu nháy kép thẳng, rất hợp để dựng JSON. formatted thay %s và %d bằng giá trị, gọn hơn nhiều so với nối chuỗi có thoát ký tự.
public class JsonTextBlock {
// text block builds multi-line JSON without escaping quotes
static String toJson(String ten, int tuoi) {
return """
{
"ten": "%s",
"tuoi": %d
}
""".formatted(ten, tuoi);
}
public static void main(String[] args) {
System.out.println(toJson("an", 20));
}
}
Xem lại bài Cú pháp hiện đại.
Câu hỏi tự kiểm
- 1Với TCP, phát biểu nào đúng về ranh giới bản tin?
- 2Phương thức accept() của ServerSocket hoạt động thế nào?
- 3Một trường được đánh dấu transient sẽ ra sao khi tuần tự hóa rồi đọc lại?
- 4record được đưa vào Java ở phiên bản nào (biết switch biểu thức là 14, text block là 15, sealed là 17)?