当前位置: 首页 > 编程日记 > 正文

C#:消息队列应用程序

文章“MSMQ:可伸缩、高可用性的负载平衡解决方案(英文)”介绍了一种解决方案,用于高可用性消息队列 (MSMQ) 的可伸缩负载平衡解决方案体系结构。此解决方案中涉及了一种将 Windows 服务用作智能消息路由器的开发方案。这样的解决方案以前只有 Microsoft Visual C++® 程序员才能实现,而 .NET 框架的出现改变了这种情况。从下面的解决方案中,您可以看到这一点。

.NET 框架应用程序
这里介绍的解决方案是一种用来处理若干消息队列的 Windows 服务;其中每个队列都是由多个线程进行处理(接收和处理消息)。处理程序使用循环法技术或应用程序特定值(消息 AppSpecific 属性)从目的队列列表中路由消息,并使用消息属性来调用组件方法。(示例进程也属于这种情况。)在后一种情况下,组件的要求是它能够实现给定的接口 IWebMessage。要处理错误,应用程序需要将不能处理的消息发送到错误队列中。

消息应用程序的结构与以前的活动模板库 (ATL) 应用程序相似,它们之间的主要不同在于用于管理服务的代码的封装和 .NET 框架组件的使用。要创建 Windows 服务,.NET 框架用户仅仅需要创建一个从 ServiceBase(来自 System.ServiceControl 程序集)继承的类。这毫不奇怪,因为 .NET 框架是面向对象的。

应用程序结构
应用程序中主要的类是 ServiceControl,它是从 ServiceBase 继承的。因而,它必须实现 OnStart 和 OnStop 方法,以及可选的 OnPause 和 OnContinue 方法。事实上,类是在静态方法 Main 内构造的:

using System;
using System.ServiceProcess;

public class ServiceControl: ServiceBase
{
// 创建服务对象的主入口点
public static void Main()
{
ServiceBase.Run(new ServiceControl());
}

// 定义服务参数的构造对象
public ServiceControl()
{
CanPauseAndContinue = true;
ServiceName = "MSDNMessageService";
AutoLog = false;
}

protected override void OnStart(string[] args) {...}
protected override void OnStop() {...}
protected override void OnPause() {...}
protected override void OnContinue() {...}
}

ServiceControl 类创建一系列 CWorker 对象,即,为需要处理的每个消息队列创建 CWorker 类的一个实例。根据定义中处理队列所需的线程数目,CWorker 类依次创建了一系列的 CWorkerThread 对象。CWorkerThread 类创建的一个处理线程将执行实际的服务工作。

使用 CWorker 和 CWorkerThread 类的主要目的是确认服务控件 Start、Stop、Pause 和 Continue 命令。因为这些进程必须是无阻塞的,命令操作最终将在后台处理线程上执行。

CWorkerThread 是一个抽象类,被 CWorkerThreadAppSpecific 、CWorkerThreadRoundRobin 和 CWorkerThreadAssembly 继承。这些类以不同的方式处理消息。前两个类通过给另一队列发送消息来处理消息(其不同之处在于确定接收队列路径的方式),最后一个类则使用消息属性来调用组件方法。

.NET 框架内部的错误处理是以基类 Exception 为基础的。当系统引发或捕获错误时,这些错误必须是从 Exception 中导出的类。CWorkerThreadException 类就是这样一种实现,它通过附加额外属性(用于定义服务是否应继续运行)来扩展基类。

最后,应用程序包含两种结构。这些值类型定义了辅助进程或线程的运行时参数,以简化 CWorker 和 CWorkerThread 对象的结构。使用值类型结构(而不是引用类型类)能够确保这些运行时参数维护的是数值(而不是引用)。

IWebMessage 接口
CWorkerThread 的实现之一是一个调用组件方法的类。这个名为 CWorkerThreadAssembly 的类使用 IWebMessage 接口来定义服务和组件之间的约定。

与当前版本的 Microsoft Visual Studio® 不同,C# 接口可以在任何语言中显式定义,而不需要创建和编译 IDL 文件。C# IWebMessage 接口的定义如下:

public interface IWebMessage
{
WebMessageReturn Process(string sMessageLabel, string sMessageBody, int iAppSpecific);
void Release();
}

ATL 代码中的 Process 方法是为处理消息而指定的。Process 方法的返回代码定义为枚举类型 WebMessageReturn:

public enum WebMessageReturn
{
ReturnGood,
ReturnBad,
ReturnAbort
}

枚举的定义如下:Good 表示继续处理,Bad 表示将消息写入错误队列,Abort 表示终止处理。Release 方法为服务提供了轻松清除类实例的途径。因为仅在垃圾回收的过程中才调用类实例的析构函数,所以确保所有占用昂贵资源(例如数据库连接)的类都有一个能够在析构之前被调用的方法,用来释放这些资源,这是一种非常好的构思。

名称空间
在这里先简单介绍一下名称空间。名称空间允许在内部和外部表示中将应用程序组织成为逻辑元素。服务内的所有代码都包含在 MSDNMessageService.Service 名称空间内。尽管服务代码包含在若干文件中,但是由于它们包含在同一名称空间中,因此用户不需要引用其他文件。

