Chemmy's Blog

chengming0916@outlook.com

先决条件
本教程假定 RabbitMQ 已经安装,并运行在localhost 标准端口(5672)。如果你使用不同的主机、端口或证书,则需要调整连接设置。

从哪里获得帮助
如果您在阅读本教程时遇到困难,可以通过邮件列表 联系我们

工作队列#

(使用 .NET Client)

教程[1] 中,我们编写了两个程序,用于从一个指定的队列发送和接收消息。在本文中,我们将创建一个_工作队列_,用于在多个工作线程间分发耗时的任务。

工作队列(又名:任务队列)背后的主要想法是避免立即执行资源密集型、且必须等待其完成的任务。相反的,我们把这些任务安排在稍后完成。我们可以将任务封装为消息并把它发送到队列中,在后台运行的工作进程将从队列中取出任务并最终执行。当您运行多个工作线程,这些任务将在这些工作线程之间共享。

这个概念在Web应用程序中特别有用,因为在一个 HTTP 请求窗口中无法处理复杂的任务。

准备#

我们将略微修改上一个示例中的_Send_程序,以其可以在命令行发送任意消息。
这个程序将调度任务到我们的工作队列中,所以让我们把它命名为NewTask

教程[1]一样,我们需要生成两个项目:

dotnet new console --name NewTask
mv NewTask/Program.cs NewTask/NewTask.cs

dotnet new console --name Worker
mv Worker/Program.cs Worker/Worker.cs

cd NewTask
dotnet add package RabbitMQ.Client
dotnet restore

cd ../Worker
dotnet add package RabbitMQ.Client
dotnet restore


var message = GetMessage(args);
var body = Encoding.UTF8.GetBytes(message);

var properties = channel.CreateBasicProperties();
properties.Persistent = true;

channel.BasicPublish(exchange: "",
                     routingKey: "task_queue",
                     basicProperties: properties,
                     body: body);

从命令行参数获取消息的帮助方法:

private static string GetMessage(string[] args)
{
    return ((args.Length > 0) ? string.Join(" ", args) : "Hello World!");
}

我们旧的Receive.cs脚本也需要进行一些更改:它需要为消息体中的每个点模拟一秒种的时间消耗。它将处理由 RabbitMQ 发布的消息,并执行任务,因此我们把它复制到Worker项目并修改:

var consumer = new EventingBasicConsumer(channel);


​ consumer.Received += (model, ea) =>
​ {
​ var body = ea.Body;
​ var message = Encoding.UTF8.GetString(body);
​ Console.WriteLine(“ [x] Received {0}”, message);


​ int dots = message.Split(‘.’).Length - 1;
​ Thread.Sleep(dots * 1000);

​ Console.WriteLine(“ [x] Done”);
​ };

channel.BasicConsume(queue: “task_queue”, autoAck: true, consumer: consumer);

模拟虚拟任务的执行时间:

int dots = message.Split('.').Length - 1;
Thread.Sleep(dots * 1000);

循环调度#

使用任务队列的优点之一是能够轻松地并行工作。如果我们正在积累积压的工作,我们仅要增加更多的工作者,并以此方式可以轻松扩展。

首先,我们尝试同时运行两个Worker实例。他们都会从队列中获取消息,但究竟如何?让我们来看看。

您需要打开三个控制台,两个运行Worker程序,这些控制台作为我们的两个消费者 - C1和C2。

cd Worker
dotnet run

cd Worker
dotnet run

在第三个控制台中,我们将发布一些新的任务。一旦你已经运行了消费者,你可以尝试发布几条消息:

cd NewTask
dotnet run "First message."
dotnet run "Second message.."
dotnet run "Third message..."
dotnet run "Fourth message...."
dotnet run "Fifth message....."

让我们看看有什么发送到了我们的Worker程序:

# shell 1
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'First message.'
# => [x] Received 'Third message...'
# => [x] Received 'Fifth message.....'


# shell 2
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'Second message..'
# => [x] Received 'Fourth message....'

默认情况下,RabbitMQ 会按顺序将每条消息发送给下一个消费者。消费者数量平均的情况下,每个消费者将会获得相同数量的消息。这种分配消息的方式称为循环(Round-Robin)。请尝试开启三个或更多的Worker程序来验证。

消息确认#

处理一项任务可能会需要几秒钟的时间。如果其中一个消费者开启了一项长期的任务并且只完成了部分就挂掉了,您可能想知道会发生什么?在我们当前的代码中,一旦 RabbitMQ 把消息分发给了消费者,它会立即将这条消息标记为删除。在这种情况下,如果您停掉某一个 Worker,我们将会丢失这条正在处理的消息,也将丢失所有分发到该 Worker 但尚未处理的消息。

但是我们不想丢失任何一个任务。如果一个 Worker 挂掉了,我们希望这个任务能被重新分发给其他 Worker。

为了确保消息永远不会丢失,RabbitMQ 支持 消息确认 机制。消费者回发一个确认信号 Ack(nowledgement) 给 RabbitMQ,告诉它某个消息已经被接收、处理并且可以自由删除它。

如果一个消费者在还没有回发确认信号之前就挂了(其通道关闭,连接关闭或者 TCP 连接丢失),RabbitMQ 会认为该消息未被完全处理,并将其重新排队。如果有其他消费者同时在线,该消息将会被会迅速重新分发给其他消费者。这样,即便 Worker 意外挂掉,也可以确保消息不会丢失。

没有任何消息会超时;当消费者死亡时,RabbitMQ 将会重新分发消息。即使处理消息需要非常非常长的时间也没关系。

默认情况下,手动消息确认 模式是开启的。在前面的例子中,我们通过将autoAck(“自动确认模式”)参数设置为true来明确地关闭手动消息确认模式。一旦完成任务,是时候删除这个标志并且从 Worker 手动发送一个恰当的确认信号给RabbitMQ。

var consumer = new EventingBasicConsumer(channel);


​ consumer.Received += (model, ea) =>
​ {
​ var body = ea.Body;
​ var message = Encoding.UTF8.GetString(body);
​ Console.WriteLine(“ [x] Received {0}”, message);


​ int dots = message.Split(‘.’).Length - 1;
​ Thread.Sleep(dots * 1000);

​ Console.WriteLine(“ [x] Done”);


​ channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
​ };



​ channel.BasicConsume(queue: “task_queue”, autoAck: false, consumer: consumer);

使用上面这段代码,我们可以确定的是,即使一个 Worker 在处理消息时,我们通过使用CTRL + C来终止它,也不会丢失任何消息。Worker 挂掉不久,所有未确认的消息将会被重新分发。

忘记确认
遗漏BasicAck是一个常见的错误。这是一个很简单的错误,但导致的后果却是严重的。当客户端退出时(看起来像是随机分发的),消息将会被重新分发,但是RabbitMQ会吃掉越来越多的内存,因为它不能释放未确认的消息。
为了调试这种错误,您可以使用rabbitmqctl来打印messages_unacknowledged字段:

sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged

在Windows上,删除sudo

rabbitmqctl.bat list_queues name messages_ready messages_unacknowledged

消息持久化#

我们已经学习了如何确保即使消费者挂掉,任务也不会丢失。但是如果 RabbitMQ 服务器停止,我们的任务还是会丢失。

当 RabbitMQ 退出或崩溃时,它会忘记已存在的队列和消息,除非告诉它不要这样做。为了确保消息不会丢失,有两件事是必须的:我们需要将队列和消息标记为持久

首先,我们需要确保 RabbitMQ 永远不会丢失我们的队列。为了做到这一点,我们需要把队列声明是_持久的(Durable)_:

// 声明队列,通过指定 durable 参数为 true,对消息进行持久化处理。 
channel.QueueDeclare(queue: "hello",
                     durable: true,
                     exclusive: false,
                     autoDelete: false,
                     arguments: null);

虽然这个命令本身是正确的,但是它在当前设置中不会起作用。那是因为我们已经定义过一个名为hello的队列,并且这个队列不是持久化的。RabbitMQ 不允许使用不同的参数重新定义已经存在的队列,并会向尝试执行该操作的程序返回一个错误。但有一个快速的解决办法 - 让我们用不同的名称声明一个队列,例如task_queue

channel.QueueDeclare(queue: "task_queue",
                     durable: true,
                     exclusive: false,
                     autoDelete: false,
                     arguments: null);

注意,该声明队列QueueDeclare方法的更改需要同时应用于生产者和消费者代码。

此时,我们可以确定的是,即使 RabbitMQ 重新启动,task_queue队列也不会丢失。现在我们需要将我们的消息标记为_持久的(Persistent)_ - 通过将IBasicProperties.Persistent设置为true

var properties = channel.CreateBasicProperties();
properties.Persistent = true;

