我想做什么:
...编写(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。实际上,消息的处理涉及更多的逻辑,但是为了一个最小的例子,这应该足以给出一个粗略的想法。
我的解决方法是一个丑陋的黑客:创建第二个套接字。
答案 0 :(得分:2)
以下是Tokio的文档中UdpSocket::framed
的签名:
pub fn framed<C: UdpCodec>(self, codec: C) -> UdpFramed<C>
请注意,它需要self
,而不是&self
;也就是说,调用此函数会占用套接字。调用它时,UdpFramed
包装器拥有底层套接字。您的编辑错误告诉您,当您调用此方法时,您正在移动socket
,但您还试图在关闭内部借用socket
(致电send_to
)。
这可能不是你想要的真正代码。使用framed()
的重点是将套接字转换为更高级别的套接字,这样您就可以直接发送编解码器的项目,而不必组装数据报。直接在套接字上使用send
或send_to
可能会破坏邮件协议的框架。在此代码中,您尝试实施简单的回显服务器,根本不需要使用framed
。但是,如果你确实希望拥有你的蛋糕并使用它并同时使用framed
和send_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更慢且更不可靠。