由于 IWebMessage 接口包含在 MSDNMessageService.Interface 名称空间中,因此使用此接口的线程类具有一个接口名称空间。

服务类
应用程序的目的是监视和处理消息队列,每一队列在收到消息时都执行不同的进程。应用程序是作为 Windows 服务来实现的。

ServiceBase 类
如前所述,服务的基本结构是从 ServiceBase 继承的类。重要的方法包括 OnStart、OnStop、OnPause 和 OnContinue,每一个替代方法都与一个服务控制操作直接对应。OnStart 方法的目的是创建 CWorker 对象,而 CWorker 类又创建 CWorkerThread 对象,然后在该对象中创建执行服务工作的线程。

服务的运行时配置(以及 CWorker 和 CWorkerThread 对象的属性)是在基于 XML 的配置文件中维护的。它的名称与创建的 .exe 文件相同,但带有一个 .cfg 后缀。配置示例如下:

<?xml version="1.0"?>
<configuration>
<ProcessList>
<ProcessDefinition
ProcessName="Worker1"
ProcessDesc="Message Worker with 2 Threads"
ProcessType="AppSpecific"
ProcessThreads="2"
InputQueue="./private$/test_load1"
ErrorQueue="./private$/test_error">
<OutputList>
<OutputDefinition OutputName="./private$/test_out11" />
<OutputDefinition OutputName="./private$/test_out12" />
</OutputList>
</ProcessDefinition>
<ProcessDefinition
ProcessName="Worker2"
ProcessDesc="Assembly Worker with 1 Thread"
ProcessType="Assembly"
ProcessThreads="1"
InputQueue="./private$/test_load2"
ErrorQueue="./private$/test_error">
<OutputList>
<OutputDefinition OutputName="C:/MSDNMessageService/MessageExample.dll" />
<OutputDefinition OutputName="MSDNMessageService.MessageSample.ExampleClass"/>
</OutputList>
</ProcessDefinition>
</ProcessList>
</configuration>

对此信息的访问通过来自 System.Configuration 程序集的 ConfigManager 类来管理。静态 Get 方法返回信息的集合,这些集合将被枚举以获得单个属性。这些属性集的设置决定了辅助对象的运行时特征。除了这一配置文件,您还应该创建定义 XML 文件结构的图元文件,并在其中引用位于服务器 machine.cfg 配置文件中的图元文件:

<?xml version ="1.0"?>
<MetaData xmlns="x-schema:CatMeta.xms">
<DatabaseMeta InternalName="MessageService">
<ServerWiring Interceptor="Core_XMLInterceptor"/>
<Collection
InternalName="Process" PublicName="ProcessList"
PublicRowName="ProcessDefinition"
SchemaGeneratorFlags="EMITXMLSCHEMA">
<Property InternalName="ProcessName" Type="String" MetaFlags="PRIMARYKEY" />
<Property InternalName="ProcessDesc" Type="String" />
<Property InternalName="ProcessType" Type="Int32" DefaultValue="RoundRobin" >
<Enum InternalName="RoundRobin" Value="0"/>
<Enum InternalName="AppSpecific" Value="1"/>
<Enum InternalName="Assembly" Value="2"/>
</Property>
<Property InternalName="ProcessThreads" Type="Int32" DefaultValue="1" />
<Property InternalName="InputQueue" Type="String" />
<Property InternalName="ErrorQueue" Type="String" />
<Property InternalName="OutputName" Type="String" />
<QueryMeta InternalName="All" MetaFlags="ALL" />
<QueryMeta InternalName="QueryByFile" CellName="__FILE" Operator="EQUAL" />
</Collection>
<Collection
InternalName="Output" PublicName="OutputList"
PublicRowName="OutputDefinition"
SchemaGeneratorFlags="EMITXMLSCHEMA">
<Property InternalName="ProcessName" Type="String" MetaFlags="PRIMARYKEY" />
<Property InternalName="OutputName" Type="String" MetaFlags="PRIMARYKEY" />
<QueryMeta InternalName="All" MetaFlags="ALL" />
<QueryMeta InternalName="QueryByFile" CellName="__FILE" Operator="EQUAL" />
</Collection>
</DatabaseMeta>
<RelationMeta
PrimaryTable="Process" PrimaryColumns="ProcessName"
ForeignTable="Output" ForeignColumns="ProcessName"
MetaFlags="USECONTAINMENT"/>
</MetaData>

由于 Service 类必须维护一个已创建辅助对象的列表,因此使用了 Hashtable 集合,用于保持类型对象的名称/数值对列表。Hashtable 不仅支持枚举,还允许通过关键字来查询值。在应用程序中,XML 进程名称是唯一的关键字:

