前言
近期要实现一个图片异步上传的需求,需要用到RabbitMQ,辅助客户端完成对高并发请求的处理。
一、安装Erlang
-
由于RabbitMQ服务器是用Erlang语言编写,所以我们需要先安装Erlang环境,本文使用的版本是erl-24.1
-
下载地址: https://github.com/erlang/otp/releases/download/OTP-24.1/otp_win64_24.1.exe
-
下载完成后,双击exe完成安装。记录安装目录,默认路径:C:\Program Files\erl-24.1
-
配置环境变量
路径:计算机–右键–属性–高级系统设置–环境变量–系统变量
step 1 新建:
变量名:ERLANG_HOME
变量值:C:\Program Files\erl-24.1
step 2 在系统变量栏找到Path
新建值%ERLANG_HOME%\bin
-
检测安装状态
Win+R输入cmd,打开命令提示符,输入erl,如下,Erlang安装结束。
二、安装RabbitMQ
- 本文使用的版本为rabbitmq-server-windows-3.9.8
下载地址:https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.9.8/rabbitmq-server-windows-3.9.8.zip - 解压到任意目录下,本文解压地址是 C:\Program Files
- 配置环境变量
方式同上,在系统变量栏
step 1 新建:
变量名:RABBITMQ_SERVER
变量值:C:\Program Files\rabbitmq_server-3.9.8
step 2 在系统变量栏找到Path
新建值%RABBITMQ_SERVER%\sbin
- 安装页面管理工具
4.1 CMD命令进入sbin目录, 或直接在该目录内Shift+右键–>在此处打开命令窗口
4.2 输入 rabbitmq-plugins.bat enable rabbitmq_management
- 启动服务
命令窗输入rabbitmq-server.bat,或直接双节打开rabbitmq-server.bat
- 测试
浏览器输入 http://localhost:15672/ ,username和password都输入 guest,登录。
三、配置账户
默认情况下,如果只有guest账户,RabbitMQ是不允许用户通过远程ip访问管理页面的。因此需要配置自定义账户,才能通过远程方式(即IP+端口号)访问RabbitMQ。
- 在Admin栏右侧,点击Virtual Host新增virtual host(虚拟消息服务器)。每个VirtualHost相当于一个相对独立的RabbitMQ服务器;每个VirtualHost之间相互隔离,互不干扰。
- 在Admin栏右侧,点击User新增用户,(新创建的用户默认是没有任何权限的)
- 为新增账号添加权限,点击新增的用户名即可编辑权限
此处权限设置,表明该用户具有访问列表中virtual host的权限
- 分配权限后,guest账户就不再需要了,删除即可,点击guest用户名
- 删除guest后,RabbitMQ会检测到当前guest账户登录失效,有弹窗提示输入账号密码,填入新创建的账号信息即可,然后退出guest账号
- 远程访问,并用新账号登录
- 如果登录失败可以尝试以下方案
方案一:将新增账户类型设置为Administrator,CMD命令,在sbin目录下输入
rabbitmqctl set_user_tags admin administrator
设置后重试登录。(admin为刚才新增的账户名)
方案二:为新增账户添加默认virtual host的权限
rabbitmqctl set_permissions -p / admin ‘.’ '.’ ‘.*’
设置后重试登录。(/ 为默认virtual host, admin为新增账号)
方案三:新建账号,并为其分配权限
rabbitmqctl add_user guest1 123456 (账号guest1 密码123456)
rabbitmqctl set_user_tags guest1 administrator (设置账号类型为administrator)
rabbitmqctl set_permissions -p / guest1 ‘.’ '.’ ‘.*’ (为guest1添加权限)
设置后使用guest1重试登录。
四、配置队列
- 创建队列
Virtual host :当前队列输入那个消息服务器
Type:队列类型默认选择Classic(经典款)
Name:队列名
Durability:Durable(默认,持久化),另一个是Transient,瞬态。持久化消息存储在磁盘,瞬态消息存储在内存中,大部分情况都选择持久化消息方式存储
Auto delete:No,关闭队列自动删除。否则当至少有一个消费者连接到队列时,该队列将删除自身,并将所有消费者的连接断开。(有点像滴滴司机抢单)
Arguments:参考网上其他博主的讲解,不在过多阐述
--------------------------------------------------------------------以下操作非必要---------------------------------------------------------------
感兴趣可以先了解一下RabbitMQ中 exchange、route、queue的关系
- 创建Exchanges,这里Type选择direct,要求routing key必须完全一致才能访问队列,其他都能网上查到,不再赘述4. 配置自定义的Exchange,配置好后可以通过点击队列名,查看Binds情况
五、编写测试脚本
- 创建两个C#控制台项目(本文使用vs2019),分别作为消息发布者和消息订阅者。.Net Framework框架版本选择为4.6.1
- 两个工程创建后,都需要添加RabbitMQ包。(安装过程有弹窗,确定即可)
- 消息发布者代码(publisher.cs):每隔150ms,向队列TestQueue1发送一条消息
using System;
using System.Text;
using System.Threading;
using RabbitMQ.Client;
namespace publisher
{
class Program
{
static void Main(string[] args)
{
//基本登录信息
var factory = new ConnectionFactory()
{
HostName = "192.168.123.29",
VirtualHost = "visual_knitting_machine",
UserName = "admin",
Password = "123456",
Port = 5672
};
//创建连接
using (var connection = factory.CreateConnection())
{
//分配channel
using (var channel = connection.CreateModel())
{
for (int i = 1; i < 1000; i++)
{
//描述队列
channel.QueueDeclare(
queue: "TestQueue1",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null
);
//要发送的消息
string message = "Hello World! -- " + i.ToString();
var body = Encoding.UTF8.GetBytes(message);
//发布消息
channel.BasicPublish(
exchange: "myTestExchange",
routingKey: "queue1_routingKey",
basicProperties: null,
body: body
);
Console.WriteLine(" {0} Sent \'{1}\'", i.ToString(), message);
Thread.Sleep(150);
}
}
}
}
}
}
4. 消息订阅者代码(consumer.cs):每隔200ms,从队列消费一条消息。消费者可以有多个。
using System;
using System.Text;
using System.Threading;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
namespace consumer
{
class Program
{
static EventingBasicConsumer consumer;
static IConnection connection;
static IModel channel;
static void Main(string[] args)
{
// 禁用控制台快速编辑模式,防止窗口假死。
// 假死后会阻塞程序,导致无法接受消息
// 可通过按回车解除假死状态
ConsoleUtil.DisbleQuickEditMode();
try
{
var factory = new ConnectionFactory()
{
HostName = "192.168.123.29",
VirtualHost = "visual_knitting_machine",
UserName = "admin",
Password = "123456",
Port = 5672
};
connection = factory.CreateConnection();
channel = connection.CreateModel();
//队列声明
channel.QueueDeclare("TestQueue1", true, false, false, null);
//公平分发,一次只为某个消费者发送一条消息,
//否则会出现其他消费者拿不到消息的情况
//也可设置一次接受多条消息,将1设置n即可。
channel.BasicQos(0, 1, false);
//构建消费者实例
consumer = new EventingBasicConsumer(channel);
//消息接收后的事件委托,异步,非阻塞。
consumer.Received += OnReceiveMessage;
//启动消费者
//关闭自动确认,避免一次性将消息全部消费
channel.BasicConsume("TestQueue1", false, consumer);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
//此循环无特殊意义,用来防止控制台自动关闭。
while (true) { }
}
static void OnReceiveMessage(object sender, BasicDeliverEventArgs e)
{
//手动确认消息被消费,只有确认消费后,RabbitMQ才会重新为该消费者发送新消息
channel.BasicAck(e.DeliveryTag, false);
var body = e.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
if (!string.IsNullOrEmpty(message))
{
Console.WriteLine(" Received '{0}'", message);
}
Thread.Sleep(200);
}
}
}
- 控制台工具类,可以不用该类,没有太大影响。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
public class ConsoleUtil
{
#region 设置控制台标题 禁用关闭按钮
[DllImport("user32.dll", EntryPoint = "FindWindow")]
extern static IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll", EntryPoint = "GetSystemMenu")]
extern static IntPtr GetSystemMenu(IntPtr hWnd, IntPtr bRevert);
[DllImport("user32.dll", EntryPoint = "RemoveMenu")]
extern static IntPtr RemoveMenu(IntPtr hMenu, uint uPosition, uint uFlags);
static void DisbleClosebtn()
{
IntPtr windowHandle = FindWindow(null, "控制台标题");
IntPtr closeMenu = GetSystemMenu(windowHandle, IntPtr.Zero);
uint SC_CLOSE = 0xF060;
RemoveMenu(closeMenu, SC_CLOSE, 0x0);
}
protected static void CloseConsole(object sender, ConsoleCancelEventArgs e)
{
Environment.Exit(0);
}
#endregion
#region 关闭控制台 快速编辑模式、插入模式
const int STD_INPUT_HANDLE = -10;
const uint ENABLE_QUICK_EDIT_MODE = 0x0040;
const uint ENABLE_INSERT_MODE = 0x0020;
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern IntPtr GetStdHandle(int hConsoleHandle);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint mode);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint mode);
public static void DisbleQuickEditMode()
{
IntPtr hStdin = GetStdHandle(STD_INPUT_HANDLE);
uint mode;
GetConsoleMode(hStdin, out mode);
mode &= ~ENABLE_QUICK_EDIT_MODE;//移除快速编辑模式
mode &= ~ENABLE_INSERT_MODE; //移除插入模式
SetConsoleMode(hStdin, mode);
}
#endregion
#region 设置控制台颜色
static void WriteColorLine(string str, ConsoleColor colorF, ConsoleColor colorB)
{
ConsoleColor currentForegroundColor = Console.ForegroundColor;
ConsoleColor currentBackgroundColor = Console.BackgroundColor;
Console.ForegroundColor = colorF;
Console.BackgroundColor = colorB;
//Console.WriteLine(str);
Console.ForegroundColor = currentForegroundColor;
Console.BackgroundColor = currentBackgroundColor;
}
#endregion
}