用Rust实现UDP Echo服务器和客户端
今天,我们先来看一看如何用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()
之类的系统调用,也不需要为每个客户端都创建一个新线程。数据通过所有客户端共享的?(待确认)套接字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)”。
也就是说,理论上可以通过捕获这个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(());
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。