private Hashtable htWorkers = new Hashtable();
IConfigCollection cWorkers = ConfigManager.Get("ProcessList", new AppDomainSelector());
foreach (IConfigItem ciWorker in cWorkers)
{
WorkerFormatter sfWorker = new WorkerFormatter();
sfWorker.ProcessName = (string)ciWorker["ProcessName"];
sfWorker.ProcessDesc = (string)ciWorker["ProcessDesc"];
sfWorker.NumberThreads = (int)ciWorker["ProcessThreads"];
sfWorker.InputQueue = (string)ciWorker["InputQueue"];
sfWorker.ErrorQueue = (string)ciWorker["ErrorQueue"];
// 计算并定义进程类型
switch ((int)ciWorker["ProcessType"])
{
case 0:
sfWorker.ProcessType = WorkerFormatter.SFProcessType.ProcessRoundRobin;
break;
case 1:
sfWorker.ProcessType = WorkerFormatter.SFProcessType.ProcessAppSpecific;
break;
case 2:
sfWorker.ProcessType = WorkerFormatter.SFProcessType.ProcessAssembly;
break;
default:
throw new Exception("Unknown Processing Type");
}
// 执行更多的工作以读取输出信息
string sProcessName = (string)ciWorker["ProcessName"];
if (htWorkers.ContainsKey(sProcessName))
throw new ArgumentException("Process Name Must be Unique: " + sProcessName);
htWorkers.Add(sProcessName, new CWorker(sfWorker));
}

在这段代码中没有包含的主要信息是输出数据的获取。每一个进程定义中都有一组相应的输出定义项。该信息是通过如下的简单查询读取的:

string sQuery = "SELECT * FROM OutputList WHERE ProcessName=" +
sfWorker.ProcessName + " AND Selector=appdomain://";
ConfigQuery qQuery = new ConfigQuery(sQuery);
IConfigCollection cOutputs = ConfigManager.Get("OutputList", qQuery);
int iSize = cOutputs.Count, iLoop = 0;
sfWorker.OutputName = new string[iSize];
foreach (IConfigItem ciOutput in cOutputs)
sfWorker.OutputName[iLoop++] = (string)ciOutput["OutputName"];

CWorkerThread 和 Cworker 类都有相应的服务控制方法,根据服务控制操作进行调用。由于 Hashtable 中引用了每一个 CWorker 对象,因此需要枚举 Hashtable 的内容,以调用适当的服务控制方法:

foreach (CWorker cWorker in htWorkers.Values)
cWorker.Start();

类似地,实现的 OnPause、OnContinue 和 OnStop 方法是通过调用 CWorker 对象上的相应方法来执行操作的。

CWorker 类
CWorker 类的主要功能是创建和管理 CWorkerThread 对象。Start、Stop、Pause 和 Continue 方法调用相应的 CWorkerThread 方法。实际的 CWorkerThread 对象是在Start 方法中创建的。与使用 Hashtable 管理辅助对象引用的 Service 类相似,CWorker 使用 ArrayList(简单的动态数组)来维护线程对象的列表。

在这个数组内部,CWorker 类创建了 CWorkerThread 类的一个实现版本。CWorkerThread 类(将在下面讨论)是一个必须继承的抽象类。导出类定义了消息的处理方式:

aThreads = new ArrayList();
for (int idx=0; idx<sfWorker.NumberThreads; idx++)
{
WorkerThreadFormatter wfThread = new WorkerThreadFormatter();
wfThread.ProcessName = sfWorker.ProcessName;
wfThread.ProcessDesc = sfWorker.ProcessDesc;
wfThread.ThreadNumber = idx;
wfThread.InputQueue = sfWorker.InputQueue;
wfThread.ErrorQueue = sfWorker.ErrorQueue;
wfThread.OutputName = sfWorker.OutputName;
// 定义辅助类型,并将其插入辅助线程结构
CWorkerThread wtBase;
switch (sfWorker.ProcessType)
{
case WorkerFormatter.SFProcessType.ProcessRoundRobin:
wtBase = new CWorkerThreadRoundRobin(this, wfThread);
break;
case WorkerFormatter.SFProcessType.ProcessAppSpecific:
wtBase = new CWorkerThreadAppSpecific(this, wfThread);
break;
case WorkerFormatter.SFProcessType.ProcessAssembly:
wtBase = new CWorkerThreadAssembly(this, wfThread);
break;
default:
throw new Exception("Unknown Processing Type");
}
// 添加对数组的调用
aThreads.Insert(idx, wtBase);
}

一旦所有的对象都已创建,就可以通过调用每个线程对象的 Start 方法来启动它们:

foreach(CWorkerThread cThread in aThreads)
cThread.Start();
Stop、Pause 和 Continue 方法在 foreach 循环里执行的操作类似。Stop 方法具有如下的垃圾收集操作:

GC.SuppressFinalize(this);
在类析构函数中将调用 Stop 方法,这样,在没有显式调用 Stop 方法的情况下也可以正确地终止对象。如果调用了 Stop 方法,将不需要析构函数。SuppressFinalize 方法能够防止调用对象的 Finalize 方法(析构函数的实际实现)。

CWorkerThread 抽象类
CWorkerThread 是一个由 CWorkerThreadAppSpecifc、CWorkerThreadRoundRobin 和 CWorkerThreadAssembly 继承的抽象类。无论如何处理消息,队列的大部分处理是相同的,所以 CWorkerThread 类提供了这一功能。这个类提供了抽象方法(必须被实际方法替代)以管理资源和处理消息。

