公司的系統需要有系統消息提醒,即別的用戶發的在線短信或者手機短信的提醒。按照以往的做法,一般是客戶端發請求,去服務器查詢數據庫,然后返回信息,有消息則提醒。這么做的弊端就是,不管用戶是否有新消息,客戶端每隔一段時間總要發送請求,這樣做的話既消耗帶寬又占用服務器資源,增加數據庫負擔,而且大多數情況用戶是沒有任何消息,都是無用功,并且消息提醒也不即時,只有客戶端去查詢才能獲知,所以這種做法并不好。而Remoting允許客戶端訂閱,服務器監聽客戶端的,如果服務器端有消息即刻通知客戶端,既不會浪費帶寬增加負擔也達到了即時提醒的效果。
說了這么多了,現在開始說如何實現的。我的實驗例子創建了3個工程,CommonLib、MessageClient、MessageServer。MessageClient就是客戶端,用來發布和接收消息,MessageServer是服務端,用來派發收到的消息,CommonLib是一個公用的類庫,被客戶端和服務端分別引用的。
CommonLib里包含了一個接口 IMessageBody 和一個事件類 MessageEvent。
IMessageBody:
public
interface
IMessageBody
{
[System.Runtime.Remoting.Messaging.OneWay]
void
SendMsg(
string
strText, DateTime datTime);
void
AddEvent(
string
strKey, MessageEvent cEvent);
void
RemoveEvent(
string
strKey, MessageEvent cEvent);
}
MessageEvent:
public
delegate
void
ReceiveMessageHandler(
string
strMsg);
public
class
MessageEvent : MarshalByRefObject
{
public event ReceiveMessageHandler ReceiveMsg;
//[System.Runtime.Remoting.Messaging.OneWay]
public void OnReceive(string strMsg)
{
if (ReceiveMsg != null)
{
ReceiveMsg(strMsg);
}
//如果消息到达太频繁(每秒上万条),应该做缓存(Buffer)处理。
}
}
MessageServer里很簡單,啟動時加載了一個配置文件,還有一個接口IMessageBody的實現類MessageBody。
private
void
Init()
{
try
{
RemotingConfiguration.Configure("ServerCfg.config", false);
this.Text = "已启动";
}
catch
{
this.Text = "启动失败";
}
}
配置文件ServerCfg.config:
<?
xml version
=
"
1.0
"
encoding
=
"
utf-8
"
?>
<
configuration
>
<
system.runtime.remoting
>
<
application
>
<
service
>
<
wellknown
mode
=
"
Singleton
"
type
=
"
MessageServer.MessageBody, MessageServer
"
objectUri
=
"
MSG
"
/>
</
service
>
<
channels
>
<
channel port
=
"
8888
"
ref
=
"
tcp
"
name
=
"
tcpServer
"
>
<
serverProviders
>
<
formatter
ref
=
"
soap
"
typeFilterLevel
=
"
Full
"
/>
<
formatter
ref
=
"
binary
"
typeFilterLevel
=
"
Full
"
/>
</
serverProviders
>
</
channel
>
</
channels
>
</
application
>
</
system.runtime.remoting
>
</
configuration
>
MessageBody:
class
MessageBody : MarshalByRefObject, CommonLib.IMessageBody
{
public event ReceiveMessageHandler ReceiveMsg;
private IDictionary<string, ReceiveMessageHandler> _EventList = new Dictionary<string, ReceiveMessageHandler>();
public void SendMsg(string strText, DateTime datTime)
{
Subscribe();
//激发客户端的消息到达事件。
if (ReceiveMsg != null)
{
ReceiveMessageHandler receicve = null;
//ReceiveMsg(strText + " " + datTime);
foreach (Delegate dele in ReceiveMsg.GetInvocationList())
{
try
{
receicve = (ReceiveMessageHandler)dele;
receicve(strText + " " + datTime);
}
catch
{
RemoveEvent(receicve);
}
ReceiveMsg -= receicve;
}
}
}
public void AddEvent(string strKey, CommonLib.MessageEvent cEvent)
{
_EventList.Add(strKey, new ReceiveMessageHandler(cEvent.OnReceive));
}
public void RemoveEvent(ReceiveMessageHandler handler)
{
foreach (string key in _EventList.Keys)
{
if (_EventList[key].Equals(handler))
{
_EventList.Remove(key);
return;
}
}
}
public void RemoveEvent(string strKey, CommonLib.MessageEvent cEvent)
{
_EventList.Remove(strKey);
}
private void Subscribe()
{
foreach (string key in _EventList.Keys)
{
this.ReceiveMsg += _EventList[key];
}
}
}
客戶端的一個窗體ClientForm:
public
partial
class
ClientForm : Form
{
private CommonLib.IMessageBody _bdBody;
private CommonLib.MessageEvent _cEvent;
private string _strKey = Guid.NewGuid().ToString();
public ClientForm()
{
InitializeComponent();
Init();
}
private void Init()
{
// 通过编程设置反序列的级别
BinaryServerFormatterSinkProvider provider = new BinaryServerFormatterSinkProvider();
provider.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;
BinaryClientFormatterSinkProvider Clientprovider = new BinaryClientFormatterSinkProvider();
// 设置通道属性
System.Collections.IDictionary props = new System.Collections.Hashtable();
props["port"] = 0;
//注册TCP通道,用于连接自己或者他人的服务器。
TcpChannel tcpChl = new TcpChannel(props, Clientprovider, provider);
ChannelServices.RegisterChannel(tcpChl, false);
}
private void btnConnect_Click(object sender, EventArgs e)
{
//与服务器创建连接
_bdBody = (CommonLib.IMessageBody)Activator.GetObject(
typeof(CommonLib.IMessageBody),
"tcp://" + ServerAddress.Text.Trim() + "/MSG");
if (_bdBody == null)
return;
try
{
_cEvent = new CommonLib.MessageEvent();
_bdBody.AddEvent(_strKey, _cEvent);
_cEvent.ReceiveMsg += new CommonLib.ReceiveMessageHandler(receive);
ServerAddress.Enabled = false;
btnConnect.Enabled = false;
this.Text = "已连接";
}
catch
{
this.Text = "无连接";
return;
}
}
private void receive(string s)
{
try
{
//转到主线程
if (this.InvokeRequired)
{
CommonLib.ReceiveMessageHandler handler = new CommonLib.ReceiveMessageHandler(Show);
Invoke(handler, s);
}
}
catch (Exception Ex)
{
MessageBox.Show(Ex.Message);
}
}
private void Show(string str)
{
txtReceived.Text = str + Environment.NewLine + txtReceived.Text;
}
private void btnSend_Click(object sender, EventArgs e)
{
string str = SendMsg.Text.Trim();
if (_bdBody != null)
_bdBody.SendMsg(str, DateTime.Now);
}
private void ClientForm_FormClosed(object sender, FormClosedEventArgs e)
{
_bdBody.RemoveEvent(_strKey, _cEvent);
}
}
以上就是示例的所有代碼,其他沒太多解釋的地方,說一下那個MessageBody類吧,這個類浪費了我不少時間來實現。
大家可以看到在AddEvent和RemoveEvent這兩個方法中,我并沒有像一般的做法那樣直接綁定事件(ReceiveMsg += new ReceiveMessageHandler(cEvent.OnReceive); 和 ReceiveMsg -= cEvent.OnReceive),而是用了一個 IDictionary<string, ReceiveMessageHandler> _EventList 的集合來存放客戶端的訂閱事件,然后只是在發送消息的時候才去綁定,也就是Subscribe()的方法,當消息發送了之后即刻取消綁定(ReceiveMsg -= receicve; )。這樣做的好處在于,改進一下 Subscribe() 方法即可對特定的人發送消息(只觸發特定的人的訂閱事件),而不用像廣播一樣全體發送。
大家還可以看到在SendMsg的方法中有一句被注釋的語句,ReceiveMsg(strText + " " + datTime);。這句話其實是系統自動做遍歷把消息發送到每個訂閱的客戶端。正常情況下如果客戶端正常退出取消訂閱則系統不會出問題,但如果客戶端非正常退出,沒有取消訂閱,那服務端在遍歷到這里的時候就會報錯,導致后面的消息無法發送。后來我在MessageEvent的OnReceive方法上,加上了[System.Runtime.Remoting.Messaging.OneWay]這個屬性,他的作用是標明此方法是單向的,執行后不用等回復,并摒棄錯誤信息。這樣做了之后,確實可以運行了,但是每當到了這里的時候都會卡一下,而且如果類似情況多了,系統會變得很慢,如果發現出錯能取消此客戶端訂閱就好了。就是這個問題話了我不少時間,總算找到了解決方法,就是不用原先的ReceiveMsg(strText + " " + datTime);,自己遍歷所有的訂閱客戶端,并發送消息,取消OnReceive的OneWay屬性,如果發送報錯則從集合中移除此訂閱,那以后就可高枕無憂了。
有人要問,我的題目是系統廣播和即時聊天,為何只有一個實例?其實,這個實例就是個即時聊天,對于系統廣播,客戶端只接收不發送,服務端只發送不接收即可實現了。呵呵。