• 利用RabbitMQ、MySQL实现超大用户级别的消息在/离线收发


    由于RabbitMQ中只有队列(queue)才能存储信息,所以用RabbitMQ实现超大用户级别(百万计)的消息在/离线收发需要对每一个用户创建一个永久队列。

    但是RabbitMQ节点内存有限,经测试后发现节点集群也无法满足数百万用户队列收发数据的要求,所以最终决定采用数据库辅助实现该功能。

    一、数据库结构

    user_list数据库下有4张表:user_info、group_info、groupmember_info、message_info。

    user_info表中含有username(主键,非空,VARCHAR(50))、password(非空,VARCHAR(50))、routingkey(非空,VARCHAR(50))、has_queue(INT UNSIGNED)四个字段。

    group_info表中含有groupname(主键,非空,VARCHAR(50))、password(非空,VARCHAR(50))、creator(非空,VARCHAR(50))三个字段。

    groupmember_info表中含有username(主键,非空,VARCHAR(50))、groupname(主键,非空,VARCHAR(50))两个字段。

    message_info表中含有sendtime(非空,VARCHAR(50))、body(非空,VARCHAR(300)),receiver(非空,VARCHAR(50))、sender(非空,VARCHAR(100))四个字段。

    二、客户端结构

    1、文件夹创建以及包依赖安装:

    dotnet new console --name Client
    mv Client/Program.cs Client/Client.cs
    cd Client
    dotnet add package RabbitMQ.Client
    dotnet add package MySql.Data dotnet restore

    2、项目结构

    Client.cs(主程序):
    using RabbitMQ.Client;
    using MySql.Data.MySqlClient;
    
    namespace Client
    {
        class Client
        {
            static void Main(string[] args)
            {
                // 连接数据库
                string connStr = "Database=user_list;datasource=127.0.0.1;port=3306;user=root;pwd=123456;";
                MySqlConnection sqlConn = new MySqlConnection(connStr);
    
                sqlConn.Open();
    
                // 连接RabbitMQ
                var factory = new ConnectionFactory() { HostName = "dev.corp.wingoht.com", VirtualHost = "cd", UserName = "ishowfun", Password = "123456" };
                using (var connection = factory.CreateConnection())
                using (var channel = connection.CreateModel())
                {
                    // 用户登录,并获取离线消息
                    UserInfo user = UserLogin.Login(sqlConn);
    
                    // 监听在线消息
                    Consumer.StartListening(channel, sqlConn, user);
    
                    // 监听按钮事件,调用不同的客户端功能
                    KeyListening.StartListening(channel, sqlConn, user);
    
                    // 退出登录
                    UserLogout.Logout(channel, sqlConn, user);
                }
    
                // 释放数据库连接
                sqlConn.Close();
            }
        }
    }
    View Code

     UserLogin.cs:

    using System;
    using MySql.Data.MySqlClient;
    
    namespace Client
    {
        class UserLogin
        {
            public static UserInfo Login(MySqlConnection conn)
            {
                UserInfo user = new UserInfo();
                
                // 用户名输入
                Console.Write("Please enter your username: ");
                user.Username = Console.ReadLine();
                while (user.Username.Contains(",") || user.Username.Contains(" "))
                {
                    Console.WriteLine("Error: Username can not contain \",\"! and \" \"");
                    Console.Write("Please enter your username again: ");
                    user.Username = Console.ReadLine();
                }
    
                MySqlCommand cmd = new MySqlCommand("select * from user_info where username='" + user.Username + "'", conn);
                MySqlDataReader reader = cmd.ExecuteReader();
    
                // 判断该用户是否已经注册
                if (reader.Read())
                {
                    // 已注册用户登录
                    user.IsNewUser = true;
    
                    // 验证密码
                    Console.Write("Please enter your password: ");
                    user.Password = reader.GetString("password");
                    while (user.Password != Console.ReadLine())
                    {
                        Console.WriteLine("Error: Username and password do not match!");
                        Console.Write("Please enter your password again: ");
                    }
                    Console.WriteLine("Welcome back, {0}!", user.Username);
                    Console.WriteLine("-------------------------------------");
    
                    // 获取当前用户的路由键
                    user.RoutingKey = reader.GetString("routingkey");
                    reader.Close();
    
                    // 读取离线消息
                    cmd = new MySqlCommand("select * from message_info where receiver='" + user.Username + "'", conn);
                    reader = cmd.ExecuteReader();
                    Console.WriteLine("Unread Message: ");
                    while (reader.Read())
                    {
                        string sender = reader.GetString("sender");
                        string sendTime = reader.GetString("sendtime");
                        string content = reader.GetString("body");
                        string[] splitSender = sender.Split(" ");
                        if (splitSender.Length == 1)
                        {
                            Console.WriteLine("Sender: {0}, SendTime: {1}, Content: {2}", sender, sendTime, content);
                        }
                        else
                        {
                            sender = splitSender[0];
                            string group = splitSender[1];
                            Console.WriteLine("Sender: {0} from {1}, SendTime: {2}, Content: {3}", sender, group, sendTime, content);
                        }
                    }
                    reader.Close();
    
                    // 删除已处理的离线消息
                    cmd = new MySqlCommand("delete from message_info where receiver=@re", conn);
                    cmd.Parameters.AddWithValue("re", user.Username);
    
                    cmd.ExecuteNonQuery();
                }
                else
                {
                    // 新用户注册并登录
                    user.IsNewUser = false;
                    reader.Close();
                    Console.WriteLine("Welcome, new user!");
    
                    // 设置密码
                    Console.Write("Please set your password: ");
                    user.Password = Console.ReadLine();
                    Console.Write("Please confirm your password: ");
                    while (user.Password != Console.ReadLine())
                    {
                        Console.WriteLine("Error: Confirmation failure!");
                        Console.Write("Please set your password again: ");
                        user.Password = Console.ReadLine();
                        Console.Write("Please confirm your password: ");
                    }
    
                    // 生成该用户的加密路由键,并保证该路由键唯一
                    user.RoutingKey = GenerateKey.GenerateRandomString(32);
                    cmd = new MySqlCommand("select * from user_info where routingkey='" + user.RoutingKey + "'", conn);
                    reader = cmd.ExecuteReader();
                    while (reader.Read())
                    {
                        reader.Close();
                        user.RoutingKey = GenerateKey.GenerateRandomString(32);
                        reader = cmd.ExecuteReader();
                    }
    
                    reader.Close();
                }
    
                Console.WriteLine("-------------------------------------");
                return user;
            }
        }
    }
    View Code

     UserInfo.cs:

    namespace Client
    {
        class UserInfo
        {
            // 唯一用户名
            public string Username { get; set; }
    
            // 登录密码
            public string Password { get; set; }
    
            // 加密路由键
            public string RoutingKey { get; set; }
    
            // 是否为新用户
            public bool IsNewUser { get; set; }
        }
    }
    View Code

     GenerateKey.cs: 

    using System;
    
    namespace Client
    {
        class GenerateKey
        {
            // 字符串中字符的取值范围
            private static char[] constant =
            {
                '0','1','2','3','4','5','6','7','8','9',
                'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
                'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'
            };
    
            // 生成指定长度的随机字符串
            public static string GenerateRandomString(int len)
            {
                System.Text.StringBuilder newRandom = new System.Text.StringBuilder(62);
                Random rd = new Random();
                for (int i = 0; i < len; i++)
                {
                    newRandom.Append(constant[rd.Next(62)]);
                }
                return newRandom.ToString();
            }
        }
    }
    View Code

     Consumer.cs:

    using System;
    using System.Text;
    
    using RabbitMQ.Client;
    using RabbitMQ.Client.Events;
    using MySql.Data.MySqlClient;
    
    namespace Client
    {
        class Consumer
        {
            public static void StartListening(IModel channel, MySqlConnection conn, UserInfo user)
            {
                // 交换机声明
                channel.ExchangeDeclare(exchange: "topic_message", type: "topic");
    
                // 队列创建与绑定
                channel.QueueDeclare(queue: user.RoutingKey,
                                     durable: true,
                                     exclusive: false,
                                     autoDelete: false,
                                     arguments: null);
    
                channel.QueueBind(queue: user.RoutingKey,
                                  exchange: "topic_message",
                                  routingKey: user.RoutingKey);
    
                var consumer = new EventingBasicConsumer(channel);
    
                // 处理收到的消息
                consumer.Received += (model, ea) =>
                {
                    var body = ea.Body;
                    string message = Encoding.UTF8.GetString(body);
                    string[] splitRes = message.Trim().Split(",");
                    string sendTime = splitRes[1];
                    string content = splitRes[2];
                    string[] splitSender = splitRes[0].Split(" ");
                    if (splitRes.Length > 3)
                    {
                        for (int i = 3; i < splitRes.Length; i++)
                            content = content + "," + splitRes[i];
                    }
    
                    Console.WriteLine();
                    if (splitSender.Length == 1)
                    {
                        string sender = splitRes[0];
                        Console.WriteLine("Sender: {0}, SendTime: {1}, Content: {2}", sender, sendTime, content);
                    }
                    else
                    {
                        string sender = splitSender[0];
                        string group = splitSender[1];
                        Console.WriteLine("Sender: {0} from {1}, SendTime: {2}, Content: {3}", sender, group, sendTime, content);
                    }
                };
    
                channel.BasicConsume(queue: user.RoutingKey,
                                     autoAck: true,
                                     consumer: consumer);
    
                MySqlCommand cmd;
    
                // 更新数据库
                if (user.IsNewUser)
                {
                    // 向数据库声明该用户已经拥有了队列,可以直接在线发送
                    cmd = new MySqlCommand("update user_info set has_queue=1 where username=@uid", conn);
                    cmd.Parameters.AddWithValue("uid", user.Username);
    
                    cmd.ExecuteNonQuery();
                }
                else
                {
                    // 防止注入地插入新用户数据
                    cmd = new MySqlCommand("insert into user_info set username=@uid,password=@pwd,routingkey=@rk,has_queue=1", conn);
                    cmd.Parameters.AddWithValue("uid", user.Username);
                    cmd.Parameters.AddWithValue("pwd", user.Password);
                    cmd.Parameters.AddWithValue("rk", user.RoutingKey);
    
                    cmd.ExecuteNonQuery();
                }
    
                Console.WriteLine("{0} Start Listening!", user.Username);
                Console.WriteLine("-------------------------------------");
            }
        }
    }
    View Code

     KeyListening.cs:

    using System;
    
    using RabbitMQ.Client;
    using MySql.Data.MySqlClient;
    
    namespace Client
    {
        class KeyListening
        {
            public static void StartListening(IModel channel, MySqlConnection conn, UserInfo user)
            {
                MQHelper.HelpTip();
                Console.WriteLine("-------------------------------------");
                ConsoleKeyInfo keyPressed;
                bool escFlag = false;
    
                while (true)
                {
    
                    keyPressed = Console.ReadKey();
                    Console.WriteLine();
                    Console.WriteLine("-------------------------------------");
                    switch (keyPressed.Key)
                    {
                        // 0: 显示帮助信息
                        case ConsoleKey.D0:
                            MQHelper.HelpTip();
                            break;
                        // 1: 创建群组
                        case ConsoleKey.D1:
                            MQHelper.CreateGroup(conn, user);
                            break;
                        // 2: 申请加入群组
                        case ConsoleKey.D2:
                            MQHelper.JoinGroup(conn, user);
                            break;
                        // 3: 退出群组
                        case ConsoleKey.D3:
                            MQHelper.LeaveGroup(conn, user);
                            break;
                        // 4: 显示群组信息    
                        case ConsoleKey.D4:
                            MQHelper.ShowGroup(conn, user);
                            break;
                        // 5: 单播
                        case ConsoleKey.D5:
                            MQHelper.BasicSend(channel, conn, user);
                            break;
                        // 6: 组播
                        case ConsoleKey.D6:
                            MQHelper.GroupSend(channel, conn, user);
                            break;
                        // ESC: 退出
                        case ConsoleKey.Escape:
                            escFlag = true;
                            break;
                        // 无意义按键
                        default:
                            Console.WriteLine("Error: Invalid press!");
                            break;
                    }
    
                    Console.WriteLine("-------------------------------------");
    
                    if (escFlag)
                        break;
                }
            }
        }
    }
    View Code

     MQHelper.cs:

    using System;
    using System.Text;
    using System.Collections.Generic;
    
    using RabbitMQ.Client;
    using MySql.Data.MySqlClient;
    
    namespace Client
    {
        // 该类包含了按钮监听事件触发的各项功能的实现
        class MQHelper
        {
            #region 显示帮助信息
            public static void HelpTip()
            {
                // 显示各按键对应功能
                Console.WriteLine("Function Press:");
                Console.WriteLine("[0]: Help");
                Console.WriteLine("[1]: Create group");
                Console.WriteLine("[2]: Join group");
                Console.WriteLine("[3]: Leave group");
                Console.WriteLine("[4]: Show groups");
                Console.WriteLine("[5]: BasicSend");
                Console.WriteLine("[6]: GroupSend");
                Console.WriteLine("[ESC]: Log out");
            }
            #endregion
    
            #region 创建群组
            public static void CreateGroup(MySqlConnection conn, UserInfo user)
            {
                // 设置群组名称以及加入密码
                Console.Write("Please set the group name: ");
                string groupName = Console.ReadLine();
                MySqlCommand groupCmd = new MySqlCommand("select * from group_info where groupname='" + groupName + "'", conn);
                MySqlDataReader groupReader = groupCmd.ExecuteReader();
    
                if (groupReader.Read())
                {
                    groupReader.Close();
                    Console.WriteLine("Error: This group name already exists!");
                }
                else
                {
                    groupReader.Close();
    
                    Console.Write("Please set the password: ");
                    string pwd = Console.ReadLine();
                    Console.Write("Please confirm the password: ");
                    while (pwd != Console.ReadLine())
                    {
                        Console.WriteLine("Error: Confirmation failure!");
                        Console.Write("Please set the password again: ");
                        pwd = Console.ReadLine();
                        Console.Write("Please confirm the password: ");
                    }
    
                    // 将群组信息插入数据库
                    groupCmd = new MySqlCommand("insert into group_info set groupname=@gid,password=@pwd,creator=@cr", conn);
                    groupCmd.Parameters.AddWithValue("gid", groupName);
                    groupCmd.Parameters.AddWithValue("pwd", pwd);
                    groupCmd.Parameters.AddWithValue("cr", user.Username);
    
                    groupCmd.ExecuteNonQuery();
    
                    groupCmd = new MySqlCommand("insert into groupmember_info set groupname=@gid,username=@uid", conn);
                    groupCmd.Parameters.AddWithValue("gid", groupName);
                    groupCmd.Parameters.AddWithValue("uid", user.Username);
    
                    groupCmd.ExecuteNonQuery();
    
                    Console.WriteLine("Successfully create the group!");
                }
            }
            #endregion
    
            #region 申请加入群组
            public static void JoinGroup(MySqlConnection conn, UserInfo user)
            {
                // 输入要加入的群名
                Console.Write("Please enter the group name: ");
                string groupName = Console.ReadLine();
                MySqlCommand groupCmd = new MySqlCommand("select * from group_info where groupname='" + groupName + "'", conn);
                MySqlDataReader groupReader = groupCmd.ExecuteReader();
    
                // 判断该群是否存在
                if (groupReader.Read())
                {
                    // 验证加入密码
                    string pwd = groupReader.GetString("password");
                    groupReader.Close();
    
                    Console.Write("Please enter your password: ");
                    if (pwd != Console.ReadLine())
                    {
                        Console.WriteLine("Error: Username and password do not match!");
                    }
                    else
                    {
                        groupCmd = new MySqlCommand("select * from groupmember_info where groupname='" + groupName + "' and username='" + user.Username + "'", conn);
                        groupReader = groupCmd.ExecuteReader();
                        
                        // 判断该用户是否已经在这个群中 
                        if (groupReader.Read())
                        {
                            groupReader.Close();
                            Console.WriteLine("Error: You already join the group!");
                        }
                        else
                        {
                            // 加入该群,并更新数据库信息
                            groupReader.Close();
                            groupCmd = new MySqlCommand("insert into groupmember_info set groupname=@gid,username=@uid", conn);
                            groupCmd.Parameters.AddWithValue("gid", groupName);
                            groupCmd.Parameters.AddWithValue("uid", user.Username);
    
                            groupCmd.ExecuteNonQuery();
                            Console.WriteLine("Successfully join the group!");
                        }
                    }
                }
                else
                {
                    groupReader.Close();
                    Console.WriteLine("Error: This group name does not exist!");
                }
            }
            #endregion
    
            #region 退出群组
            public static void LeaveGroup(MySqlConnection conn, UserInfo user)
            {
                // 输入要退出群的名称
                Console.Write("Please enter the group name: ");
                string groupName = Console.ReadLine();
                MySqlCommand groupCmd = new MySqlCommand("select * from groupmember_info where groupname='" + groupName + "' and username='" + user.Username + "'", conn);
                MySqlDataReader groupReader = groupCmd.ExecuteReader();
    
                // 判断你是否在这个群中
                if (groupReader.Read())
                {
                    // 退出该群,并更新数据库信息
                    groupReader.Close();
                    groupCmd = new MySqlCommand("delete from groupmember_info where groupname=@gid and username=@uid", conn);
                    groupCmd.Parameters.AddWithValue("gid", groupName);
                    groupCmd.Parameters.AddWithValue("uid", user.Username);
                    groupCmd.ExecuteNonQuery();
                    Console.WriteLine("Successfully leave the group!");
                }
                else
                {
                    groupReader.Close();
                    Console.WriteLine("Error: You didn't join the group!");
                }
            }
            #endregion
    
            #region 显示群组信息
            public static void ShowGroup(MySqlConnection conn, UserInfo user)
            {
                MySqlCommand groupCmd = new MySqlCommand("select * from groupmember_info where username='" + user.Username + "'", conn);
                MySqlDataReader groupReader = groupCmd.ExecuteReader();
    
                // 显示加入的所有群名称
                while (groupReader.Read())
                {
                    Console.WriteLine(groupReader.GetString("groupname"));
                }
                groupReader.Close();
            }
            #endregion
    
            #region 单播
            public static void BasicSend(IModel channel, MySqlConnection conn, UserInfo user)
            {
                // 输入收信人用户名
                Console.Write("Please enter the receiver: ");
                string receiver = Console.ReadLine();
                Console.WriteLine("-------------------------------------");
    
                // 输入待发送的消息
                Console.WriteLine("Please enter the message: ");
                string content = Console.ReadLine();
                Console.WriteLine("-------------------------------------");
                MySqlCommand sendCmd = new MySqlCommand("select * from user_info where username='" + receiver + "'", conn);
                MySqlDataReader sendReader = sendCmd.ExecuteReader();
    
                bool user_flag = sendReader.Read();
    
                // 判断是否存在该收信人
                if (user_flag)
                {
                    string receiverKey = sendReader.GetString("routingkey");
                    int hasQueue = sendReader.GetInt32("has_queue");
                    string sendTime = DateTime.Now.ToString();
                    string message = user.Username + "," + sendTime + "," + content;
                    var body = Encoding.UTF8.GetBytes(message);
    
                    sendReader.Close();
    
                    // 目标队列存在,则直接发布信息;否则将信息数据存入数据库
                    if (hasQueue == 1)
                    {
                        // 在线发布消息
                        var properties = channel.CreateBasicProperties();
                        properties.Persistent = true;
    
                        channel.BasicPublish(exchange: "topic_message",
                                             routingKey: receiverKey,
                                             basicProperties: properties,
                                             body: body);
                    }
                    else
                    {
                        // 离线发布消息
                        sendCmd = new MySqlCommand("insert into message_info set sendtime=@st,body=@body,receiver=@re,sender=@se", conn);
                        sendCmd.Parameters.AddWithValue("st", sendTime);
                        sendCmd.Parameters.AddWithValue("body", content);
                        sendCmd.Parameters.AddWithValue("re", receiver);
                        sendCmd.Parameters.AddWithValue("se", user.Username);
    
                        sendCmd.ExecuteNonQuery();
                    }
                    Console.WriteLine("You sent [ {0} ] to {1} at {2}.", content, receiver, sendTime);
                }
                else
                {
                    sendReader.Close();
                    Console.WriteLine("Error: The receiver does not exist!");
                }
            }
            #endregion
    
            #region 组播
            public static void GroupSend(IModel channel, MySqlConnection conn, UserInfo user)
            {
                // 输入在那个群组中发布消息
                Console.Write("Please enter the receiver: ");
                string group = Console.ReadLine();
                Console.WriteLine("-------------------------------------");
    
                // 输入待发布消息
                Console.WriteLine("Please enter the message: ");
                string content = Console.ReadLine();
                Console.WriteLine("-------------------------------------");
    
                // 读取群组中的所有成员
                MySqlCommand sendCmd = new MySqlCommand("select * from groupmember_info where groupname='" + group + "'", conn);
                MySqlDataReader sendReader = sendCmd.ExecuteReader();
                List<string> memberList = new List<string>();
                while (sendReader.Read())
                {
                    memberList.Add(sendReader.GetString("username"));
                }
                sendReader.Close();
    
                string sender = user.Username + " " + group;
                string sendTime = DateTime.Now.ToString();
    
                // 交换机声明           
                channel.ExchangeDeclare(exchange: "topic_message", type: "topic");
    
                var properties = channel.CreateBasicProperties();
                properties.Persistent = true;
    
                // 逐个处理成员,在线用户在线发送消息,离线用户离线发送消息
                foreach (string member in memberList)
                {
                    sendCmd = new MySqlCommand("select * from user_info where username='" + member + "'", conn);
                    sendReader = sendCmd.ExecuteReader();
    
                    sendReader.Read();
                    string receiverKey = sendReader.GetString("routingkey");
                    int hasQueue = sendReader.GetInt32("has_queue");
                    string message = sender + "," + sendTime + "," + content;
                    var body = Encoding.UTF8.GetBytes(message);
                    sendReader.Close();
    
                    // 目标队列存在,则直接发布信息;否则将信息数据存入数据库
                    if (hasQueue == 1)
                    {
                        // 在线发送消息
                        channel.BasicPublish(exchange: "topic_message",
                                             routingKey: receiverKey,
                                             basicProperties: properties,
                                             body: body);
                    }
                    else
                    {
                        // 离线发送消息
                        sendCmd = new MySqlCommand("insert into message_info set sendtime=@st,body=@body,receiver=@re,sender=@se", conn);
                        sendCmd.Parameters.AddWithValue("st", sendTime);
                        sendCmd.Parameters.AddWithValue("body", content);
                        sendCmd.Parameters.AddWithValue("re", member);
                        sendCmd.Parameters.AddWithValue("se", sender);
    
                        sendCmd.ExecuteNonQuery();
                    }
                }
    
                Console.WriteLine("You sent [ {0} ] to {1} at {2}.", content, group, sendTime);
            }
            #endregion
        }
    }
    View Code

     UserLogout.cs:

    using System;
    
    using RabbitMQ.Client;
    using MySql.Data.MySqlClient;
    
    namespace Client
    {
        class UserLogout
        {
            public static void Logout(IModel channel, MySqlConnection conn, UserInfo user)
            {
                Console.WriteLine("Goodbye, {0}!", user.Username);
    
                // 删除队列,并更新数据库,表明后面发送的消息应该转为离线发送
                channel.QueueDelete(user.RoutingKey);
                MySqlCommand cmd = new MySqlCommand("update user_info set has_queue=0 where username=@uid", conn);
                cmd.Parameters.AddWithValue("uid", user.Username);
    
                cmd.ExecuteNonQuery();
            }
        }
    }
    View Code
    百度云链接:https://pan.baidu.com/s/1Y93rcqnsv1cA9ZIxH2xrBw 密码:zfc5

  • 相关阅读:
    团队站立会议09
    团队站立会议08
    团队绩效
    团队站立会议07
    团队站立会议06
    团队站立会议05
    团队站立会议04
    团队站立会议03
    团队站立会议02
    反转链表
  • 原文地址:https://www.cnblogs.com/lucifer1997/p/9342024.html
Copyright © 2020-2023  润新知