WCF除了支持传统的“请求-应答”的调用模式之外还支持“单向操作”和“回调操作”两种调用模式,还可以使用流在客户端和服务器端之间传递大对象。
一、“请求-应答”模式(Request-Reply Operations):“请求-应答”模式是默认的操作模式,在此之前我们所做的例子都采用的是“请求-应答”模式进行调用的。
其调用过程是:客户端发送请求,阻塞客户端进程,服务端返回操作结果,客户端收到返回结果后继续向下执行,如果服务器没有在预期的时间内响应请求,客户端就会产生TimeoutException异常信息。
除了NetPeerTcpBinding和NetMsmqBinding两种绑定方式之外,所有的操作都支持请求与响应操作。
这种模式很简单,也是一种默认的模式,所以在这里不多说了。
二、“单向操作”模式(One-Way Operations)简单来说,“单向操作”没有返回值,客户端只管调用,不管结果。其调用过程是:“单向操作”客户端一旦发出请求,WCF会生成一个请求,不会给客户端返回任何消息。“单向操作”不同于异步操作,虽然“单向操作”只是在发出调用的瞬间阻塞客户端,但如果发出多个单向调用,WCF会将请求调用放入队列,并在某个时候执行。队列存储调用的个数是有限的,一旦发出的调用个数超出了队列存储调用的设置值,则会发生阻塞现象,因为调用无法放入队列。当队列的请求出列后,产生阻塞的调用就会放入队列,并解除对客户端的阻塞。
所有的WCF绑定通信协议都支持“单向操作”。
配置“单向操作”的方式也很简单, WCF的OperationContract 定义了IsOneWay属性,IsOneWay默认是false(因为默认是请求-应答模式,不是单向操作模式),我们在方法契约中指定IsOneWay属性为true就可以了。
[ServiceContract]interface IMyContract
{
[OperationContract(IsOneWay = true)] void MyMethod( );
}
在客户端调用服务端的IsOneWay操作契约时,是透明的。
服务端代码:
[ServiceContract]
public interface IOneWayOperation
{
[OperationContract(IsOneWay=true)] void OneWayInvoke();
[OperationContract]
void NormalRequest();}
public class OneWayOperation : IOneWayOperation{
public void OneWayInvoke()
{
Debug.WriteLine("OneWayInvoke "+DateTime.Now.ToString());
}
public void NormalRequest()
{
Debug.WriteLine("NormalRequest "+DateTime.Now.ToString());
}
}
上面这个WCF服务中有两个方法:OneWayInvoke()方法是单向调用模式,NormalRequest()是传统的请求响应模式。那我们在客户端分别调用这两个方法:
OneWayOP.OneWayOperationClient ow = new Client.OneWayOP.OneWayOperationClient();
ow.OneWayInvoke();
ow.NormalRequest();
Console.ReadLine();
运行效果:
《图1》
从上面的代码中我们可以看到,单向调用操作和请求-响应操作,只是在定义方法契约的时候不同,在客户端使用的时候是一样的。
设置为单向调用的方法契约应当返回void值。因为单向调用不需要得到服务的返回结果。
单向调用并不意味着客户端不关心服务器上的运行情况。也不要认为单向调用就是个“单行道”,服务不返回任何信息。
1.当客户端向服务器端发送单向调用时,由于通信问题(网络中断,地址错误,宿主不可用等)产生的错误,都会在客户端产生异常。
2.当传输信道不支持会话时(BasicHttpBinding绑定或不带安全、可靠状态的WSHttpBinding绑定),如果客户端发出的单向调用在服务端产生了异常,那客户端不会受到任何影响,可以继续通过代理向服务端发出下次调用。
3.当传输信道支持会话在时(安全不可靠的的WSHttpBinding绑定或不可靠的NetTcpBinding绑定或NetNamedPipeBinding绑定),如果服务端产生异常,将会破坏信道,客户端就不能再用当前代理向服务端再发出调用了,甚至客户端都无法正常关闭当前代理。
4.当传输信道支持会话在时(安全可靠的WSHttpBind绑定或可靠的NetTcpBinding绑定),如果服务端产生异常,将不会破坏信道,客户端将仍可能向服务端发出调用。
一般在单向调用时,我们也应当打开服务的可靠性,这样能确保调用请求能够发送给远程服务。
虽然在PerSession的实例模式下可以使用单向调用,但这并不是一个好的设计。建议在percall模式和signleton模式下使用单向调用,不要在persession模式下使用单向调用,。
三、“回调操作”模式 (Callback Operations)
“回调操作”又称为“双向操作”,它不只是客户端调用服务端的方法契约,还可以从服务端调用客户端的方法契约。
并不是所有的绑定协议都支持回调操作,BasicHttpBinding,WSHttpBinding绑定协议不支持回调操作;NetTcpBinding和NetNamedPipeBinding绑定支持回调操作;WSDualHttpBinding绑定是通过设置两个HTTP信道来支持双向通信,所以它也支持回调操作。
双向操作原理:
《图4》
从图中我们可以看出,在回调操作模式中,客户端对服务端调用实际上是进行了4次通信
a.Service Request:客户端向服务端发出调用,在调用的过程中会把回调实例的引用一起发送到服务端去。
b.Callback Request:服务端从上一步的请求中取得对回调实例的引用,通过该回调实例向客户端发出的回调请求。
c.Callback Response:客户端执行完回调后向服务端返回的响应,如果回调契约中的方法契约被定义为IsOneWay的话,这一步不会发生。
d.Service Response:服务端方法执行完后向客户端返回信息(方法的返回值等)。
1.定义回调契约。回调契约是服务器端定义客户端回调类的规范。将来客户端需要根据此契约生成回调类,服务端就通过此契约来调用客户端相应的方法。
回调方法是在客户端执行的,而回调契约是定义在服务端,它规定客户端回调类的实现契约。public interface IWCFCallBack{
[OperationContract(IsOneWay=true)] //[OperationContract]
void SayHelloCallBack();
}
上面是回调契约的定义,可以看到在回调契约上并没有添加ServiceContractAttribute属性声明。但在回调契约中的方法声要添加OperationContractAttribute声明。这是因为回调契约会在后面的服务契约中进行声明,客户端会根据服务端的服务契约声明在代理类中生成对应的服务契约和回调契约。
关于方法契约的声明,网上好多文章都要求把回调契约中的方法契约声明为单向操作契约 [OperationContract(IsOneWay=true)],意味着服务端对客户端的回调只单向调用。根据我的理解与实验,我认为在这里的方 法契约可以声明为单向操作也可不声明为单向操作,究竟是否需要设置为单向操作,要根据服务的并发状态来判断:如果服务并发状态是 ConcurrencyMode.Reentrant,则回调方法契约不需要单向操作,否则需要单向操作。
2.定义服务契约
服务契约就是在服务器端定义的供客户端调用的服务规范,如果要实现回调操作,需要在定义服务契约的时候指定其对应回调契约。当客户端生成代理类的时候会根据服务契约的定义自动在客户端生成对应的服务契约与回调契约。
[ServiceContract(CallbackContract=typeof(IWCFCallBack))]public interface IWCFService
{
[OperationContract] string SayHelloToUser();
}
//[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)]public class WCFService : IWCFService
{
IWCFCallBack callback = OperationContext.Current.GetCallbackChannel<IWCFCallBack>(); public string SayHelloToUser()
{
callback.SayHelloCallBack();
return "WCF Service " + DateTime.Now.ToString();
}
}
上面的代码中,
a.[ServiceContract(CallbackContract=typeof(IWCFCallBack))]把服务契约与回调契约联系起来
b.[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)]这一句并不是非要加,如果回调契约中的方法契约是oneway操作时,则不需要加。
c.IWCFCallBack callback = OperationContext.Current.GetCallbackChannel<IWCFCallBack>();
当客户端向服务端发送请求的时候会把回调信息一起发送到服务端,在服务端可以通过上面的方法,在当前的操作上下文中获取当前操作的客户端的回调通道,通过这个回调通道对象来调用客户端的回调方法。
3.客户端定义回调契约的实现
在服务器端定义了回调契约,在生成代理类的时候,会在客户端生成对应的回调契约,但回调契约的名子发生了变化。
《图2》
从图中我们可以看到客户端生成的回调契约并不与服务器端的回调契约一致,而是在服务端的服务契约之后加上CallBack来命名的,这是因为客户端的回调契约就是通过服务契约声明来取得的。
class ClientCallBack : CallBackOP.IWCFServiceCallback{
public void SayHelloCallBack() {
Console.WriteLine("callback is writting");
}
}
上面的代码,我们就是实现了客户端的回调契约,以供服务器端回调。
4.编写客户端的主调代码
在这里的主调代码就是写在客户端Main函数中的代码//回调对象实例
ClientCallBack cc = new ClientCallBack();
//实例上下文对象
InstanceContext context = new InstanceContext(cc);//实例化代理对象
CallBackOP.WCFServiceClient ws = new Client.CallBackOP.WCFServiceClient(context);
//调用代理对象中的方法
ws.SayHelloToUser();
Console.ReadLine();
ws.Close();
这段代码中最核心的对象是“实例上下文(InstanceContext)”对象,该对象中包含回调对象的实例,调用服务端方法的时候,实例上下文被一起传递给服务端,这样服务端就知道了客户端回调对象的信息了。
在好多情况下,客户端自身就实现了回调契约,这时候我们需要做两件事情:
a.在客户端类中把代理类作为成员变量,在相应的方法中实例化代理类,通过代理调用服务。
b.让当前类实现IDisposable接口,并在Dispose()方法中关闭代理。
这样客户端的代码就可以作如下改动了:
class Program : CallBackOP.IWCFServiceCallback,IDisposable{
private CallBackOP.WCFServiceClient prox = null;
public void SayHelloCallBack() {
Console.WriteLine("callback is writting");
}
public void CallService()
{
InstanceContext context = new InstanceContext(this);
prox = new Client.CallBackOP.WCFServiceClient(context);
string str = prox.SayHelloToUser(); Console.WriteLine("Client:" + str);
}
public void Dispose() {
prox.Close();
}
static void Main(string[] args)
{
Program p = new Program();
p.CallService();
Console.ReadLine();
}
}
四、“发布-订阅”回调操作模式(Callback Operations)的应用
“发布-订阅”是回调模式的一个应用。就是多个客户端向一个服务中注册一下自己,然后在服务端可以对注册的所有的客户端进行调用。实际上就是一个双向调用的过程。
案例功能说明:
服务端有个容器保存注册上来的客户端对象。客户端是个Windows窗口,上面有两个按钮(连接,断开)和一个列表框(显示在服务器断注册的所有客户端)。
先启动服务端程序,再启动三个客户端程序。三个客户端刚启动时并没有在服务器端注册,当我们点击“连接”按钮时,服务会注册当前客户端,并通知所有已注册的客户端,在它们的列表框中添加刚刚注册的客户端代号;
《图5》
当点击“断开”按钮时,服务端会注销选中的客户端,并通知已注册的客户端,从它们的列表中删除刚刚注销的客户端代号。
《图6》
服务端代码:
1.定义回调契约public interface ISubscriber
{
[OperationContract] void Notify(string id);
}
提供服务器调用的方法契约Notify()。
2.定义服务契约[ServiceContract(CallbackContract=typeof(ISubscriber))]public interface IPublisher
{
[OperationContract] void Subscribe(string id);
[OperationContract] void UnSubscribe(string id);}
Subscribe方法契约:用来向服务端注册客户端
UnSubscribe方法契约:用来向服务端注销客户端
[ServiceContract(CallbackContract=typeof(ISubscriber))]是在服务契约上声明回调契约
3.编写服务类
public class PublisherService : IPublisher{
//保存已注册客户端的列表
private static Dictionary<string,ISubscriber> list = new Dictionary<string,ISubscriber>(); //从客户端获取回调对象
private ISubscriber sub = OperationContext.Current.GetCallbackChannel<ISubscriber>(); #region IPublisher 成员
//客户端向服务端注册
public void Subscribe(string id) {
if (!list.Keys.Contains(id))
{
list.Add(id, sub); }
//刷新所有已注册的客户端
NotifyAll(id); }
//客户端从服务端注销自己
public void UnSubscribe(string id) {
if (list.Keys.Contains(id))
{
//下面三行是向被断开的客户端发送“已断开连接”的消息
List<string> empty = new List<string>();
empty.Add("已断开连接");
list[id].Notify(empty);
list.Remove(id);
}
//刷新所有已注册的客户端
NotifyAll(id); }
//通知所有客户端进行刷新客户端列表
public void NotifyAll(string value) {
foreach (KeyValuePair<string, ISubscriber> pair in list)
{
List<string> keys = new List<string>();
keys.AddRange(list.Keys);
pair.Value.Notify(keys); }
}
#endregion
}
a.使用静态集合成员变量保存客户端信息,静态成员在内存中共享一个存储空间,这样可以保证多个客户端访问的是一个容器。
b.静态集合使用的是泛型字典集合,它的Key值是个GUID代号,Value值是回调契约ISubscriber对象。
c.OperationContext对象是操作契约上下文对象,它包含从客户端带来的操作契约的信息。我们用它获取当客户端的实例通道。
private ISubscriber sub = OperationContext.Current.GetCallbackChannel<ISubscriber>();
d.在UnSubscribe方法中,我们在从集合中移出当前客户端前,先向当前要注销的客户端的列表中发送“已断开连接”消息。因为,当前客户端一旦被从集合中删除后就不再会被服务端调用取,这样做以免客户端不知道自已被断开。
List<string> empty = new List<string>();
empty.Add("已断开连接");
list[id].Notify(empty);
e.NotifyAll方法不是方法契约,它会被Subscribe和UnSubscribe方法契约调用。在此方法中主要是:遍历集合容器,调用其中每个回调契约,在客户端刷新显示当前在册的客户端代号。
foreach (KeyValuePair<string, ISubscriber> pair in list)
{
List<string> keys = new List<string>();
keys.AddRange(list.Keys);
pair.Value.Notify(keys);
}
4.编写客户端回调服务:
在这里我们不单独编写回调类,我们让当前Form1 类实现IPublisherCallback回调接口。这也是我们开发过程中常使用方法。
public partial class Form1 : Form,Publisher.IPublisherCallback{
private delegate void UpdateListBoxDelegate(string Message);
private delegate void ClearListBoxDelegate(); public void Notify(string[] list) {
if (listBox1.InvokeRequired)
{
listBox1.Invoke(new ClearListBoxDelegate(Clear)); foreach (string id in list)
{
listBox1.Invoke(new UpdateListBoxDelegate(Add), id); }
}
else
{
Clear();
foreach (string id in list)
{
Add(id);
}
}
}
}
Notify方法是被服务端调用的客户端的方法,它接收服务端传入的在册客户端列表,并在ListBox列表框中显示出来。由于该方法是被 服务端回调的方法,所以当前窗口的实例与点击“按钮”按钮发送主调请求的窗口不是一个实例,它不能直接操作ListBox列表框对象。我们使用 ListBox1.Invoke()来通过代理实现对ListBox1控件的操作。
5.编写客户端操作
public partial class Form1 : Form,Publisher.IPublisherCallback{
//代理,用来取得客户代号列表中选中的客户端代号
private delegate string GetListBoxSelectedItem(); //WCF服务的客户端代理类
private Publisher.PublisherClient client;
public Form1()
{
InitializeComponent();
}
//向列表中添加客户端
public void Add(string id)
{
listBox1.Items.Add(id);
} //清空列表
public void Clear()
{
listBox1.Items.Clear();
} //取得列表中选中项的内容
public string GetSelectedItem()
{
if (listBox1.SelectedItem != null)
{
return listBox1.SelectedItem.ToString();
}
return "-1";
} private void Form1_Load(object sender, EventArgs e)
{
//创建实例上下文
InstanceContext context = new InstanceContext(this); //使用实例上下文创建代理对象
client = new WindowsFormsApplication1.Publisher.PublisherClient(context, "NetTcpBinding_IPublisher"); }
private void button1_Click(object sender, EventArgs e)
{
//启动异步线程连接服务端
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += new DoWorkEventHandler(worker_DoWork);
worker.RunWorkerAsync(); }
private void button2_Click(object sender, EventArgs e)
{
//启动异步线程断开与服务端连接
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += new DoWorkEventHandler(worker_StopWork);
worker.RunWorkerAsync(); }
//“连接”按钮后台线程的执行方法,用来向服务端发送注册请求
void worker_DoWork(object sender, DoWorkEventArgs e)
{
client.Subscribe(Guid.NewGuid().ToString()); }
//“断开”按钮后台线程的执行方法,用来向服务端发送注销请求
void worker_StopWork(object sender, DoWorkEventArgs e)
{
string id = "";
//取得列表框中选中项的内容
if (listBox1.InvokeRequired)
{
id = listBox1.Invoke(new GetListBoxSelectedItem(GetSelectedItem)).ToString(); }
else
{
id = GetSelectedItem();
}
client.UnSubscribe(id); }
}
a.InstanceContext:在Form1_Load中我们实例化此对象,并用此对象实例化代理。此对象我称之为实例上下文,它包含当前发起调用的客户端的回调信息。使用它来实例化代理后,代理在向服务端发送的调用中就包含此对象的信息,从而使服务端可以实现对客户端的回调。
InstanceContext context = new InstanceContext(this);
client = new WindowsFormsApplication1.Publisher.PublisherClient(context, "NetTcpBinding_IPublisher");
b.BackgroundWorker:后台线程类,使用它我们可以很轻松实现简单的多线程运行。 常用事件
worker.DoWork:当后线程开始执行的时候触发。
worker.RunWorkerCompleted:当后台线程执行完成的时候触发。
常用方法
worker.RunWorkerAsync():后台线程开始异步执行
worker.CancelAsync():取消正在异步执行的后台线程
常用属性
worker.IsBusy:判断当前后台线程中是否正在忙于执行异步线程。
在后台的异步线程中无法取得当前线程中的对象,要想在后台异步线程中操作当前线程中的界面控件,我们还得使用control.Invoke()来调用。
c.在这里为什么要使用多线程?
如果客户端是控制台界面,那客户端没有必要使用多线程操作。这个例子中客户端是WinForm界面,如果不使用多线程的话那回出现服务器端调用客户端回调操作过时的情况。
(图7) 由上面我们知道,这种“回调操作”模式需要客户与服务器端进行四次交互(ServiceRequest,CallbackRequest,CallbackResponse,ServiceResponse),但是由于客户端没有在发出ServiceRequest后,一直处理阻塞等待状态,服务端向客户端发送的CallbackRequest调用一直处于等待状态,直到过期。因此要使用多线程来防止这种等待过期的情况。
还有另一种解决方案:就是把“服务契约”和“回调契约”中的方法契约声明都设为“单向操作”模式。这样当客户端向服务端发送 ServiceRequest调用后就不再处理阻塞等待状态,当服务端向客户端发送的CallbackRequest调用,服务就可以调到客户端对象的方 法契约了。 这样服务端服务契约声明可以修改成如下形式:
public interface ISubscriber
{
[OperationContract(IsOneWay = true)] void Notify(string id);
}
[ServiceContract(CallbackContract=typeof(ISubscriber))] public interface IPublisher
{
[OperationContract(IsOneWay = true)] void Subscribe(string id);
[OperationContract(IsOneWay = true)] void UnSubscribe(string id);
}
在“连接”按钮调用中就不用再使用BackgroundWorker进行多线程调用了。
private void button1_Click(object sender, EventArgs e)
{
//BackgroundWorker worker = new BackgroundWorker();
//worker.DoWork += new DoWorkEventHandler(worker_DoWork);
//worker.RunWorkerAsync();
client.Subscribe(this.GetHashCode().ToString()); }