关于消息持久性的说明
将消息标记为Persistent并不能完全保证消息不会丢失。尽管它告诉 RabbitMQ 将消息保存到磁盘,但当 RabbitMQ 接收到消息并且尚未保存消息时仍有一段时间间隔。此外,RabbitMQ 不会为每条消息执行fsync(2) - 它可能只是保存到缓存中,并没有真正写入磁盘。消息的持久化保证并不健壮,但对于简单的任务队列来说已经足够了。如果您需要一个更加健壮的保证,可以使用 发布者确认

公平调度#

您可能已经注意到调度仍然无法完全按照我们期望的方式工作。例如,在有两个 Worker 的情况下,假设所有奇数消息都很庞大、偶数消息都很轻量,那么一个 Worker 将会一直忙碌,而另一个 Worker 几乎不做任何工作。是的,RabbitMQ 并不知道存在这种情况,它仍然会平均地分发消息。

发生这种情况是因为 RabbitMQ 只是在消息进入队列后就将其分发。它不会去检查每个消费者所拥有的未确认消息的数量。它只是盲目地将第 n 条消息分发给第 n 位消费者。

为了改变上述这种行为,我们可以使用参数设置prefetchCount = 1basicQos方法。

这就告诉 RabbitMQ 同一时间不要给一个 Worker 发送多条消息。或者换句话说,不要向一个 Worker 发送新的消息,直到它处理并确认了前一个消息。
相反,它会这个消息调度给下一个不忙碌的 Worker。

channel.BasicQos(0, 1, false);

关于队列大小的说明
如果所有的 Worker 都很忙,您的队列可能会被填满。请留意这一点,可以尝试添加更多的 Worker,或者使用其他策略。

组合在一起#

我们NewTask.cs类的最终代码:

using System;
using RabbitMQ.Client;
using System.Text;

class NewTask
{
    public static void Main(string[] args)
    {
        
        var factory = new ConnectionFactory() { HostName = "localhost" };


​ using(var connection = factory.CreateConnection())
​ using(var channel = connection.CreateModel())
​ {

​ channel.QueueDeclare(queue: “task_queue”,
​ durable: true,
​ exclusive: false,
​ autoDelete: false,
​ arguments: null);


​ var message = GetMessage(args);
​ var body = Encoding.UTF8.GetBytes(message);


​ var properties = channel.CreateBasicProperties();
​ properties.Persistent = true;


​ channel.BasicPublish(exchange: “”,
​ routingKey: “task_queue”,
​ basicProperties: properties,
​ body: body);

​ Console.WriteLine(“ [x] Sent {0}”, message);
​ }

Console.WriteLine(“ Press [enter] to exit.”);
Console.ReadLine();
}

    private static string GetMessage(string[] args)
    {
        return ((args.Length > 0) ? string.Join(" ", args) : "Hello World!");
    }
}

(NewTask.cs 源码)

还有我们的Worker.cs

using System;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
using System.Threading;

class Worker
{
    public static void Main()
    {
        
        var factory = new ConnectionFactory() { HostName = "localhost" };


​ using(var connection = factory.CreateConnection())
​ using(var channel = connection.CreateModel())
​ {

​ channel.QueueDeclare(queue: “task_queue”,
​ durable: true,
​ exclusive: false,
​ autoDelete: false,
​ arguments: null);


​ channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);

​ Console.WriteLine(“ [*] Waiting for messages.”);


​ var consumer = new EventingBasicConsumer(channel);


​ consumer.Received += (model, ea) =>
​ {
​ var body = ea.Body;
​ var message = Encoding.UTF8.GetString(body);
​ Console.WriteLine(“ [x] Received {0}”, message);


​ int dots = message.Split(‘.’).Length - 1;
​ Thread.Sleep(dots * 1000);

​ Console.WriteLine(“ [x] Done”);


​ channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
​ };

​ channel.BasicConsume(queue: “task_queue”,
​ autoAck: false,
​ consumer: consumer);

Console.WriteLine(“ Press [enter] to exit.”);
Console.ReadLine();
}
}
}

(Worker.cs 源码)

使用消息确认机制和BasicQ您可以创建一个工作队列。即使 RabbitMQ 重新启动,通过持久性选项也可让任务继续存在。

有关IModel方法和IBasicProperties的更多信息,您可以在线浏览 RabbitMQ .NET客户端API参考

现在,我们可以继续阅读 教程[3],学习如何向多个消费者发送相同的消息。

写在最后#

本文翻译自 RabbitMQ 官方教程 C# 版本。如本文介绍内容与官方有所出入,请以官方最新内容为准。水平有限,翻译的不好请见谅,如有翻译错误还请指正。

先决条件
本教程假定 RabbitMQ 已经安装,并运行在localhost 标准端口(5672)。如果你使用不同的主机、端口或证书,则需要调整连接设置。

从哪里获得帮助
如果您在阅读本教程时遇到困难,可以通过邮件列表 联系我们

发布/订阅#

(使用 .NET Client)

教程[2] 中,我们创建了一个工作队列,假设在工作队列中的每一个任务都只被分发给一个 Worker。那么在这一章节,我们要做与之完全不同的事,那就是我们将要把一条消息分发给多个消费者。这种模式被称为“发布/订阅”。

为了说明、体现这种模式,我们将会建一个简单的日志系统。它将会包含两个程序 - 第一个用来发送日志消息,第二个用来接收并打印它们。

在我们建立的日志系统中,每个接收程序的运行副本都会收到消息。这样我们就可以运行一个接收程序接收消息并将日志写入磁盘;同时运行另外一个接收程序接收消息并将日志打印到屏幕上。

实质上,发布的日志消息将会被广播给所有的接收者。

交换器#

在教程的前几部分,我们是发送消息到队列并从队列中接收消息。现在是时候介绍 Rabbit 中完整的消息传递模型了。

让我们快速回顾一下前面教程中的内容:

  • _生产者_是发送消息的用户应用程序。
  • _队列_是存储消息的缓冲区。
  • _消费者_是接收消息的用户应用程序。

在 RabbitMQ 中,消息传递模型的核心理念是生产者从来不会把任何消息直接发送到队列,其实,通常生产者甚至不知道消息是否会被分发到任何队列中。

然而,生产者只能把消息发送给_交换器_。交换器非常简单,一方面它接收来自生产者的消息,另一方面又会把接收的消息推送到队列中。交换器必须明确知道该如何处理收到的消息,应该追加到一个特定队列中?还是应该追加到多个队列中?或者应该把它丢弃?这些规则都被定义在_交换器类型_中。

Exchanges

目前交换器类型有这几种:directtopicheadersfanout。我们先重点关注最后一个fanout,我们创建一个这种类型的交换器,将其命名为logs

channel.ExchangeDeclare("logs", "fanout");

fanout类型交换器非常简单,正如您可能从名字中猜出的那样,它会把收到的所有消息广播到它已知的所有队列中。这恰巧是我们的日志系统目前所需要的。

列举交换器
要列举出服务器上的交换器,您可以使用非常有用的rabbitmqctl命令行工具:

sudo rabbitmqctl list_exchanges

执行上述命令后,出现的列表中将会有一些amq.*交换器和默认(未命名)交换器。这些是默认创建的,不过目前您可能用不到它们。

默认交换器
在教程的前些部分,我们对交换器这一概念还一无所知,但仍然可以把消息发送到队列。之所以这样,是因为我们使用了一个用空字符串("")标识的默认交换器。

回顾一下我们之前如何发布消息:

var message = GetMessage(args);
var body = Encoding.UTF8.GetBytes(message);
channel.BasicPublish(exchange: "",
                     routingKey: "hello",
                     basicProperties: null,
                     body: body);

第一个参数就是交换器的名称,空字符串表示默认或匿名交换器:将消息路由到routingKey指定的队列(如果存在)中。

现在,我们可以把消息发布到我们指定的交换器:

var message = GetMessage(args);
var body = Encoding.UTF8.GetBytes(message);
channel.BasicPublish(exchange: "logs",
                     routingKey: "",
                     basicProperties: null,
                     body: body);

临时队列#

您是否还记得之前我们使用过的队列,它们都有一个特定的名称(记得应该是hellotask_queue吧)。给队列命名对我们来说是至关重要的 – 因为我们可能需要多个 Worker 指向同一个队列;当您想要在生产者和消费者之间共享队列时,给队列一个名称也是非常重要的。

但是,我们创建的日志系统并不希望如此。我们希望监听所有的日志消息,而不仅仅是其中一部分。我们也只对目前流动的消息感兴趣,而不是旧消息。为解决这个问题,我们需要做好两件事。

