微软是如何解决 PC 端程序多开问题的——内部实现

Posted dotNET跨平台

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微软是如何解决 PC 端程序多开问题的——内部实现相关的知识,希望对你有一定的参考价值。

前言

上次,我们通过《引用 Microsoft.VisualBasic 解决程序多开的问题》。

虽然它非常简单,但是仅适用于 WinForm 应用程序,而且还需要引用不常用的Microsoft.VisualBasic类库。

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

原理

通过查看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

可以分析出整个流程如下:

  1. 创建一个NamedPipeServerStream实例

  2. 如果创建成功,则用WaitForClientConnectionsAsync等待第 2 个应用实例进行连接

  3. 如果创建失败,则用SendSecondInstanceArgsAsync向第 1 个应用实例发送数据

NamedPipeServerStream

使用NamedPipeServerStream类可以创建命名管道。

命名管道在管道服务器和一个或多个管道客户端之间提供进程间通信。命名管道可以是单向的,也可以是双向的。它们支持基于消息的通信,并允许多个客户端使用相同的管道名称同时连接到服务器进程。

详细使用说明,请参阅官方文档《使用命名管道进行网络进程间通信》[1]

实现

下面我们用控制台程序进行演示:

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" firstInstance

start " " "ConsoleApp1.exe" secondInstance

start " " "ConsoleApp1.exe" thirdInstance

执行后,我们发现程序只能打开一次。

并且收到了其它多开应用发过来的数据:

结论

使用NamedPipeServerStream相对互斥锁Mutex的实现要复杂。

但是由于可以进行通讯,因此可以做到更灵活的控制。

比如,应用定时启动自己的另一个实例去下载更新,下载完成后通知当前应用提示用户是否更新。

想了解更多内容,请关注我的个人公众号”My IO“

参考资料

[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

以上是关于微软是如何解决 PC 端程序多开问题的——内部实现的主要内容,如果未能解决你的问题,请参考以下文章

PC微信(WeChat)电脑端多开分析+源码

单字节修改实现PC版微信多开

控制程序的启动数量(限制游戏多开)

my.资料

微软必应词典案例分析

前端如何优雅的实现跨终端开发(PC端+移动端)