Trong ngôn ngữ Rust, std::net::UdpSocket là thành phần cơ bản để thực hiện các giao tiếp mạng thông qua giao thức UDP (User Datagram Protocol). Khác với TCP, UDP là giao thức không hướng kết nối, cho phép gửi các gói tin (datagram) một cách nhanh chóng nhưng không đảm bảo thứ tự hay độ tin cậy.
Khởi tạo và liên kết (Binding)
Phương thức bind được sử dụng để tạo một socket mới và gắn nó với một địa chỉ IP cùng cổng cụ thể trên máy cục bộ.
use std::net::UdpSocket;
fn main() -> std::io::Result<()> {
// Liên kết socket với địa chỉ localhost tại cổng 8080
let socket = UdpSocket::bind("127.0.0.1:8080")?;
println!("Socket đã được tạo: {:?}", socket);
Ok(())
}
Bạn cũng có thể cung cấp một danh sách các địa chỉ. Rust sẽ thử liên kết với từng địa chỉ cho đến khi thành công.
use std::net::{SocketAddr, UdpSocket};
fn main() {
let targets = [
SocketAddr::from(([127, 0, 0, 1], 8080)),
SocketAddr::from(([127, 0, 0, 1], 8081)),
];
let server = UdpSocket::bind(&targets[..]).expect("Không thể bind địa chỉ");
println!("Đang lắng nghe tại: {:?}", server.local_addr().unwrap());
}
Thiết lập địa chỉ đích với connect
Mặc dù UDP không tạo kết nối thực sự, phương thức connect cho phép bạn thiết lập một địa chỉ mặc định cho socket. Sau khi gọi connect, bạn có thể sử dụng send và recv thay vì phải chỉ định địa chỉ mỗi lần.
use std::net::UdpSocket;
fn main() -> std::io::Result<()> {
let socket = UdpSocket::bind("0.0.0.0:0")?;
socket.connect("127.0.0.1:9000")?;
// Giờ đây có thể gọi socket.send() để gửi đến 127.0.0.1:9000
Ok(())
}
Kiểm tra lỗi bất đồng bộ
Phương thức take_error cho phép lấy ra lỗi cuối cùng xảy ra trên socket từ hàng đợi lỗi của hệ điều hành (ví dụ: lỗi ICMP khi cổng đích không tồn tại) và xóa nó sau khi đọc.
use std::net::UdpSocket;
fn main() {
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
socket.connect("127.0.0.1:1").unwrap(); // Cổng không tồn tại
let _ = socket.send(b"test data");
match socket.take_error() {
Ok(Some(err)) => println!("Lỗi phát hiện: {:?}", err),
Ok(None) => println!("Không có lỗi"),
Err(e) => println!("Lỗi hệ thống: {:?}", e),
}
}
Truyền và nhận dữ liệu
Gửi dữ liệu
send: Dùng khi socket đã đượcconnect.send_to: Gửi đến một địa chỉ cụ thể mà không cầnconnecttrước.
use std::net::UdpSocket;
fn main() -> std::io::Result<()> {
let socket = UdpSocket::bind("0.0.0.0:0")?;
let message = b"Hello from Rust";
// Gửi đến một địa chỉ đích cụ thể
let bytes_sent = socket.send_to(message, "127.0.0.1:8080")?;
println!("Đã gửi {} bytes", bytes_sent);
Ok(())
}
Nhận dữ liệu
Tương tự như gửi, việc nhận dữ liệu có hai phương thức chính là recv và recv_from. Các hàm này sẽ chặn (block) luồng thực thi cho đến khi nhận được dữ liệu.
use std::net::UdpSocket;
fn main() -> std::io::Result<()> {
let socket = UdpSocket::bind("127.0.0.1:8080")?;
let mut buffer = [0u8; 1024];
// Nhận dữ liệu và thông tin người gửi
let (amt, src_addr) = socket.recv_from(&mut buffer)?;
println!("Nhận được {} bytes từ {}", amt, src_addr);
println!("Nội dung: {:?}", &buffer[..amt]);
Ok(())
}
Sử dụng Peek để xem trước dữ liệu
Các phương thức peek và peek_from cho phép bạn đọc dữ liệu từ hàng đợi mà không xóa nó. Lần gọi recv tiếp theo vẫn sẽ nhận được dữ liệu cũ đó.
use std::net::UdpSocket;
fn main() -> std::io::Result<()> {
let socket = UdpSocket::bind("127.0.0.1:8080")?;
let mut buf = [0u8; 10];
// Xem trước mà không tiêu thụ dữ liệu trong buffer kernel
if let Ok((size, _)) = socket.peek_from(&mut buf) {
println!("Xem trước: {:?}", &buf[..size]);
}
// Dữ liệu vẫn còn đó để nhận thực sự
let (size, _) = socket.recv_from(&mut buf)?;
println!("Nhận thực tế: {:?}", &buf[..size]);
Ok(())
}
Cấu hình thời gian chờ (Timeout)
Bạn có thể thiết lập thời gian chờ để các hàm nhận/gửi không bị treo vĩnh viễn.
use std::net::UdpSocket;
use std::time::Duration;
fn main() {
let socket = UdpSocket::bind("0.0.0.0:8080").unwrap();
socket.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let mut buf = [0; 512];
match socket.recv_from(&mut buf) {
Ok((n, addr)) => println!("Nhận {} bytes từ {}", n, addr),
Err(e) => println!("Hết thời gian chờ hoặc có lỗi: {}", e),
}
}
Chế độ phát quảng bá (Broadcast)
Để gửi gói tin đến toàn bộ thiết bị trong cùng lớp mạng, bạn cần kích hoạt quyền set_broadcast.
use std::net::UdpSocket;
fn main() -> std::io::Result<()> {
let socket = UdpSocket::bind("0.0.0.0:0")?;
socket.set_broadcast(true)?;
// Gửi đến địa chỉ broadcast của lớp mạng
socket.send_to(b"Broadcast message", "255.255.255.255:9000")?;
Ok(())
}
Chế độ không chặn (Non-blocking)
Khi chuyển socket sang chế độ non-blocking, các hàm nhận dữ liệu sẽ trả về lỗi ngay lập tức nếu không có gói tin nào sẵn sàng thay vì dừng chương trình chờ đợi.
use std::net::UdpSocket;
use std::io::ErrorKind;
fn main() -> std::io::Result<()> {
let socket = UdpSocket::bind("0.0.0.0:8888")?;
socket.set_nonblocking(true)?;
let mut buf = [0u8; 1024];
loop {
match socket.recv_from(&mut buf) {
Ok((size, addr)) => {
println!("Nhận {} bytes từ {}", size, addr);
}
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
// Không có dữ liệu, thực hiện việc khác
println!("Chưa có dữ liệu, đang chờ...");
std::thread::sleep(std::time::Duration::from_millis(500));
}
Err(e) => return Err(e),
}
}
}