首先,我们无论何时连接 Rabbit,都需要一个新的、空的队列。要做到这一点,我们可以使用随机名称来创建队列,或许,甚至更好的方案是让服务器为我们选择一个随机队列名称。

其次,一旦我们与消费者断开连接,与之相关的队列应该被自动删除。

在 .NET 客户端中,如果不向QueueDeclare()方法提供任何参数,实际上就是创建了一个非持久化、独占、且自动删除的随机命名队列:

var queueName = channel.QueueDeclare().QueueName;

您可以在 队列指南 中了解更多关于exclusive参数和其他队列属性的信息。

此时,queueName包含一个随机队列名称。例如,它看起来可能像amq.gen-JzTY20BRgKO-HjmUJj0wLg

绑定#

Bindings

我们已经创建好了一个fanout 交换器和一个队列。现在我们需要告诉交换器把消息发送到我们的队列。而交换器和队列之间的关系就称之为_绑定_。

channel.QueueBind(queue: queueName,
                  exchange: "logs",
                  routingKey: "");

从现在起,logs交换器会把消息追加到我们的队列中。

列举绑定
您可以使用(您或许已经猜到了),列举出现有的绑定。

sudo rabbitmqctl list_bindings

组合在一起#

生产者程序负责分发消息,这与之前的教程看起来没有太大区别。

最重要的变化是我们现在想把消息发布到我们的logs交换器,而不是匿名交换器。在发送时我们需要提供一个路由键routingKey,但是对于fanout交换器,它的值可以被忽略。这里是EmitLog.cs文件的代码:

using System;
using RabbitMQ.Client;
using System.Text;

class EmitLog
{
    public static void Main(string[] args)
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using(var connection = factory.CreateConnection())
        using(var channel = connection.CreateModel())
        {
            channel.ExchangeDeclare(exchange: "logs", type: "fanout");

            var message = GetMessage(args);
            var body = Encoding.UTF8.GetBytes(message);
            channel.BasicPublish(exchange: "logs",
                                 routingKey: "",
                                 basicProperties: null,
                                 body: body);
            Console.WriteLine(" [x] Sent {0}", message);
        }

        Console.WriteLine(" Press [enter] to exit.");
        Console.ReadLine();
    }

    private static string GetMessage(string[] args)
    {
        return ((args.Length > 0)
               ? string.Join(" ", args)
               : "info: Hello World!");
    }
}

EmitLog.cs 源码)

如你所见,在建立连接后,我们声明了交换器。这一步非常有必要,因为发布消息到一个不存在的交换器,这种情况是被禁止的。

如果没有队列绑定到交换器上,消息将会丢失,但这对我们来说并没有什么没问题;如果没有消费者正在监听,我们是可以放心地把消息丢弃的。

ReceiveLogs.cs的代码:

using System;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;

class ReceiveLogs
{
    public static void Main()
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using(var connection = factory.CreateConnection())
        using(var channel = connection.CreateModel())
        {
            channel.ExchangeDeclare(exchange: "logs", type: "fanout");

            var queueName = channel.QueueDeclare().QueueName;
            channel.QueueBind(queue: queueName,
                              exchange: "logs",
                              routingKey: "");

            Console.WriteLine(" [*] Waiting for logs.");

            var consumer = new EventingBasicConsumer(channel);
            consumer.Received += (model, ea) =>
            {
                var body = ea.Body;
                var message = Encoding.UTF8.GetString(body);
                Console.WriteLine(" [x] {0}", message);
            };
            channel.BasicConsume(queue: queueName,
                                 autoAck: true,
                                 consumer: consumer);

            Console.WriteLine(" Press [enter] to exit.");
            Console.ReadLine();
        }
    }
}

ReceiveLogs.cs 源码)

按照 教程[1]中的设置说明生成EmitLogsReceiveLogs 项目。

如果您想把日志保存到文件中,只需打开一个控制台并输入:

cd ReceiveLogs
dotnet run > logs_from_rabbit.log

如果你想在屏幕上看到日志,我可以新开一个终端并运行:

cd ReceiveLogs
dotnet run

当然,分发日志需要输入:

cd EmitLog
dotnet run

使用rabbitmqctl list_bindings命令,您可以验证代码是否真正创建了我们想要的绑定和队列。当有两个ReceiveLogs.cs程序运行时,您应该看到如下所示的内容:

sudo rabbitmqctl list_bindings




对执行结果的解释简洁明了:来自logs交换器的数据转发到了两个由服务器随机分配名称的队列。这正是我们期待的结果。

想要了解如何监听消息的这一块内容,让我们继续阅读 教程[4]

写在最后#

本文翻译自 RabbitMQ 官方教程 C# 版本。如本文介绍内容与官方有所出入,请以官方最新内容为准。水平有限,翻译的不好请见谅,如有翻译错误还请指正。

先决条件
本教程假定 RabbitMQ 已经安装,并运行在localhost 标准端口(5672)。如果你使用不同的主机、端口或证书,则需要调整连接设置。

从哪里获得帮助
如果您在阅读本教程时遇到困难,可以通过邮件列表 联系我们

介绍#

RabbitMQ 是一个消息中间件:它接收并转发消息。您可以把它想象为一个邮局:当您把需要寄出的邮件投递到邮箱,邮差最终会把邮件送给您的收件人。在这个比喻中,RabbitMQ 就是一个邮箱,也可以理解成邮局和邮递员。

RabbitMQ 和邮局的主要区别在于它不处理纸张,而是接收、存储和转发二进制数据块 - _消息_。

RabbitMQ 和消息传递通常使用一些术语。

生产 的意思无非就是发送。发送消息的程序就是一个 _生产者_:

Producer

队列 就是 RabbitMQ 内部“邮箱”的名称。虽然消息流经 RabbitMQ 和您的应用程序,但它们只能存储在 队列 中。_队列_ 只受主机的内存和磁盘的限制,它本质上就是一个很大的消息缓冲区。多个 生产者 可以发送消息到一个队列,并且多个 消费者 可以尝试从一个 队列 接收数据。这就是我们代表队列的方式:

Queue

消费 与接收有相似的含义,等待接收消息的程序就是一个 _消费者_:

Consumer

注意:生产者、消费者和中间件不是必须部署在同一主机上,实际上在大多数应用程序中它们也不是这样的。

“Hello World”#

使用 .NET / C#Client

在教程的这一部分,我们将用 C# 编写两个程序:一个发送单条消息的生产者,以及接收消息并将其打印出来的消费者。我们将忽略 .NET 客户端 API 中的一些细节,专注于更简单的开始。这是一个消息传递的“Hello World”。

在下图中,P是我们的生产者,C是我们的消费者。中间的盒子是队列 - RabbitMQ 代表消费者保存的消息缓冲区。

.NET 客户端库

RabbitMQ 支持多种协议,本教程使用AMQP 0-9-1,它是一种开放的、通用的消息传递协议。RabbitMQ 提供了一些针对不同 语言环境 的客户端,我们将使用 RabbitMQ 提供的 .NET 客户端。

客户端支持 .NET Core 以及 .NET Framework 4.5.1+。本教程将使用 .NET Core,因此您需要确保客户端已 安装 并且路径添加到PATH系统变量。

您也可以使用 .NET Framework 来完成本教程,但设置步骤会有所不同。

RabbitMQ .NET 客户端 5.0 及更高版本通过 nuget 发布。

本教程假定您在 Windows 上使用 PowerShell。在 MacOS 和 Linux 上,几乎所有 shell 也都可以正常工作。

安装#

首先让我们验证您在PATH系统变量是否有 .NET Core 工具链:

dotnet --help

应该产生帮助信息。

现在,让我们生成两个项目,一个用于发布者,另一个用于消费者:

dotnet new console --name Send
mv Send/Program.cs Send/Send.cs
dotnet new console --name Receive
mv Receive/Program.cs Receive/Receive.cs

这将创建两个名为SendReceive的新目录。

然后,我们添加客户端依赖项。

cd Send
dotnet add package RabbitMQ.Client
dotnet restore
cd ../Receive
dotnet add package RabbitMQ.Client
dotnet restore

我们已经建立了 .NET 项目,现在我们可以编写一些代码。

发送#

我们将调用我们的消息发布者(发送者)Send.cs和我们的消息消费者(接收者)Receive.cs。发布者将连接到 RabbitMQ,发送一条消息,然后退出。

Send.cs 中,我们需要使用一些命名空间:

using System;
using RabbitMQ.Client;
using System.Text;

设置类:

class Send
{
    public static void Main()
    {
        ...
    }
}

然后,我们可以创建一个连接,连接到服务器:

class Send
{
    public static void Main()
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using (var connection = factory.CreateConnection())
        {
            using (var channel = connection.CreateModel())
            {
                ...
            }
        }
    }
}

