use parking_lot::Mutex;
use thiserror::Error;
use crate::gateway::{GatewayError, GatewayTransport};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")]
pub enum GatewayCommand {
SendToClient {
client_id: String,
message: String,
},
SendToAll {
message: String,
},
SendToGroup {
group: String,
message: String,
},
CloseClient {
client_id: String,
},
}
impl GatewayCommand {
fn to_json(&self) -> Result<String, GatewayError> {
serde_json::to_string(self)
.map_err(|e| GatewayError::Serialize(format!("GatewayCommand 序列化失败: {e}")))
}
pub fn from_json(json: &str) -> Result<Self, GatewayError> {
serde_json::from_str(json)
.map_err(|e| GatewayError::Serialize(format!("GatewayCommand 反序列化失败: {e}")))
}
}
#[derive(Debug, Clone)]
pub struct RedisGatewayConfig {
pub redis_url: String,
pub key_prefix: String,
}
impl Default for RedisGatewayConfig {
fn default() -> Self {
Self {
redis_url: "redis://127.0.0.1:6379".to_string(),
key_prefix: "sz-rust".to_string(),
}
}
}
impl RedisGatewayConfig {
pub fn new(redis_url: impl Into<String>) -> Self {
Self {
redis_url: redis_url.into(),
key_prefix: "sz-rust".to_string(),
}
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.key_prefix = prefix.into();
self
}
fn online_key(&self) -> String {
format!("{}:online", self.key_prefix)
}
fn client_groups_key(&self, client_id: &str) -> String {
format!("{}:cgroups:{}", self.key_prefix, client_id)
}
fn group_key(&self, group: &str) -> String {
format!("{}:group:{}", self.key_prefix, group)
}
fn channel_all(&self) -> String {
format!("{}:ch:all", self.key_prefix)
}
fn channel_group(&self, group: &str) -> String {
format!("{}:ch:group:{}", self.key_prefix, group)
}
fn channel_client(&self, client_id: &str) -> String {
format!("{}:ch:client:{}", self.key_prefix, client_id)
}
fn channel_close(&self, client_id: &str) -> String {
format!("{}:ch:close:{}", self.key_prefix, client_id)
}
}
pub struct RedisGatewayTransport {
conn: Mutex<redis::Connection>,
config: RedisGatewayConfig,
}
impl RedisGatewayTransport {
pub fn connect(config: RedisGatewayConfig) -> Result<Self, GatewayError> {
let client = redis::Client::open(config.redis_url.as_str())
.map_err(|e| GatewayError::Transport(format!("Redis Client 创建失败: {e}")))?;
let conn = client
.get_connection()
.map_err(|e| GatewayError::Transport(format!("Redis 连接失败: {e}")))?;
Ok(Self {
conn: Mutex::new(conn),
config,
})
}
pub fn from_client(
client: redis::Client,
config: RedisGatewayConfig,
) -> Result<Self, GatewayError> {
let conn = client
.get_connection()
.map_err(|e| GatewayError::Transport(format!("Redis 连接失败: {e}")))?;
Ok(Self {
conn: Mutex::new(conn),
config,
})
}
pub fn register_client(&self, client_id: &str) -> Result<(), GatewayError> {
if client_id.is_empty() {
return Err(GatewayError::InvalidClientId(client_id.to_string()));
}
let mut conn = self.conn.lock();
redis::cmd("SADD")
.arg(self.config.online_key())
.arg(client_id)
.query::<()>(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("Redis 写入失败: {e}")))?;
Ok(())
}
pub fn unregister_client(&self, client_id: &str) -> Result<(), GatewayError> {
if client_id.is_empty() {
return Err(GatewayError::InvalidClientId(client_id.to_string()));
}
let mut conn = self.conn.lock();
let mut pipe = redis::pipe();
pipe.atomic()
.cmd("SREM")
.arg(self.config.online_key())
.arg(client_id)
.ignore()
.cmd("SMEMBERS")
.arg(self.config.client_groups_key(client_id));
let (removed, groups): (i64, Vec<String>) = pipe
.query(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("注销客户端查询群组失败: {e}")))?;
if removed == 0 {
return Err(GatewayError::ClientNotFound(client_id.to_string()));
}
let mut cleanup_pipe = redis::pipe();
cleanup_pipe.atomic();
cleanup_pipe
.cmd("DEL")
.arg(self.config.client_groups_key(client_id))
.ignore();
for group in &groups {
cleanup_pipe
.cmd("SREM")
.arg(self.config.group_key(group))
.arg(client_id)
.ignore();
}
cleanup_pipe
.query::<()>(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("Redis 写入失败: {e}")))?;
Ok(())
}
pub fn config(&self) -> &RedisGatewayConfig {
&self.config
}
fn publish(&self, channel: &str, command: &GatewayCommand) -> Result<(), GatewayError> {
let json = command.to_json()?;
let mut conn = self.conn.lock();
redis::cmd("PUBLISH")
.arg(channel)
.arg(json)
.query::<()>(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("Redis 写入失败: {e}")))?;
Ok(())
}
fn query<T: redis::FromRedisValue>(&self, cmd: &mut redis::Cmd) -> Result<T, GatewayError> {
let mut conn = self.conn.lock();
cmd.query(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("Redis 命令执行失败: {e}")))
}
}
impl GatewayTransport for RedisGatewayTransport {
fn send_to_client(&self, client_id: &str, message: &str) -> Result<(), GatewayError> {
if client_id.is_empty() {
return Err(GatewayError::InvalidClientId(client_id.to_string()));
}
let mut conn = self.conn.lock();
let is_member: bool = redis::cmd("SISMEMBER")
.arg(self.config.online_key())
.arg(client_id)
.query(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("SISMEMBER 失败: {e}")))?;
if !is_member {
return Err(GatewayError::ClientNotFound(client_id.to_string()));
}
drop(conn);
let command = GatewayCommand::SendToClient {
client_id: client_id.to_string(),
message: message.to_string(),
};
self.publish(&self.config.channel_client(client_id), &command)
}
fn send_to_clients(&self, client_ids: &[String], message: &str) -> Result<(), GatewayError> {
if client_ids.is_empty() {
return Ok(());
}
let mut conn = self.conn.lock();
for client_id in client_ids {
if client_id.is_empty() {
return Err(GatewayError::InvalidClientId(client_id.to_string()));
}
let is_member: bool = redis::cmd("SISMEMBER")
.arg(self.config.online_key())
.arg(client_id)
.query(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("SISMEMBER 失败: {e}")))?;
if !is_member {
return Err(GatewayError::ClientNotFound(client_id.to_string()));
}
}
drop(conn);
for client_id in client_ids {
let command = GatewayCommand::SendToClient {
client_id: client_id.to_string(),
message: message.to_string(),
};
self.publish(&self.config.channel_client(client_id), &command)?;
}
Ok(())
}
fn send_to_all(&self, message: &str) -> Result<(), GatewayError> {
let command = GatewayCommand::SendToAll {
message: message.to_string(),
};
self.publish(&self.config.channel_all(), &command)
}
fn send_to_group(&self, group: &str, message: &str) -> Result<(), GatewayError> {
let command = GatewayCommand::SendToGroup {
group: group.to_string(),
message: message.to_string(),
};
self.publish(&self.config.channel_group(group), &command)
}
fn get_all_client_ids(&self) -> Result<Vec<String>, GatewayError> {
self.query(redis::cmd("SMEMBERS").arg(self.config.online_key()))
}
fn get_client_id_list_by_group(&self, group: &str) -> Result<Vec<String>, GatewayError> {
self.query(redis::cmd("SMEMBERS").arg(self.config.group_key(group)))
}
fn get_groups_by_client_id(&self, client_id: &str) -> Result<Vec<String>, GatewayError> {
if client_id.is_empty() {
return Err(GatewayError::InvalidClientId(client_id.to_string()));
}
let mut conn = self.conn.lock();
let is_member: bool = redis::cmd("SISMEMBER")
.arg(self.config.online_key())
.arg(client_id)
.query(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("SISMEMBER 失败: {e}")))?;
if !is_member {
return Err(GatewayError::ClientNotFound(client_id.to_string()));
}
redis::cmd("SMEMBERS")
.arg(self.config.client_groups_key(client_id))
.query(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("SMEMBERS 失败: {e}")))
}
fn join_group(&self, client_id: &str, group: &str) -> Result<(), GatewayError> {
if client_id.is_empty() {
return Err(GatewayError::InvalidClientId(client_id.to_string()));
}
let mut conn = self.conn.lock();
let is_member: bool = redis::cmd("SISMEMBER")
.arg(self.config.online_key())
.arg(client_id)
.query(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("SISMEMBER 失败: {e}")))?;
if !is_member {
return Err(GatewayError::ClientNotFound(client_id.to_string()));
}
let mut pipe = redis::pipe();
pipe.atomic()
.cmd("SADD")
.arg(self.config.client_groups_key(client_id))
.arg(group)
.ignore()
.cmd("SADD")
.arg(self.config.group_key(group))
.arg(client_id)
.ignore();
pipe.query::<()>(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("Redis 写入失败: {e}")))?;
Ok(())
}
fn leave_group(&self, client_id: &str, group: &str) -> Result<(), GatewayError> {
if client_id.is_empty() {
return Err(GatewayError::InvalidClientId(client_id.to_string()));
}
let mut conn = self.conn.lock();
let is_member: bool = redis::cmd("SISMEMBER")
.arg(self.config.online_key())
.arg(client_id)
.query(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("SISMEMBER 失败: {e}")))?;
if !is_member {
return Err(GatewayError::ClientNotFound(client_id.to_string()));
}
let group_exists: bool = redis::cmd("EXISTS")
.arg(self.config.group_key(group))
.query(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("EXISTS 失败: {e}")))?;
if !group_exists {
return Err(GatewayError::GroupNotFound(group.to_string()));
}
let mut pipe = redis::pipe();
pipe.atomic()
.cmd("SREM")
.arg(self.config.client_groups_key(client_id))
.arg(group)
.ignore()
.cmd("SREM")
.arg(self.config.group_key(group))
.arg(client_id)
.ignore();
pipe.query::<()>(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("Redis 写入失败: {e}")))?;
Ok(())
}
fn ungroup(&self, group: &str) -> Result<(), GatewayError> {
let mut conn = self.conn.lock();
let client_ids: Vec<String> = redis::cmd("SMEMBERS")
.arg(self.config.group_key(group))
.query(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("SMEMBERS 失败: {e}")))?;
if client_ids.is_empty() {
let exists: bool = redis::cmd("EXISTS")
.arg(self.config.group_key(group))
.query(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("EXISTS 失败: {e}")))?;
if !exists {
return Err(GatewayError::GroupNotFound(group.to_string()));
}
}
let mut pipe = redis::pipe();
pipe.atomic();
pipe.cmd("DEL").arg(self.config.group_key(group)).ignore();
for client_id in &client_ids {
pipe.cmd("SREM")
.arg(self.config.client_groups_key(client_id))
.arg(group)
.ignore();
}
pipe.query::<()>(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("Redis 写入失败: {e}")))?;
Ok(())
}
fn is_online(&self, client_id: &str) -> Result<bool, GatewayError> {
let mut conn = self.conn.lock();
redis::cmd("SISMEMBER")
.arg(self.config.online_key())
.arg(client_id)
.query(&mut *conn)
.map_err(|e| GatewayError::Transport(format!("SISMEMBER 失败: {e}")))
}
fn get_client_count(&self) -> Result<usize, GatewayError> {
let count: i64 = self.query(redis::cmd("SCARD").arg(self.config.online_key()))?;
Ok(count as usize)
}
fn get_client_count_by_group(&self, group: &str) -> Result<usize, GatewayError> {
let count: i64 = self.query(redis::cmd("SCARD").arg(self.config.group_key(group)))?;
Ok(count as usize)
}
fn close_client(&self, client_id: &str) -> Result<(), GatewayError> {
if client_id.is_empty() {
return Err(GatewayError::InvalidClientId(client_id.to_string()));
}
self.unregister_client(client_id)?;
let command = GatewayCommand::CloseClient {
client_id: client_id.to_string(),
};
self.publish(&self.config.channel_close(client_id), &command)
}
}
pub struct RedisGatewaySubscriber {
pubsub: redis::aio::PubSub,
config: RedisGatewayConfig,
}
#[derive(Debug, Error)]
pub enum SubscribeError {
#[error("Redis 错误: {0}")]
Redis(#[from] redis::RedisError),
#[error("命令反序列化失败: {0}")]
Deserialize(String),
}
impl RedisGatewaySubscriber {
pub async fn connect(config: RedisGatewayConfig) -> Result<Self, SubscribeError> {
let client = redis::Client::open(config.redis_url.as_str())?;
let pubsub = client.get_async_pubsub().await?;
Ok(Self { pubsub, config })
}
pub async fn subscribe_all(&mut self) -> Result<(), SubscribeError> {
let prefix = &self.config.key_prefix;
let patterns = [
format!("{prefix}:ch:all"),
format!("{prefix}:ch:group:*"),
format!("{prefix}:ch:client:*"),
format!("{prefix}:ch:close:*"),
];
for pattern in &patterns {
self.pubsub.psubscribe(pattern).await?;
}
Ok(())
}
pub async fn next_command(&mut self) -> Result<GatewayCommand, SubscribeError> {
use futures::StreamExt;
let msg = self.pubsub.on_message().next().await;
match msg {
Some(msg) => {
let payload: String = msg.get_payload().map_err(|e| {
SubscribeError::Deserialize(format!("payload 类型转换失败: {e}"))
})?;
GatewayCommand::from_json(&payload)
.map_err(|e| SubscribeError::Deserialize(e.to_string()))
}
None => Err(SubscribeError::Deserialize("pub/sub 流结束".to_string())),
}
}
pub fn config(&self) -> &RedisGatewayConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_command_send_to_client_roundtrip() {
let cmd = GatewayCommand::SendToClient {
client_id: "7f00000108fc00000001".to_string(),
message: "hello".to_string(),
};
let json = cmd.to_json().unwrap();
let decoded = GatewayCommand::from_json(&json).unwrap();
match decoded {
GatewayCommand::SendToClient { client_id, message } => {
assert_eq!(client_id, "7f00000108fc00000001");
assert_eq!(message, "hello");
}
other => panic!("期望 SendToClient, 实际 {other:?}"),
}
}
#[test]
fn test_command_send_to_all_roundtrip() {
let cmd = GatewayCommand::SendToAll {
message: "broadcast".to_string(),
};
let json = cmd.to_json().unwrap();
let decoded = GatewayCommand::from_json(&json).unwrap();
match decoded {
GatewayCommand::SendToAll { message } => {
assert_eq!(message, "broadcast");
}
other => panic!("期望 SendToAll, 实际 {other:?}"),
}
}
#[test]
fn test_command_send_to_group_roundtrip() {
let cmd = GatewayCommand::SendToGroup {
group: "room1".to_string(),
message: "group-msg".to_string(),
};
let json = cmd.to_json().unwrap();
let decoded = GatewayCommand::from_json(&json).unwrap();
match decoded {
GatewayCommand::SendToGroup { group, message } => {
assert_eq!(group, "room1");
assert_eq!(message, "group-msg");
}
other => panic!("期望 SendToGroup, 实际 {other:?}"),
}
}
#[test]
fn test_command_close_client_roundtrip() {
let cmd = GatewayCommand::CloseClient {
client_id: "7f00000108fc00000001".to_string(),
};
let json = cmd.to_json().unwrap();
let decoded = GatewayCommand::from_json(&json).unwrap();
match decoded {
GatewayCommand::CloseClient { client_id } => {
assert_eq!(client_id, "7f00000108fc00000001");
}
other => panic!("期望 CloseClient, 实际 {other:?}"),
}
}
#[test]
fn test_command_from_json_invalid() {
let result = GatewayCommand::from_json("not json");
assert!(result.is_err());
}
#[test]
fn test_command_json_tag_format() {
let cmd = GatewayCommand::SendToAll {
message: "x".to_string(),
};
let json = cmd.to_json().unwrap();
assert!(json.contains("\"cmd\":\"send_to_all\""));
}
#[test]
fn test_redis_gateway_config_default() {
let config = RedisGatewayConfig::default();
assert_eq!(config.redis_url, "redis://127.0.0.1:6379");
assert_eq!(config.key_prefix, "sz-rust");
}
#[test]
fn test_redis_gateway_config_builder() {
let config = RedisGatewayConfig::new("redis://10.0.0.1:6380").with_prefix("my-app");
assert_eq!(config.redis_url, "redis://10.0.0.1:6380");
assert_eq!(config.key_prefix, "my-app");
}
#[test]
fn test_redis_gateway_config_keys() {
let config = RedisGatewayConfig::new("redis://127.0.0.1:6379").with_prefix("test");
assert_eq!(config.online_key(), "test:online");
assert_eq!(config.client_groups_key("client1"), "test:cgroups:client1");
assert_eq!(config.group_key("room1"), "test:group:room1");
assert_eq!(config.channel_all(), "test:ch:all");
assert_eq!(config.channel_group("room1"), "test:ch:group:room1");
assert_eq!(config.channel_client("client1"), "test:ch:client:client1");
assert_eq!(config.channel_close("client1"), "test:ch:close:client1");
}
#[test]
#[ignore = "需要 Redis 服务器运行在 127.0.0.1:6379"]
fn test_redis_connect_and_register() {
let transport = RedisGatewayTransport::connect(
RedisGatewayConfig::new("redis://127.0.0.1:6379").with_prefix("test-gateway"),
)
.expect("连接 Redis 失败");
let client_id = "test-client-001";
transport.register_client(client_id).unwrap();
assert!(transport.is_online(client_id).unwrap());
assert_eq!(transport.get_client_count().unwrap(), 1);
transport.unregister_client(client_id).unwrap();
assert!(!transport.is_online(client_id).unwrap());
assert_eq!(transport.get_client_count().unwrap(), 0);
}
#[test]
#[ignore = "需要 Redis 服务器运行在 127.0.0.1:6379"]
fn test_redis_group_operations() {
let transport = RedisGatewayTransport::connect(
RedisGatewayConfig::new("redis://127.0.0.1:6379").with_prefix("test-gateway"),
)
.expect("连接 Redis 失败");
let client_a = "test-client-a";
let client_b = "test-client-b";
transport.register_client(client_a).unwrap();
transport.register_client(client_b).unwrap();
transport.join_group(client_a, "room1").unwrap();
transport.join_group(client_b, "room1").unwrap();
assert_eq!(transport.get_client_count_by_group("room1").unwrap(), 2);
let groups = transport.get_groups_by_client_id(client_a).unwrap();
assert_eq!(groups, vec!["room1".to_string()]);
transport.leave_group(client_a, "room1").unwrap();
assert_eq!(transport.get_client_count_by_group("room1").unwrap(), 1);
transport.unregister_client(client_a).unwrap();
transport.unregister_client(client_b).unwrap();
}
#[test]
#[ignore = "需要 Redis 服务器运行在 127.0.0.1:6379"]
fn test_redis_send_to_all() {
let transport = RedisGatewayTransport::connect(
RedisGatewayConfig::new("redis://127.0.0.1:6379").with_prefix("test-gateway"),
)
.expect("连接 Redis 失败");
transport.send_to_all("hello cluster").unwrap();
}
#[test]
#[ignore = "需要 Redis 服务器运行在 127.0.0.1:6379"]
fn test_redis_send_to_group() {
let transport = RedisGatewayTransport::connect(
RedisGatewayConfig::new("redis://127.0.0.1:6379").with_prefix("test-gateway"),
)
.expect("连接 Redis 失败");
transport.send_to_group("room1", "group message").unwrap();
}
#[test]
#[ignore = "需要 Redis 服务器运行在 127.0.0.1:6379"]
fn test_redis_send_to_client_not_online() {
let transport = RedisGatewayTransport::connect(
RedisGatewayConfig::new("redis://127.0.0.1:6379").with_prefix("test-gateway"),
)
.expect("连接 Redis 失败");
let err = transport.send_to_client("nonexistent", "msg").unwrap_err();
match err {
GatewayError::ClientNotFound(id) => assert_eq!(id, "nonexistent"),
other => panic!("期望 ClientNotFound, 实际 {other:?}"),
}
}
#[test]
#[ignore = "需要 Redis 服务器运行在 127.0.0.1:6379"]
fn test_redis_invalid_client_id() {
let transport = RedisGatewayTransport::connect(
RedisGatewayConfig::new("redis://127.0.0.1:6379").with_prefix("test-gateway"),
)
.expect("连接 Redis 失败");
let err = transport.register_client("").unwrap_err();
assert!(matches!(err, GatewayError::InvalidClientId(_)));
let err = transport.send_to_client("", "msg").unwrap_err();
assert!(matches!(err, GatewayError::InvalidClientId(_)));
}
#[test]
#[ignore = "需要 Redis 服务器运行在 127.0.0.1:6379"]
fn test_gateway_with_redis_transport() {
use crate::gateway::{Gateway, GatewayConfig};
use std::sync::Arc;
let transport = Arc::new(
RedisGatewayTransport::connect(
RedisGatewayConfig::new("redis://127.0.0.1:6379").with_prefix("test-gateway"),
)
.expect("连接 Redis 失败"),
);
let gateway = Gateway::new(GatewayConfig::new("127.0.0.1:1238"), transport.clone());
transport.register_client("gw-test-client").unwrap();
assert!(gateway.is_online("gw-test-client").unwrap());
gateway.send_to_all("gateway broadcast").unwrap();
gateway.close_client("gw-test-client").unwrap();
assert!(!gateway.is_online("gw-test-client").unwrap());
}
}