65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (11投票s)

2009年8月20日

CPOL

4分钟阅读

viewsIcon

85693

downloadIcon

3476

在 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 日:首次发布
© . All rights reserved.