C# WCF 客户端/服务器(无 HTTP,带回调),轻松实现






4.80/5 (11投票s)
在 C# 程序中实现回调的一种简单方法
引言
这是一个 MCF 服务器/客户端,允许客户端向服务器发送内容,服务器再将该内容发送给所有已连接的其他客户端。它们是同一个解决方案中的两个控制台应用程序(使用 VS 2005 编写)。我开发了两个独立的类,每个应用程序都可以创建一个。我在窗体应用程序和不同计算机之间测试过它们,并且都工作正常,但为了方便起见,我只包含控制台应用程序,它们都连接到端口 8000 的 localhost。如果您连接有问题,那可能是防火墙或网络问题。
背景
我是一名硬核 C++ 程序员,编写过大量的 DCOM 程序。然而,我的公司正在转向 C#,我也不得不跟上。在 C# 中编写 COM 应用程序的方法有很多,但有人向我提到了 WCF(Windows Communication Foundation),我想尝试一下。
学习曲线比 COM 更陡峭。我花了一周时间研究了许多声称可以实现此功能的文章,但它们要么无法编译,要么注释很少,要么根本没有说明代码是客户端代码还是服务器代码。
我在这里发布此内容,是为了给大家提供一个清晰、简洁且注释到位、能够编译并正常工作的程序!
Using the Code
打开 `WCFServer` 解决方案。它同时包含客户端和服务器应用程序。右键单击工作区,然后选择“设置启动项目”。确保服务器应用程序位于最上面,并且两个应用程序都设置为“启动”。按下 F5,它将创建一个客户端和一个服务器。然后,右键单击 `WCFClient` 项目,选择“调试”,然后单击“启动新实例”。根据需要启动任意多个客户端实例(我已经测试过 10 个)。在其中一个客户端中键入内容,它将广播到所有其他客户端。确保已包含对“System.ServiceModel
”的引用。如果没有,请在两个项目的引用中添加它。
让我们开始吧。首先看客户端代码(整个类包含在代码中)。
namespace WCFClient
{
//These are the interface declarations for the client
[ServiceContract]
interface IMessageCallback
{
//This is the callback interface declaration for the client
[OperationContract(IsOneWay = true)]
void OnMessageAdded(string message, DateTime timestamp);
}
[ServiceContract(CallbackContract = typeof(IMessageCallback))]
public interface IMessage
{
//these are the interface declarations for the server.
[OperationContract]
void AddMessage(string message);
[OperationContract]
bool Subscribe();
[OperationContract]
bool Unsubscribe();
}
起初,这对我来说并不直观(我actually 花了几个小时才弄对)。为什么实际的回调函数服务契约没有声明为回调,而服务器的 `ServiceContract`(它们不是回调)却声明为回调?我不是 MCF 专家,但我想这是因为比尔·盖茨的团队是一群……该死的 Genius?(或者别的什么)。总之,必须这样声明。实际回调函数上的“isOneWay
”参数可以防止每个客户端都通过每个回调进行报告。这可能导致所有东西都卡死。
现在是创建连接到服务器的类
class RCRProxy : IMessageCallback, IDisposable
{
IMessage pipeProxy = null;
public bool Connect()
{
/*note the "DuplexChannelFactory". This is necessary for Callbacks.
A regular "ChannelFactory" won't work with callbacks.*/
DuplexChannelFactory<IMessage> pipeFactory =
new DuplexChannelFactory<IMessage>(
new InstanceContext(this),
new NetTcpBinding(),
new EndpointAddress("net.tcp://:8000/ISubscribe"));
try
{
//Open the channel to the server
pipeProxy = pipeFactory.CreateChannel();
//Now tell the server who is connecting
pipeProxy.Subscribe();
return true;
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return false;
}
}
public void Close()
{
pipeProxy.Unsubscribe();
}
//This function sends a string to the server so that it can broadcast
// it to all other clients that have called Subscribe().
public string SendMessage(string message)
{
try
{
pipeProxy.AddMessage(message);
return "sent >>>> " + message;
}
catch(Exception e)
{
return e.Message;
}
}
//This is the function that the SERVER will call
public void OnMessageAdded(string message, DateTime timestamp)
{
Console.WriteLine(message + ": " + timestamp.ToString("hh:mm:ss"));
}
//We need to tell the server that we are leaving
public void Dispose()
{
pipeProxy.Unsubscribe();
}
}
首先,请注意它派生自 `IMessageCallbacack` 接口。注意“DuplexChannelFactory
”?我尝试了所有方法来使用普通的“ChannelFactory
”都未能成功,尽管许多回调示例在代码中都展示了它们。但同样,这些似乎都是不完整的解决方案,而这个不是。
客户端类就这些了。你可以把它放入任何程序,创建它并让它连接。例如:
namespace WCFClient
{
class Program
{
static void Main(string[] args)
{
RCRProxy rp = new RCRProxy();
if (rp.Connect() == true)
{
string tmp = Console.ReadLine();
while (tmp != "EXIT")
{
rp.SendMessage(tmp);
tmp = Console.ReadLine();
}
}
if(((ICommunicationObject)rp).State == CommunicationState.Opened)
rp.Close();
}
}
}
现在我们来看看 `SERVER` 类的代码,分几部分。同样,整个类都在可下载的项目中。
//interface declarations just like the client but the callback
//declaration is a little different
[ServiceContract]
interface IMessageCallback
{
[OperationContract(IsOneWay = true)]
void OnMessageAdded(string message, DateTime timestamp);
}
//This is a little different than the client
// in that we need to state the SessionMode as required or
// it will default to "notAllowed"
[ServiceContract(CallbackContract =
typeof(IMessageCallback),SessionMode = SessionMode.Required)]
public interface IMessage
{
[OperationContract]
void AddMessage(string message);
[OperationContract]
bool Subscribe();
[OperationContract]
bool Unsubscribe();
}
[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall)]
这里的服务器函数的 `ServiceContract` 必须将 `SessionMode` 设置为“Required
”,否则它将默认为“NotAllowed
”,回调会话将被拒绝。
在代码行
// [ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall)]
我将 `InstanceContextMode` 设置为“PerCall
”,以便在每次调用时捕获会话信息。如果您频繁传输大量小数据包,可以将其更改为“PerSession
”。这是 `SERVER` 类的其余代码。
class RCRServer : IMessage
{
private static List<IMessageCallback> subscribers = new List<IMessageCallback>();
public ServiceHost host = null;
public void Connect()
{
//I'm doing this next part programmatically instead of in app.cfg
// because I think it makes it easier to understand (and XML is stupid)
using (ServiceHost host = new ServiceHost(
typeof(RCRServer),
new Uri("net.tcp://:8000")))
{
//notice the NetTcpBinding? This allows programs instead of web stuff
// to communicate with each other
host.AddServiceEndpoint(typeof(IMessage),
new NetTcpBinding(),
"ISubscribe");
try
{
host.Open();
Console.WriteLine("Successfully opened port 8000.");
Console.ReadLine();
host.Close();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
public bool Subscribe()
{
try
{
//Get the hashCode of the connecting app and store it as a connection
IMessageCallback callback =
OperationContext.Current.GetCallbackChannel<IMessageCallback>();
if (!subscribers.Contains(callback))
subscribers.Add(callback);
return true;
}
catch(Exception e)
{
Console.WriteLine(e.Message);
return false;
}
}
public bool Unsubscribe()
{
try
{
//remove any connection that is leaving
IMessageCallback callback =
OperationContext.Current.GetCallbackChannel<IMessageCallback>();
if (subscribers.Contains(callback))
subscribers.Remove(callback);
return true;
}
catch
{
return false;
}
}
public void AddMessage(String message)
{
//Go through the list of connections and call their callback function
subscribers.ForEach(delegate(IMessageCallback callback)
{
if (((ICommunicationObject)callback).State == CommunicationState.Opened)
{
Console.WriteLine("Calling OnMessageAdded on callback
({0}).", callback.GetHashCode());
callback.OnMessageAdded(message, DateTime.Now);
}
else
{
subscribers.Remove(callback);
}
});
}
}
这个类派生自 `IMessage` 接口。请务必区分两者,否则一切都无法正常工作。这个类应该被放入一个实际的“Service”中,该服务一直运行,允许客户端连接。我认为服务器代码中的所有注释都解释了这个类。
现在来看服务器类的示例
namespace WCFService
{
static class Program
{
static void Main()
{
RCRServer server = new RCRServer();
server.Connect();
}
}
}
就这样。当然,两个类的 includes 是:
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
精彩内容
虽然 MCF 的学习曲线有点陡峭,但如果回想我刚开始学习 COM 的 12 到 13 年前,我觉得学习曲线差不多。然而,我无法像现在这样只用几行代码在 COM 中创建这两个程序。
结论
我对 MCF 的看法?嗯,我编写了另一个单实例控制台应用程序,它向其他 10 个客户端发送了 10,000 条消息,我发现它有点慢。它确实工作了,但有点慢。因此,我认为我将继续使用我的原始套接字类。它们的代码量更大,但速度非常快,大量的代码就是工作保障。
希望这对正在尝试理解 MCF 中回调的其他人有所帮助。
历史
- 2009 年 8 月 20 日:首次发布