微軟是如何解決 PC 端程式多開問題的

微軟技術棧發表於2022-05-17

不久前,在嘗試使用 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 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();
        }
    }
}

▌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" firstInstance
​
start " " "ConsoleApp1.exe" secondInstance
​
start " " "ConsoleApp1.exe" thirdInstance

執行後,我們發現程式只能開啟一次。並且收到了其它多開應用發過來的資料:


微軟最有價值專家(MVP)


微軟最有價值專家是微軟公司授予第三方技術專業人士的一個全球獎項。29年來,世界各地的技術社群領導者,因其線上上和線下的技術社群中分享專業知識和經驗而獲得此獎項。

MVP是經過嚴格挑選的專家團隊,他們代表著技術最精湛且最具智慧的人,是對社群投入極大的熱情並樂於助人的專家。MVP致力於通過演講、論壇問答、建立網站、撰寫部落格、分享視訊、開源專案、組織會議等方式來幫助他人,並最大程度地幫助微軟技術社群使用者使用 Microsoft 技術。

更多詳情請登入官方網站:
https://mvp.microsoft.com/zh-cn


長按識別二維碼關注微軟中國MSDN

相關文章