一个简单的 UDP 服务器和客户端

UDP 服务器和我们之前写的 TCP 服务器之间存在一些语义差异。 与 TCP 不同,UDP 没有流结构。这源于两个协议之间的语义差异。我们来看看 UDP 服务器的是什么:

// code/chapter3/udp-echo-server.rs

use std::net::UdpSocket;
use std::thread;

fn main() {
   let socket = UdpSocket::bind("0.0.0.0:8888").expect("Could not bind socket");

   loop {
       let mut buf = [0u8; 1500];
       let sock = socket.try_clone().expect("Failed to clone socket");

       match socket.recv_from(&mut buf) {
           Ok((_, src)) => {
               thread::spawn(move || {
                   println!("Handling connection from {}", src);
                   sock.send_to(&buf, &src).expect("Failed to send response");
               });
           }
           Err(e) => eprintln!("couldn't recieve a datagram: {}", e),
       }
   }
}

与 TCP 一样,我们从绑定到给定端口上的本地地址开始,我们处理绑定可能失败的可能性。由于 UDP 是无连接协议,因此我们不需要使用滑动窗口来读取所有数据。因此,我们可以只分配给定大小的静态缓冲区。动态检测底层网卡的 MTU 并将缓冲区大小设置为更好的想法,因为这是每个 UDP 数据包可以具有的最大大小。但是,由于普通 LAN 的 MTU 大约为1,500,因此我们可以在这里分配这个大小的缓冲区。 try_clone 方法克隆给定的套接字并返回一个新的套接字,该套接字被移入闭包中。

然后我们从套接字读取,它返回数据读取的长度和 Ok() 情况下的源。然后我们生成一个新线程,在其中我们将相同的缓冲区写回给定的套接字。对于任何可能失败的事情,我们需要像处理 TCP 服务器一样处理错误。

与此服务器交互与上次使用 nc 完全相同。唯一的区别是,在这种情况下,我们需要传递 -u 来强制 nc 使其仅使用 UDP 。看一下下面的例子:

$ nc -u 127.0.0.1 8888 
test 
test 
test 
test 
^C

现在,让我们编写一个简单的 UDP 客户端来实现相同的结果。正如我们将看到的,TCP 服务器与此之间存在一些细微差别:

// code/chapter3/udp-client.rs

use std::net::UdpSocket;
use std::{io, str};

fn main() {
    let socket = UdpSocket::bind("127.0.0.1:8000").expect("Could not bind client socket");
    socket
        .connect("127.0.0.1:8888")
        .expect("Could not connect to server");

    loop {
        let mut input = String::new();
        let mut buffer = [0u8; 150000];
        io::stdin().read_line(&mut input).expect("Failed to read ");
        socket
            .send(input.as_bytes())
            .expect("Failed to write to server");
        socket
            .recv_from(&mut buffer)
            .expect("Could not read into buffer");
        print!(
            "{}",
            str::from_utf8(&buffer).expect("Could not write buffer as string")
        );
    }
}

这个基本客户端和我们在上一节中看到的 TCP 客户端之间存在重大差异。在这种情况下,在连接到服务器之前首先绑定到客户端套接字是绝对必要的。 完成后,示例的其余部分基本相同。在客户端和服务器端运行它会产生类似于 TCP 情况的类似结果。这是服务器端的会话:

$ rustc udp-echo-server.rs && ./udp-echo-server 
Handling connection from 127.0.0.1:8000 
Handling connection from 127.0.0.1:8000 
Handling connection from 127.0.0.1:8000 
^C

客户端:

$ rustc udp-client.rs && ./udp-client 
test 
test 
foo 
foo 
bar
bar 
^C 

UDP 组播

UdpSocket 中有许多 TCP 没有的方法。其中最有趣的是多播和广播。让我们用一个示例看一下多播如何在服务器和客户端工作的。 对于此示例,我们将客户端和服务器组合在一个文件中。在 main函数中,我们将检查是否已传递CLI参数。如果有,我们将运行客户端;否则,我们将运行服务器。请注意,不会使用参数的值;它将被视为一个布尔值:

// code/chapter3/udp-multicast.rs


use std::{env, str};
use std::net::{UdpSocket, Ipv4Addr};

fn main() {
    let mcast_group: Ipv4Addr = "233.0.0.1".parse().unwrap();
    let port: u16 = 6000;
    let any = "0.0.0.0".parse().unwrap();
    let mut buffer = [0u8; 1600];
    if env::args().count() > 1 {
        // client case
        let socket = UdpSocket::bind((any, port)).expect("Could not bind client socket");
        socket.join_multicast_v4(&mcast_group, &any)
            .expect("Could not join multicast group");
        socket.recv_from(&mut buffer).expect("Failed to write to server");
        print!("{}", str::from_utf8(&buffer).expect("Could not write buffer as string"));

    } else {
        // server case
        let socket = UdpSocket::bind((any, 0))
            .expect("Could not write buffer as string");
        socket.send_to("Hello, world!".as_bytes(), &(mcast_group, port)).expect("Failed to write data");
    }
}

这里的客户端和服务器部分大致类似于我们之前讨论过的内容。一个区别是 join_multicast_v4 调用使当前套接字加入一个传递了地址的多播组。对于服务器和客户端,我们在绑定时不指定单个地址。 相反,我们使用表示任何可用地址的特殊地址 0.0.0.0 。这相当于将 INADDR_ANY 传递给基础 setsockopt 调用。在服务器的情况下,我们将其发送到多播组。运行这个有点棘手。由于无法在标准库中设置 SO_REUSEADDRSO_REUSEPORT ,因此我们需要在多台不同的机器上运行客户机,在另一台机器上运行服务器。为此,所有这些都需要在同一网络中,并且多播组的地址需要是有效的多播地址(前四位应为1110)。 UdpSocket类 型还支持离开多播组,广播等。请注意,广播对 TCP 没有意义,因为根据定义它是两个主机之间的连接。

运行上一个示例很简单;在一台主机上,我们将运行服务器,另一台运行客户端。 鉴于此设置,输出应在服务器端看起来像这样:

$ rustc udp-multicast.rs && ./udp-multicast server 
Hello world!