透過C#實作Windows具名管道(Named Pipes)
在設計Console類型的應用程式,對於輸出訊息我們通常會使用Console.WriteLine()
來輸出,更進一步頂多就是使用Log之類的概念來輸出到文字檔紀錄,但是有時候在沒有介面的情況下(例如:Windows Services 服務)執行會變得很像瞎子摸象,這時候我們可以透過Windows的具名管道(Named Pipes)來達到這個目的。依據微軟對於Named Pipes
的解釋如下:具名管道是一個命名、單向或雙工管道,用於管道伺服器與一或多個管道用戶端之間的通訊。 具名管道的所有實例都會共用相同的管道名稱,但每個實例都有自己的緩衝區和控制碼,並提供個別的管道供用戶端/伺服器通訊使用。 實例的使用可讓多個管道用戶端同時使用相同的具名管道。
建立具名管道類別(NamePipe Class)
/// <summary>
/// 透過NamePipe命名管道來實作共用訊息
/// </summary>
public static class NamedPipe
{
private static string cPipeName = "MyNamedPipeTest";
private const int iMaxClients = 5;
private static readonly System.Collections.Generic.List<System.IO.Pipes.NamedPipeServerStream> _oClients = new System.Collections.Generic.List<System.IO.Pipes.NamedPipeServerStream>();
private static readonly object _oLock = new object();
private static bool _bIsListening = false;
static NamedPipe()
{ System.Threading.Tasks.Task.Factory.StartNew(() => ListenForClients(), System.Threading.Tasks.TaskCreationOptions.LongRunning); }
private static async System.Threading.Tasks.Task ListenForClients()
{
_bIsListening = true;
while (_bIsListening)
{
lock (_oLock)
{
if (_oClients.Count >= iMaxClients)
{
System.Threading.Monitor.Wait(_oLock, 100);
continue;
}
}
var oNamedPipeServer = new System.IO.Pipes.NamedPipeServerStream(
cPipeName,
System.IO.Pipes.PipeDirection.Out,
iMaxClients,
System.IO.Pipes.PipeTransmissionMode.Message,
System.IO.Pipes.PipeOptions.Asynchronous
);
try
{
await oNamedPipeServer.WaitForConnectionAsync();
lock (_oLock)
{ _oClients.Add(oNamedPipeServer); }
System.Threading.Tasks.Task.Factory.StartNew(() => MonitorClient(oNamedPipeServer), System.Threading.Tasks.TaskCreationOptions.LongRunning);
}
catch
{ oNamedPipeServer.Dispose(); }
}
}
private static async System.Threading.Tasks.Task MonitorClient(System.IO.Pipes.NamedPipeServerStream oClient)
{
try
{
while (oClient.IsConnected)
{ await System.Threading.Tasks.Task.Delay(500); }
}
catch
{ /* 異常狀況下視同斷線 */ }
finally
{
lock (_oLock)
{
_oClients.Remove(oClient);
System.Threading.Monitor.Pulse(_oLock);
}
oClient.Dispose();
}
}
/// <summary>
/// 將指定的文字訊息發送給所有連線中的客戶端
/// </summary>
/// <param name="cMessage">欲傳送的文字訊息</param>
public static void Write(string cMessage)
{
byte[] buffer = System.Text.Encoding.UTF8.GetBytes(cMessage);
lock (_oLock)
{
foreach (var client in _oClients.ToArray())
{
if (client.IsConnected)
{
try
{
client.Write(buffer, 0, buffer.Length);
client.Flush();
}
catch
{
_oClients.Remove(client);
client.Dispose();
}
}
else
{
_oClients.Remove(client);
client.Dispose();
}
}
}
}
}
在C#中使用具名管道伺服器(NamePipe Server)
接著我們就可以輕易地在C# Console應用程式中使用具名管道來進行訊息的傳遞。
while (true)
{
var cDate = $"{System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}\n";
NamedPipe.Write(cDate);
Console.Write(cDate);
System.Threading.Thread.Sleep(100); //測試高速輸出(間接驗證類別效能)
}
在C#中使用具名管道客戶端(NamePipe Client)
假設NamePipe Server已經在執行中,我們可以透過以下程式碼來建立NamePipe Client來接收Server端的訊息。
using var oClient = new System.IO.Pipes.NamedPipeClientStream(".", "MyNamedPipeTest", System.IO.Pipes.PipeDirection.In);
oClient.Connect();
byte[] bytBuffer = new byte[1024];
while (oClient.IsConnected)
{
int bytRead = oClient.Read(bytBuffer, 0, bytBuffer.Length);
if (bytRead > 0)
{ Console.Write(System.Text.Encoding.UTF8.GetString(bytBuffer, 0, bytRead)); }
}
在PowerShell中使用具名管道接收伺服器端的訊息
如果已經採用了NamePipe具名管道,這代表我們可以用各式的終端機工具來接收Server端的訊息,這反而是最常見的方法,舉例來說我們可以使用PowerShell來接收伺服器端的訊息。
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$pipeServer = "." # 如果是遠端機器的話請填入伺服器IP位址
$pipeName = "MyNamedPipeTest"
$bufferSize = 1024
$disconnectTimeout = 180
Write-Host "*** NamedPipe Client ***`n"
$disconnectStartTime = $null
while ($true) {
Write-Host ("Connecting to named pipe ($pipeName)...")
try {
$pipe = New-Object System.IO.Pipes.NamedPipeClientStream($pipeServer, $pipeName, [System.IO.Pipes.PipeDirection]::In)
$pipe.Connect(5000)
# 連上並重設斷線時間
$disconnectStartTime = $null
Write-Host "Connected, starting to receive messages...`n"
while ($pipe.IsConnected) {
$buffer = New-Object byte[] $bufferSize
$bytesRead = $pipe.Read($buffer, 0, $bufferSize)
if ($bytesRead -gt 0) {
$receivedData = [System.Text.Encoding]::UTF8.GetString($buffer, 0, $bytesRead)
[Console]::Write($receivedData)
}
}
Write-Host "`nServer disconnected.`n"
$pipe.Dispose()
# 記錄這次斷線發生的時間
$disconnectStartTime = Get-Date
}
catch {
# 嘗試連線失敗
if (-not $disconnectStartTime) {
$disconnectStartTime = Get-Date
}
if ($pipe) {
$pipe.Dispose()
}
}
$elapsed = (Get-Date) - $disconnectStartTime
if ($elapsed.TotalSeconds -ge $disconnectTimeout) {
Write-Host ("Disconnection time exceeded $disconnectTimeout seconds. Exiting.")
break
}
else {
Write-Host ("Waiting for next connection. Elapsed time: {0:N0} seconds.`n" -f $elapsed.TotalSeconds)
Start-Sleep -Seconds 5
}
}
跨主機的具名管道之相關說明
A. 如果需要跨主機使用具名管道,類別程式碼就需要添加安全性的參數,例如:
//設定具名管道的安全性(WellKnownSidType.WorldSid = Everyone)
var oSecurity = new System.IO.Pipes.PipeSecurity();
oSecurity.AddAccessRule(new System.IO.Pipes.PipeAccessRule(
new System.Security.Principal.SecurityIdentifier(System.Security.Principal.WellKnownSidType.WorldSid, null),
System.IO.Pipes.PipeAccessRights.FullControl,
System.Security.AccessControl.AccessControlType.Allow
));
var oNamedPipeServer = new System.IO.Pipes.NamedPipeServerStream(
cPipeName,
System.IO.Pipes.PipeDirection.Out,
iMaxClients,
System.IO.Pipes.PipeTransmissionMode.Message,
System.IO.Pipes.PipeOptions.Asynchronous,
4096,
4096,
oSecurity //在此添加安全性參數
);
B. 提供具名管道訊息的機器,需要在具有進階安全性的Windows Defender防火牆
(wf.msc)中,針對輸入規則
>檔案及印表機共用(SMB-In)
,進行啟用
,這樣才能讓其他機器透過具名管道來進行通訊。
C. 進行跨主機具名管道連線前,建議先在Windows底下建立好對方伺服器的帳號密碼(例如在檔案總管輸入\\ServerIP\pipe\NamepipeName
),並勾選儲存密碼
,這樣在連線時會比較順利。