C# 管道服务(通常指命名管道 Named Pipes 或匿名管道 Anonymous Pipes)是基于 流 (Stream) 的机制,主要用于进程间通信 (IPC)。
从技术底层来看,管道传输的唯一数据类型是 字节数组 (byte[])。
然而,在实际应用中,您可以传输任何可以被转换为字节流的数据类型。以下是几种常见的数据传输类型和方式:
1. 基础数据类型 (底层机制)
字节数组 (byte[])
这是管道服务直接处理的原始数据类型。无论您发送什么高级数据,它最终都必须被编码或序列化为字节数组才能通过管道传输。
2. 常见应用数据类型
字符串 (String)
字符串是最常用的数据类型之一。要通过管道传输字符串,您必须使用特定的编码 (Encoding) 将其转换为字节数组。
-
发送方:
csbyte[] data = Encoding.UTF8.GetBytes("Hello, Pipe!"); pipeStream.Write(data, 0, data.Length); -
接收方:
csstring message = Encoding.UTF8.GetString(receivedBytes);
常用的编码包括 UTF8、ASCII 或 Unicode。
复杂对象 (Objects)
对于自定义类、结构体、列表等复杂数据结构,您需要使用序列化 (Serialization) 技术将其转换为字节流。
常用的序列化格式包括:
-
JSON (JavaScript Object Notation)
- 这是目前最推荐和最流行的方式,因为它跨平台、可读性高,且 C# 内置了高效的
System.Text.Json库。 - 流程: 对象 -> JSON 字符串 -> 字节数组。
- 这是目前最推荐和最流行的方式,因为它跨平台、可读性高,且 C# 内置了高效的
-
XML (Extensible Markup Language)
- 适用于需要高度结构化和元数据描述的场景。
-
二进制格式 (Binary)
- 虽然传统的
BinaryFormatter因为安全和性能问题已被微软弃用,但像 Protobuf (Protocol Buffers) 这样的高效二进制序列化库仍然是追求极致性能时的优秀选择。
- 虽然传统的
总结
C# 管道服务本身只传输字节。
您可以传输任何 数据类型,前提是您在发送端能够将其可靠地序列化 (转换为字节流),并在接收端使用相同的逻辑和编码方式进行反序列化(将字节流还原为原始数据类型)。
| 数据类型 | 传输前处理方式 | 推荐库/方法 |
|---|---|---|
| 原始数据 | 无需处理 | byte[] |
| 字符串 | 编码 (Encoding) | System.Text.Encoding.UTF8 |
| 复杂对象 | 序列化 (Serialization) | System.Text.Json (JSON) |
| 高性能对象 | 二进制序列化 | Protobuf (需要安装 NuGet 包) |
一.两软件间使用管道命令实现JSON数据交互
由于管道传输的是原始字节流,您需要解决两个核心问题:
- 序列化与反序列化: 将 C# 对象转换为 JSON 字符串,再转换为字节数组发送;接收时反向操作。
- 消息边界处理: 确保接收方知道一个完整的 JSON 消息在哪里结束,另一个消息在哪里开始。
下面是实现这一目标的详细步骤和代码结构。
核心步骤:带长度前缀的传输协议
为了解决消息边界问题,我们通常采用一个简单的协议:在发送实际的 JSON 数据之前,先发送一个表示数据长度的整数(通常是 4 字节)。
发送流程:
- 对象 -> JSON 字符串。
- JSON 字符串 -> 字节数组 (
dataBytes)。 - 获取
dataBytes的长度 (length)。 - 将
length转换为 4 字节的数组 (lengthBytes)。 - 写入
lengthBytes(4 字节)。 - 写入
dataBytes(N 字节)。
接收流程:
- 从管道读取 4 字节 (
lengthBytes)。 - 将
lengthBytes转换回整数 (length)。 - 从管道读取
length字节 (dataBytes)。 - 将
dataBytes转换为 JSON 字符串。 - JSON 字符串 -> C# 对象。
示例代码结构
我们将使用 System.Text.Json 进行序列化,并使用命名管道 (NamedPipeServerStream 和 NamedPipeClientStream)。
1. 定义数据模型
首先,定义一个要在两个应用程序之间传输的数据类。(两个项目内都有有这个数据类)
cs
public class MessageData
{
public int Id { get; set; }
public string Content { get; set; }
public DateTime Timestamp { get; set; } = DateTime.Now;
}
2. 服务端 (发送方) 示例
服务端负责创建管道,序列化数据,并发送带长度前缀的消息。
cs
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace PipeServer
{
internal class Program
{
private const string PipeName = "JsonPipeExample";
static async Task Main(string[] args)
{
await StartServerAsync();
}
public static async Task StartServerAsync()
{
Console.WriteLine("等待用户端连接...");
// 创建命名管道服务器
using var pipeServer = new NamedPipeServerStream(
PipeName,
PipeDirection.Out,
1, // 最大实例数
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous);
await pipeServer.WaitForConnectionAsync();
Console.WriteLine("用户端连接成功.");
// 准备要发送的数据
var dataToSend = new MessageData { Id = 101, Content = "Hello from Server!" };
// 序列化对象为 JSON 字符串
string jsonString = JsonSerializer.Serialize(dataToSend);
// 转换为 UTF8 字节数组
byte[] dataBytes = Encoding.UTF8.GetBytes(jsonString);
int length = dataBytes.Length;
// 1. 写入长度前缀 (4 字节)
byte[] lengthBytes = BitConverter.GetBytes(length);
await pipeServer.WriteAsync(lengthBytes, 0, lengthBytes.Length);
// 2. 写入 JSON 数据
await pipeServer.WriteAsync(dataBytes, 0, dataBytes.Length);
Console.WriteLine($"Sent JSON message (Length: {length} bytes): {jsonString}");
pipeServer.Disconnect();
}
}
}
3. 客户端 (接收方) 示例
客户端负责连接管道,读取长度前缀,然后读取相应长度的数据,并进行反序列化。
cs
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace PipeClient
{
internal class Program
{
private const string PipeName = "JsonPipeExample";
static async Task Main(string[] args)
{
await StartClientAsync();
}
public static async Task StartClientAsync()
{
Console.WriteLine("连接到服务器...");
// 创建命名管道客户端
using var pipeClient = new NamedPipeClientStream(".", PipeName, PipeDirection.In);
try
{
pipeClient.Connect(5000); // 尝试连接,超时5秒
Console.WriteLine("已连接至服务器");
// 1. 读取长度前缀 (4 字节)
byte[] lengthBytes = new byte[4];
int bytesRead = await pipeClient.ReadAsync(lengthBytes, 0, 4);
if (bytesRead < 4)
{
Console.WriteLine("无法读取消息长度");
return;
}
// 将 4 字节转换为整数长度
int dataLength = BitConverter.ToInt32(lengthBytes, 0);
// 2. 读取 JSON 数据
byte[] dataBytes = new byte[dataLength];
int totalBytesRead = 0;
// 循环读取,直到读完所有数据
while (totalBytesRead < dataLength)
{
int currentRead = await pipeClient.ReadAsync(dataBytes, totalBytesRead, dataLength - totalBytesRead);
if (currentRead == 0) break; // 连接断开
totalBytesRead += currentRead;
}
if (totalBytesRead != dataLength)
{
Console.WriteLine($"收到的数据不完整。 预期的: {dataLength}, 实际的: {totalBytesRead}");
return;
}
// 3. 反序列化
string jsonString = Encoding.UTF8.GetString(dataBytes);
// 反序列化 JSON 字符串为对象
var receivedData = JsonSerializer.Deserialize<MessageData>(jsonString);
Console.WriteLine("\n--- 读取数据 ---");
Console.WriteLine($"Raw JSON: {jsonString}");
Console.WriteLine($"ID: {receivedData.Id}");
Console.WriteLine($"Content: {receivedData.Content}");
Console.WriteLine($"Timestamp: {receivedData.Timestamp}");
}
catch (TimeoutException)
{
Console.WriteLine("连接超时:");
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
}
}
运行建议
要测试上述代码,您需要创建两个独立的 C# 控制台应用程序:
- App A (Server): 调用
PipeServer.StartServerAsync()。 - App B (Client): 调用
PipeClient.StartClientAsync()。
注意: 您必须先启动服务器应用程序,然后再启动客户端应用程序,以便客户端能够连接到正在等待的命名管道。
