NetMQ将消息通信分成4种模型
- 一对一结对模型(Exclusive-Pair):一个TCP Connection,但是TCP Server只能接受一个连接,数据可以双向流动
- 请求回应模型(Request-Reply):跟一对一结对模型的区别在于请求端可以是1 ~N个,该模型主要用于远程调用及任务分配等
- 发布订阅模型(Publish-Subscribe):发布端单向分发数据,且不关心是否把全部信息发送给订阅端。如果发布端开始发布信息时, 订阅端尚未连接上来,则这些信息会被直接丢弃。订阅端未连接导致信息丢失的问题,可以通过与请求回应模型组合来解决。订阅端只负责接收,而不能反馈,且在订阅端消费速度慢于发布端的情况下,会在订阅端堆积数据。该模型主要用于数据分发。天气预报、微博明星粉丝可以应用这种经典模型。
- 推拉模型(Push-Pull):Server端作为Push端,而Client端作为Pull端,如果有多个Client端同时连接到Server端,则Server端会在内部做一个负载均衡,采用平均分配的算法,将所有消息均衡发布到Client端上。与发布订阅模型相比,推拉模型在没有消费者的情况下,发布的消息不会被消耗掉;在消费者能力不够的情况下,能够提供多消费者并行消费解决方案。该模型主要用于多任务并行。
对象介绍
- RequestSocket:经典的请求Socket构件,一般和ResponseSocket一起组合成请求应答模式。
- ResponseSocket:请求应答中的应答方,中间可以加入XPublishSocket,RouterSocket等扩展最终到达RequestSocket。
- RouterSocket、DealerSocket: 当需要保证请求应答模式中可扩展性时需要在两者之间添加一个中间方隔离两端的耦合。这时候就需要RouterSocket+DealerSocket组 合。 RouterSocket负责连接RequestSocket,DealerSocket则负责Response的一头
- PublisherSocket:发布订阅中的发布方。注意由于ZeroMQ的高效,注意尽量让订阅方先启动,保证不丢失消息。
- SubscriberSocket:发布订阅模式中的订阅方,注意由于发布订阅模式实际是在订阅方做消息筛选的,所有实际上订阅方将接收所有的发布消息再更加自己的订阅清理不需要的。
- XSubscriberSocket、XPublisherSocket:可能您的发布订阅又是会需要跨网络的广播,这时候您需要在另一个网络中有一个代理,XSubscriberSocket + XPublisherSocket就是为此而生的,XSubscriberSocket负责承上,XPublisherSocke负责承上。
- PairSocket:当你的一个任务需要跨线程、跨进程甚至跨服务器时就会用到PairSocket模式,它可以在自己任务启动线程指向第一步的队列,然后等待最后一步所在的队列返回结果即可,开始和结束队列直接可以有多个步骤队列,以流水线的方式连接再一起工作。
- PushSocket:当你不满足于PariSocket只能单线管道模式之下时,你会用到推拉模式,这种模式允许你在任任务流水线的任一环节做并行处理,并在并行后的下一环节归拢整理结果。
- Pullsocket:推拉模式中的拉的一方。
本文介绍请求相应模式:
最多支持10个通道
public class SyncMqIpc:ISyncIpc
{
/// <summary>
/// 超时时间 默认15s
/// </summary>
public int TimeOut { get; set; } = 15;
string ip = "127.0.0.1";
private CancellationTokenSource cancel = new CancellationTokenSource();
/// <summary>
/// 服务端集合
/// </summary>
private ConcurrentDictionary<string, IPCServer> _serviceDict = new ConcurrentDictionary<string, IPCServer>();
/// <summary>
/// 注册的客户端消息通道
/// </summary>
private Dictionary<string, WeakReference> clientMsgTunnel = new Dictionary<string, WeakReference>();
/// <summary>
/// 客户端集合
/// </summary>
private ConcurrentDictionary<string, IPCClient> _clientDict = new ConcurrentDictionary<string, IPCClient>();
NetMQPoller mQPoller;
NetMQTimer timer;
#region 服务端
///给方法添加超时的机制会导致并发时 执行效率变慢
///添加超时是为了释放通道 可考虑单独线程释放通道
/// <summary>
/// 开启服务默认支持10个并发
/// </summary>
/// <param name="pooler">并发数量</param>
/// <returns></returns>
public int StartService(int pooler = 10)
{
if (_serviceDict.Count() == 20)
{
return 9999;
}
var res = 0;
int port = 12138;
timer = new NetMQTimer(TimeSpan.FromMilliseconds(1000));
timer.Elapsed += (sender, args) =>
{
foreach (var item in _serviceDict)
{
var service = item.Value;
if (service.IsBusy && service.HasOut == true && service.HasIn == false)
{
if (DateTime.Now.Subtract(service.DateTime.Value).TotalMilliseconds > TimeOut * 1000)
{
service.IsBusy = false;
service.DateTime = null;
var msg = new IpcMsg()
{
IpcProtocol = new IpcProtocol(IpcMsgType.System)
};
SetTimeoutMsg(msg, new TimeoutException("服务处理消息超时!"), MsgDirection.ToClient);
service.MQSocket.SendMoreFrame(item.Key).SendFrame(Newtonsoft.Json.JsonConvert.SerializeObject(
msg,
new JsonSerializerSettings()
{
StringEscapeHandling = StringEscapeHandling.EscapeNonAscii,
}).Compress());
LogManager.Log()?.Error($"服务接收[{item.Key}]超时");
}
}
}
};
mQPoller = new NetMQPoller { timer };
mQPoller.RunAsync();
for (int i = 0; i < pooler; i++)
{
var currPort = port + i;
try
{
var serverSocket = new ResponseSocket($"@tcp://{ip}:{currPort}");
var iPCServer = new IPCServer() { MQSocket = serverSocket, Address = $"tcp://{ip}:{currPort}", };
//服务持久化
_serviceDict.AddOrUpdate(iPCServer.Address, iPCServer, (key, value) => iPCServer);
Task.Run(() =>
{
while (!cancel.IsCancellationRequested)
{
try
{
NetMQMessage netMQFrames = serverSocket.ReceiveMultipartMessage();
_serviceDict[serverSocket.Options.LastEndpoint].IsBusy = true;
_serviceDict[serverSocket.Options.LastEndpoint].DateTime = DateTime.Now;
var from = netMQFrames.FirstOrDefault().ConvertToString();
var str = netMQFrames.LastOrDefault().ConvertToString(Encoding.Default).Decompress();
IpcMsg Msg = Newtonsoft.Json.JsonConvert.DeserializeObject<IpcMsg>(str);
WeakReference wr = null;
if (clientMsgTunnel.TryGetValue(Msg.IpcProtocol.ClientKey, out wr))
{
//如果服务端有原始消息记录
//那收到了就清理掉
if (wr.IsAlive)
{
//尝试清除下消息
IIpcServerMsgHandle handle = wr.Target as IIpcServerMsgHandle;
if (handle != null && !handle.StopRecive)
{
handle.HandleIpcMsg(Msg);
}
}
}
if (_serviceDict[serverSocket.Options.LastEndpoint].IsBusy && serverSocket.HasOut == true && serverSocket.HasIn == false)//避免超时重复发送
{
serverSocket.SendMoreFrame(serverSocket.Options.LastEndpoint).SendFrame(Newtonsoft.Json.JsonConvert.SerializeObject(
Msg,
new JsonSerializerSettings()
{
StringEscapeHandling = StringEscapeHandling.EscapeNonAscii,
}).Compress());
_serviceDict[serverSocket.Options.LastEndpoint].IsBusy = false;
_serviceDict[serverSocket.Options.LastEndpoint].DateTime = null;
}
else
{
}
}
catch (TerminatingException te)
{
serverSocket.Dispose();
break;
}
catch (Exception ex)
{
if (!cancel.IsCancellationRequested && _serviceDict[serverSocket.Options.LastEndpoint].IsBusy && serverSocket.HasOut == true && serverSocket.HasIn == false)
{
var msg = new IpcMsg()
{
IpcProtocol = new IpcProtocol(IpcMsgType.System),
};
SetErroMsg(msg, ex,MsgDirection.ToClient);
serverSocket.SendMoreFrame(serverSocket.Options.LastEndpoint).SendFrame(Newtonsoft.Json.JsonConvert.SerializeObject(
msg,
new JsonSerializerSettings()
{
StringEscapeHandling = StringEscapeHandling.EscapeNonAscii,
}).Compress());
LogManager.Log()?.Error($"服务接收[{currPort}]异常", ex);
}
}
}
}, cancel.Token);
}
catch (Exception ex)
{
LogManager.Log()?.Error($"启动服务[{currPort}]失败", ex);
}
}
return res;
}
/// <summary>
/// 关闭服务
/// </summary>
/// <returns></returns>
public int StopService()
{
if (_serviceDict == null) return -1;
cancel.Cancel();
clientMsgTunnel.Clear();
timer.Enable = false;
mQPoller.Stop();
mQPoller.Remove(timer);
mQPoller.Dispose();
foreach (var item in _serviceDict)
{
item.Value.MQSocket.Unbind(item.Key);
item.Value.MQSocket.Close();
item.Value.MQSocket.Dispose();
}
//NetMQConfig.Cleanup(false);
_serviceDict.Clear();
return 0;
}
/// <summary>
/// 注册信道
/// 信道的注册原则使后来的替换原有的,以新的为准
/// </summary>
/// <param name="action"></param>
public void SubscribeReciveClientTunnel(IIpcServerMsgHandle action)
{
if (action == null)
{
return;
}
if (action == null || action.SubscribedClientKeys == null)
{
return;
}
for (int x = 0; x < action.SubscribedClientKeys.Length; x++)
{
string key = action.SubscribedClientKeys[x];
WeakReference wr = null;
if (clientMsgTunnel.TryGetValue(key, out wr) && wr != null)
{
if (wr.IsAlive)
{
if (!ReferenceEquals(wr.Target, action))
{
wr = new WeakReference(action);
}
else
{
//do nothing
}
}
else
{
wr = new WeakReference(action);
}
}
else
{
clientMsgTunnel[key] = new WeakReference(action);
}
}
}
//取消注册
public void UnSubscribeReciveClientTunnel(object subobj)
{
if (subobj == null)
{
return;
}
IIpcServerMsgHandle action = subobj as IIpcServerMsgHandle;
if (action == null || action.SubscribedClientKeys == null)
{
return;
}
action.StopRecive = true;
for (int x = 0; x < action.SubscribedClientKeys.Length; x++)
{
string key = action.SubscribedClientKeys[x];
WeakReference wr = null;
if (clientMsgTunnel.TryGetValue(key, out wr) && wr != null)
{
if (wr.IsAlive)
{
if (ReferenceEquals(wr.Target, action))
{
clientMsgTunnel.Remove(key);
}
}
else
{
clientMsgTunnel.Remove(key);
}
}
}
}
#endregion
#region 客户端
/// <summary>
/// 客户端默认支持10个并发
/// </summary>
/// <param name="pooler">并发数量</param>
/// <returns></returns>
public int StartClient(int pooler = 20)
{
var res = 0;
int port = 12138;
for (int i = 0; i < pooler; i++)
{
var currPort = port + i;
var clientSocket = new RequestSocket($">tcp://{ip}:{currPort}");
var iPCClient = new IPCClient() { MQSocket = clientSocket, Address = $"tcp://{ip}:{currPort}", IsBusy = false };
_clientDict.AddOrUpdate(iPCClient.Address, iPCClient, (key, value) => iPCClient);
}
return res;
}
public int StopClient()
{
cancel.Cancel();
foreach (var item in _clientDict)
{
item.Value.MQSocket.Disconnect(item.Key);
item.Value.MQSocket.Close();
item.Value.MQSocket.Dispose();
}
//NetMQConfig.Cleanup(false);
_clientDict.Clear();
return 0;
}
/// <summary>
/// 发送服务请求消息
/// </summary>
/// <param name="msg"></param>
/// <returns></returns>
public Task<IpcMsg> SendMsgToService(IpcMsg msg)
{
var clients = _clientDict.Where(s =>
s.Value.IsBusy == false &&
s.Value.HasOut == true &&
s.Value.HasIn == false).ToList();
if (clients.Count() == 0)
{
msg.ServerMsg = "节点数量不足!";
return Task.Run(() => msg);
}
Random rd = new Random();
var index = rd.Next(0, clients.Count());
var client = clients[index];
client.Value.IsBusy = true;
return Task.Run(() =>
{
client.Value.MQSocket.SendMoreFrame(client.Key.ToString()).SendFrame(Newtonsoft.Json.JsonConvert.SerializeObject(
msg,
new JsonSerializerSettings()
{
StringEscapeHandling = StringEscapeHandling.EscapeNonAscii,
}).Compress());
NetMQMessage netMQFrames = client.Value.MQSocket.ReceiveMultipartMessage();
var from = netMQFrames.FirstOrDefault().ConvertToString();
_clientDict[from].IsBusy = false;
var str = netMQFrames.LastOrDefault().ConvertToString(Encoding.Default).Decompress();
IpcMsg Msg = Newtonsoft.Json.JsonConvert.DeserializeObject<IpcMsg>(str);
return Msg;
}, cancel.Token);
}
#endregion
void SetTimeoutMsg(IpcMsg msg, TimeoutException ex, MsgDirection msgDirection)
{
msg.RspCode = -9;
msg.RspMsg = ex.Message;
msg.IpcProtocol.Direction = msgDirection;
msg.IpcProtocol.ReceiveTime = DateTime.Now;
}
void SetErroMsg(IpcMsg msg, Exception ex, MsgDirection msgDirection)
{
msg.RspCode = -1;
msg.RspMsg = ex.Message;
msg.IpcProtocol.Direction = msgDirection;
msg.IpcProtocol.ReceiveTime = DateTime.Now;
}
#region Sync 方法超时机制 同步方法阻塞时永远卡死
public delegate TR TimeOutDelegate<in T, out TR>(T param);
public delegate void TimeOutDelegate<in T>(T param);
/// <summary>
/// Execute a method with timeout check
/// </summary>
/// <typeparam name="T">Target method parameter type</typeparam>
/// <typeparam name="TR">The result type of execution</typeparam>
/// <param name="timeoutMethod">Target method</param>
/// <param name="param">Target method parameter</param>
/// <param name="result">The result of execution</param>
/// <param name="timeout">Set timeout length</param>
/// <returns>Is timeout</returns>
public bool Execute<T, TR>(TimeOutDelegate<T, TR> timeoutMethod, T param, out TR result, long timeout)
{
TimeSpan timeSpan = TimeSpan.FromMinutes(timeout);
var asyncResult = timeoutMethod.BeginInvoke(param, null, null);
if (!asyncResult.AsyncWaitHandle.WaitOne(timeSpan, false))
{
result = default(TR);
return true;
}
result = timeoutMethod.EndInvoke(asyncResult);
return false;
}
public bool Execute<T>(TimeOutDelegate<T> timeoutMethod, T param, long timeout)
{
TimeSpan timeSpan = TimeSpan.FromSeconds(timeout);
//var asyncResult = timeoutMethod.BeginInvoke(param, null, null);
//if (!asyncResult.AsyncWaitHandle.WaitOne(timeSpan, false))
//{
// return true;
//}
//timeoutMethod.EndInvoke(asyncResult);
timeoutMethod.Invoke(param);
return false;
}
#endregion
}