类的工作再一次通过 Start、Stop、Pause 和 Continue 方法来实现。在 Start 方法中引用了输入和错误队列。在 .NET 框架中,消息由 System.Messaging 名称空间处理:

// 尝试打开队列,并设置默认的读写属性
MessageQueue mqInput = new MessageQueue(sInputQueue);
mqInput.MessageReadPropertyFilter.Body = true;
mqInput.MessageReadPropertyFilter.AppSpecific = true;
MessageQueue mqError = new MessageQueue(sErrorQueue);
// 如果使用 MSMQ COM,则将格式化程序设置为 ActiveX
mqInput.Formatter = new ActiveXMessageFormatter();
mqError.Formatter = new ActiveXMessageFormatter();

一旦定义了消息队列引用,即会创建一个线程用于实际的处理函数(称为 ProcessMessages)。在 .NET 框架中,使用 System.Threading 名称空间很容易实现线程处理:

procMessage = new Thread(new ThreadStart(ProcessMessages));
procMessage.Start();

ProcessMessages 函数是基于 Boolean 值的处理循环。当数值设为 False,处理循环将终止。因此,线程对象的 Stop 方法只设置这一 Boolean 值,然后关闭打开的消息队列,并加入带有主线程的线程:

// 加入服务线程和处理线程
bRun = false;
procMessage.Join();
// 关闭打开的消息队列
mqInput.Close();
mqError.Close();

Pause 方法只设置一个 Boolean 值,使处理线程休眠半秒钟:

if (bPause)
Thread.Sleep(500);

最后,每一个 Start、Stop、Pause 和 Continue 方法将调用抽象的 OnStart、OnStop、OnPause 和 OnContinue 方法。这些抽象方法为实现的类提供了挂钩,以捕获和释放所需的资源。

ProcessMessages 循环具有如下基本结构:

接收 Message。


如果 Message 具有成功的 Receive,则调用抽象 ProcessMessage 方法。


如果 Receive 或 ProcessMessage 失败,将 Message 发送至错误队列中。
Message mInput;
try
{
// 从队列中读取,并等候 1 秒
mInput = mqInput.Receive(new TimeSpan(0,0,0,1));
}
catch (MessageQueueException mqe)
{
// 将消息设置为 null
mInput = null;
// 查看错误代码,了解是否超时
if (mqe.ErrorCode != (-1072824293) ) //0xC00E001B
{
// 如果未超时,发出一个错误并记录错误号
LogError("Error: " + mqe.Message);
throw mqe;
}
}
if (mInput != null)
{
// 得到一个要处理的消息,调用处理消息抽象方法
try
{
ProcessMessage(mInput);
}
// 捕获已知异常状态的错误
catch (CWorkerThreadException ex)
{
ProcessError(mInput, ex.Terminate);
}
// 捕获未知异常,并调用 Terminate
catch
{
ProcessError(mInput, true);
}
}

ProcessError 方法将错误的消息发送至错误队列。另外,它也可能引发异常来终止线程。如果ProcessMessage 方法引发了终止错误或 CWorkerThreadException 类型,它将执行此操作。

CworkerThread 导出类
任何从 CWorkerThread 中继承的类都必须提供 OnStart、OnStop、OnPause、OnContinue 和 ProcessMessage 方法。OnStart 和 OnStop 方法获取并释放处理资源。OnPause 和 OnContinue 方法允许临时释放和重新获取这些资源。ProcessMessage 方法应该处理消息,并在出现失败事件时引发 CWorkerThreadException 异常。

由于 CWorkerThread 构造函数定义运行时参数,导出类必须调用基类构造函数:

public CWorkerThreadDerived(CWorker v_cParent, WorkerThreadFormatter v_wfThread)
: base (v_cParent, v_wfThread) {}

导出类提供了两种类型的处理:将消息发送至另一队列,或者调用组件方法。接收和发送消息的两种实现使用了循环技术或应用程序偏移(保留在消息 AppSpecific 属性中),作为使用哪一队列的决定因素。此方案中的配置文件应该包括队列路径的列表。实现的 OnStart 和 OnStop 方法应该打开和关闭对这些队列的引用:

iQueues = wfThread.OutputName.Length;
mqOutput = new MessageQueue[iQueues];
for (int idx=0; idx<iQueues; idx++)
{
mqOutput[idx] = new MessageQueue(wfThread.OutputName[idx]);
mqOutput[idx].Formatter = new ActiveXMessageFormatter();
}

在这些方案中,消息的处理很简单:将消息发送必要的输出队列。在循环情况下,这个进程为:

try
{
mqOutput[iNextQueue].Send(v_mInput);
}
catch (Exception ex)
{
// 如果错误强制终止异常
throw new CWorkerThreadException(ex.Message, true);
}
// 计算下一个队列号
iNextQueue++;
iNextQueue %= iQueues;