该连接抽象了套接字连接,并为我们处理协议版本的协商和身份验证等。在这里,我们连接的是本地机器上的代理, 因此是localhost。如果我们想连接到其他机器上的代理,我们只需在此指定其名称或 IP 地址。

接下来,我们创建一个通道,该 API 的主要功能是把获得信息保存起来。

想要发送消息,我们必须为需要发送的消息声明一个队列,然后我们就可以把消息发布到队列中:

using System;
using RabbitMQ.Client;
using System.Text;

class Send
{
    public static void Main()
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using(var connection = factory.CreateConnection())
        using(var channel = connection.CreateModel())
        {
            channel.QueueDeclare(queue: "hello",
                                 durable: false,
                                 exclusive: false,
                                 autoDelete: false,
                                 arguments: null);

            string message = "Hello World!";
            var body = Encoding.UTF8.GetBytes(message);

            channel.BasicPublish(exchange: "",
                                 routingKey: "hello",
                                 basicProperties: null,
                                 body: body);
            Console.WriteLine(" [x] Sent {0}", message);
        }

        Console.WriteLine(" Press [enter] to exit.");
        Console.ReadLine();
    }
}

声明队列是 幂等 的 - 只有当它不存在时才会被创建。消息内容是一个字节数组,所以您可以用喜欢的任意方式编码。

当上面的代码完成运行时,通道和连接将被释放。这就是我们的发布者。

Send.cs 源码)

发送不起作用!

如果这是您第一次使用 RabbitMQ,并且您没有看到“已发送”消息,那么您可能会挠着头想知道错误出在什么地方。也许是代理程序启动时没有足够的可用磁盘空间(默认情况下,它至少需要50 MB空闲空间),因此拒绝接收消息。
必要时检查代理程序日志文件来确认和减少限制。配置文件 文档 将告诉您如何设置disk_free_limit

接收#

至于消费者,它是把消息从 RabbitMQ 拉取过来。因此,与发布消息的发布者不同,我们会保持消费者持续不断地运行,监听消息并将其打印出来。

代码(在 Receive.cs 中)具有与Send差不多一样的using声明:

using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;

设置与发布者相同;我们开启一个连接和一个通道,并声明我们将要使用的队列。请注意,这需要与Send发布到的队列相匹配。

class Receive
{
    public static void Main()
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using (var connection = factory.CreateConnection())
        {
            using (var channel = connection.CreateModel())
            {
                channel.QueueDeclare(queue: "hello",
                                     durable: false,
                                     exclusive: false,
                                     autoDelete: false,
                                     arguments: null);
                ...
            }
        }
    }
}

请注意,我们在这里也声明了队列。因为我们可能会在发布者之前启动消费者,所以我们希望在我们尝试从它中消费消息之前确保队列已存在。

我们即将告诉服务器将队列中的消息传递给我们。由于它会异步推送消息,因此我们提供了一个回调。这就是EventingBasicConsumer.Received事件处理程序所做的事情。

using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;

class Receive
{
    public static void Main()
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using(var connection = factory.CreateConnection())
        using(var channel = connection.CreateModel())
        {
            channel.QueueDeclare(queue: "hello",
                                 durable: false,
                                 exclusive: false,
                                 autoDelete: false,
                                 arguments: null);

            var consumer = new EventingBasicConsumer(channel);
            consumer.Received += (model, ea) =>
            {
                var body = ea.Body;
                var message = Encoding.UTF8.GetString(body);
                Console.WriteLine(" [x] Received {0}", message);
            };
            channel.BasicConsume(queue: "hello",
                                 autoAck: true,
                                 consumer: consumer);

            Console.WriteLine(" Press [enter] to exit.");
            Console.ReadLine();
        }
    }
}

Receive.cs 源码)

组合在一起#

打开两个终端。

运行消费者:

cd Receive
dotnet run

运行生产者:

cd Send
dotnet run

消费者将打印它通过 RabbitMQ 从发布者处获得的消息。消费者将继续运行、等待新消息(按Ctrl-C将其停止),可以尝试从开启另一个终端运行发布者。

接下来可以跳转到 教程[2],构建一个简单的工作队列。

写在最后#

本文翻译自 RabbitMQ 官方教程 C# 版本。如本文介绍内容与官方有所出入,请以官方最新内容为准。水平有限,翻译的不好请见谅,如有翻译错误还请指正。

先决条件
本教程假定 RabbitMQ 已经安装,并运行在localhost 标准端口(5672)。如果你使用不同的主机、端口或证书,则需要调整连接设置。

从哪里获得帮助
如果您在阅读本教程时遇到困难,可以通过邮件列表 联系我们

主题#

(使用 .NET 客户端)

教程[4] 中,我们改进了我们日志系统。我们用direct交换器替换了只能呆滞广播消息的fanout交换器,从而可以有选择性的接收日志。

虽然使用direct交换器改进了我们的系统,但它仍然有局限性 - 不能基于多个标准进行路由。

在我们的日志系统中,我们可能不仅要根据日志的严重性订阅日志,可能还要根据日志分发源来订阅日志。或许您可能从 unix syslog 工具中了解过这种概念,syslog 工具在路由日志的时候是可以既基于严重性(info/warn/crit…)又基于设备(auth/cron/kern…)的。

这种机制会给我们带来极大的灵活性 - 我们可以仅监听来自cron的关键错误日志,与此同时,监听来自kern的所有日志。

要在我们的日志系统中实现这一特性,我们需要学习更复杂的topic交换器。

Topic交换器#

发送到topic交换器的消息不能随意指定routing key,它必须是一个由点分割的单词列表,这些单词可以是任意内容,但通常会在其中指定一些与消息相关的特性。请看一些合法的路由键示例:stock.usd.nysenyse.vmwquick.orange.rabbit,路由键可以包含任意数量的单词,但不能超过255个字节的上限。

binding key也必须是相同的形式,topic交换器的背后逻辑与direct交换器类似 - 使用指定路由键发送的消息会被分发到与其绑定键匹配的所有队列中。不过对于绑定键来说,有两个重要的特殊情况需要注意:

  • *(星号)可以代替一个单词。
  • #(哈希)可以代替零个或多个单词。

下图示例是对上述内容最简单的解释:

在这个示例中,我们打算发送的消息全是用来描述动物的,这些消息会使用由三个单词(两个点)组成的路由键来发送。在路由键中,第一个单词用来描述行动速度、第二个是颜色、第三个是物种,即:<speed>.<colour>.<species>

我们创建了三个绑定:Q1绑定了键.orange.,Q2绑定了键*.*.rabbitlazy.#

这些绑定可以被概括为:

  • Q1对所有橙色的动物感兴趣。
  • Q2对兔子以及所有行动缓慢的动物感兴趣。

路由键为quick.orange.rabbit的消息会被发送到这两个队列,消息lazy.orange.elephant也会被发送到这两个队列。另外,quick.orange.fox只会进入第一个队列,lazy.brown.fox只会进入第二个队列。lazy.pink.rabbit只会被发送到第二个队列一次,尽管它匹配了两个绑定(避免了消息重复)。quick.brown.fox没有匹配的绑定,因此它将会被丢弃。

如果我们打破约定,发送使用一个或四个单词(例如:orangequick.orange.male.rabbit)作路由键的消息会发生什么?答案是,这些消息因为没有匹配到任何绑定,将被丢弃。

但是,另外,例如路由键为lazy.orange.male.rabbit的消息,尽管它有四个单词,也会匹配最后一个绑定,并将被发送到第二个队列。

Topics 交换器
topic交换器的功能是很强大的,它可以表现出一些其他交换器的行为。
当一个队列与键(哈希)绑定时, 它会忽略路由键,接收所有消息,这就像fanout交换器一样。
当特殊字符*(星号)和(哈希)未在绑定中使用时,topic交换器的行为就像direct交换器一样。

组合在一起#

我们将要在我们的日志系统中使用topic交换器,首先假设日志的路由键有两个单词组成:<facility>.<severity>

代码与上一篇 教程 中的代码几乎相同。

EmitLogTopic.cs的代码:

using System;
using System.Linq;
using RabbitMQ.Client;
using System.Text;

class EmitLogTopic
{
    public static void Main(string[] args)
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using(var connection = factory.CreateConnection())
        using(var channel = connection.CreateModel())
        {
            channel.ExchangeDeclare(exchange: "topic_logs",
                                    type: "topic");

            var routingKey = (args.Length > 0) ? args[0] : "anonymous.info";
            
            var message = (args.Length > 1)
                          ? string.Join(" ", args.Skip(1).ToArray())
                          : "Hello World!";
            var body = Encoding.UTF8.GetBytes(message);
            
            channel.BasicPublish(exchange: "topic_logs",
                                 routingKey: routingKey,
                                 basicProperties: null,
                                 body: body);
                                 
            Console.WriteLine(" [x] Sent '{0}':'{1}'", routingKey, message);
        }
    }
}

