zoukankan      html  css  js  c++  java
  • 传说中的WCF(12):服务器回调有啥用

    你说,服务器端回调有啥用呢?这样问,估计不好回答,是吧。不急,先讨论一个情景。

    假设现有服务器端S,客户端A开始连接S并调用相关操作,其中有一个操作,在功能上有些特殊,调用后无法即时回复,因为在服务器上要作一些后续,而这些处理也许会消耗一定时间,比如:

    向服务器上传了一个文件,但是,为了节约空间或出于其他目的,服务器要对刚上传的文件进行处理(压缩或者多媒体文件转码),这些操作无法马上向客户端回复,而客户端也不可能就停在这里一直在等。我们希望,在客户端上传文件后马上返回,而服务器对文件处理完成后再通知一下客户端。

    这样就引出一个东东——回调,E文叫Call Back。我估计用E文表述可能更好理解,Call back就是相对于Call to而言的,即调用的方向与Call to相反。

    是啊,有必要解释一下,什么叫回调。我讲一个故事吧。

    有一天,脑残去书店买书,之前他听别人说有一本书叫《吹牛沉思录》很好看,于是脑残也想买一本。可是,当他到书店后,东找西寻了一番,硬是没看见那本书的影子。

    于是,他跑到柜台问工作人员:“我想找《吹牛沉思录》,没找到。”

    工作人员马上启动书店的信息管理系统,但可以由于该系统品德不太好,居然用了35秒才启动,然后,工作人员在上面查了一下,回过头说:“抱歉,这本书太抢手了,卖完了,需要拿货。”

    脑残追问:“那要啥时候有货?”

    工作人员说:“大概两三天后吧,这样吧,你留个联系方式,等到货到了我再联系你。”

    ……

    对的,这就是回调的故事。

    脑残(调用方)不知道书店什么时候有货(不清楚调用的操作什么时候返回),但他总不能每天都跑去书店看看,这样太不滑算(消耗资源),于是,书店(被调用方)建议,留下联系方式(只保留内存中函数指针的地址,即回调地址),只要货到了就通知脑残(反调用)。

    回调比较典型的一种就是事件,事件驱动模型以前是在VB中被大量使用,后来.NET也继承了这些优点,在此之前,C++/MFC大家都知道的,是通过消息来处理的(消息循环),其实,事件就是对消息的进一步封装,这使得应用更加简便和灵活。

    在.NET中我们知道,事件其实就是一个委托,由于委托可以同时绑定多个方法的特点,故被选为事件的表现类型,估计是这样的。

    比如,我们常用的,为按钮的Click事件定义一个处理。

    button.Click += new EventHandler(onClick)

    这样,事件Click的订阅者就是onClick方法,所谓订阅事件,就像我们平时订阅XX杂志一样,只要有新一期发布就发快递给你,你不用天天打电话去杂志社问。

    onClick并不是每一刻都去问button:“你被Click了吗?”,onClick就像一个报警系统,只要特定的事件发生,它就会报警。这就是一种回调,onClick不必主动去调用button,只要处于监听状态即可,只要button被Click,onClick就会执行,不用你去调用它。

    讲了这么多,不知道各位理解了没?

    在WCF中使用回调,只需要多定义一个接口即可,这个接口的方法和服务协定一样,要附加OperationContractAttribute特性。

    然后在定义服务类时,在ServiceContractAttribute的CallbackContract中设置一个回调接口的Type。

    在服务操作中,通过OperationContext的GetCallbackChannel方法取出回调协定的实例,调用回调的方法,就会在客户端寻找回调接口的实现类并调用对应的成员。

    这样说显然不好理解,还是实践出真知。我们来做一个选号程序。

    一:服务端实现

    这次我们的实现和前些有些区别,我们这里增加了配置文件来代替部分程序功能。

    第一步,新建一个控制台应用程序。

    第二步,定义一个回调接口。

    复制代码
    namespace Server
    {
        interface ICallback
        {
            // 回调操作也必须One Way
            [OperationContract(IsOneWay = true)]
            void CallClient(int v);
        }
    }
    复制代码

    第三步,定义服务协定。

    复制代码
    namespace Server
    {
        [ServiceContract(
            Namespace = "MyNamespace",
            CallbackContract = typeof(ICallback), /* 标注回调协定 */
            SessionMode = SessionMode.Required /* 要求会话 */
            )]
        public interface IServer
        {
            // 会话从调用该操作启动
            [OperationContract(IsOneWay = true, /* 必须 */
                IsInitiating = true, /* 启动会话 */
                IsTerminating = false)]
            void CallServerOp();
    
            // 调用该操作后,会话结束
            [OperationContract(IsOneWay = true, /* 使用回调,必须为OneWay */
                IsTerminating = true, /* 该操作标识会话终止 */
                IsInitiating = false)]
            void End();
        }
    }
    复制代码

    CallbackContract属性指向ICallback的Type。因为我要使用计时器每隔3秒钟生成一个随机数,并回调到客户端,故要启用会话。
    第四步,实现服务协定。

    复制代码
    namespace Server
    {
        public class MyServer : IServer, IDisposable
        {
            private ICallback icb;
            private Timer timer = null;//计时器,定时干活
            Random rand = null;//生成随机整数
    
            public void CallServerOp()
            {
                this.icb = OperationContext.Current.GetCallbackChannel<ICallback>();
                rand = new Random();
                // 生成随整数,并回调到客户端
                // 每隔3秒生成一次
                timer = new Timer((obj) => icb.CallClient(rand.Next()), null, 10, 3000);
            }
    
            public void Dispose()
            {
                timer.Dispose();
                Console.WriteLine("{0} - 服务实例已释放。", DateTime.Now.ToLongTimeString());
            }
    
    
            public void End()  //结束
            {
                Console.WriteLine("会话即将结束。");
            }
        }
    }
    复制代码

    第五步,完成服务器端的配置(通过App.config配置文件)。

     新建配置文件,命名App.config,内容如下。

    复制代码
    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <system.serviceModel>
        <client />
        <services>
          <service name="Server.MyServer"  behaviorConfiguration="Mybehavior" >
            <endpoint binding="netTcpBinding" bindingConfiguration="MynetTcpBinding" contract="Server.IServer" address="net.tcp://localhost:1211/rr">
              <identity>
                <dns value="localhost"/>
              </identity>
            </endpoint>
            <host>
              <baseAddresses>
                <add baseAddress="http://localhost:1378/services" />
              </baseAddresses>
            </host>
          </service>
        </services>
        <behaviors>
          <serviceBehaviors>
            <behavior name="Mybehavior" >
              <serviceMetadata httpGetEnabled="True"/>
            </behavior>
          </serviceBehaviors>
        </behaviors>
        <bindings>
          <netTcpBinding>
            <binding name="MynetTcpBinding">
              <security mode="None" />
            </binding>
          </netTcpBinding>
        </bindings>
      </system.serviceModel>
    </configuration>
    复制代码

    既支持会话,传输速度又快的,非TCP莫属了,所以这里我选择NetTcpBinding,这样在默认行为下,每启动一个会话就创建一个服务实例,而当会话结束时就会释放。

    第六步,实现服务的寄宿程序。

    第二-五步实现了服务端的功能,结下来我们建一个服务的寄宿程序,用于服务的启动和停止。

    添加Window服务类:

    复制代码
    namespace Server
    {
        partial class Host : ServiceBase
        {
            public Host()
            {
                InitializeComponent();
            }
    
            #region 启动入口
            /// <summary>
            /// 启动入口
            /// </summary>
            /// <param name="args"></param>
            static void Main(string[] args)
            {
                var host = new Host();
                try
                {
                    host.OnStart(args);
                    Console.WriteLine("服务已启动");
                    Console.ReadLine();
                }
                catch (Exception e)
                {
                }
            }
            #endregion
    
            protected override void OnStart(string[] args)
            {
                var service = new ServiceHost(typeof(Server.MyServer));
                service.Open();
            }
    
            protected override void OnStop()
            {
                // TODO: Add code here to perform any tear-down necessary to stop your service.
            }
        }
    }
    复制代码

    至此服务端的代码就完成了。

    第五-六步功能类似于:

    复制代码
            static void Main(string[] args)
            {
                Console.Title = "WCF服务端";
                // 服务器基址
                Uri baseAddress = new Uri("http://localhost:1378/services");
                // 声明服务器主机
                using (ServiceHost host = new ServiceHost(typeof(MyService), baseAddress))
                {
                    // 添加绑定和终结点
                    // tcp绑定支持会话
                    NetTcpBinding binding = new NetTcpBinding();
                    binding.Security.Mode = SecurityMode.None;
                    host.AddServiceEndpoint(typeof(IService), binding, "net.tcp://localhost:1211/rr");
                    // 添加服务描述
                    host.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true });
                    try
                    {
                        // 打开服务
                        host.Open();
                        Console.WriteLine("服务已启动。");
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                    Console.ReadKey();
                }
            }
    复制代码

    二:客户端实现

    第一步,新建一个wpf窗体应用项目。

    第二步,到对应目录以管理员身份运行服务器端,然后在客户端添加服务引用。可以看到引用后客户端增加了app.config文件。

    复制代码
    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
        <system.serviceModel>
            <bindings>
                <netTcpBinding>
                    <binding name="NetTcpBinding_IServer">
                        <security mode="None" />
                    </binding>
                </netTcpBinding>
            </bindings>
            <client>
                <endpoint address="net.tcp://localhost:1211/rr" binding="netTcpBinding"
                    bindingConfiguration="NetTcpBinding_IServer" contract="ServiceReference1.IServer"
                    name="NetTcpBinding_IServer">
                    <identity>
                        <dns value="localhost" />
                    </identity>
                </endpoint>
            </client>
        </system.serviceModel>
    </configuration>
    复制代码

    第三步,在客户端实现回调接口。

    复制代码
    namespace Client
    {
        /// <summary>
        /// 实现回调接口
        /// </summary>
        class MyCallback : ServiceReference1.IServerCallback
        {
            // 因为该方法是由服务器调用的
            // 如果希望在客户端能即时作出响应
            // 应当使用事件
            public void CallClient(int v)
            {
                if (this.ValueCallbacked != null)
                {
                    this.ValueCallbacked(this, v);
                }
            }
    
            /// <summary>
            /// 回调引发该事件
            /// </summary>
            public delegate void EventHandler(Object sender, int e);
            public event EventHandler ValueCallbacked;
        }
    }
    复制代码

    注意,回调的接口是在客户端实现的,不是服务器端。

    第四步,设计窗口。

    复制代码
    namespace Client
    {
        /// <summary>
        /// Interaction logic for MainWindow.xaml
        /// </summary>
        public partial class MainWindow : Window
        {
            ServiceReference1.ServerClient cl = null;
            MyCallback cb = null;
    
            public MainWindow()
            {
                InitializeComponent();
                cb = new MyCallback();
                cb.ValueCallbacked += new MyCallback.EventHandler(cb_ValueCallbacked);
            }
    
            void cb_ValueCallbacked(object sender, int e)
            {
                this.label2.Content = e.ToString();
            }
    
            private void button1_Click(object sender, RoutedEventArgs e)
            {
                cl = new ServiceReference1.ServerClient(new System.ServiceModel.InstanceContext(cb));
                cl.CallServerOp();
                button1.IsEnabled = false;
                button2.IsEnabled = true;
            }
    
            private void button2_Click(object sender, RoutedEventArgs e)
            {
                cl.End();
                button1.IsEnabled = true;
                button2.IsEnabled = false;
            }
        }
    }
    复制代码

    现在来测试一下吧。

    本文源码:http://files.cnblogs.com/yuanli/MyApp12.zip

  • 相关阅读:
    [20210908]Reverse Shell with Bash.txt
    [20210831]bbed读取数据块6.txt
    自主学习 之 用Python玩转数据
    简单四则运算(PSP)
    永久免费云服务器搭建国内Moon服务加速ZeroTier
    INDEX
    openjdk 8 的 hotspot 源码目录结构
    CentOS 7 编译 openjdk 8
    23
    22
  • 原文地址:https://www.cnblogs.com/ywsoftware/p/3663879.html
Copyright © 2011-2022 走看看