前言
上次,我们通过《引用 Microsoft.VisualBasic 解决程序多开的问题》。
虽然它非常简单,但是仅适用于 WinForm 应用程序,而且还需要引用不常用的Microsoft.VisualBasic
类库。
因此,我们决定深挖一下,看看具体是如何实现的。
原理
通过查看WindowsFormsApplicationBase
的Run
方法实现(代码有删减):
Public Sub Run(commandLine As String())If Not IsSingleInstance ThenDoApplicationModel()Else' This is a Single-Instance applicationDim pipeServer As NamedPipeServerStream = NothingIf TryCreatePipeServer(ApplicationInstanceID, pipeServer) Then' --- This is the first instance of a single-instance application to run.Using pipeServerWaitForClientConnectionsAsync(pipeServer, AddressOf OnStartupNextInstanceMarshallingAdaptor, cancellationToken:=tokenSource.Token)DoApplicationModel()End UsingElseDim awaitable = SendSecondInstanceArgsAsync(ApplicationInstanceID, commandLine, cancellationToken:=tokenSource.Token).ConfigureAwait(False)awaitable.GetAwaiter().GetResult()End IfEnd If 'Single-Instance application
End Sub
可以分析出整个流程如下:
创建一个
NamedPipeServerStream
实例如果创建成功,则用
WaitForClientConnectionsAsync
等待第 2 个应用实例进行连接如果创建失败,则用
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" firstInstancestart " " "ConsoleApp1.exe" secondInstancestart " " "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