3

我想做的事:

...编写一个(1)个服务器/(N)个客户端(网络游戏)架构,它使用 UDP 套接字作为通信的底层基础。

  • 消息发送为,通过(crate)Vec<u8>编码bincode

  • 我还希望能够偶尔发送可能超过MTU~ 的典型最大值的数据报,1500 bytes并在接收端正确组装,包括发送ack-messages 等。(我假设我必须自己实现,对吧?)

对于UdpSocket我想过使用tokio的实现,也许framed。我不确定这是否是一个不错的选择,因为这似乎会引入一个不必要的映射步骤Vec<u8>(由 序列化bincode)到Vec<u8>(由 需要UdpCodectokio(?)

考虑这个最小的代码示例:

Cargo.toml (服务器)

bincode = "1.0"
futures = "0.1"
tokio-core = "^0.1"

Serde并且serde-derive用于shared定义协议的 crate 中!)

(我想尽快tokio-core更换tokio

fn main() -> () {
    let addr = format!("127.0.0.1:{port}", port = 8080);
    let addr = addr.parse::<SocketAddr>().expect(&format!("Couldn't create valid SocketAddress out of {}", addr));

    let mut core = Core::new().unwrap();
    let handle = core.handle();
    let socket = UdpSocket::bind(&addr, &handle).expect(&format!("Couldn't bind socket to address {}", addr));


    let udp_future = socket.framed(MyCodec {}).for_each(|(addr, data)| {
        socket.send_to(&data, &addr); // Just echo back the data
        Ok(())
    });

    core.run(udp_future).unwrap();
}

struct MyCodec;

impl UdpCodec for MyCodec {
    type In = (SocketAddr, Vec<u8>);
    type Out = (SocketAddr, Vec<u8>);

    fn decode(&mut self, src: &SocketAddr, buf: &[u8]) -> io::Result<Self::In> {
        Ok((*src, buf.to_vec()))
    }

    fn encode(&mut self, msg: Self::Out, buf: &mut Vec<u8>) -> SocketAddr {
        let (addr, mut data) = msg;
        buf.append(&mut data);
        addr
    }
}

这里的问题是:

let udp_future = socket.framed(MyCodec {}).for_each(|(addr, data)| { | ------ 移到这里的值 ^^^^^^^^^^^^^ 之后捕获到这里的值move | = 注意: move 发生是因为socket有 type tokio_core::net::UdpSocket,它没有实现Copytrait

该错误完全有道理,但我不确定如何创建如此简单的回显服务。实际上,消息的处理涉及到更多的逻辑,但为了一个最小的例子,这应该足以给出一个粗略的想法。

我的解决方法是一个丑陋的黑客:创建第二个套接字。

4

1 回答 1

2

UdpSocket::framed这是来自 Tokio 文档的签名:

pub fn framed<C: UdpCodec>(self, codec: C) -> UdpFramed<C>

请注意,它需要self,而不是&self;也就是说,调用这个函数会消耗套接字。当UdpFramed您调用它时,包装器拥有底层套接字。您的编译错误告诉您,socket当您调用此方法时您正在移动,但您也试图socket在闭包内部借用(调用send_to)。

这可能不是您想要的真实代码。使用的全部framed()目的是将您的套接字变成更高级别的东西,因此您可以直接发送编解码器的项目,而不必组装数据报。使用sendsend_to直接在套接字上可能会破坏消息协议的框架。在这段代码中,您尝试实现一个简单的回显服务器,您根本不需要使用framed。但是,如果您确实想拥有自己的蛋糕并吃掉它并同时使用它们framedsend_to幸运的是,UdpFramed您仍然可以借用. 您可以通过以下方式解决您的问题:UdpSocketget_ref

let framed = {
    let socket = UdpSocket::bind(&addr, &handle).expect(&format!("Couldn't bind socket to address {}", addr));
    socket.framed(MyCodec {})
}

let udp_future = framed.for_each(|(addr, data)| {
    info!(self.logger, "Udp packet received from {}: length: {}", addr, data.len());
    framed.get_ref().send_to(&data, &addr); // Just echo back the data

    Ok(())
});

我没有检查过这段代码,因为(正如 Shepmaster 正确指出的那样)你的代码片段还有其他问题,但无论如何它应该会给你这个想法。我将重复我之前的警告:如果您在实际代码中执行此操作,它将破坏您正在使用的网络协议。get_ref的文档是这样写的:

请注意,应注意不要篡改传入的底层数据流,因为它可能会破坏正在处理的帧流。


要回答您问题的新部分:是的,您需要自己处理重组,这意味着您的编解码器确实需要对您发送的字节进行一些帧处理。通常,这可能涉及Vec<u8>. 开始序列让您在数据包丢失后识别下一条消息的开始(这在 UDP 中经常发生)。如果 中没有不能出现的字节序列,则Vec<u8>需要在它出现时对其进行转义。然后您可以发送消息的长度,然后是数据本身;或者只是数据,后跟一个结束序列和一个校验和,这样你就知道没有丢失。这些设计有利有弊,这本身就是一个很大的话题。

您还需要UdpCodec包含数据:从SocketAddr到当前正在进行的部分重组消息的映射。在decode中,如果您收到消息的开头,请将其复制到地图中并返回Ok。如果给你消息的中间,并且你已经在 map 中有消息的开头(为此SocketAddr),将缓冲区附加到现有缓冲区并返回Ok。当您到达消息的末尾时,返回整个内容并清空缓冲区。为了启用此用例而UdpCodec采取的方法。&mut self注意理论上,您还应该处理乱序到达的数据包,但这在现实世界中实际上很少见。)

encode简单得多:您只需要添加相同的框架并将消息复制到缓冲区中。

让我在这里重申,您不需要也不应该在调用底层套接字后framed()使用它。UdpFramed既是源又是接收器,因此您也可以使用该对象来发送回复。你甚至可以使用它split()来获得分离StreamSink实现,如果这使得你的应用程序中的所有权更容易。


总的来说,现在我已经看到了你正在努力解决的问题,我建议只使用几个 TCP 套接字而不是 UDP。如果您想要一个面向连接的可靠协议,TCP 已经存在并且可以为您做到这一点。很容易花大量时间在 UDP 之上创建一个“可靠”层,它比 TCP 慢且不可靠。

于 2018-05-14T09:31:21.340 回答