后一种调用带消息参数的组件的实现方法比较有趣。ProcessMessage 方法使用 IWebMessage 接口调入一个 .NET 组件。OnStart 和 OnStop 方法获取和释放此组件的引用。

此方案中的配置文件应该包含两个项目:完整的类名和类所在文件的位置。按照 IWebMessage 接口中的定义,在组件上调用 Process 方法。

要获取对象引用,需要使用 Activator.CreateInstance 方法。此函数需要一个程序集类型。在这里,它是从程序集文件路径和类名中导出的。一旦获取对象引用,它将被放入合适的接口:

private IWebMessage iwmSample;
private string sFilePath, sTypeName;
// 保存程序集路径和类型名称
sFilePath = wfThread.OutputName[0];
sTypeName = wfThread.OutputName[1];
// 获取对必要对象的引用
Assembly asmSample = Assembly.LoadFrom(sFilePath);
Type typSample = asmSample.GetType(sTypeName);
object objSample = Activator.CreateInstance(typSample);
// 定义给对象的必要接口
iwmSample = (IWebMessage)objSample;

获取对象引用后,ProcessMessage 方法将在 IWebMessage 接口上调用 Process 方法:

WebMessageReturn wbrSample;
try
{
// 定义方法调用的参数
string sLabel = v_mInput.Label;
string sBody = (string)v_mInput.Body;
int iAppSpecific = v_mInput.AppSpecific;
// 调用方法并捕捉返回代码
wbrSample = iwmSample.Process(sLabel, sBody, iAppSpecific);
}
catch (InvalidCastException ex)
{
// 如果在消息内容中发生错误,则强制发出一个非终止异常
throw new CWorkerThreadException(ex.Message, false);
}
catch (Exception ex)
{
// 如果错误调用程序集,则强制发出终止异常
throw new CWorkerThreadException(ex.Message, true);
}
// 如果没有错误,则检查对象调用的返回状态
switch (wbrSample)
{
case WebMessageReturn.ReturnBad:
throw new CWorkerThreadException
("Unable to process message: Message marked bad", false);
case WebMessageReturn.ReturnAbort:
throw new CWorkerThreadException
("Unable to process message: Process terminating", true);
default:
break;
}

提供的示例组件将消息正文写入数据库表。如果捕获到严重数据库错误,您可能希望终止处理过程,但是在这里,仅仅将消息标记为错误的消息。

由于此示例中创建的类实例可能会获取并保留昂贵的数据库资源,所以用 OnPause 和 OnContinue 方法释放和重新获取对象引用。

检测设备
就象在所有优秀的应用程序中一样,检测设备用于监测应用程序的状态。.NET 框架大大简化了将事件日志、性能计数器和 Windows 管理检测设备 (WMI) 纳入应用程序的过程。消息应用程序使用时间日志和性能计数器,二者都是来自 System.Diagnostics 程序集。

在 ServiceBase 类中,您可以自动启用事件日志。另外,ServiceBase EventLog 成员支持写入应用程序事件日志:

EventLog.WriteEntry(sMyMessage, EventLogEntryType.Information);

对于写入事件日志而不是应用程序日志的应用程序,它能够很容易地创建和获取 EventLog 资源的引用(正如在 CWorker 类中所做的一样),并能够使用 WriteEntry 方法记录日志项:

private EventLog cLog;
string sSource = ServiceControl.ServiceControlName;
string sLog = "Application";
// 查看源是否存在,如果不存在,则创建源
if (!EventLog.SourceExists(sSource))
EventLog.CreateEventSource(sSource, sLog);
// 创建日志对象,并引用现在定义的源
cLog = new EventLog();
cLog.Source = sSource;
// 在日志中写入条目,表明创建成功
cLog.WriteEntry("已成功创建", EventLogEntryType.Information);

.NET 框架大大简化了性能计数器。对于每一个处理线程、线程导出的用户和整个应用程序,这一消息应用程序都能提供计数器,用于跟踪消息数量和每秒钟处理消息的数量。要提供此功能,您需要定义性能计数器的类别,然后增加相应的计数器实例。

性能计数器的类别在服务 OnStart 方法中定义。这些类别代表两种计数器——消息总数和每秒钟处理的消息数:

CounterCreationData[] cdMessage = new CounterCreationData[2];
cdMessage[0] = new CounterCreationData("Messages/Total", "Total Messages Processed",
PerformanceCounterType.NumberOfItems64);
cdMessage[1] = new CounterCreationData("Messages/Second", "Messages Processed a Second",
PerformanceCounterType.RateOfChangePerSecond32);
PerformanceCounterCategory.Create("MSDN Message Service", "MSDN Message Service Counters", cdMessage);

一旦定义了性能计数器类别,将创建 PerformanceCounter 对象以访问计数器实例功能。PerformanceCounter 对象需要类别、计数器名称和一个可选的实例名称。对于辅助进程,将使用来自 XML 文件的进程名称,代码如下:

pcMsgTotWorker = new PerformanceCounter("MSDN Message Service", "Messages/Total", sProcessName);
pcMsgSecWorker = new PerformanceCounter("MSDN Message Service", "Messages/Second", sProcessName);
pcMsgTotWorker.RawValue = 0;
pcMsgSecWorker.RawValue = 0;

