微软是如何解决 PC 端程序多开问题的

不久前,在尝试使用 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-命名管道

通过查看WindowsFormsApplicationBaseRun方法实现(代码有删减):

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 application
End 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();
        }
    }
}

  • 循环等待客户端连接

  • 读取客户端发送的数据,转换成字符串

  • 调用callback处理字符串,这里是str => Console.WriteLine(str)

  • 断开客户端连接

▌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);
    }
}

  • 创建客户端连接本地管道服务

  • 向服务端发送func产生的数据,这里是()=> $"call from {args[0]}"

Demo

创建多开脚本:

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

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

参考资料

  • [1]《使用命名管道进行网络进程间通信》:

    https://docs.microsoft.com/zh-cn/dotnet/standard/io/how-to-use-named-pipes-for-network-interprocess-communication?WT.mc_id=DT-MVP-38491


微软最有价值专家(MVP)

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

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

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

关注微软中国MSDN公众号了解更多

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值