ReceiveLogsTopic.cs的代码:

using System;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;

class ReceiveLogsTopic
{
    public static void Main(string[] args)
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using(var connection = factory.CreateConnection())
        using(var channel = connection.CreateModel())
        {
            channel.ExchangeDeclare(exchange: "topic_logs", type: "topic");
            var queueName = channel.QueueDeclare().QueueName;

            if(args.Length < 1)
            {
                Console.Error.WriteLine("Usage: {0} [binding_key...]",
                                        Environment.GetCommandLineArgs()[0]);
                Console.WriteLine(" Press [enter] to exit.");
                Console.ReadLine();
                Environment.ExitCode = 1;
                return;
            }

            foreach(var bindingKey in args)
            {
                channel.QueueBind(queue: queueName,
                                  exchange: "topic_logs",
                                  routingKey: bindingKey);
            }

            Console.WriteLine(" [*] Waiting for messages. To exit press CTRL+C");

            var consumer = new EventingBasicConsumer(channel);
            consumer.Received += (model, ea) =>
            {
                var body = ea.Body;
                var message = Encoding.UTF8.GetString(body);
                var routingKey = ea.RoutingKey;
                Console.WriteLine(" [x] Received '{0}':'{1}'",
                                  routingKey,
                                  message);
            };
            channel.BasicConsume(queue: queueName,
                                 autoAck: true,
                                 consumer: consumer);

            Console.WriteLine(" Press [enter] to exit.");
            Console.ReadLine();
        }
    }
}

请运行以下示例:

要接收所有日志:

cd ReceiveLogsTopic
dotnet run "#"

要接收来自设备kern的所有日志:

cd ReceiveLogsTopic
dotnet run "kern.*"

或者,如果您只想监听级别为critical的日志:

cd ReceiveLogsTopic
dotnet run "*.critical"

您可以创建多个绑定:

cd ReceiveLogsTopic
dotnet run "kern.*" "*.critical"

使用路由键kern.critical发出日志:

cd EmitLogTopic
dotnet run "kern.critical" "A critical kernel error"

希望运行这些程序能让您玩得开心。要注意的是,这些代码没有针对路由键和绑定键做任何预设,您可以尝试使用两个以上的路由键参数。

EmitLogTopic.csReceiveLogsTopic.cs 的完整源码)

接下来,在 教程[6] 中将了解如何将往返消息作为远程过程调用。

写在最后#

本文翻译自 RabbitMQ 官方教程 C# 版本。如本文介绍内容与官方有所出入,请以官方最新内容为准。水平有限,翻译的不好请见谅,如有翻译错误还请指正。

  • 原文链接:RabbitMQ tutorial - Topics
  • 实验环境:RabbitMQ 3.7.4 、.NET Core 2.1.3、Visual Studio Code
  • 最后更新:2018-09-06

一、 核心概念与工具

1.1 启动模式检测:UEFI vs BIOS

在开始分区前,需确定系统的启动模式,这将决定分区表类型。

1
2
# 检查是否为 UEFI 启动
ls /sys/firmware/efi/efivars
  • 目录存在:系统以 UEFI 模式启动,必须使用 GPT 分区表
  • 目录不存在:系统以传统 BIOS/CSM 模式启动,通常使用 MBR 分区表
1.2 分区工具选择
  • fdisk:传统工具,简单易用,但不支持 GPT 分区表,无法处理大于 2TB 的磁盘。
    1
    2
    fdisk -l # 查看所有分区
    # 常用交互命令:p(显示), n(新建), d(删除), t(改类型), w(写入), q(退出)
  • parted:功能强大的现代化工具,支持 GPT 和 MBR,可处理任意大小的磁盘,支持脚本化。
1.3 parted 命令详解

基本语法parted [选项] <设备> [命令]

  • 常用选项
    • -l:列出所有设备的分区信息。
    • -s:脚本模式,不提示用户。
    • -i:交互模式(默认)。

交互模式常用命令

命令 说明
print 打印当前磁盘的分区表。
mklabel <类型> 创建新的磁盘标签(分区表),如 gptmsdos (MBR)。
mkpart <类型> [文件系统] 起点 终点 创建分区。类型:primary(主), extended(扩展), logical(逻辑)。
rm <分区号> 删除指定编号的分区。
set <分区号> <标志> on/off 设置分区标志,如 boot, lvm
unit <单位> 设置显示单位,如 GB, MB, s(扇区), %(百分比)。
quit 退出。

示例:创建 GPT 分区表并分区

1
2
3
4
5
6
parted /dev/sda
(parted) mklabel gpt
(parted) mkpart primary ext4 1MiB 500GiB
(parted) mkpart primary linux-swap 500GiB 508GiB
(parted) print
(parted) quit
1.4 GPT 分区表简介

对于 UEFI 系统或大容量磁盘(>2TB),推荐使用 **GPT (GUID Partition Table)**。

  • 优点
    • 支持超过 2TB 的磁盘。
    • 最多支持 128 个主分区。
    • 包含冗余的备份分区表,更健壮。
    • 使用全局唯一标识符 (GUID)。
  • 结构:包含一个保护性 MBR (LBA 0)、GPT 头 (LBA 1) 和分区表项 (LBA 2-33)。备份结构位于磁盘末尾。

二、 实战分区流程(以 Arch Linux 安装为例)

2.1 环境检查与规划
  1. 检查启动模式(见 1.1)。
  2. 规划分区方案(示例):
挂载点 设备 大小 文件系统 说明
/boot/efi (UEFI) 或 /boot (BIOS) /dev/sda1 512 MiB FAT32 EFI 系统分区 (ESP) 或 BIOS 启动分区。
[SWAP] /dev/sda2 8 GiB swap 交换空间。
/ /dev/sda3 200 GiB ext4 根分区。
/home /dev/sda4 剩余空间 ext4 用户家目录。
2.2 执行分区操作

使用 partedfdisk 按规划创建分区。以下是 parted 的 UEFI+GPT 方案示例:

1
2
3
4
5
6
7
8
9
parted /dev/sda
(parted) mklabel gpt
(parted) mkpart ESP fat32 1MiB 513MiB
(parted) set 1 esp on # 为 ESP 分区设置 `esp` 标志
(parted) mkpart primary linux-swap 513MiB 8705MiB # 8GiB swap
(parted) mkpart primary ext4 8705MiB 208705MiB # 200GiB root
(parted) mkpart primary ext4 208705MiB 100% # 剩余空间给 home
(parted) print
(parted) quit
2.3 格式化分区
1
2
3
4
5
6
7
8
9
10
11
# UEFI: 格式化 ESP 分区为 FAT32
mkfs.fat -F 32 /dev/sda1

# 格式化交换分区
mkswap /dev/sda2
swapon /dev/sda2 # 启用

# 格式化根分区和家目录分区
mkfs.ext4 /dev/sda3
mkfs.ext4 /dev/sda4
# 或使用其他文件系统,如 xfs: mkfs.xfs /dev/sda4
2.4 挂载分区
1
2
3
4
5
6
7
8
9
10
11
12
13
# 挂载根分区
mount /dev/sda3 /mnt

# 创建并挂载其他目录
mkdir -p /mnt/boot/efi # UEFI
mount /dev/sda1 /mnt/boot/efi

mkdir -p /mnt/home
mount /dev/sda4 /mnt/home

# BIOS 系统挂载 /boot (如果单独分区)
# mkdir -p /mnt/boot
# mount /dev/sda1 /mnt/boot
2.5 生成引导(GRUB 示例)
  • BIOS 系统
    1
    2
    3
    pacman -S grub os-prober
    grub-install --target=i386-pc --recheck /dev/sda # 注意是磁盘 (sda),不是分区 (sda1)
    grub-mkconfig -o /boot/grub/grub.cfg
  • UEFI 系统
    1
    2
    3
    pacman -S grub efibootmgr
    grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=GRUB
    grub-mkconfig -o /boot/grub/grub.cfg

三、 高级案例与脚本

3.1 故障处理:坏盘更换与重建

案例:在 RAID 卡(如 MegaRAID)环境下更换故障硬盘并重建。

1
2
3
4
5
6
7
8
# 1. 清除控制器上保留的缓存(如果之前有虚拟磁盘丢失)
/opt/MegaRAID/MegaCli/MegaCli64 -DiscardPreservedCache -L<VD编号> -a0

