今天,我们先来看一看如何用 Rust 实现基于 UDP 协议的 Echo 服务器和客户端,然后通过微调代码,来验证几个有趣的有关 UDP 协议的细节问题。
UDP Echo 服务器
UDP Echo 服务器的主要代码片段(完整的代码在文末)如下所示。
fn main() {
// ...
let address = "127.0.0.1:1234";
serve(address).unwrap_or_else(|e| error!("{}", e));
}
pub fn serve(address: &str) -> Result<(), failure::Error> {
let server_socket = UdpSocket::bind(address)?;
loop {
let mut buffer = [0u8; 1024];
let (size, src) = server_socket.recv_from(&mut buffer)?;
debug!("Handling data from {}", src);
print!("{}", str::from_utf8(&buffer[..size])?);
server_socket.send_to(&buffer, src)?;
}
}
复制代码
相较于 TCP Echo 服务器(参考用Rust实现TCP Echo服务器),UDP Echo 服务器要简单不少,不但没有listen()
和accept()
之类的系统调用,也不需要为每个客户端都创建一个新线程。数据通过*所有客户端共享的?<sup>(待确认)</sup>*套接字server_socket
到达 UDP Echo 服务器后,服务器就将数据再通过这个套接字原样返回,仅此而已。
我们可以先用 telnet 来测试一下 UDP Echo 服务器。
$ cargo run
$ telnet 127.0.0.1 1234
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello
hello
复制代码
输入了hello
,也返回了hello
,一切正常。
UDP Echo 客户端
接下来,我们再来看看 UDP Echo 客户端的代码片段。
fn main() {
// ...
let address = "127.0.0.1:1234";
send_to(address).unwrap_or_else(|e| error!("{}", e));
}
pub fn send_to(address: &str) -> Result<(), failure::Error> {
let socket = UdpSocket::bind("127.0.0.1:0")?;
loop {
let mut input = String::new();
io::stdin().read_line(&mut input)?;
socket.send_to(input.as_bytes(), address)?;
let mut buffer = [0u8; 1024];
socket.recv_from(&mut buffer).expect("failed to receive");
print!("{}", str::from_utf8(&buffer).expect("failed to convertto String"));
}
}
复制代码
客户端的逻辑同样非常简单,只是绑定端口0
(0
表示让操作系统提供一个未使用的端口号),然后发送数据,最后读取服务器返回的数据。 与 TCP Echo 客户端(参考用Rust实现TCP Echo客户端)最大的不同点在于,UDP 在发送数据之前既无须建立连接,也不会检查服务端是否存在(如果服务端不存在会发生什么呢?)。
让我们再来运行一下 UDP Echo 客户端。这里有一个小技巧,可以把客户端的代码放到src/bin/client.rs
中,这样就可以使用cargo run --bin efern
来运行客户端程序了。
$ cargo run --bin client
Compiling udp-echo v0.1.0 (/Users/huyi/Projects/z-huyi/rust/udp-echo)
Finished dev [unoptimized + debuginfo] target(s) in 2.48s
Running `target/debug/client`
hello
hello
复制代码
很好,一切正常。
有关 UDP 协议的细节问题
UDP Echo 服务器和客户端的代码看起来很简单,但其中隐藏了一些有趣的问题,如
如果客户端发送的数据量大于服务端中缓冲区(let mut buffer = [0u8; 1024];
)的大小会发生什么?
客户端一次最多能够发送多少数据?发送的数据量有上限吗?
如果服务端不存在或未启动会发生什么呢?客户端能感知到数据没有成功发送吗?
下面我们通过微调代码来看看这几个问题的答案。
Q1. 如果客户端发送的数据量大于服务端中缓冲区的大小会发生什么?
为了回答这个问题,我们只需减小服务端的缓冲区的大小,然后重新编译运行服务端。
pub fn serve(address: &str) -> Result<(), failure::Error> {
let server_socket = UdpSocket::bind(address)?;
loop {
// let mut buffer = [0u8; 1024];
let mut buffer = [0u8; 10];
// ...
复制代码
$ cargo run
error: `cargo run` could not determine which binary to run. Use the `--bin` option to specify a binary, or the `default-run` manifest key.
available binaries: client, udp-echo
$ cargo run --bin udp-echo
复制代码
注意,由于我们在src/bin/
中创建了客户端的源文件,此时cargo
就不知道要运行的是哪个程序了,所以需要通过--bin udp-echo
(udp-echo
是执行cargo new
时指定的名字)指明要运行的是服务端的程序。
从下面客户端的输出可以看出,服务端只是简单丢弃了第 10 个字符(字节)之后的数据,并没有产生任何错误。
$ cargo run --bin client
# ...
hello world!
hello worl
复制代码
recv_from()
方法的注释也指出,丢弃超过缓冲区大小的数据是预期的行为:
Receives a single datagram message on the socket. On success, returns the number of bytes read and the origin.
The function must be called with valid byte array buf of sufficient size to hold the message bytes. If a message is too long to fit in the supplied buffer, excess bytes may be discarded.
那客户端一次能够发送的数据量有上限吗?
Q2. UDP 客户端一次最多能够发送多少数据?
我们编写一个模拟通过 UDP 发送大量数据的函数,验证一下发送的数据量是不是存在上限。
fn main() {
// ...
let address = "127.0.0.1:1234";
// send_to(address).unwrap_or_else(|e| error!("{}", e));
for i in 10..=20 {
send_nbytes_to(address, 1 << i).unwrap_or_else(|e| error!("{}", e));
}
}
pub fn send_to(address: &str) -> Result<(), failure::Error> {
// ...
}
pub fn send_nbytes_to(address: &str, nbytes: usize) -> Result<(), failure::Error> {
let socket = UdpSocket::bind("127.0.0.1:0")?;
let input = "x".to_string().repeat(nbytes);
debug!("Sending {} bytes to {}", input.len(), address);
socket.send_to(input.as_bytes(), address).expect("couldn't send data");
return Ok(());
}
复制代码
cargo run --bin client
# ...
[2023-10-25T07:30:31Z DEBUG client] Sending 1024 bytes to 127.0.0.1:1234
[2023-10-25T07:30:31Z DEBUG client] Sending 2048 bytes to 127.0.0.1:1234
[2023-10-25T07:30:31Z DEBUG client] Sending 4096 bytes to 127.0.0.1:1234
[2023-10-25T07:30:31Z DEBUG client] Sending 8192 bytes to 127.0.0.1:1234
[2023-10-25T07:30:31Z DEBUG client] Sending 16384 bytes to 127.0.0.1:1234
thread 'main' panicked at 'couldn't send data: Os { code: 40, kind: Uncategorized, message: "Message too long" }', src/bin/client.rs:34:47
复制代码
可以看到,在 macOS 上运行时,当待发送的数据达到 16384 字节时就报错了。这与 macOS 上的如下配置有关(参考https://stackoverflow.com/questions/22819214/udp-message-too-long)。
$ sysctl -a | fgrep net.inet.udp.maxdgram
net.inet.udp.maxdgram: 9216
复制代码
显然,16384 大于 9216。但在 Linux 上运行 UDP Echo 客户端,情况又有所不同了。
$ uname -a
Linux ubuntu-bionic 4.15.0-121-generic #123-Ubuntu SMP Mon Oct 5 16:16:40 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
$ cargo run --bin client
# ...
[2023-10-25T10:36:43Z DEBUG client] Sending 1024 bytes to 127.0.0.1:1234
[2023-10-25T10:36:43Z DEBUG client] Sending 2048 bytes to 127.0.0.1:1234
[2023-10-25T10:36:43Z DEBUG client] Sending 4096 bytes to 127.0.0.1:1234
[2023-10-25T10:36:43Z DEBUG client] Sending 8192 bytes to 127.0.0.1:1234
[2023-10-25T10:36:43Z DEBUG client] Sending 16384 bytes to 127.0.0.1:1234
[2023-10-25T10:36:43Z DEBUG client] Sending 32768 bytes to 127.0.0.1:1234
[2023-10-25T10:36:43Z DEBUG client] Sending 65536 bytes to 127.0.0.1:1234
thread 'main' panicked at src/bin/client.rs:34:47:
couldn't send data: Os { code: 90, kind: Uncategorized, message: "Message too long" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
复制代码
但如果改成65507
又是可以发送成功的,
$ cargo run --bin client
# ...
[2023-10-25T10:44:22Z DEBUG client] Sending 65507 bytes to 127.0.0.1:1234
[2023-10-25T10:44:22Z DEBUG client] Sending 65508 bytes to 127.0.0.1:1234
thread 'main' panicked at src/bin/client.rs:35:47:
couldn't send data: Os { code: 90, kind: Uncategorized, message: "Message too long" }
复制代码
那么问题来了,在 Linux 中,UDP 包(数据报)的大小上限到底是如何决定的?
如果你不想太过关注细节,那么答案是因为承载 UDP 数据的 IP 数据包的最大长度为 65535 字节,再减去 IP 数据包的首部和 UDP 数据包(数据报)的首部,就只剩下 65507 了。
好了,细节来了。
UDP 不同于 TCP,没有分割来自上层(应用层)的数据(示例代码中的若干个字符'x')的机制。UDP 会把接收到的数据原样传递到下一层的 IP 层。而 IP 数据包的大小是有上限的,这是因为在 IP 数据包首部中,表示数据包总长度(首部长度+数据长度,单位字节)的字段只有 16 比特,16 比特所能表示的最大数值是 2^16 - 1 = 65535。
因此,UDP 的最大负载(实际数据)大小的计算方法是:
UDP 的最大负载 = IP 数据包的最大长度 -(IP 首部大小(无选项))-(UDP 首部大小)= 65535 - 20 - 8= 65507
其实,IP 数据包也并不是总能够以最大包长 65535 传输的,这是因为 IP 的下层数据链路层具有最大传输单元 MTU 这个属性。不同类型的数据链路的使用目的不同,可承载的 MTU 也就不同,例如以太网的默认 MTU 是 1500 字节,而 FDDI 是 4352 字节。
为了能够发送超过数据链路层 MTU 的数据,IP 提供了称为 IP fragmentation 的数据分割机制。IP 需要根据数据链路层的 MTU 来分割 IP 数据包(类似点外卖时,有些大菜要分成多个打包盒装),并对分割后的小包进行编号。当这些小包到达目的主机后,目的主机需要将这些小包按照序号重新连接在一起,重新组装成完整的 IP 数据包。
总之,使用 UDP 时,发送的数据量存在上限。
UDP 客户端能否感知 UDP 服务端不存在呢?
要回答这个问题,最简单的方法还是修改 UDP 客户端的代码,只需要修改服务端的地址即可。
fn main() {
// ...
// let address = "127.0.0.1:1234";
let address = "127.0.0.1:1235";
// ...
复制代码
$ cargo run --bin client
...
message
# (no echo)
复制代码
可以看到,即使经过长时间等待,输入的message
也没有原样输出,但也没有报错。要弄清楚按下回车键后到底发生了什么,需要使用 Wireshark。
我们先启动 Wireshark,并输入udp.port == 1235
作为过滤条件,然后再次启动 UDP Echo 客户端并输入message
。可以看到,伴随着 UDP 的数据包,还有一个 ICMP 的数据包,提示“Destination unreachable (Port unreachable)”。
udp icmp wireshark
也就是说,理论上可以通过捕获这个 ICMP 的包来判断服务端是否可达。但在 Rust 中如何实现,还需要进一步研究(recv_from()
是阻塞的,也没法通过它的返回值判断)。
好了,现在我们知道了如何用 Rust 实现 UDP Echo 服务器和客户端,也了解了有关 UDP 协议的一些细节。
附
Cargo.toml
[package]
name = "udp-echo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4"
env_logger = "0.6.1"
failure = "0.1.5"
复制代码
main.rs UDP Echo 服务器
#[macro_use]
extern crate log;
use std::env;
use std::net::UdpSocket;
use std::str;
fn main() {
env::set_var("RUST_LOG", "debug");
env_logger::init();
let address = "127.0.0.1:1234";
serve(address).unwrap_or_else(|e| error!("{}", e));
}
pub fn serve(address: &str) -> Result<(), failure::Error> {
let server_socket = UdpSocket::bind(address)?;
loop {
let mut buffer = [0u8; 10];
let (size, src) = server_socket.recv_from(&mut buffer)?;
debug!("Handling data from {}", src);
print!("{}", str::from_utf8(&buffer[..size])?);
server_socket.send_to(&buffer, src)?;
}
}
复制代码
src/bin/client.rs UDP Echo 客户端
#[macro_use]
extern crate log;
use std::env;
use std::net::UdpSocket;
use std::{io, str};
fn main() {
env::set_var("RUST_LOG", "debug");
env_logger::init();
let address = "127.0.0.1:1235";
send_to(address).unwrap_or_else(|e| error!("{}", e));
// for i in 10..=20 {
// send_nbytes_to(address, 1 << i).unwrap_or_else(|e| error!("{}", e));
// }
}
pub fn send_to(address: &str) -> Result<(), failure::Error> {
let socket = UdpSocket::bind("127.0.0.1:0")?;
loop {
let mut input = String::new();
io::stdin().read_line(&mut input)?;
socket.send_to(input.as_bytes(), address)?;
let mut buffer = [0u8; 1024];
socket.recv_from(&mut buffer).expect("failed to receive");
print!("{}", str::from_utf8(&buffer).expect("failed to convertto String"));
}
}
pub fn send_nbytes_to(address: &str, nbytes: usize) -> Result<(), failure::Error> {
let socket = UdpSocket::bind("127.0.0.1:0")?;
let input = "x".to_string().repeat(nbytes);
debug!("Sending {} bytes to {}", input.len(), address);
socket.send_to(input.as_bytes(), address).expect("couldn't send data");
return Ok(());
}
复制代码
评论