Named Pipe 跨程序傳輸演練 - PowerShell/.NET、JavaScript、Python、Golang 大亂鬥
2 |
故事從我想在兩個獨立執行 PowerShell 程序間傳輸資料說起。
最開始的想法是寫個迷你 WebAPI,但是寫 WebAPI 不是 PowerShell 的強項,改用 C# 實現有點繞遠路,於是我想起 Named Pipe,一個簡單輕巧的 IPC 選項。過去沒認真研究過,剛好利用機會讓技能樹多長一支新芽。
實際動手後發現程式比想像好寫,跨語言應用也很簡單,於是我分別再試寫了 Python、JavaScript 與 Go 版客戶端,完成一場大亂鬥,筆記留念。 先補充背景知識。
跨程序溝通有個專門術語 - Interprocess Communictaion(IPC),在 Windows 平台有以下選擇:Clipboard、COM、Data Copy、DDE、File Mapping、Mailslots、Pipes、RPC、Windows Sockets。(延伸閱讀:Windows IPC 方式整理) 而 Pipes 分為匿名及具名,匿名僅適用於父子程序溝通,具名管道 Named Pipe 便是本次的主角。
我用 PowerShell 實作了一個簡單的 Named Pipe 伺服器,透過 while 無窮迴圈不斷建立新管道等待連線及處理需求。這種寫法使用單一執行緒消化請求,多個請求併發時需排隊,針對高負載情境一般要改成多緒平行處理,但本次的重點在學習原理,就不多花功夫研究。相關說明我寫在註解,直接奉上程式碼:
$pipeName = 'PSNamedPipeDemo'
Write-Host "* Named Pipe Server [$pipeName]"
$running = $true
while ($running) {
# 不斷建立新管道,採讀寫雙向模式
$pipe = new-object System.IO.Pipes.NamedPipeServerStream($pipeName, [System.IO.Pipes.PipeDirection]::InOut)
try {
# 等待連線
$pipe.WaitForConnection()
# 建立 StreamReader 和 StreamWriter 以便讀寫資料
$sr = new-object System.IO.StreamReader($pipe)
$sw = new-object System.IO.StreamWriter($pipe)
$sw.AutoFlush = $true # 寫完訊息立即送出緩衝區內容
$msg = $sr.ReadLine()
switch ($msg) {
'/exit' { # 接受 '/exit' 指令,結束 Server
Write-Host 'Exiting...'
$sw.WriteLine('Goodbye!')
$running = $false
}
'/guid' { # 接受 '/guid' 指令,回傳 GUID
$guid = New-Guid
Write-Host "Generating GUID: " $guid
$sw.WriteLine($guid)
}
Default { # 其他情況,複誦對方傳送內容
Write-Host $msg
$sw.WriteLine("echo: $msg")
}
}
}
finally {
# 等待管道清空(對方讀取完畢)
$pipe.WaitForPipeDrain()
$pipe.Dispose()
}
}
客戶端的做法很類似,差別在改用 NamedPipeClientStream,若伺服器端在本機,伺服器名稱用 ".",讀寫資料一樣用 StreamWriter、StreamReader 即可。
param(
[parameter(Mandatory=$true)]
[string]$msg
)
$pipeName = 'PSNamedPipeDemo'
$pipe = new-object System.IO.Pipes.NamedPipeClientStream(".", $pipeName, [System.IO.Pipes.PipeDirection]::InOut)
$pipe.Connect()
$sw = new-object System.IO.StreamWriter($pipe)
$sw.AutoFlush = $true
$sw.WriteLine($msg)
$pipe.WaitForPipeDrain()
$sr = new-object System.IO.StreamReader($pipe)
Write-Host $sr.ReadLine()
$pipe.WaitForPipeDrain()
$pipe.Dispose()
測試成功。
C# 寫法與 PowerShell 大同小異,接著來試試其他語言。
JavaScript / Node.js 我選擇用 net.createConnection() 實現 IPC 通訊:
const net = require('net');
const readline = require('readline');
const pipePath = '\\\\.\\pipe\\PSNamedPipeDemo';
if (process.argv.length > 2) {
message = process.argv[2];
sendMessage(process.argv[2]).then((response) => {
console.log(response);
});
}
else {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('Enter message: ', (answer) => {
sendMessage(answer).then((response) => {
console.log(response);
});
rl.close();
});
}
function sendMessage(msg) {
return new Promise((resolve, reject) => {
const client = net.createConnection(pipePath);
client.write(msg + '\n');
client.on('data', (data) => {
resolve(data.toString());
});
});
}
Python 把 \\.\pipe\PSNamedPipeDemo
路徑當成檔案讀寫即可:
import os
import sys
if len(sys.argv) > 1:
message = sys.argv[1]
else:
message = input("Enter a message: ")
pipe_path = r'\\.\pipe\PSNamedPipeDemo'
try:
with open(pipe_path, 'r+b', buffering=0) as f:
f.write((message + "\n").encode())
f.flush()
data = f.read(100)
reply = data.decode('utf-8')
print(reply)
except Exception as e:
print(f"Error: {e}")
Go 也是將 Named Pipe 路徑視為一般檔案,用 os.openFile()
搞定。
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
args := os.Args
var message string
if len(args) > 1 {
message = args[1]
} else {
fmt.Print("Enter a message: ")
reader := bufio.NewReader(os.Stdin)
message, _ = reader.ReadString('\n')
}
pipePath := `\\.\pipe\PSNamedPipeDemo`
f, err := os.OpenFile(pipePath, os.O_RDWR, 0777)
if err != nil {
log.Fatalf("error opening file: %v", err)
}
defer f.Close()
f.WriteString(message + "\n")
err = f.Sync()
if err != nil {
log.Fatalf("error syncing file: %v", err)
}
data := make([]byte, 100)
n, err := f.Read(data)
if err != nil {
log.Fatalf("error reading file: %v", err)
}
reply := string(data[:n])
fmt.Println(reply)
}
啟動 PowerShell Named Pipe 伺服器,分別從 JavaScript、Python、Go 客戶端送出訊息並讀取回應,完成多語言大亂鬥。
最後,我好奇這個 while ($true) 的單執行緒版本伺服器是否能扛住併發請求,便修改了 Go 版客戶端借用前陣子剛學會的 Goroutine 平行運算功能同時發出 100 次請求。其中的眉角是當伺服器唯一的 Instance 忙著處理其他請求時,os.OpenFile() 會得到 All pipe instances are busy
錯誤;若遇到結束上次 NamedPipeServerStream 還來不及新建的空窗期,會得到 The system cannot find the file specified.
錯誤。因此,因應併發情境得自己加上等待重試邏輯或是改用 go-winio 等專用程式庫。
我捏了一個大致可跑完 100 次的土砲重試版:(數量再多需處理 NamedPipeServerStream 空窗問題,先跳過)
package main
import (
"fmt"
"log"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
)
func main() {
pipePath := `\\.\pipe\PSNamedPipeDemo`
var wg sync.WaitGroup
var succCount int32
for i := 0; i < 100; i++ {
wg.Add(1)
msg := "Req-" + strconv.Itoa(i) + "\n"
go func() {
defer wg.Done()
var f *os.File
var err error
for {
f, err = os.OpenFile(pipePath, os.O_RDWR, 0777)
if err != nil {
if strings.Contains(err.Error(), "All pipe instances are busy") {
time.Sleep(10 * time.Millisecond) // Wait for 1ms before retrying
continue
} else {
log.Fatalf("error opening file: %v", err)
}
}
break
}
defer f.Close()
f.WriteString(msg)
err = f.Sync()
if err != nil {
log.Fatalf("error syncing file: %v", err)
}
data := make([]byte, 100)
n, err := f.Read(data)
if err != nil {
log.Fatalf("error reading file: %v", err)
}
reply := string(data[:n])
fmt.Print(reply)
atomic.AddInt32(&succCount, 1)
}()
}
wg.Wait()
fmt.Println("Total Success Count: ", succCount)
}
併發 100 次請求測試通過。
演練完畢,收工。
The article discusses using Named Pipes for inter-process communication (IPC) in PowerShell, comparing it with other methods like WebAPI. It provides a PowerShell implementation of a Named Pipe server and clients in PowerShell, JavaScript, Python, and Go, testing cross-language communication. It also explores handling high load with parallel requests in Go.
Comments
# by 貴
看 SQL SERVER 的網路組態設定有一個具名管道,一直沒看懂是做什麼用的,應該就是跟這篇提到的技術一樣?使用具名管道連上 SQL SERVER? 不知道用這種連接方式連上 SQL SERVER 是否有什麼不可取代的優點?或是有不可取代的地方?
# by Jeffrey
to 貴,若伺服器與客戶端在同一台機器,Named Pipe 比 TCP/IP 輕巧,Overhead 低,有些古老客戶端只能使用 Named Pipe (我太嫩了,沒見過)。 除此之外,SQL Server 使用 TCP 還是主流做法,我覺得沒必要刻意選擇 Named Pipe。