这段时间公司的一个项目打算使用Named Pipe进行进程间的通讯,刚好花了点时间了解了一下,这里做一下笔记。

Named Pipe(命名管道),顾名思义,是通过在两个进程间搭建一个管道来进行通讯,这种方式的好处在于两者可以进行全双工的通讯,服务端也可以通过管道向客户端发送消息,对于两个进程之间的通讯来说再合适不过了,使用起来也相对比较灵活。

服务端(Server)

Named Pipe 命名空间

1
using System.IO.Pipes;

创建管道

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
PipeSecurity security = new PipeSecurity();  // 管道权限
// 设置规则,只有用户admin可以对管道进行读写,其它用户无权访问
security.AddAccessRule(new PipeAccessRule("admin", PipeAccessRights.ReadWrite, AccessControlType.Allow));

NamedPipeServerStream server = new NamedPipeServerStream(
    "SimpleServer",  // pipe name
    PipeDirection.InOut,  // 数据传输方向,这里使用双工通讯
    1,               // MaxNumberOfServerInstance
    PipeTransmissionMode.Byte,  // 字节流传输
    PipeOptions.Asynchronous | PipeOptions.WriteThrough,
    4096,  // 输入缓冲大小
    4096,  // 输出缓冲大小
    security);  // 管道访问权限,这里只做笔记,通常不需要设置

管道创建好之后还不能立刻发送数据,因为管道的另一端(客户端)还没有连接,所以服务端需要等待连接。

1
server.WaitForConnection();  // 阻塞方式

这里使用非阻塞的方式等待连接,当然也可以用阻塞的方式等待连接,不过需要放到一个新的线程中,避免将主线程阻塞。

1
server.BeginWaitForConnection(new AsyncCallback(WaitConnectionCallback), server);  // 非阻塞方式
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
private void WaitConnectionCallback(IAsyncResult asyncResult)
{
    NamedPipeServerStream server = asyncResult.AsyncState as NamedPipeServerStream;
    server.EndWaitForConnection(asyncResult);

    StartListen(server);
}


private void StartListen(NamedPipeServerStream server)
{
    Task.Run(async () =>
    {
        int bytesToRead;
        byte[] buffer;

        // server.WaitForConnection();
        while (true)
        {
            try
            {
                bytesToRead = 256 * server.ReadByte();
                bytesToRead += server.ReadByte();

                // 演示
                // 将收到的数据立马发送出去
                server.WriteByte((byte)(bytesToRead / 256));
                server.WriteByte((byte)(bytesToRead % 256));

                buffer = new byte[bytesToRead];
                server.Read(buffer, 0, bytesToRead);  // 读取消息

                // 演示
                // 将收到的数据立马发送出去
                await server.WriteAsync(buffer, 0, bytesToRead);
            }
            catch (System.IO.IOException)
            {
                // break if another pipe end closed
                break;
            }
            catch (Exception)
            {
                break;
            }

            // 处理数据
            string content = Encoding.UTF8.GetString(buffer);
            Console.WriteLine(content);
        }

        server.Disconnect();  // 断开连接
        // 重新等待连接
        server.BeginWaitForConnection(new AsyncCallback(WaitConnectionCallback), server);
    });
}

在客户端连接之后,立马启动一个新的线程循环读取来自客户端的消息,这里的消息前两个字节指定了消息的长度。同时将收到的消息马上返回到管道的另一端(这里用于测试是否真的是全双工工作)。

最后将消息的读取放到try{ ... } catch(...){ ... }中,因为并没有消息或者事件通知服务端客户端已经断开连接。但当客户端断开之后,服务端在读取时会抛出IOException,可以通过抓取这个错误来判断管道是否已经断开。

当客户端断开之后,中断读取循环,服务端也断开连接,并再次等待客户端连接。

客户端(Client)

创建客户端

1
2
3
4
5
NamedPipeClientStream client = new NamedPipeClientStream(
    ".", // The name of the remote computer, "." 指本机
    "SimpleServer",  // pipe name
    PipeDirection.InOut,  // 数据传输方向
    PipeOptions.Asynchronous | PipeOptions.WriteThrough);

连接到服务端

1
client.Connect();  // 阻塞方式
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Task.Run(() =>
{
    int bytesToRead;
    byte[] buffer;

    client.Connect();  // 连接管道
    while (true)
    {
        try
        {
            // 读取消息长度
            bytesToRead = 256 * client.ReadByte();
            bytesToRead += client.ReadByte();

            buffer = new byte[bytesToRead];
            client.Read(buffer, 0, bytesToRead);  // 读取消息
        }
        catch (System.IO.IOException)
        {
            break;
        }
        catch (Exception)
        {
            break;
        }

        // 处理消息
        string content = Encoding.UTF8.GetString(buffer);
        Console.WriteLine(content);
    }

    client.Close();
});

这里使用一个新的线程去连接管道服务端,连接成功后循环读取来自服务端的消息。

客户端发送

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
string text = "Hello";
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(text);

client.WriteByte((byte)(bytes.Length / 256));  // 发送消息头
client.WriteByte((byte)(bytes.Length % 256));
client.Write(bytes, 0, bytes.Length);          // 发送消息

text = "World";
bytes = System.Text.Encoding.UTF8.GetBytes(text);

client.WriteByte((byte)(bytes.Length / 256));  // 发送消息头
client.WriteByte((byte)(bytes.Length % 256));
client.Write(bytes, 0, bytes.Length);          // 发送消息

总结

总的来说,Named Pipe使用还是比较简单的,结合序列化就可以直接在两个进程中传递消息对象了。需要注意的是一个服务端只能有一个客户端连接,而且在客户端断开连接之后,服务端也需要断开连接,并重新等待客户端连接,不然再有客户端尝试连接管道也无法建立。

参考

How to: Use Named Pipes for Network Interprocess Communication