[C#]基于命名管道的一对多进程间通讯

在工作中碰到了一个进程间通讯的问题,大概是这样的:

项目本身是.net Core做的,但是有部分功能Core中不方便实现,有的是依赖Framework,有的是因为权限和安全问题。

那基于这个问题,问了问度娘进程通讯的问题,但是一水大神都在说,Socket啊,WebApi啊,内存共享啊,文件共享啊,等等。好不容易有个人在问管道的问题,大家都是一个口气:“用这么古老的东西干什么?”

既然大家都说管道这个“老古董”,那我今天就来扒扒这个坟。

 先来尝试一下管道通讯

首先要清楚什么是管道,有不少大神写了,我就这里就不废话了,给个链接  Windows中的管道技术 

基础概念知道了,接下来看看网上的别人怎么做的 C#命名管道通信

这篇Blog中用的是 System.IO.Pipes 下的 NamedPipeClientStream 和 NamedPipeServerStream,从微软的官方看这个命名空间是System.Core.dll中提供的,无论是Framework还是Core都可以使用

  

建立一个解决方案,下面两个控制台应用,一个Framework的作为服务端,一个Core的作为客户端。把Blog的代码粘进去,运行OK。(废话)

 

接下来修改代码,让程序支持半双工通讯。

为什么是半双工?不是单工或者双工?

C/S的沟通方式还是模拟的“请求-响应”模式,既然需要“响应”那么单工自然不满足需求,而服务端本身不需要在客户端发送请求数据的同时回传数据,自然全双工也没有意义,并发也不是靠双工解决的。所以选择了实现比较简单的半双工模式。

修改后的代码:

 1 using System;
 2 using System.IO;
 3 using System.IO.Pipes;
 4 
 5 namespace Server
 6 {
 7     class Program
 8     {
 9         static void Main(string[] args)
10         {
11             using (NamedPipeServerStream pipeServer = new NamedPipeServerStream("testpipe", PipeDirection.InOut, 1))
12             {
13                 try
14                 {
15                     pipeServer.WaitForConnection();
16                     pipeServer.ReadMode = PipeTransmissionMode.Byte;
17                     StreamWriter Writer = new StreamWriter(pipeServer);
18                     StreamReader Reader = new StreamReader(pipeServer);
19                    
20                     while (true)
21                     {
22                         var input = Reader.ReadLine();
23                         if (string.IsNullOrEmpty(input))
24                         {
25                             break;
26                         }
27                         
28                         Console.WriteLine($"Server Get Message:{input}");
29                         Writer.WriteLine($"Server Get Message:{input}");
30                         Writer.Flush();
31                     }
32                 }
33                 catch (IOException e)
34                 {
35                     throw e;
36                 }
37             }
38             Console.ReadKey();
39         }
40     }
41 }
服务端
 1 using System;
 2 using System.IO;
 3 using System.IO.Pipes;
 4 using System.Security.Principal;
 5 using System.Threading.Tasks;
 6 
 7 namespace Client
 8 {
 9     class Program
10     {
11         static void Main(string[] args)
12         {
13             try
14             {
15                 using (NamedPipeClientStream pipeClient = new NamedPipeClientStream("localhost", "testpipe", PipeDirection.InOut, PipeOptions.None, TokenImpersonationLevel.None))
16                 {
17                     pipeClient.Connect(3000);//连接服务端
18                     StreamWriter sw = new StreamWriter(pipeClient);
19                     StreamReader sr = new StreamReader(pipeClient);
20                     while (true)
21                     {
22                         Console.WriteLine("SendMessage:");
23                         var Input = Console.ReadLine();
24                         sw.WriteLine(Input);
25                         sw.Flush();
26 
27                         var Result = sr.ReadLine();
28                         Console.WriteLine($"Reply:{Result}");
29                     }
30                 }
31             }
32             catch (Exception ex)
33             {
34                 throw ex;
35             }
36         }
37         
38     }
39 }
客户端

这样一个一对一的半双工通讯的程序就完成了

但是Web端肯定会有并发存在,如果这么一对一的通讯我总不能在Web后台长期阻塞等待服务器相应吧?如果采用消息列队就得不到服务器的响应了。

 

开始第一版踩坑设计

接下来说回正题,如何让管道进行“一对多”通讯。

首先客户端和服务端,再等待输入输出的时候线程都是阻塞的,那多个管道要进行同时通讯,就是要每个管道一个线程。

 

开始设计的“分发式”

1、客户端 链接 服务端 的主管道

2、服务端根据客户端发过来的密钥,生成一个带安全认证的私有管道

3、私有管道的名称和密钥发送回客户端,之后断开链接等待其他客户端链接

4、客户端根据发回来密钥和名称来链接私有管道

5、开始正常私有管道通讯,客户端断开链接后,私有管道自动销毁。

 

这个设计有个3个问题

1、首先客户端需要创建2个管道

2、所有的客户端都要先链接主管道,即使极大的减少了管道处理量,但是依旧会产生阻塞

3、占用大量的管道名称

 

 

等代码都写完了,回头重新产看微软官方的文档的时候,发现其实这种“分发式”是极为的脑残的

微软爸爸早就提供了更优雅的解决方案,其实同名的管道可以存在多个实例,每个实例可以链接一个不同的客户端。

那么之前的设计就需要改改了

 

最终的来了

经过修改,改成了“自分裂式”

设计思路

1、创建一个管道池,保持内部有一个待链接的管道实例

2、客户端去链接,系统会自动分配给客户端一个等待链接的服务端实例

3、当服务端链接后,再创建一个新的待链接的管道实例

4、当客户端断开链接或者超时,那么将自动销毁服务端实例

保持管道池中至少有1个待链接的实例,但是不能超过上限数量。

 

我就不上图了,直接上代码

Pipeline.cs

作为管理管道用的类

 1 using System;
 2 using System.Collections.Generic;
 3 using System.IO;
 4 using System.IO.Pipes;
 5 using System.Threading;
 6 using System.Threading.Tasks;
 7 
 8 namespace Server
 9 {
10     public class Pipeline : IDisposable
11     {
12         public Guid ID { get; }
13 
14         private NamedPipeServerStream Server;
15         private Task Task;
16         private AutoResetEvent Get, Got;
17         private string inputContext;
18         private StreamWriter Writer;
19         private StreamReader Reader;
20 
21         public const int MaxServer = 100;
22         public const string ServerName = "testpipe";
23         public const int ServerWaitReadMillisecs = 10000; //10s
24         public const int MaxTimeout = 3;
25 
26         public Pipeline()
27         {
28             ID = Guid.NewGuid();
29             Get = new AutoResetEvent(false);
30             Got = new AutoResetEvent(false);
31             Server = new NamedPipeServerStream(ServerName, PipeDirection.InOut, MaxServer);
32         }
33 
34         public void Start()
35         {
36             Task = Task.Factory.StartNew(TaskRun);
37         }
38 
39         private void TaskRun()
40         {
41             Server.WaitForConnection();
42             PipelinePool.CreatePipeLineAsync();
43             try
44             {
45                 Writer = new StreamWriter(Server);
46                 Reader = new StreamReader(Server);
47                 while (true)
48                 {
49                     var input = TryReadLine();
50                     if (string.IsNullOrEmpty(input)) break;
51                     //Do Somethin....
52                     Console.WriteLine($"Server {ID} Get Message:{input}");
53                     Writer.WriteLine($"Server Get Message:{input}");
54                     Writer.Flush();
55                 }
56             }
57             catch (TimeoutException)
58             {
59                 Console.WriteLine($"管道{ID}超时次数过多,视为丢失链接");
60             }
61             Console.WriteLine($"管道{ID}即将关闭");
62             Dispose();
63         }
64 
65         private void readerThread()
66         {
67             Get.WaitOne();
68             inputContext = Reader.ReadLine();
69             Got.Set();
70         }
71 
72         private string TryReadLine()
73         {
74             int TimeOutCount = 0;
75             var thread = new Thread(readerThread);
76             thread.Start();
77             Get.Set();
78             while (!Got.WaitOne(ServerWaitReadMillisecs))
79             {
80                 if (TimeOutCount++ > MaxTimeout)
81                 {
82                     thread.Abort();
83                     throw new TimeoutException();
84                 }
85                 Console.WriteLine($"管道{ID}第{TimeOutCount}次超时");
86             }
87             return inputContext;
88         }
89 
90         public void Dispose()
91         {
92             Server.Close();
93             Server.Dispose();
94             Get.Dispose();
95             Got.Dispose();
96             PipelinePool.DisposablePipeLineAsync(ID);
97         }
98     }
99 }

 

其中值得一提的是 TryReadLine 这个方法。

在普通的ReadLine时候,线程是阻塞的,造成后面的代码无法为运行。

如果客户端因为某个问题早成死锁或者崩溃,但是又未丢掉链接,这个线程就会一直阻塞下去。也无法释放。

无奈上了下最大的同性交友技术交流网站Stackoverflow,大神的确多,找到了这么一个解决方案 How to add a Timeout to Console.ReadLine()?

 经过修改,就成了上面的TryReadLine方法。

 

接下来池子就比较简单了

 1 using System;
 2 using System.Collections.Concurrent;
 3 using System.Threading.Tasks;
 4 
 5 namespace Server
 6 {
 7     public class PipelinePool
 8     {
 9         /// <summary>
10         /// 用于存储和管理管道的进程池
11         /// </summary>
12         private static ConcurrentDictionary<Guid, Pipeline> ServerPool = new ConcurrentDictionary<Guid, Pipeline>();
13 
14         /// <summary>
15         /// 创建一个新的管道
16         /// </summary>
17         private static void CreatePipeLine()
18         {
19             lock (ServerPool)
20             {
21                 if (ServerPool.Count < Pipeline.MaxServer)
22                 {
23                     var pipe = new Pipeline();
24                     pipe.Start();
25                     ServerPool.TryAdd(pipe.ID, pipe);
26                 }
27 
28             }
29             Console.WriteLine($"管道池添加新管道 当前管道总数{ServerPool.Count}");
30         }
31 
32         /// <summary>
33         /// 根据ID从管道池中释放一个管道
34         /// </summary>
35         private static void DisposablePipeLine(Guid Id)
36         {
37             lock (ServerPool)
38             {
39                 Console.WriteLine($"开始尝试释放,管道{Id}");
40                 if (ServerPool.TryRemove(Id, out Pipeline pipe))
41                     Console.WriteLine($"管道{Id},已经关闭,并完成资源释放");
42                 else
43                     Console.WriteLine($"未找到ID为{Id}的管道");
44                 if (ServerPool.Count == 0)
45                     CreatePipeLine();
46             }
47         }
48 
49         /// <summary>
50         /// (异步)创建一个新的管道进程
51         /// </summary>
52         public static async void CreatePipeLineAsync() => await Task.Run(new Action(CreatePipeLine));
53 
54         /// <summary>
55         /// (异步)根据ID从管道池中释放一个管道
56         /// </summary>
57         /// <param name="id"></param>
58         public static async void DisposablePipeLineAsync(Guid id) => await Task.Run(() => { DisposablePipeLine(id); });
59 
60     }
61 }

 

 

然后程序的入口方法

 1 using System;
 2 
 3 namespace Server
 4 {
 5     class Program
 6     {
 7         
 8         static void Main(string[] args)
 9         {
10             PipelinePool.CreatePipeLineAsync();
11             Console.ReadKey();
12         }
13      
14     }
15    
16 }

 

 

服务端的修改就完成了

 

客户端其实保持不变就可以

为了测试,修改一下

 1 using System;
 2 using System.Collections.Generic;
 3 using System.IO;
 4 using System.IO.Pipes;
 5 using System.Security.Principal;
 6 using System.Threading.Tasks;
 7 using Models;
 8 
 9 namespace Client
10 {
11     class Program
12     {
13         static void Main(string[] args)
14         {
15             List<PipeTest> list = new List<PipeTest>();
16             for (int i = 0; i < 10; i++)
17                 list.Add(new PipeTest(i));
18             list.ForEach(a => Task.Factory.StartNew(a.run));
19             Console.ReadKey();
20         }
21     }
22 
23     public class PipeTest
24     {
25         public int id { get; }
26 
27         public PipeTest(int id)
28         {
29             this.id = id;
30         }
31 
32         private List<string> Message = new List<string> { "aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg" };
33 
34         public void run()
35         {
36             try
37             {
38                 using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(
39                     PipeCommunicationConfiguration.ServerName, PipeCommunicationConfiguration.PipeName,
40                     PipeDirection.InOut, PipeOptions.None, TokenImpersonationLevel.None)
41                 )
42                 {
43                     pipeClient.Connect(PipeCommunicationConfiguration.ClientConnectTimeout);//连接服务端
44                     StreamWriter sw = new StreamWriter(pipeClient);
45                     StreamReader sr = new StreamReader(pipeClient);
46                     foreach(string msg in Message)
47                     {
48                         Console.WriteLine($"Client {id} SendMessage:" + msg);
49                         sw.WriteLine(msg);//传递消息到服务端
50                         sw.Flush();//注意一定要有,同服务端一样
51                         
52                         string temp = sr.ReadLine();//获取服务端返回信息
53                         if (!pipeClient.IsConnected)
54                         {
55                             Console.WriteLine("Pipe is Broken");
56                             break;
57                         }
58                         Console.WriteLine("replyContent:" + temp);
59                     }
60                     pipeClient.Close();
61                 }
62             }
63             catch (Exception ex)
64             {
65             }
66             Console.WriteLine($"Client {id} end of conversation");
67         }
68     }
69 }

 

这样客户端就可以同时启动多个线程,并向服务端通讯

 

运行结果

 

测试成功!

 

总结:

管道本身只能实现一对一进行通信,这个是不能改变的。

那多管道通信就成了必然,对管道数量的伸缩和进程的管理就成了主要的问题。

一开始没有好好看文档走了不少弯路,还整了个“分发式”...笑哭😂

整个学习过程主要还是耗费在了多线程的管理上。

写篇博客留以后复习用

如果有问题,欢迎指出

posted @ 2018-03-21 20:38  写代码的相声演员  阅读(10816)  评论(6编辑  收藏  举报