要增加计数器的值,仅仅需要调用适当的方法:

pcMsgTotWorker.IncrementBy(1);
pcMsgSecWorker.IncrementBy(1);

最后说明一点,服务终止时,安装的性能计数器类别应该从系统中删除:

PerformanceCounterCategory.Delete("MSDN Message Service");

由于性能计数器在 .NET 框架中工作,因此需要运行一项特殊的服务。此服务 (PerfCounterService) 提供了共享内存。计数器信息将写入共享内存,并被性能计数器系统读取。

安装
在结束以前,我们来简要介绍一下安装以及称为 installutil.exe 的安装工具。由于此应用程序是 Windows 服务,它必须使用 installutil.exe 来安装。因此,需要使用一个从 System.Configuration.Install 程序集中继承的 Installer 类:

public class ServiceRegister: Installer
{
private ServiceInstaller serviceInstaller;
private ServiceProcessInstaller processInstaller;
public ServiceRegister()
{
// 创建服务安装程序
serviceInstaller = new ServiceInstaller();
serviceInstaller.StartType = ServiceStart.Manual;
serviceInstaller.ServiceName = ServiceControl.ServiceControlName;
serviceInstaller.DisplayName = ServiceControl.ServiceControlDesc;
Installers.Add(serviceInstaller);
// 创建进程安装程序
processInstaller = new ServiceProcessInstaller();
processInstaller.RunUnderSystemAccount = true;
Installers.Add(processInstaller);
}
}

如此示例类所示,对于一个 Windows 服务,服务和服务进程各需要一个安装程序,以定义运行服务的帐户。其他安装程序允许注册事件日志和性能计数器等资源。

总结
从这个 .NET 框架应用程序示例中可以看出,以前只有 Visual C++ 程序员能够编写的应用程序,现在使用简单的面向对象程序即可实现。尽管我们的重点是 C#,但本文所述的内容也同样适用于 Visual Basic 和 Managed C++。新的 .NET 框架使开发人员能够使用任何编程语言来创建功能强大、可伸缩的 Windows 应用程序和服务。

新的 .NET 框架不仅简化和扩展了编程的种种可能,还能够轻松地将人们经常遗忘的应用程序检测设备(例如性能监测计数器和事件日志通知)合并到应用程序中。尽管这里的应用程序没有使用 Windows 管理检测设备 (WMI),但 .NET 框架同样也可以应用它。

参考资料
可伸缩的高可用性业务对象结构(英文)


MSMQ:可伸缩的高可用性负载平衡解决方案(英文)


C# 简介和概述(英文)


C# 参考(英文)


MSDN Online .NET 信息(英文)
关于作者
Carl Nolan 在北加利福尼亚的 Microsoft 电子商务解决方案小组的西区工作。该小组的工作重点是使用 Microsoft Windows .NET 平台开发基于 Internet 的解决方案。他的电子邮件地址是 carlnol@microsoft.com。

相关文章:

从腾讯实时音视频发家史,看爆发中的 RTC 将何去何从

作者 | 夕颜头图 | 下载于视觉中国出品 | AI 科技大本营&#xff08;ID:rgznai100&#xff09;早在2015年左右&#xff0c;直播和短视频的兴起渗透进普通人的日常生活&#xff0c;人们信息消费的内容已经开始从文字向语音、视频信息转变。而疫情期间全民“家里蹲”的窘境&#…

山寨c 标准库中的getline 函数

2019独角兽企业重金招聘Python工程师标准>>> 要山寨一个函数&#xff0c;只要看两点 原版函数的形参。原函数的返回值。下面是函数原型。 ssize_t getline(char **lineptr, size_t *n, FILE *stream); 函数返回值。 RETURN VALUE On success, getline() and getde…

封ip对爬虫的影响

今天要聊的是封ip对爬虫的影响。我认为封ip能拒绝一部分网络请求&#xff0c;减轻服务器的压力&#xff0c;但是如果要是建立一个好的ip池&#xff0c;封对爬虫的影响不大。 爬取国内一个拍卖公司的网站&#xff0c;刚开始用多进程下载&#xff0c;每分钟能爬取 1000个页面&…

Python 的一万种用法:制作 Web 可视化页面

来源 | 法纳斯特头图 | 下载于ICphoto一谈到Web页面&#xff0c;可能大家首先想到就是HTML、CSS或JavaScript。本次小F给大家介绍一下如何用Python制作一个数据可视化网页&#xff0c;使用到的是Streamlit库&#xff0c;轻松将一个Excel数据文件转换为一个Web页面&#xff0c;提…

【Go语言】LiteIDE使用的个人使用方法

Go语言开发 可以使用的IDE很多 &#xff08;Goclipse&#xff0c;sublime&#xff0c;notepad&#xff0c;vim等&#xff09;目前使用的最顺手的就是LiteIDE了 但是尽管这样&#xff0c;一开始使用LiteIDE也有很多不习惯的地方&#xff0c;下面主要总结了一些自己喜欢的用法 首…

