不久前,在尝试应用 C# 解决 PC 端程序多开问题时,发现 VB.NET 的 WinForm 程序提供了一个非常简单的实现形式:


无需任何代码,只需打个勾。

我对它的实现原理很感兴趣,因而对其进行了探索。明天就通过这篇文章为大家介绍微软是如何解决 PC 端程序多开问题的,以及如何在 C# 中实现雷同性能。

 

原理1——WindowsFormsApplicationBase基类

编译一个 VB.NET 的 WinForm 程序,反编译源代码,找到入口Main办法:

该入口类MyApplication继承自WindowsFormsApplicationBase基类,理论执行的基类的Run办法。Run办法外部应用IsSingleInstance属性判断是否启用单个实例应用程序:

而IsSingleInstance属性在入口类的构造函数中被赋值:

实现1

通过援用 NuGet 包 Microsoft.VisualBasic,咱们在 C# 中同样能够继承WindowsFormsApplicationBase基类,为IsSingleInstance属性赋值,实现单个实例应用程序:

class Program :  WindowsFormsApplicationBase{    public Program()    {        IsSingleInstance = true;    }    protected override void OnCreateMainForm()    {        MainForm = new Form1();    }     [STAThread]    static void Main(string[] args)    {        new Program().Run(args);    }}

尽管上述实现非常简单,然而仅实用于 WinForm 应用程序,而且还须要援用Microsoft.VisualBasic类库。

因而,咱们决定深挖一下,看看具体是如何实现的。

原理2-命名管道

通过查看WindowsFormsApplicationBase的Run办法实现(代码有删减):

Public Sub Run(commandLine As String())    If Not IsSingleInstance Then        DoApplicationModel()    Else        ' This is a Single-Instance application        Dim pipeServer As NamedPipeServerStream = Nothing        If TryCreatePipeServer(ApplicationInstanceID, pipeServer) Then            ' --- This is the first instance of a single-instance application to run.            Using pipeServer                WaitForClientConnectionsAsync(pipeServer, AddressOf OnStartupNextInstanceMarshallingAdaptor, cancellationToken:=tokenSource.Token)                DoApplicationModel()            End Using        Else            Dim awaitable = SendSecondInstanceArgsAsync(ApplicationInstanceID, commandLine, cancellationToken:=tokenSource.Token).ConfigureAwait(False)            awaitable.GetAwaiter().GetResult()        End If    End If 'Single-Instance applicationEnd Sub

能够剖析出,微软解决 PC 端程序多开问题的外部实现原理如下:

  1. 创立一个NamedPipeServerStream 命名管道服务器实例
  2. 如果创立胜利,则用WaitForClientConnectionsAsync期待第 2 个利用实例进行连贯
  3. 如果创立失败,则用SendSecondInstanceArgsAsync向第 1 个利用实例发送数据

命名管道在管道服务器和一个或多个管道客户端之间提供过程间通信。命名管道能够是单向的,也能够是双向的。它们反对基于音讯的通信,并容许多个客户端应用雷同的管道名称同时连贯到服务器过程。

具体应用阐明,请参阅官网文档《应用命名管道进行网络过程间通信》[1]

实现2

上面,咱们用控制台程序进行演示,如何实现单个实例应用程序:

const string pipeName = "MyIO";const PipeOptions NamedPipeOptions = PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly;static async Task Main(string[] args){    try    {        using (var pipeServer = new NamedPipeServerStream(                pipeName: pipeName,                direction: PipeDirection.In,                maxNumberOfServerInstances: 1,                transmissionMode: PipeTransmissionMode.Byte,                options: NamedPipeOptions))        {            WaitForClientConnectionsAsync(pipeServer,str => Console.WriteLine(str));            Console.WriteLine($"start server {args[0]}");            Console.ReadKey();        }    }    catch    {        await SendSecondInstanceArgsAsync(()=> $"call from {args[0]}").ConfigureAwait(false);    }}

须要留神的是,WaitForClientConnectionsAsync不能加await,否则后续代码不能执行。

▌WaitForClientConnectionsAsync办法实现

实现代码如下:

private static async Task WaitForClientConnectionsAsync(NamedPipeServerStream pipeServer, Action<string> callback){    CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();    while (true)    {        await pipeServer.WaitForConnectionAsync(cancellationTokenSource.Token).ConfigureAwait(false);        try        {            const int bufferLength = 1024;            var buffer = new byte[bufferLength];            using (var stream = new MemoryStream())            {                while (true)                {                    var bytesRead = await pipeServer.ReadAsync(buffer.AsMemory(0, bufferLength), cancellationTokenSource.Token).ConfigureAwait(false);                    if (bytesRead == 0)                    {                        break;                    }                    stream.Write(buffer, 0, bytesRead);                }                stream.Seek(0, SeekOrigin.Begin);                callback(Encoding.UTF8.GetString(stream.ToArray()));            }        }        finally        {            pipeServer.Disconnect();        }    }}

▌SendSecondInstanceArgsAsync办法实现

实现代码如下:

private static async Task SendSecondInstanceArgsAsync(Func<string> func){    using (var pipeClient = new NamedPipeClientStream(        serverName: ".",        pipeName: pipeName,        direction: PipeDirection.Out,        options: NamedPipeOptions))    {        CancellationTokenSource cancellationTokenSource2 = new CancellationTokenSource();        cancellationTokenSource2.CancelAfter(2500);        await pipeClient.ConnectAsync(cancellationTokenSource2.Token).ConfigureAwait(false);        await pipeClient.WriteAsync(Encoding.UTF8.GetBytes(func()), cancellationTokenSource2.Token).ConfigureAwait(false);    }}

Demo

创立多开脚本:

start " " "ConsoleApp1.exe" firstInstancestart " " "ConsoleApp1.exe" secondInstancestart " " "ConsoleApp1.exe" thirdInstance

执行后,咱们发现程序只能关上一次。并且收到了其它多开利用发过来的数据:


微软最有价值专家(MVP)


微软最有价值专家是微软公司授予第三方技术专业人士的一个寰球奖项。29年来,世界各地的技术社区领导者,因其在线上和线下的技术社区中分享专业知识和教训而取得此奖项。

MVP是通过严格筛选的专家团队,他们代表着技术最精湛且最具智慧的人,是对社区投入极大的激情并乐于助人的专家。MVP致力于通过演讲、论坛问答、创立网站、撰写博客、分享视频、开源我的项目、组织会议等形式来帮忙别人,并最大水平地帮忙微软技术社区用户应用 Microsoft 技术。

更多详情请登录官方网站:
https://mvp.microsoft.com/zh-cn


长按辨认二维码关注微软中国MSDN