上一章我们成功的将客户端连接到元宇宙服务器中,并实现了最简单的 登录协议 —— 在 http/https 头中附加客户端获得的认证 token,也就是 agent 服务支持 内部的 rpc 协议:
#[derive(Debug, Clone)]
pub enum AgentRequest {
Login(String, crossbeam_channel::Sender<AgentMsg>),
TokenMsg(String, AgentMsg),
IDMsg(u32, AgentMsg),
Notify(NotifyType)
}
复制代码
Login 协议附带了 Websocket 接收数据的 Sender,这样以后服务器的数据可以通过这个 Sender 发送到 保持这个 Websocket 的异步线程,并转发到连接上来的客户端,上一章包含了简单的转发代码。
客户端连接上服务器之后,有下面几种状态,适用不同的消息类型。
一个最典型的 TokenMsg 是创建角色,为说明概念,我们这里仅设定角色 昵称,创建角色,随机设置位置,并让角色进入 0 号世界中,创建角色的代码如下:
pub fn create(&mut self, token: &str, nick: &str)-> u32 {
let pos = Position{x: (rand::random::<i32>() % 1024) as f32, y: (rand::random::<i32>() % 1024) as f32};
let id = self.agents.add(token.into(), Player{nick: nick.into(), world: 0, pos});
self.save(token);
id
}
复制代码
加载和存储的代码我们已经在 12 章列出。
为了让客户端可以创建角色,我们扩充 AgentMsg 消息如下:
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum AgentMsg {
LoginOk(u32, Player),
LoginFail(String),
Create(String),
Notify(NotifyType)
}
复制代码
Create 使用昵称来创建角色,Notify 通知 客户端元宇宙的变化。
我们在客户端增加下面的代码实现命令行的处理:
let mut input = String::new();
stdin().read_line(&mut input).unwrap();
let words: Vec<&str> = input.split_ascii_whitespace().collect();
if words[0].eq_ignore_ascii_case("create") {
game.send(&AgentMsg::Create(words[1].into()));
}
复制代码
将输入根据分隔符拆分为 &str,并使用 collect 收集为一个数组,可以看到 Rust 使用函数编程模式之后表达效率的极大提升,一行代码可以实现 C/C++需要数十行代码完成到功能。
let words: Vec<&str> = input.split_ascii_whitespace().collect();
复制代码
这样在客户端执行
create 就可以 将创建角色的协议发送到服务器端。
我们在服务端维持一个 token_id 的 tuple 保持 客户端的 状态
let mut token_id = (None, None);
msg = socket.recv() => {
match msg {
Some(Ok(Message::Binary(buf)))=> {
let mut de = rmp_serde::Deserializer::from_read_ref(&buf);
let agent_msg: AgentMsg = Deserialize::deserialize(&mut de).unwrap();
if let Some(id) = token_id.1 {
rpc::run(&agent_tx, AgentRequest::IDMsg(id, agent_msg));
} else {
rpc::run(&agent_tx, AgentRequest::TokenMsg(token_id.0.clone().unwrap(), agent_msg));
}
},
Some(Err(e))=> {
println!("error -> {:?}", e);
break;
},
_=> {}
}
}
复制代码
服务器保持 客户端 Websocket 的线程,从客户端接受到二进制的消息之后,首先使用 messagepack 解码,如果没有获得 id,则使用 AgentRequest::TokenMsg 转发这个客户端消息到 agent 服务进行处理,如果已经获得了 id,则使用 AgentRequest::IDMsg 转发到 agent 服务处理。
服务器 agent 服务相关处理的代码如下:
AgentRequest::Login(token, _tx)=> {
if let Some((id, player)) = self.load(&token) {
super::rpc::run(world, WorldRequest::Enter(id, player.pos.clone()));
_tx.send(AgentMsg::LoginOk(id, player));
self.id_txs.insert(id, _tx);
} else {
_tx.send(AgentMsg::LoginFail("token do not exist".into()));
self.token_txs.insert(token.clone(), _tx);
}
},
AgentRequest::TokenMsg(token, msg)=> {
match msg {
AgentMsg::Create(nick)=> {
let id = self.create(&token, &nick);
if let Some(tx) = self.token_txs.remove(&token) {
tx.send(AgentMsg::LoginOk(id, self.agents[id].clone().unwrap())).unwrap();
self.id_txs.insert(id, tx);
}
},
_=> {}
}
},
复制代码
如果是登录消息,获得 ID 或者 token 并将 发送消息到客户端的 tx 存放到 token 的 HashMap 或者 ID 的 BTreeMap 中。
如果是创建角色,则将创建后的 id 和角色信息发送到 客户端。
评论