如何使用tokio的UdpSocket来处理1台服务器中的消息:N个客户端设置?

时间:2018-05-13 20:18:39

标签: rust udp closures future rust-tokio

我想做什么:

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

  • 讯息以Vec<u8>的形式发送,通过bincode(crate)编码

  • 我还希望能够偶尔发送可能超过〜MTU的典型最大1500 bytes的数据报,并在接收端正确汇编,包括发送ack -messages等 (我想我必须自己实现,对吗?)

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

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

Cargo.toml(服务器)

tokio

bincode = "1.0" futures = "0.1" tokio-core = "^0.1" Serde用于定义协议的serde-derive箱中!)

(我想将shared替换为tokio-core asap)

tokio

这里的问题是:

  

让udp_future = socket.framed(MyCodec {})。for_each(|(addr,data)| {       | ------搬到这里的价值^^^^^^^^^^^^^^搬家后的价值       |       = note:移动发生,因为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 } } 的类型为socket,但未实现tokio_core::net::UdpSocket特征

这个错误很有意义,但我不确定如何创建这样一个简单的echo-service。实际上,消息的处理涉及更多的逻辑,但是为了一个最小的例子,这应该足以给出一个粗略的想法。

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

1 个答案:

答案 0 :(得分:2)

以下是Tokio的文档中UdpSocket::framed的签名:

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

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

这可能不是你想要的真正代码。使用framed()的重点是将套接字转换为更高级别的套接字,这样您就可以直接发送编解码器的项目,而不必组装数据报。直接在套接字上使用sendsend_to可能会破坏邮件协议的框架。在此代码中,您尝试实施简单的回显服务器,根本不需要使用framed。但是,如果你确实希望拥有你的蛋糕并使用它并同时使用framedsend_to,幸运的是UdpFramed仍允许你借用基础的{{1}使用get_ref。您可以通过这种方式解决问题:

UdpSocket

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

  

请注意,应注意不要篡改进入的基础数据流,因为它可能会破坏正在使用的帧流。

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

您还需要Vec<u8>包含数据:从UdpCodec到当前正在进行的部分重新组合的消息的地图。在SocketAddr中,如果您收到消息的开头,请将其复制到地图中并返回decode。如果给出了消息的中间部分,并且您已经在地图中开始了消息(对于Ok),请将缓冲区附加到现有缓冲区并返回SocketAddr。当您到达消息的末尾时,返回整个内容并清空缓冲区。 Ok上的方法采用UdpCodec以启用此用例。 ( NB 理论上,您还应该处理无序到达的数据包,但这在现实世界中实际上非常罕见。)

&mut self更加简单:您只需要添加相同的框架并将消息复制到缓冲区中。

我在此重申,在调用encode之后,您不需要也不应该使用底层套接字。 framed()既是源也是接收器,因此您也可以使用该对象发送回复。您甚至可以使用UdpFramed来实现单独的split()Stream实施,如果这样可以让您的应用更轻松。

总的来说,现在我已经看到了你正在努力解决的问题,我建议只使用几个TCP套接字而不是UDP。如果您需要面向连接的可靠协议,TCP已经存在并为您完成。花很多时间制作一个可靠的&#34;非常容易。在UDP之上的层,它比TCP更慢且更不可靠。