NetMQ是我在网吧项目中所使用的一种消息队列,是ZeroMQ在.net平台上的移植版本,非常轻量级,使用方便,响应速度快,代码也便于阅读和编写,但是功能比较少,适合在小型项目中进行使用。
这次在项目中,需要对发往客户端的消息进行加密,然后客户端自行解密来确保发送的消息不会被别人看到,在这种情况下,很自然的想到了使用非对称加密算法来对消息进行加密(其实在之前有想过采用类似WCF中的身份认证方式来防止别人连接到这个接口,但是我翻阅了一下官方文档,NetMQ似乎并没有这个功能,所以最后采用了消息加密的功能,这个当然也是一个比较普遍的方式)。
NetMQ的使用还是非常方便的,在项目中直接引用netmq.dll就可以直接使用了,所以这里就不再赘述如何下载安装,直接进入正题:如何使用NetMQ。
public class ShowServer
{
private static ShowServer showServer = null;
private RouterSocket ServerSocket;
private NetMQPoller Poller;
private NetMQMonitor Monitor;
public Queue<MsgObject> SendMessageQueue;
public IProgress<MsgObject> ReceiveHanlder;
private IProgress<MsgObject> receiveHanlder;
private Queue<MsgObject> EventQueue;
public bool IsRunning { get; set; }
///
/// 构造函数
///
private ShowServer()
{
SendMessageQueue = new Queue<MsgObject>();
EventQueue = new Queue<MsgObject>();
ServerSocket = new RouterSocket("tcp://0.0.0.0:6667");
ServerSocket.ReceiveReady += ServerSocket_ReceiveReady;
ServerSocket.SendReady += ServerSocket_SendReady;
ServerSocket.Options.RouterHandover = true;
Monitor = new NetMQ.Monitoring.NetMQMonitor(ServerSocket, "inproc://show.inproc", SocketEvents.All);
Monitor.Accepted += Monitor_Accepted;
Monitor.Disconnected += Monitor_Disconnected;
receiveHanlder = new Progress<MsgObject>(HandleMsg);
ReceiveHanlder = receiveHanlder;
Poller = new NetMQPoller() { ServerSocket };
}
public static ShowServer GetServer()
{
if(showServer == null){
showServer = new ShowServer();
}
return showServer;
}
private void Monitor_Accepted(object sender, NetMQMonitorSocketEventArgs e)
{
System.Diagnostics.Debug.WriteLine(DateTime.Now + "连接上 ,address:" + e.Address);
}
private void Monitor_Disconnected(object sender, NetMQMonitorSocketEventArgs e)
{
System.Diagnostics.Debug.WriteLine(DateTime.Now + "" + e.Address);
}
private void ServerSocket_SendReady(object sender, NetMQSocketEventArgs e)
{
while (SendMessageQueue.Count > 0)
{
try
{
var sendMsg = SendMessageQueue.Dequeue();
if (sendMsg.Content == null)
{
continue;
}
var msgFrame = ConvertToNetMessage(sendMsg);
ServerSocket.SendMultipartMessage(msgFrame);
}
catch (Exception ex)
{
Log.WriteLog_EX(ex.ToString());
}
}
//发送数据间隔
Thread.Sleep(10);
}
private NetMQMessage ConvertToNetMessage(MsgObject sendMsg)
{
var msgFrame = new NetMQMessage();
msgFrame.Append(sendMsg.MachineName);
msgFrame.AppendEmptyFrame();
msgFrame.Append(sendMsg.Content, Encoding.UTF8);
return msgFrame;
}
private MsgObject ConvertToMsgObj(NetMQMessage sendMsg)
{
if (sendMsg.FrameCount == 3)
{
var newMsg = new MsgObject();
var machineName = sendMsg[0].ConvertToString();
//var content = sendMsg[2].ConvertToString();
var content = Encoding.UTF8.GetString(sendMsg[2].Buffer);
newMsg.MachineName = machineName;
newMsg.Content = content;
return newMsg;
}
return null;
}
private void ServerSocket_ReceiveReady(object sender, NetMQSocketEventArgs e)
{
var msg = ConvertToMsgObj(ServerSocket.ReceiveMultipartMessage());
//接收到消息,让handler处理
if (msg != null)
{
ReceiveHanlder?.Report(msg);
}
}
public void Start()
{
IsRunning = true;
Poller.RunAsync();
//Monitor.StartAsync();
}
public void Stop()
{
IsRunning = false;
Poller.StopAsync();
}
public void EventEnqueue(MsgObject msg)
{
EventQueue.Enqueue(msg);
}
}
在这个例子中展示了如何生成一个服务端,当然这边的代码并不按照官网网站上所示例的。官网上的示例代码是这个样子的:
using (var rep1 = new ResponseSocket("@tcp://*:5001"))
using (var rep2 = new ResponseSocket("@tcp://*:5002"))
using (var poller = new NetMQPoller { rep1, rep2 })
{
// these event will be raised by the Poller
rep1.ReceiveReady += (s, a) =>
{
// receive won't block as a message is ready
string msg = a.Socket.ReceiveString();
// send a response
a.Socket.Send("Response");
};
rep2.ReceiveReady += (s, a) =>
{
// receive won't block as a message is ready
string msg = a.Socket.ReceiveString();
// send a response
a.Socket.Send("Response");
};
// start polling (on this thread)
poller.Run();
}
在官方文档上的代码写的更加简洁一点(这个例子其实是接收端的,官网上的发送端的例子居然还是TODO),直接使用了using语句,using语句是指函数在离开这个using块的时候直接dispose using括号内所占用的资源。然后官网例子上的receiveRead是直接使用了lambda表达式写一个比较简洁的函数,然后把这个函数挂载到事件钩子上。
poller是一个比较重要的点,poller的意思为轮询器,当需要一次产生多个端口的时候,则采用poller是一个比较合理的方式,根据官方文档上的说明,如果不使用poller,接收消息的时候将会永远阻塞在第一个rep1处。同时,NetMQ并非是一个线程安全的消息队列,比如你想在一个端口上接收消息,然后在另外一个端口上发送消息的话也必须使用poller。
关于NetMQMonitor我并没有在官方网站上找到相应的说明,可能是已经不再使用了,也可能是提供给我这个dll的人魔改过了源码。但是从功能来看应该是一个监视器,当端口接收或者发送消息的时候能够挂载一些其他的方法。构造函数中的第一个参数是端口,第二个参数应该是进程间通信的一个管道名,第三个则是端口的哪些事件被监听。
ReceiveHanlder则是一个接收到消息之后的钩子,在这边可以添加一个消息处理函数,根据不同的消息进行不同的处理。
在这次的项目中,单一的消息类型肯定是不能解决所有问题的,所以还需要消息分类,消息处理函数根据消息的不同类型进行不同的操作。
public class MsgObject
{
public string MachineName;
public string Content;
public class ShowLoginMsg
{
public string cardId;
public string realName;
public string memLevel;
public string machineName;
public string startTime;
public long timeStamp = TimeHelper.ConvertToTimestamp(DateTime.Now);
}
public MsgObject CreatShowLoginMsg(string machineName, Customer customer)
{
ShowLoginMsg temp = new ShowLoginMsg();
temp.cardId = customer.TypeId;
temp.machineName = machineName;
temp.memLevel = customer.MemLevel;
temp.realName = customer.Name;
temp.timeStamp = TimeHelper.ConvertToTimestamp(DateTime.Now);
MsgObject msg = new MsgObject();
msg.Content = JsonConvert.SerializeObject(temp,
new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });
return msg;
}
}
在这个示例中,MsgObject是一个总体的消息类,一共有两个属性:machineName和content,对于发送端来说,machineName标志了应该把消息发送给哪一台机器,content则是序列化后的消息类,在上面的代码片段中可以看到,我在MsgObject类中声明了一个CreatShowLoginMsg方法,在这个方法中传入一些必要的参数,然后返回一个msg。当然也可以直接把这个msg压入消息队列中。
private void HandleMsg(MsgObject msg)
{
var temp2 = JsonConvert.DeserializeObject(msg.Content,
new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });
if (temp2.GetType() == typeof(ShowMsg))
{
ShowMsg tempMsg = (ShowMsg)temp2;
if (tempMsg.publickey.Equals(RSAEncryption.PublicKey))
{
while (EventQueue.Count != 0)
{
try
{
MsgObject sendmsg = EventQueue.Dequeue();
sendmsg.Content = RSAEncryption.RSAEncrypt(sendmsg.Content);
sendmsg.MachineName = msg.MachineName;
SendMessageQueue.Enqueue(sendmsg);
}
catch(ArgumentException ex)
{
Log.WriteLog_EX(ex.ToString());
}
catch (Exception ex)
{
Log.WriteLog_EX(ex.ToString());
}
}
}
}
}
在这个方法中,服务器接收到消息后,先验证一下消息中传过来的公钥是否与本地保存的公钥是否相同,如果相同的话则把本地消息事件队列中的消息读取出来,加密之后再压入消息发送方队列中(这边主要是由于需求所致,其实只需要发送消息的时候把消息内容进行加密,然后压入发送消息队列就行了)。
public class RSAEncryption
{
private static string publicKey;
public static string PublicKey
{
get
{
return publicKey;
}
set
{
publicKey = value;
}
}
///
/// 生成公私钥对
///
///
///
public static void RSAGenerateKey(ref string privateKey, ref string publicKey)
{
if (!File.Exists("PrivateKey.xml"))
{
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
//私钥
using (StreamWriter writer = new StreamWriter("PrivateKey.xml"))
{
privateKey = rsa.ToXmlString(true);
writer.WriteLine(rsa.ToXmlString(true));
}
//公钥
using (StreamWriter writer = new StreamWriter("PublicKey.xml"))
{
publicKey = rsa.ToXmlString(false);
writer.WriteLine(rsa.ToXmlString(false));
}
}
else
{
using (StreamReader read = new StreamReader("PrivateKey.xml"))
{
privateKey = read.ReadLine();
}
using (StreamReader read = new StreamReader("PublicKey.xml"))
{
publicKey = read.ReadLine();
}
}
}
///
/// 重新生成公私钥对
///
///
///
public static void RSAReGenerateKey(ref string privateKey, ref string publicKey)
{
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
//私钥
using (StreamWriter writer = new StreamWriter("PrivateKey.xml", false))
{
privateKey = rsa.ToXmlString(true);
writer.WriteLine(rsa.ToXmlString(true));
}
//公钥
using (StreamWriter writer = new StreamWriter("PublicKey.xml", false))
{
publicKey = rsa.ToXmlString(false);
writer.WriteLine(rsa.ToXmlString(false));
}
}
///
/// 控制台获得公钥
///
static public void GetPublickey()
{
if (File.Exists("PublicKey.xml"))
{
using (StreamReader read = new StreamReader("PublicKey.xml"))
{
PublicKey = read.ReadLine();
}
}else
{
PublicKey = "";
}
}
///
/// 控制台更改公钥
///
///
static public void ChangePublickey(string publickey)
{
using (StreamWriter writer = new StreamWriter("PublicKey.xml", false))
{
PublicKey = publickey;
writer.WriteLine(publickey);
}
}
///
/// 加密
///
/// 所加密的内容
/// 加密后的内容
static public string RSAEncrypt(string rawInput)
{
if (string.IsNullOrEmpty(rawInput))
{
return string.Empty;
}
if (string.IsNullOrWhiteSpace(PublicKey))
{
throw new ArgumentException("Invalid Public Key");
}
using (var rsaProvider = new RSACryptoServiceProvider())
{
var inputBytes = Encoding.UTF8.GetBytes(rawInput);//有含义的字符串转化为字节流
rsaProvider.FromXmlString(PublicKey);//载入公钥
int bufferSize = (rsaProvider.KeySize / 8) - 11;//单块最大长度
var buffer = new byte[bufferSize];
using (MemoryStream inputStream = new MemoryStream(inputBytes),
outputStream = new MemoryStream())
{
while (true)
{ //分段加密
int readSize = inputStream.Read(buffer, 0, bufferSize);
if (readSize <= 0)
{
break;
}
var temp = new byte[readSize];
Array.Copy(buffer, 0, temp, 0, readSize);
var encryptedBytes = rsaProvider.Encrypt(temp, false);
outputStream.Write(encryptedBytes, 0, encryptedBytes.Length);
}
return Convert.ToBase64String(outputStream.ToArray());//转化为字节流方便传输
}
}
}
///
/// 解密
///
/// 私钥
/// 加密后的内容
/// 解密后的内容
static public string RSADecrypt(string privateKey, string encryptedInput)
{
if (string.IsNullOrEmpty(encryptedInput))
{
return string.Empty;
}
if (string.IsNullOrWhiteSpace(privateKey))
{
throw new ArgumentException("Invalid Private Key");
}
using (var rsaProvider = new RSACryptoServiceProvider())
{
var inputBytes = Convert.FromBase64String(encryptedInput);
rsaProvider.FromXmlString(privateKey);
int bufferSize = rsaProvider.KeySize / 8;
var buffer = new byte[bufferSize];
using (MemoryStream inputStream = new MemoryStream(inputBytes),
outputStream = new MemoryStream())
{
while (true)
{
int readSize = inputStream.Read(buffer, 0, bufferSize);
if (readSize <= 0)
{
break;
}
var temp = new byte[readSize];
Array.Copy(buffer, 0, temp, 0, readSize);
var rawBytes = rsaProvider.Decrypt(temp, false);
outputStream.Write(rawBytes, 0, rawBytes.Length);
}
return Encoding.UTF8.GetString(outputStream.ToArray());
}
}
}
}
这个类基本没什么好说的,这里比较值得一提的是加密解密的方法,如果根据C#RSA库中的示例方法来写的话会报明文长度过长的错误,这是因为RSA的明文长度和公钥长度有关,具体详情可以查看这个博客,所以这边需要把明文分段加密,解密也是同样。
public class NetMQClient
{
private DealerSocket ClientSocket;
private NetMQPoller Poller;
public Queue<MsgObject> SendMessageQueue;
public IProgress<MsgObject> ReceiveHanlder;
public bool IsRunning { get; set; }
public string OriginalConIP1
{
get
{
return OriginalConIP;
}
set
{
OriginalConIP = value;
}
}
private string OriginalConIP;
public NetMQClient(string ConIP,string machineName,Progress<MsgObject> receiveHanlder)
{
SendMessageQueue = new Queue<MsgObject>();
Poller = new NetMQPoller();
ClientSocket = new DealerSocket($"tcp://{ConIP}:5556");
try
{
ClientSocket.Options.Identity = Encoding.Default.GetBytes(machineName);
}
catch (Exception ex)
{
ClientSocket.Options.Identity = Encoding.Default.GetBytes("001");
Log.WriteLog(ex.ToString());
}
ClientSocket.ReceiveReady += ServerSocket_ReceiveReady;
ClientSocket.SendReady += ServerSocket_SendReady;
Poller.Add(ClientSocket);
ReceiveHanlder = receiveHanlder;
OriginalConIP1 = ConIP;
}
private void ServerSocket_SendReady(object sender, NetMQSocketEventArgs e)
{
if (SendMessageQueue.Count > 0)
{
var sendMsg = SendMessageQueue.Dequeue();
try
{
if (sendMsg.Content == null)
{
return;
}
var msgFrame = ConvertToNetMessage(sendMsg);
ClientSocket.SendMultipartMessage(msgFrame);
//Log.WriteLog(DateTime.Now + "msg.content = "+msgFrame+"IP:"+OriginalConIP);
}
catch(Exception ex)
{
Log.WriteExLog(ex.ToString());
}
}
//发送数据间隔
Thread.Sleep(10);
}
private NetMQMessage ConvertToNetMessage(MsgObject sendMsg)
{
var msgFrame = new NetMQMessage();
// msgFrame.Append(sendMsg.MachineName);
msgFrame.AppendEmptyFrame();
msgFrame.Append(sendMsg.Content,Encoding.UTF8);
return msgFrame;
}
private MsgObject ConvertToMsgObj(NetMQMessage sendMsg)
{
if (sendMsg.FrameCount == 2)
{
var newMsg = new MsgObject();
var machineName = sendMsg[0].ConvertToString();
//var content = sendMsg[1].ConvertToString();
var content = Encoding.UTF8.GetString(sendMsg[1].Buffer);
newMsg.MachineName = machineName;
newMsg.Content = content;
return newMsg;
}
return null;
}
private void ServerSocket_ReceiveReady(object sender, NetMQSocketEventArgs e)
{
var msg = ConvertToMsgObj(ClientSocket.ReceiveMultipartMessage());
//接收到消息,让handler处理
if (msg != null)
{
ReceiveHanlder?.Report(msg);
}
}
public void Start()
{
IsRunning = true;
Poller.RunAsync();
}
public void Close()
{
try
{
ClientSocket.Disconnect($"tcp://{OriginalConIP1}:5556");
}
catch (Exception ex)
{
Log.WriteExLog(ex.ToString());
}
}
public void Stop()
{
IsRunning = false;
Poller.StopAsync();
}
public void ChangeIP(string ConIP, string machineName)
{
ClientSocket.Disconnect($"tcp://{OriginalConIP1}:5556");
ClientSocket.Connect($"tcp://{ConIP}:5556");
OriginalConIP1 = ConIP;
}
public void AddClientSocket(string ConIP, string machineName)
{
DealerSocket temp = new DealerSocket($"tcp://{ConIP}:5556");
temp.Options.Identity = Encoding.Default.GetBytes(machineName);
temp.ReceiveReady += ServerSocket_ReceiveReady;
temp.SendReady += ServerSocket_SendReady;
Poller.Add(temp);
}
}
这边代码基本和服务端的代码相差不大,添加一个需要的消息处理类就可以对传递过来的消息类进行解密,然后接可以做一些自己想做的事情了。服务器这边可以生成一个DLL然后客户端这边就可以直接调用RSA类和Msg类了。
上篇博客本来说是要写一篇关于长连接的博客的,但是后来由于某人不同意我将之前的一些http请求改换为长连接的模式,所以这个地方就暂时搁置下来了。之后做了使用NetMQ来进行消息传递的功能。不得不NetMQ还是非常好用的,相比起微软提供的WCF方便了不少,不用写一些奇奇怪怪的配置文件。当然,在现在的项目中,我个人消息队列肯定是必不可少的一环,之前在使用WCF的时候,我没有使用队列模式所以经常会出现消息丢失的情况。
总的来说,这次的功能实现让我对消息队列了解更近一步,感觉自己写代码的方式比一年前稍微规范了一些。