进程间的通讯经常需要走网络。网络的交互方式有很多,最基础的就是Socket,.NET平台还封装了TcpListener和TcpClient,跟Socket相差不大,属于简单封装。
另外我们公司常用的交互方式还有ZeroMQ和RabbitMQ。
其实C++那边用ZeroMQ比较多。C++开发经常也开前后台,有人负责前台的界面部分,可能会使用MFC或QT,然后另外有人负责后台的业务代码。C++的前后端交互有时候经常也直接使用ZeroMQ,一些状态和配置的东西,有时候也会借助Redis数据库。
ZeroMQ他们使用最多的还是发布/订阅模式。
C#这边,ZeroMQ的实现对应是NetMQ,一些常用的模式都有。
但在之前的项目使用中,发些一些问题:
1、请求/响应模式,其实相当的脆弱,请求端和响应端的收发必须保持严格的一致,中间如果出现任意一个请求端的操作不正确,会导致响应端的线程也崩掉,导致服务也会不正常。
这点上个人感觉还是不太能接触。其实类似的TCP的监听和HTTP的监听,某一个终端交互的异常,可能会导致和这一路的终端交互会异常或中断,但一般都不会影响其它终端的交互和监听响应端的后绫正常工作。
所以强烈建议不要使用这个请求/响应模式,这个问题,我不是知道是NetMQ的问题,还是所有的ZeroMQ都会有这个问题。
2、NetMQ的发布/订阅,底层的实现一般都还是一个个TCP Socket,当然还可以设置其它的连接方式。
这种方式会有什么问题呢?相当于一个终端的订阅就是一个Socket连接,这种方式其实并不适合大批量大流量的订阅发布。
之前我们有个项目,拿发布/订阅模式去分发文件。服务端用的是千兆网卡。连了30台机器,其实每个终端分到的有效带宽是3MB/S。
发布端往消息队列里面放文件数据的时候,只要超过3MB/S,相当于整个的带宽已经不足够把数据发出去了。
这样的NetMQ底层的队列就会积压缓存,过一段时间,队列就会满出来。队列满出来以后,NetMQ就会去丢掉一些数据包。类似一些大文件,丢掉一些包,就会导致这个文件传送就失败了。
所以当时在遇到很多终端连着,并且传送大文件的时候,是有问题的。
连接的终端比较少,或者传送小文件的时候是没有问题的。连接的终端少时,每个终端分到的带宽就比较多,传输数据就比较快,就不容易出现堆积的情况。传送小文件的时候,因为本身持续的时间比较短,消息队列的缓存还没满,整个发送过程就结束了。
因为当时已经是在项目后期,我们没办法重新更换网络传输架构,所以我们当时的解决方式是,发布端控制一下往队列里面放数据的速度,比如我们就按3MB/S的速度往队列里面放,达到这个速率了以后,我们就暂停。
因为我们主要用来传输一些文档类的小文件,所以这样限制其实问题不大。
但这样的限制,在连接的终端比较少的情况下,其实是比较浪费的。相当于千兆的网络,如果就连了两台的机子,也限制了3MB/S的速度,其实就是在浪费带宽。所以其实还需要根据连接的终端情况来调整带宽,所以相对就比较复杂。
所以后面的时间,我其实也有一直在研究这个文件大批量分开发的问题。个人感觉比较好的理念,其实是P2P的方式,当然也可以使用组播这些方式。但是我做的那个项目里面,有种情况,有些终端可能会后续连进来的,所以想完全靠组播来一劳永逸的解决也比较难。
所以当时的想法就是想着,不要把流量全部集中在服务端。大文件的话可以采用分块的方式,一般终端已经下载了,没下的终端可以从已经下载的终端来获取。
一种解决方案是借助IPFS。这种方式需要在服务器和每个终端上先安装一个IPFS的服务。然后借助IpfsShipyard.Ipfs.Http.Client,调用IPFS的服务来实现。
Install-Package IpfsShipyard.Ipfs.Http.Client
示例代码:
using Ipfs.Http;
var ipfs = new IpfsClient();
const string filename = "QmXarR6rgkQ2fDSHjSY5nM2kuCXKYGViky5nohtwgF65Ec/about";
string text = await ipfs.FileSystem.ReadAllTextAsync(filename);
还有一种方式,就是完全自己实现,自己切块,每个终端都去监听连接。自己去控制去哪里获取分块。
分块采HASH值标识和保存。
SOCKET的使用
很多同事都不喜欢直接使用SOCKET,主要是觉得状态的维护比较麻烦。包括C++那边,平常喜欢用ZeroMQ,主要也还是觉得它不用维护终端和连接状态。
但是个人的话,不排诉Socket,有时候甚至喜欢用Socket,主要也是因为状态的连接和可控。
用了第三方封装的通讯库,有个很大的问题,一些基础的信息和连接状态,有时候反而变得比较难感知。
关于Socket的使用,最近也有一些想法。
个人感觉其实不同的操作,可以用不同的通道来处理。
类似有些操作,其实比较像请求响应,其实可以使用单次连接的方式,连上去,发消息,然后收消息,收完以后就把连接给关闭了。
还有一些有点像发布订阅的方式,连上去,告诉服务端,我要什么东西,然后服务端按要求持续的发送数据。
另外还有一些情况,采集的间隔可变的,客户端发一个采集的请求,服务端就回一帧的。什么时候停止也是客户端自己决定的。其实也可以起一个专门的线程和连接去完成这部分的操作。
我个人的理解,其实分线程分连接去完成上面三部分的工作。可以不要用同一个Socket连接,这样的话,逻辑相对会比较简单。