在流模式下保持服務實例的狀態的兩種設計方式
在WCF程序設計中,服務對象如何實例化,對于應用程序的性能有很大的影響,這同時要兼顧到性能與可擴展,由于在WCF中服務類實例化與客戶端有關,服務實例會根據客戶端的請求類型來確定服務實例的管理方式,當然也可以以聲明的方式來顯式定義服務的實例化的方式。
WCF中支持三種實例激活的類型:單調服務(Per Call Service),會為每次客戶端請求分配(銷毀)一個服務實例。會話服務(Sessionful Service)則為每次客戶端連接分配一個服務實例。單調服務(SIngletom Service)所有的客戶端會為所有的連接和激活對象共享一個相同的服務實例。
通過以上的分析我們發現單調服務是為每次的請求分配一個新的實例,當然如果服務中要保持一些狀態信息,那么這些狀態信息也會被初始化。這意味著每次請求都會將狀態重新初始化。現在我們有一個場景,我們要以二進
制流的方式在服務和客戶之間傳遞文件。根據WCF中使用流模式的約定,我們只能以單個的Stream 對象作為操作的輸入輸出參數,因此,為了上傳一個文件,我們就得使用一些輔助的方法來實現諸如文件名,路徑等信息的傳送。下面給出了一個這樣的場景:
在這兒我們聲明了三個字段:file用來保存客戶傳過來的文件名;path用來保存文件的存儲路徑;outStream以得到寫文件的流。
相應的這兒也定義了操作契約:
這兩個操作契約有一些約定:即先要調用RelativeSetting()方法來設置字段的值,為文件流的傳輸做準備,接著我們就可以調用SendStream()方法來上傳文件流了。
服務契約定義
[ServiceContract ]
public interface ISendStreamService{
[OperationContract]
void SendStream(Stream stream);
[OperationContract]
void RelativeSetting(string file, string destinationPath);//設置文件屬性}
正如我們所預料的那樣,如果這兒采用默認的服務實例化(單調服務,即在服務行為中設置InstanceContextMode=InstanceContextMode.PerCall),那么在調用服務的第一個方法不會出現問題,不過在調用第二個方法SendStream()會拋出outStream對象為空的異常。
這種方式的原因很明顯,在調用RelatvieSetting()時服務會實例化對象,接著調用SendStream()時又會再次實例化服務對象。也就是在一個客戶調用中,我們實例化了兩次服務實例,所以我們聲明的字段每次都會為空。這是相應的服務類型:
服務類型實現
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);
}
假設客戶端以如下的形式調用。
客戶調用過程
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、在服務實現在獲得消息頭:
代碼1:通過消息頭得到信息
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包含進來,否則在調用時會出現對象為空的異常:
代碼2:注入消息頭
//注意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.rzrgm.cn/jillzhang/archive/2010/04/13/1711304.html


浙公網安備 33010602011771號