在流模式下保持服务实例的状态的两种设计方式
在WCF程序设计中,服务对象如何实例化,对于应用程序的性能有很大的影响,这同时要兼顾到性能与可扩展,由于在WCF中服务类实例化与客户端有关,服务实例会根据客户端的请求类型来确定服务实例的管理方式,当然也可以以声明的方式来显式定义服务的实例化的方式。
WCF中支持三种实例激活的类型:单调服务(Per Call Service),会为每次客户端请求分配(销毁)一个服务实例。会话服务(Sessionful Service)则为每次客户端连接分配一个服务实例。单调服务(SIngletom Service)所有的客户端会为所有的连接和激活对象共享一个相同的服务实例。
通过以上的分析我们发现单调服务是为每次的请求分配一个新的实例,当然如果服务中要保持一些状态信息,那么这些状态信息也会被初始化。这意味着每次请求都会将状态重新初始化。现在我们有一个场景,我们要以二进制流的方式在服务和客户之间传递文件。根据WCF中使用流模式的约定,我们只能以单个的Stream 对象作为操作的输入输出参数,因此,为了上传一个文件,我们就得使用一些辅助的方法来实现诸如文件名,路径等信息的传送。下面给出了一个这样的场景:
在这儿我们声明了三个字段:file用来保存客户传过来的文件名;path用来保存文件的存储路径;outStream以得到写文件的流。
相应的这儿也定义了操作契约:
这两个操作契约有一些约定:即先要调用RelativeSetting()方法来设置字段的值,为文件流的传输做准备,接着我们就可以调用SendStream()方法来上传文件流了。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
[ServiceContract ]
public interface ISendStreamService{
[OperationContract]
void SendStream(Stream stream);
[OperationContract]
void RelativeSetting(string file, string destinationPath);//设置文件属性}
正如我们所预料的那样,如果这儿采用默认的服务实例化(单调服务,即在服务行为中设置InstanceContextMode=InstanceContextMode.PerCall),那么在调用服务的第一个方法不会出现问题,不过在调用第二个方法SendStream()会抛出outStream对象为空的异常。
这种方式的原因很明显,在调用RelatvieSetting()时服务会实例化对象,接着调用SendStream()时又会再次实例化服务对象。也就是在一个客户调用中,我们实例化了两次服务实例,所以我们声明的字段每次都会为空。这是相应的服务类型:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public class SendStreamService:ISendStreamService {
string file;
string path;
FileStream outStream;
int length;
int pos;
byte[] buffer;
public void SendStream(System.IO.Stream stream){
int maxLength = 8192;
buffer = new byte[maxLength];
while ((pos = stream.Read(buffer, 0, maxLength)) > 0){
outStream.Write(buffer, 0, pos);
length += pos;
}
}
public void RelativeSetting(string file, string destinationPath){//设置字段值
this.file = file;
path = destinationPath;
outStream = new FileStream(path + "\\" + file, FileMode.OpenOrCreate, FileAccess.Write);
}
假设客户端以如下的形式调用。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
protected void Button1_Click(object sender, EventArgs e) {
string file = @"F:\电影\牙仙.rmvb";
inStream = new FileStream(file, FileMode.Open, FileAccess.Read);
file = file.Substring(file.LastIndexOf("\\") + 1);
string path = Server.MapPath(Request.Path);
path = path.Substring(0, path.LastIndexOf("\\"));
client = new SendStreamClient("clientEndpoint");//这儿是一个客户类
client.Open();
client.RelativeSetting(file, path);//执行第1步调用
client.SendStream(inStream);//执行第2步调用
}
这儿为了解决多次服务实例化的情况,我们就得考虑到服务实例的管理方式。在上面我们已经知道,这儿采用了单调服务(PerCall),这儿不符合这个场景;同样由于在流传输模式中不允许会话模式,所以会话服务(PerSession)也可以舍去;剩下就只有单例服务(Singleton)了,另外还可以采用静态字段的形式。下面就介绍这两种方式:
方法一:采用静态字段
由于静态字段独立于对象实例而存在,一般我们会把他作为一个类似全局的变量使用。这里我们还可以采用静态字段的方式来保存字段,因为这两个方法在同一个客户端的第1步调用时会初始化静态字段,在第2步调用时这个静态字段会保持状态。所以在第2步中不会出现outStream对象为空的情况。
private static string file;
private static string path;
private static FileStream outStream;//改为静态字段
这里我们还有一个使用了一个私有的静态方法来对静态字段设置状态,然后在RelativeSetting()方法中调用这个方法,执行状态设置。
不过这种方式只适合一个客户调用的情况,如果存在两个以上的客户调用时,会得到导致方法多次执行,为了避免出现这种情况,我们还要将服务行为中设置为不允许并发调用,设置如下:
[ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Single)]
public class SendStreamService:ISendStreamService
这样我们就可以顺利地执行以上的方法了
方法二:采用单例调用:
改用单例调用很简单,因为单例调用会在服务中维持一个服务实例,并发客户只能依次使用服务实例为其服务,这样可能减少并发的效率,不过由于它会为每个客户维护一个实例,所以我们就可以不使用静态字段来保持状态了。为了使用这种方式,我们只需要在服务行为中设定InstanceContextMode为Single即可。
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class SendStreamService:ISendStreamService
当然这儿仍然会出现方法的所遇到的情况,因为服务在只维持一个服务实例,这也意味着我们在第二个客户调用时,会得到第一个客户的状态值,不过这个限制我们可以通过设定实例停用的技术来克服这个限制。
所谓实例停用,就是允许WCF独立地停用实例。这个ReleaseInstanceMode属性有四个值:
None:意味着实例的生命周期不受调用的影响;
BeforeCall:如果会话时已经存在一个实例,那么在转发调用之前WCF将停止实例,创建一个新的实例来维持所要转发的调用;
AfterCall:WCF会在调用方法之后停止实例;
BeforeAndAfterCall:WCF在执行调用之前,如果上下文包含了一个实例,那么WCF会在调用前停止实例,并创建一个新的实例去维持调用,然后在调用后就停止新建的实例。
这儿我们我们选择在之后调用停止实例。在操作行为的SendStream()方法上显示设置这个属性,相应的设置如下:
[OperationBehavior (ReleaseInstanceMode=ReleaseInstanceMode.AfterCall )]
public void SendStream(System.IO.Stream stream)
在这儿我们利用了WCF服务实例管理来对我们的文件流传输场景保持状态信息的一个示例,从这个例子中我们可以发现,实例管理在WCF设计中居于很重要的位置。不管是使用单调服务,还是使用会话服务,在保存状态信息时都要经过特定场景进行精心的设计,才能得到良好的性能和正确的结果。实际应用中我们可以根据具体要求来实现,不过不推荐使用静态字段的方式来保存状态,因为这与我们的WCF管理方式有点不同,它属于CRL的管理范畴。
完整的示例请点击:附件
在发表了这篇随笔之后,我得到了Robin大师的指点,那就是在流传输时,我们可以将文件信息放在自定义的消息头来传输到服务实例:
1、在服务实现在获得消息头:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public void SendStream(System.IO.Stream stream)
{
//首先得到指定的消息头的索引号
int index = OperationContext.Current.IncomingMessageHeaders.FindHeader("fileName", "http://tempuri.org");
//接着我们就可以根据索引得到消息头的信息了
string file= OperationContext.Current.IncomingMessageHeaders.GetHeader<string>(index).ToString();
FileStream outStream = new FileStream(file, FileMode.OpenOrCreate, FileAccess.Write);
int maxLength = 8192;
buffer = new byte[maxLength];
while ((pos = stream.Read(buffer, 0, maxLength)) > 0)
{
outStream.Write(buffer, 0, pos);
length += pos;
}
}
2、在客户调用中附加指定的头,注意在客户端调用中需使用OperationContextScope类的对象创建一个块,将OperationContext包含进来,否则在调用时会出现对象为空的异常:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//注意OperationContextScope的运用
using (OperationContextScope scope = new OperationContextScope(client.InnerChannel))
{
MessageHeader fileHeader = MessageHeader.CreateHeader("fileName", "http://tempuri.org", string.Format("{0}//{1}", path, file));
OperationContext.Current.OutgoingMessageHeaders.Add(fileHeader);
client.Open();
client.BeginSendStream(inStream, new AsyncCallback(Callback), null);
}
详细的说明可以去访问
http://www.cnblogs.com/jillzhang/archive/2010/04/13/1711304.html