发挥大数据及其产业在推动发展方式转变上的作用

大数据时代的到来&#xff0c;互联网成为基础设施&#xff0c;数据变成重要资源&#xff0c;这不仅意味着海量、多样、快速的数据处理和技术创新&#xff0c;更为重要的是改变了传统要素的组合方式。这种变化客观上要求必须转变传统的经济增长方式&#xff0c;实现创新驱动发展…

Google 全球 IP 地址库一览表(更新中)

IP 地址来源&#xff1a;http://www.kookle.co.nr&#xff0c;共计4351个。【链接】http://www.kookle.co.nr/https://github.com/justjavac/Google-IPsBulgaria93.123.23.193.123.23.293.123.23.393.123.23.493.123.23.593.123.23.693.123.23.793.123.23.893.123.23.993.123.2…

Linux下的阻塞(Block)

阻塞&#xff08;Block&#xff09;这个概念。当进程调用一个阻塞的系统函数时&#xff0c;该进程被置于睡眠&#xff08;Sleep&#xff09;状态&#xff0c;这时内核调度其它进程运行&#xff0c;直到该进程等待的事件发生了&#xff08;比如网络上接收到数据包&#xff0c;或…

发布 128 核 Altra Max,自研内核,明年推出 5nm 处理器,“性能怪兽”Ampere 搞大事?...

作者 | 伍杏玲出品 | AI 科技大本营&#xff08;ID:rgznai100&#xff09;头图 | 下载于ICphoto2015 年&#xff0c;在英特尔就职 28 年的总裁 Renee James 辞职&#xff0c;正在大众纷纷猜测她将如何开启下一段旅程时&#xff0c;她有了创业的想法&#xff0c;2017 年带领新团…

纷纷布局的全光网,是你所熟知的吗?

近日&#xff0c;中国电信甘肃公司举行甘肃全光网建成发布会&#xff0c;至2017年4月30日&#xff0c;甘肃省已建成14个全光网市州、87个全光网县区、1234个全光网乡镇、10000个全光网行政村&#xff0c;全省市、县、乡光网宽带覆盖率达到95%以上&#xff0c;全面实现光纤到户;…

内存转换Image到Icon

时候我们需要在内存中转换Image格式到Icon 根据经验&#xff0c;通常我们应该可以这样做 Image image xxxx;///假设这里已经有一个Image对象 System.IO.MemoryStream mStream new System.IO.MemoryStream();///创建内存流 image.Save(mStream, System.Drawing.Imaging.Ima…

Spring注解@Component、@Repository、@Service、@Controller,@Autowired、@Resource用法

一、Spring定义bean&#xff0c;Component、Repository、Service 和 Controller Spring 2.5 中除了提供 Component 注释外&#xff0c;还定义了几个拥有特殊语义的注释&#xff0c;它们分别是&#xff1a;Repository、Service 和 Controller。在目前的 Spring 版本中&#xff0…

谁是“艾灵”?是腾讯的真国风 AI 虚拟人!

近日&#xff0c;腾讯AI虚拟人艾灵再秀出新技能&#xff0c;首次展示AI作诗、AI书法等国风才艺&#xff0c;并与青年歌手白举纲跨次元合作&#xff0c;共同演唱国风新歌《百川千仞》。AI“艾灵”诞生于腾讯AI Lab&#xff0c;来自实验性、探索性技术项目“多模态虚拟人”。机器…

[Java实现] 图片择优(选择最清楚的图片)

FuzzyDetection 图片择优&#xff08;选择最清楚的图片&#xff09;【Java实现】效果不错&#xff0c;大家可以根据我的源码改成自己使用的语言并应用到自己项目中。 实现思路 获取图片的灰度图数组使用拉普拉斯算子进行卷积运算 {0, 1, 0, 1, -4, 1, 0, 1, 0}获取结果的方差与…

C#获取硬盘序列号