# 2. 将新物理盘做成单盘 RAID0(根据 Enclosure ID 和 Slot Number)
/opt/MegaRAID/MegaCli/MegaCli64 -CfgLdAdd -r0[8:11] WB RA Direct -a0

# 3. 开始重建(可选,通常会自动开始)
/opt/MegaRAID/MegaCli/MegaCli64 -PDRbld -ShowProg -PhysDrv [8:11] -a0

之后,对新磁盘 (/dev/sdk) 进行分区、格式化、更新 /etc/fstab 并挂载。

注意:如果 mount -a 失败,尝试执行 systemctl daemon-reload 后重试。

3.2 自动化分区脚本

以下脚本用于将单块磁盘快速分区为单个 GPT 分区并格式化为 ext4。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
# fenqu.sh - 快速格式化单磁盘为单个分区
disk_to_parted="$1"
if [ $# -eq 0 ]; then
echo "Usage: $0 /dev/sdX"
exit 1
fi

echo "****** Partitioning ${disk_to_parted} ... ******"
parted -s ${disk_to_parted} mklabel gpt
parted -s ${disk_to_parted} mkpart primary 0% 100%

echo "****** Formatting ${disk_to_parted}1 as ext4 ... ******"
mkfs.ext4 ${disk_to_parted}1

echo "****** New UUID: ******"
blkid ${disk_to_parted}1

使用方式sh fenqu.sh /dev/sdb

3.3 批量分区与挂载

在多磁盘环境中(如 Hadoop 数据节点),可以结合循环和 blkid 实现批量操作。

1
2
3
4
5
6
7
8
9
10
11
# 1. 批量分区格式化(排除系统盘 sda 和特定盘 sdb)
for i in $(lsblk | grep -v sda | awk '{print $1}' | sed '1d' | grep -v sdb); do
sh fenqu.sh /dev/${i}
done

# 2. 获取所有新分区的 UUID,用于编写 /etc/fstab
blkid | grep -v sda | grep -v sdb | awk -F '"' '{print "UUID="$2" /hadoop/"NR" ext4 defaults 0 0"}'

# 3. 创建挂载点并挂载
for i in {1..8}; do mkdir -p /hadoop/0$i; done
mount -a

四、 关键注意事项与技巧

  1. 分区对齐:使用 parted 时,建议起点从 1MiB (2048扇区) 开始,以获得最佳性能,避免“未对齐”警告。
  2. 谨慎操作parted 命令在交互模式下立即生效,没有类似 fdisk 的“写入”确认步骤。删除或格式化前务必确认设备名。
  3. 低格填零:如需彻底清空磁盘(如安全擦除),可使用 dd,但极其耗时且伤盘。
    1
    dd if=/dev/zero of=/dev/sda bs=16M status=progress
  4. 文件系统选择
    • ext4:稳定、兼容性好,通用选择。
    • xfs:擅长处理大文件和高并发,适合数据存储。
    • btrfs:支持快照、压缩等高级特性,但相对年轻。
  5. **/etc/fstab**:使用 UUID= 而非 /dev/sdX 来标识分区,避免设备名变化导致启动失败。使用 blkid 命令获取 UUID。

1. 下载镜像 制作启动U盘

Arch Linux 官方网站 https://www.archlinux.org/

制作启动盘工具 Rufus - 轻松创建 USB 启动盘

Linux下

1
dd if=*iso of /dev/sdb bs=41M

2. 网络连接

参考 Linux配置网络及SSH配置

3. 选择软件源

推荐国内的用户选择http://mirrors.ustc.edu.cn 默认的mirrorlist是开启所有源的,因此我们使用sed先在所有源的前面加上#

1
2
#sed -i "s/^\b/#/g" /etc/pacman.d/mirrorlist
#nano /etc/pacman.d /mirrorlist

将mirrors.ustc.edu.cn前面的#去掉

4. 分区/格式化/挂载

参考 Linux硬盘分区

5. 安装基本系统

1. 将基本系统安装到根目录上去

1
#pacstrap /mnt base base-devel
其实,这里安装的基本系统也肯定有自己用不到的冗余功能,例如我就用不到nano文本编辑器,但系统会默认给安上。如果知道基本系统每个文件的作用,其实也完全可以自定义安装。比如:
1
2
#pacstrap /mnt bash coreutils file filesystem grub2 linux pacman \
procps-ng syslog-ng glibc systemd-sysvcompat shawd dhcpcd vi
> 如果你想使用ifconfig之类的工具,请在上面加上net-tools

2. 生成fstab

用下面命令生成 fstab。如果想使用 UUIDs,使用 -U 选项;如果想使用标签,用 -L 选项.

1
#genfstab -U -p /mnt >>/mnt/etc/fstab
> [red]**后面如果出现问题,请不要再次运行genfstab**[red],如果需要,手动编辑/etc/fstab /etc/fstab文件在运行genfstab后应该被检查一下。如果之前你生成了一个EFI系统分区,那么 genfstab给EFI分区添加了错误的选项,会导致无法启动。因此你需要移除EFI分区的所有选项,除了noatime. 对其他分区, 替换"codepage=cp437" 为 "codepage=437" , 会挂载失败导致systemd进入恢复模式。

3. 切换到新系统中

1
2
#arch-chroot /mnt
#sh-4.2#bash
> 到这一步之后,开始系统的主要配置,如果下面文件不存在,需要手动创建。 > 理解并完全安装步骤设置是保证系统配置成功的关键。

4. 对新的基本系统进行设置

写入本机的字符编码方式

1
2
3
#nano /etc/locale.conf #

LANG=en_US.UTF-8 #简略写法 echo LANG= en_US.UTF-8 >> locale.conf

locale.conf 文件默认不存在,一般设置LANG就行了,它是其它设置的默认值。

1
2
3
4
/etc/locale.conf

LANG=zh_CN.UTF-8
LC_TIME=en_GB.UTF-8

修改本机编码

1
# nano /etc/locale.gen  将用不到的编码全删掉,只保留en_US与zh_CN的几行。 

默认情况下 /etc/locale.gen 是一个仅包含注释文档的空文件。选定你需要的本地化类型(移除前面的#即可), 比如中文系统可以使用:

1
2
3
4
5
en_US.UTF-8 UTF-8
zh_CN.GB18030 GB18030
zh_CN.GBK GBK
zh_CN.UTF-8 UTF-8
zh_CN GB2312

对系统的编码进行更新

1
#locale-gen 

写入本机的名称

1
# nano /etc/hostname #简略写法:echo {name} >/etc/hostname,也是一样的。

写入键盘布局方案

1
#nano /etc/vconsole.conf

美式键盘,如下:

1
2
3
KEYMAP=us
FONT=
FONT_MAP=

写入时区

1
2
3
# nano /etc/timezone

Asia/Shanghai

建立时区的软链接

1
#ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

设定系统将用的时间方案

1
#hwclock --systohc --utc

这个时间方案我是试过很多次,如果是双系统,电脑里还有win系统的话,建议设为:–localtime,否则可设为—utc。不过,我现在虽然也用双系统,但还是设的utc,因为设为–localtime虽然在win下时间不会出错,但回到linux下,经常系统会有些古怪的问题,比如,升级系统之时,报密钥错误。使用–utc,虽然在linux下时间会慢8个多小时,但毕竟对整个系统没有影响。

生成内核的启动镜象。

1
#mkinitcpio -p linux

安装必要工具

安装必要的网络工具以便于开机后可以配置网络连接(包括无线)

1
2
3
4
#pacman –S wpa_supplicant net-tools
#pacman -S dialog
#pacman -S netctl
#pacman -S wireless_tools

6. 安装引导

安装grub

1
2
#pacman -S grub-bios os-prober
#grub-install /dev/sda

UEFI 注意分区,参考: Linux分区.格式化.挂载

1
2
#pacman -S grub-bios efibootmgr os-prober
#grub-install --efi-directory=/boot/efi --bootloader-id=arch-grub --target=x86_64-efi

生成启动菜单

1
2
3

#grub-mkconfig -o /boot/grub/grub.cfg
#nano /boot/grub/grub.cfg

生成grub引导windows

如何生成grub引导文件grub.cfg 这里我们需要充分参考点击打开链接grub的说明。首先,需要额外安装一个 os-prober的软件包,直接pacman就行;然后grub-makeconfig 到/boot/grub/grub.cfg 。此时才能生成可以引导多系统的引导文件.如下图。
Window引导

开机自启网络

1
2
#systemctl enable dhcpcd@.service
#dhcpcd

卸载挂载的分区并重启

1
2
#umount /mnt/{boot,home,mnt}
# reboot

基本系统已安装完成

7. 系统配置

忘记安装net-tools补救

1
2
ip link show #查看网卡
ip link set eth0 up # 启用网卡

如果是DHCP的当然简单,直接dhcpcd即可,如果是固定IP的,则要如下操作:

1
2
3
#ip addr add 固定IP/24 dev eth0
#ip link set dev eth0 up
#ip route add default via 网关

系统更新

1
#pacman –Syu

添加用户

1
2
3
#useradd -m 新用户 #新建用户
#passwd 新用户 #指定密码:
#usermod -a -G video,audio,lp,log,wheel,optical,scanner,games,users,storage,power 新用户 #指定用户所在的组

sudo权限

1
nano /etc/sudoers (添加sudo权限)

放开%wheel %sudo权限

sudo命令补全

1
2
#sudo pacman -S bash-completion 
#echo "source /usr/share/bash-completion/bash_completion" >>/home/$USER/.bashrc

更新源列表

1
#pacman -S reflector 

reflector是一个可以从arch官方MirrorStatus列表取回最新mirrorlist的脚本,并且可以根据最新同步时间和速度排序。
下面先说如何自动配置源列表。直接终端输入命令5(注意备份原有源列表)

1
#reflector --verbose --country 'China' -l 200 -p http --sort rate --save /etc/pacman.d/mirrorlist

安装yaourt

1
#vim /etc/pacman.conf
1
2
3
4
[archlinuxcn]
#The Chinese Arch Linux communities packages.
SigLevel = Optional TrustAll
Server = http://repo.archlinuxcn.org/$arch
1
# pacman -Syu yaourt

安装powerpill

1
#yaourt -S powerpill

powerpill是一个可以从多个源多线程下载软件包的程序,类似于迅雷一样,可以明显提升更新速度,相当于pacman的外壳程序,使用方法完全和pacman相同。下面说说powerpill,玩arch的人不知道powerpill是不行的,需要注意的是它也是要调用reflector的,但并不是作为依赖。如果安装reflector后powerpill更新前会默认从mirrorstatus取回45个最新更新的源地址,然后并行下载,否则就是读取/etc/pacman.d/mirrorlist然后配置下载。当然我们推荐第一种,总不能每次都手动执行

7. 驱动显卡

安装显卡驱动

1
2
3
4
5
6
7
# pacman -S mesa
# lspci | grep VGA(查看本机的显卡类型)
# pacman -Ss xf86-video | less(查看能够安装的显卡类型)
# pacman -S …… 安装显卡驱动(或者可以直接所有驱动都自动安装)
# pacman –S xf86-video-vesa
# pacman –S xf86-video-nouveau #如果是ATI显卡的话,要安xf86-video-ati;
# pacman –S virtualbox-guest-utils #虚拟机

安装系统基础程序:

1
2
# pacman -S xorg-server xorg-xinit xorg-utils xorg-server-utils dbus # 先安装x-window服务
# pacman –S xterm xorg-xclock xorg-twm # 安装测试环境

重设系统的编码方式

编辑.xinitrc,把以下内容添加到文件最开始。内可以使用你所喜欢的编辑器,比如nano

1
2
LANG=zh_CN.UTF-8
LC_ALL="zh_CN.UTF-8"

更新系统的编码:

1
#locale-gen

更新一下系统的时间

1
2
# date -s "2013-01-14 14:40:10"
# hwclock --systohc

音频管理

1
# pacman -S alsa-utils pulseaudio-alsa

安装网络管理工具

1
2
3
# pacman –S networkmanager network-manager-applet wireless_tools
# systemctl enable NetworkManager
# systemctl start NetworkManager

安装桌面

击右键菜单,找到文件管理器,然后进入到目录/usr/share/applications/下,你会看到你已经安装完成的程序,全都可以从这儿启动。此时,你不妨复制几个常用的到你的用户目录:/home/新用户/桌面/下去。复制之后,你会在你的桌面上,看到这些程序的启动器。

安装完ibus之后,在/home/$USER/.xinitrc文件中,写入:

1
2
3
4
export GTK_IM_MODULE=ibus
export QT_IM_MODULE=ibus
export XMODIFIERS=@im=ibus
ibus-daemon -d -x

Windows下的磁盘挂载
参考Windows下的磁盘挂载

Xfce主题

字体及补丁

1
2
3
# pacman -S ttf-dejavu ttf-ubuntu-font-family ttf-arphic-ukai ttf-arphic-uming
# pacman -S wqy-microhei wqy-bitmapfont wqy-zenhei ttf-fireflysung
$ yaourt -S cairo-ubuntu libxft-ubuntu freetype2-ubuntu fontconfig-ubuntu #以普通用户身份执行

安装系统主题:

1
sudo pacman -S gtk-aurora-engine gtk-engine-murrine 

鼠标主题:

1
sudo pacman -S xcursor-vanilla-dmz xcursor-vanilla-dmz-aa

图标主题:

1
# pacman -S gnome-icon-theme-extras oxygen-icons human-icon-theme lxde-icon-theme tangerine-icon-theme

针对笔记本电脑的配置:(Speed-step 、 Suspend 等功能)

1
2
# pacman -S  gnome-power-manager  volumeicon
$ yaourt -S laptop-mode-tools pmount

Grub主题
在启动过程中发现Xfce桌面启动载入真心简陋,没有关系,我们在AUR里下载一个balou并设置就好了。

1
$ yaourt -S archlinux-themes-balou

下面来配置grub的启动界面。AUR里有一个非常棒的包grub2-theme-archlinux

1
$ yaourt -S grub2-theme-archlinux

安装后编辑/etc/default/grub,
#GRUB_THEME="/path/to/gfxtheme"改为GRUB_THEME="/boot/grub/themes/Archlinux/theme.txt"
GRUB_GFXMODE=auto改为GRUB_GFXMODE=1024x768修改完成后重新生成一下启动文件

1
# grub-mkconfig -o /boot/grub/grub.cfg

安装 i3 窗口管理器

1
# pacman -S i3

安装 lightdm 显示管理器,

1
# pacman -S lightdm-gtk3-greeter

然后

1
2
# systemctl enable lightdm
# systemctl start lightdm

8. 桌面及美化

本文方案适用于Microsoft Sql Server 2008/2012/2012 r2/2014版本,以下简称MSSQLSERVER。

MSSQL默认是不允许远程连接,并且禁用sa账户的。如果想要在本地用SSMS连接远程服务器上的MSSQLSERVER,需要做两个部分的配置:

1. SQL SERVER MANAGEMENT STUDIO(SSMS)

2. SQL SERVER配置管理器(SQL SERVER CONFIGURATION MANAGER - SSCM)

并且需要注意的是,有些地方如果没有生效,请重启一下sql server(可以从SSCM里,也可以从系统服务中找),下面是详细的步骤:

STEP1. 打开SSMS,使用Windows身份连接数据库,登录后,右键选择“属性”

STEP 2. 选择“安全性”,选中SQL SERVER和Windows身份验证模式

STEP 3. 再选择“连接”,勾选“允许远程连接此服务器”,然后点击“确定”按钮。

STEP 4. 展开“安全性” -》登录名 -》sa,右键选择“属性”

STEP 5. 在“常规”中,改好你自己的密码,这是你sa登录的密码。

STEP 6. 在“状态”中,启用sa登录,点击“确定”按钮

STEP 7. 右键数据库server,选择“方面”

STEP 8. 选择“服务器配置”,找到RemoteAccessEnabled,设置为“True”

STEP 9. 重新启动SQL SERVER服务,退出当前的连接,这时候应该可以用sa进行登录了。

STEP 10. 配置SSCM,选中左侧的“SQL SERVER服务”,确保右侧的“SQL SERVER”以及“SQL SERVER BROWER”正在运行,选择“网络配置”,双击TCP/IP,确保状态为“启用”

STEP 11. 在Client里也确保TCP/IP是启用的,默认的端口都是1433,可以自己修改,非默认端口需要在连接字符串里显式指明。

STEP 12. 到这里再次重启SQL SERVER服务,应该就可以用了。不过还不能用,确认防火墙端口设置,并把SQL SERVER安装目录下,C:\Program Files\Microsoft SQL Server\MSSQL10.SQLEXPRESS\MSSQL\Binn\sqlservr.exe添加到允许的列表中。

国标ID是在国标GB/T28181中使用的ID,在一个空间下保持唯一,命名规则应遵从GB/T 28181-2016《公共安全视频监控联网系统信息传输、交换、控制技术要求》。

国标ID由中心编码(8位)、行业编码(2位)、类型编码(3位)、网络标识(1位)和序号(6位)共20位十进制数字字符构成,其中:

  • 中心编码指用户或设备所归属的监控中心编码,按照监控中心所在地的行政区划代码确定,当不是基层单位时空余为0,行政区划分代码采用GB/T 2260-2007规定的行政区划代码表示。
  • 行业编码是指用户或设备所归属的行业,规则说明见“行业编码规则”。
  • 类型编码指定了设备或用户的具体类型。
  • 详细说明如下“详细规则说明”。

详细编码规则

gb2

行业编码规范

参考

1、安装nuget包MSBuildTasks

2、编辑项目的csproj文件,找到被注释掉的target的beforebuild,去掉注释,添加如下代码。代码如下。

1
2
3
4
5
6
7
8
9
<Target Name="BeforeBuild">
<Version VersionFile="Propertiesversion.txt" Major="1" Minor="0" BuildType="Automatic" StartDate="09/01/2017" RevisionType="BuildIncrement">
<Output TaskParameter="Major" PropertyName="Major" />
<Output TaskParameter="Minor" PropertyName="Minor" />
<Output TaskParameter="Build" PropertyName="Build" />
<Output TaskParameter="Revision" PropertyName="Revision" />
</Version>
<AssemblyInfo CodeLanguage="CS" OutputFile="Properties\FileVersionInfo.cs" AssemblyFileVersion="$(Major).$(Minor).$(Build).$(Revision)" />
</Target>     

3、编译项目一次就会在项目文件夹下生成 Propertiesversion.txt

4、在AssemblyInfo.cs文件中包含了AssemblyVersion和AssemblyFileVersion,这里把AssemblyFileVersion单独放到了FileVersionInfo.cs中,编译时会自动生成FileVersionInfo.cs文件,其内容为AssemblyFileVersion,把该文件包含到项目中即可。这里并没有让程序自动生成AssemblyVersion。

这种方式生成版本号会在vs编译时更新版本号。

1.1 InstallShield的安装

  InstallShield 2010下载地址  点击这里 ,安装方法网上有很多就不赘述了。

1.2 绿色版安装包的前期准备

如果你需要打包的绿色版软件比较小(1G以内),或者文件结构比较简单,请忽视此段,由于我所要打包的软件体积比较大,最重要的是文件结构比较复杂,内部文件夹嵌套较多,且例如图片,xml等小文件较多,这样 InstallShield软件在打包导入的时候会非常慢,我尝试导入了10多个小时也没有结束就放弃了,所以在这里我建议对文件结构复杂的软件进行压缩处理,压缩方式请转

RAR打包为自解压exe文件教程,得到一个单独的 EXE文件(自解压格式)后进行接下来的操作。

2.1创建新的 Windows Installer | InstallScript MSI Projcet

这种类型的工程既有打包向导,又可以写一点脚本实现自定义操作,比较适合使用。

2.2 打包向导主界面

这里是InstallShield提供的向导主界面,我们是通过点击选择下方的动作条进行操作。

2.3应用程序信息

公司信息,应用程序名称等等,这里没什么好说的。

2.4 安装要求

这里要说一下,大家可能会把这里理解为是安装依赖,但是这里与其说是依赖不如说是安装的前提或要求,这里勾选的环境 会在你双击setup后立即开始检测,并且在不满足条件时,只会对你做相应的提醒(如:请安装Adobe reader6!)后直接结束安装并退出,并没有引导用户安装的选项。明显与我们想将缺少的依赖环境集成引导加以安装的需求不相符,但是考虑到有些人会有这样的需求,也在接下来简单讲一下。

2.4.1 系统安装要求的勾选

对于InstallShield软件已经列出的选项直接勾选就可以,不再赘述。

2.4.2 自定义系统安装要求

对于InstallShield没有列出的我们可以通过左侧 创建一个自定义软件条件 来自行添加,如下图,你可以选择不同的满足条件(如 注册表某一项的值为XXX或 系统的某一路径下存在某一文件等等),这里的选项可以满足自定义几乎所有的安装要求。

2.5安装体系结构

在这里可以新建多个Feature,每一个Feature代表一类安装软件,具体以例子说明,见下面第二张图。

这中类型的图片大家一定很熟悉,这是大多数软件点击自定义安装后出现的界面, 以下图为例,My sql数据库,MuseMail这两个选项就对应两个Feature,也就是上一幅图中的defaultFeature和New_Feature2这两个,然后属于musemail类里面的多个软件就被添加到MuseMail的Feature下,对于MuseMail->初始化工具,则要创建初始化工具的Feature,以此类推。

2.6应用程序文件

在此界面为每一个Feature添加文件。若有多个文件可选择添加文件夹。 这些Feature将是用户自定义安装时选择的对象。

2.7 应用程序快捷方式

在这里添加在上一部分添加的文件中,每一个在你安装后的系统可能用到的文件的快捷方式,建议只添加必须的。右侧可以选择快捷方式出现的位置。

2.8应用程序注册表

在这里可以选择应用程序想要写入注册表的信息,右键添加即可,比较简单。

2.9安装本地化

这一部分用来选择安装语言

2.10构建安装

选择生成安装包。讲到这里比较简单的安装包已经可以打包好了,已经可以点击生成了,选择对应的生成类型即可。

选择安装设计器界面—>应用程序数据—>可再分发,在这里可以选择系统所需的环境,并且在环境不满足的情况下弹出下载安装的提醒。

InstallShield软件已经集成了部分依赖环境,可以勾选添加,并且可以在部署前下载,这样可以添加到安装包中,免除用户在安装过程中下载过程。但是InstallShield中不包含的怎么办呢,接下来来讲自定义依赖环境。

InstallShield提供的依赖环境是以 .prq文件的方式提供的,目录是    X:\..\InstallShield\2010\SetupPrerequisites,在这里有很多.prq文件,自定义不存在的依赖环境就是在网上下载.prq文件,或自己创建

常用prq文件地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?xml version="1.0" encoding="UTF-8"?>
<SetupPrereq>
<!-- 安装条件:检测注册表 -->
<conditions>
<condition Type="2"
Comparison="2"
Path="HKEY_LOCAL_MACHINE\SOFTWARE\test"
FileName="test"
ReturnValue="1"/>
</conditions>

<!-- 操作系统支持 -->
<operatingsystemconditions>
<!-- Windows XP SP2+ -->
<operatingsystemcondition MajorVersion="5" MinorVersion="1"
PlatformId="2" CSDVersion=""
Bits="1" ProductType="1"
ServicePackMajorMin="2"/>
<!-- Windows Server 2003 -->
<operatingsystemcondition MajorVersion="5" MinorVersion="2"
PlatformId="2" ProductType="2|3"/>
<!-- Windows Server 2003 x64 -->
<operatingsystemcondition MajorVersion="5" MinorVersion="2"
PlatformId="2" Bits="2" ProductType="1"/>
<!-- Windows Vista -->
<operatingsystemcondition MajorVersion="6" MinorVersion="0"
PlatformId="2"/>
<!-- Windows Server 2008 -->
<operatingsystemcondition MajorVersion="6" MinorVersion="0"
PlatformId="2" ProductType="2|3"/>
<!-- Windows 7 / Server 2008 R2 -->
<operatingsystemcondition MajorVersion="6" MinorVersion="0"
PlatformId="2" ProductType="1"/>
</operatingsystemconditions>

<!-- 文件下载配置 -->
<files>
<file LocalFile="<ISProductFolder>\SetupPrerequisites\test\test 3.5\test3.5.exe"
URL="http://download.test.com/download/test3.5.exe"
CheckSum="D481CDA2625D9DD2731A00F482484D86"
FileSize="0,242743296"/>
</files>

<!-- 执行命令配置 -->
<execute file="Helper.exe"
cmdline="/p dotnetfx35.exe /l 1033 /v &quot;/q /norestart&quot;"
cmdlinesilent="/p dotnetfx35.exe /l 1033 /v &quot;/q /norestart&quot;"
returncodetoreboot="1641,3010"
requiresmsiengine="1"/>

<!-- 依赖包属性 -->
<properties Id="{074EE22F-2485-4FED-83D1-AAC36C3D9ED0}"
Description="This prerequisite installs the .NET Framework 3.5 Service Pack 1 full package."
AltPrqURL="http://saturn.installshield.com/is/prerequisites/microsoft .net framework 3.5 sp1.prq"/>

<!-- 重启行为 -->
<behavior Reboot="2"/>
</SetupPrereq>

关键配置说明

配置项 说明
<conditions> 安装前检测条件,如注册表键值
<operatingsystemconditions> 支持的操作系统版本
<files> 需要下载的依赖文件
<execute> 安装执行命令及参数
<properties> 依赖包标识和描述
<behavior> 安装后的重启行为

使用步骤

  1. .prq 文件保存至 InstallShield\SetupPrerequisites 目录
  2. 重启 InstallShield 软件
  3. 应用程序数据 → 可再分发 中勾选该依赖
0%