using System; using System.IO; using System.Runtime.InteropServices; using System.Text; using Microsoft.Win32; namespace Wjb.ReadOrWriteIniAndReg { /// <summary> /// HardDiskVal 的摘要说明。 /// 读取指定盘符的硬盘序列号 /// 功能&#xff1a;读…

Arm 发布移动端 v9 体系新架构,CPU、GPU、IP全囊括了

作者 | 夕颜头图 | 下载于ICphoto出品 | AI 科技大本营&#xff08;ID:rgznai100&#xff09;2021年5月25日晚&#xff0c;Arm发布了针对移动端的Armv9体系新架构&#xff0c;除了公布首款全面计算&#xff08;Total Compute&#xff09;解决方案&#xff0c;Arm还发布了首批基…

16条很有用的Chrome浏览器命令

为什么80%的码农都做不了架构师&#xff1f;>>> Google Chrome浏览器有很多的特性在界面菜单中是没有体现的&#xff0c;你可以通过「chrome://命令」来访问。在Chrome的浏览器地址栏中输入命令&#xff0c;就会返回相应的结果。下面是16个非常有用的chrome://命令…

Spring集成Redis方案(spring-data-redis)(基于Jedis的单机模式)(待实践)

说明&#xff1a;请注意Spring Data Redis的版本以及Spring的版本&#xff01;最新版本的Spring Data Redis已经去除Jedis的依赖包&#xff0c;需要自行引入&#xff0c;这个是个坑点。并且会与一些低版本的Spring有冲突&#xff0c;要看官方文档和不断的测试。 继上一篇文章ht…

leetcode -- 3 sum

3-sum 题目描写叙述&#xff1a; Given an array S of n integers, are there elements a, b, c in S such that a b c 0? Find all unique triplets in the array which gives the sum of zero. 题目要求&#xff1a; Elements in a triplet (a,b,c) must be in non-desc…

C#中如何得到机器的IP地址

如何使用DNS类并得到机器的IP地址的技巧 介绍 这篇文章并不是技术纵览或大型讨论&#xff0c;而更像是关于如何得到IP地址或主机名称的技巧集锦。在 Win32 API编程中你可以使用NetWork API&#xff0c;在.NET平台中也是类似的。唯一的不同之处是你要找到并理解为完成这个任务需…

让浏览器开挂的插件,测评师教你如何选

CSDN下起了红包雨399 元智能音箱199 元天猫精灵300元现金红包/会员100元红包/会员更有千万流量曝光100%有奖......作为日常总发现 " 宝藏 " 的你总体验过一些 " 王炸 " 级别的chrome插件让你想 “ 真诚 ” 安利所以&#xff0c;CSDN开启了彩虹屁chrome插件…

JQuery:JQuery 中的CSS()方法

JQuery:CSS()方法jQuery css()方法&#xff1a;css()方法设置或返回被选元素的一个或多个样式属性。1、返回 CSS 属性如需返回指定的 CSS 属性的值&#xff0c;请使用如下语法&#xff1a;css("propertyname");下面的例子将返回首个匹配元素的 background-color 值&a…

j.u.c.locks.AbstractQueuedSynchronizer.Node

2019独角兽企业重金招聘Python工程师标准>>> AQS是JUC当中最核心的部分&#xff0c;大部分多线程讲解&#xff0c;都不会详细讲AQS&#xff0c;AQS的源代码&#xff0c;要看明白还是有点困难的。但是一旦看明白了&#xff0c;结构还是蛮清晰的。这里我们把AQS拆开&a…

使用C#开发COM+组件

一般来说&#xff0c;在IT技术界以及硬件产业&#xff0c;技术的更新换代速度非常得惊人&#xff0c;而惯例是所有的新技术都会遵循向下兼容的原则&#xff0c;但是.NET技术不仅仅做到了这一点&#xff0c;.NET甚至实现了相互之间的各自调用&#xff0c;这一点是非常难能可贵的…

香奈儿的 AI 实验室里,发生了什么?

作者 | 库珀来源 | 数据实战派头图 | 下载于ICphotoAI 已经能够在给你播报今日天气时提供穿衣建议。相信你大多数情况下都听进去了。如果它给你提供美妆建议呢&#xff1f;包括香奈儿在内&#xff0c;越来越多的美容品牌正在将 AI 技术结合到其产品之中。可是&#xff0c;人工智…

VS code for python开发利器

转发点赞支持引言最近在整理python自动化测试课程的内容&#xff0c;发现了微软出的vs code编辑器太牛逼了&#xff0c;非常好用&#xff0c;而且轻量的不要不要的&#xff0c;特此记录下&#xff0c;有选择纠结症的朋友我强烈推荐使用ta。PS&#xff1a;兼容win10且兼容高分辨…

C#编码标准--命名约定和风格

命名约定和风格 1&#xff0e; 使用Pascal的命名规范命名类型和方法的名字。 public class SomeClass { public SomeMethod(){} } 2&#xff0e; 使用camel命名规范命名局部变量和方法的参数。 int number; void MyMethod(int someNumber) {} 3&#xff0e; 在命名接…

与AMD合并后,赛灵思与英特尔、英伟达在数据中心市场呈“三足鼎立”之势

被以350亿美元的价格收购后&#xff0c;全球独一家FPGA公司赛灵思归于芯片巨头AMD的麾下&#xff0c;正式成为AMD的一份子。如果英伟达收购ARM顺利进行&#xff0c;无疑将让半导体行业格局再次发生巨变。 赛灵思为什么会选择归于AMD旗下&#xff1f;成为AMD的一份子之后&#…

Android -- Fragment注意事项

ViewPagerFragment 让Fragment成为ViewPager的一页时&#xff0c;FragmentManager会一直保存管理创建好了的Fragment&#xff0c;即使当前不是显示的这一页&#xff0c;Fragment对象也不会被销毁&#xff0c;…

C#编码标准--编码习惯

1. 避免将多个类放在一个文件里面。 2. 一个文件应该只有一个命名空间&#xff0c;避免将多个命名空间放在同一个文件里面。 3. 一个文件最好不要超过500行的代码&#xff08;不包括机器产生的代码&#xff09;。 4. 一个方法的代码长度最好不要超过25行。 5. 避免方法中有超过…