WCF為傳輸層實現數據流在客戶和服務之間進行傳輸提供了很好的支持,不過 在使用這種方式時,我們必須遵循相應的約定。WCF服務在啟動時會首先檢查操 作契約是否符合這種規范。因為通常模式下我們不能簡單地在客戶中使用特定的 流,如我們在傳輸文件時,我們目的是要得到文件對象,而不是流對象。因為我 們使用了不同類型的文件(如:*.doc,*.exe等),那麼在另一端我們應該能夠 重現這種類型,不過由於使用流傳輸帶來很好的性能,於是我們想在文件傳輸中 使用這種流模式。那麼就得附加相應的文件信息給異端,以便重現文件。這時我 們就可以使用SOAP消息頭來附加這些信息了。
1流模式的操作契約約定:
首先我們先來了解一下使用流模式的基本的操作契約要求。要使用流模式, 我們在操作契約中只能是以單個的輸入輸出流作為參數,也就是說方法的參數和 返回參數,要麼是Stream對象或派生類對象,要麼void,形如以下的方法簽名可 認可:
void SendStream(Stream inStream);
Stream ReceiveStream();
Stream SendAndReceiveStream(Stream inStream);
void SendAndReceiveStream(Stream inStream,out Stream outStream) ;
void ReceiveStream(out Stream outStream)
從上面的簽名我們可以看出如果我們要在服務和客戶之間傳遞一個文件流, 在方法中是無法傳遞一個參數來達到的,所以這兒為了傳遞文件名和路徑,我們 選擇使用消息頭附加這些信息的方式來實現,這兒定義操作契約為:
[ServiceContract ]
public interface ISendStreamService
{
//利用流的傳輸模式來實現,消息頭附加信息
[OperationContract]
void SendStream(Stream stream);
}
2 WCF消息頭相關的類和方法
OperationContext類代表操作上下文,它提供的IncomingMessageHeaders和 OutgoingMessageHeaders屬性來操作輸入輸出消息頭:
public sealed class OperationContext :
IExtensibleObject<OperationContext>{
public MessageHeaders OutgoingMessageHeaders { get; }
public MessageHeaders IncomingMessageHeaders { get; }
……
}
MessageHeader<T>代表SOAP 標頭的內容,用泛型類來增強類型的安全 。
public class MessageHeader<T>{
public MessageHeader();
public MessageHeader(T content);
public MessageHeader(T content, bool mustUnderstand, string actor, bool relay);
//得到與類型無關的原始消息頭
public MessageHeader GetUntypedHeader(string name, string ns);
}
MessageHeaders代表消息頭的消息集合,這兒我們只用到 GetHeader<T>()泛型方法
public sealed class MessageHeaders : IEnumerable<MessageHeaderInfo>, IEnumerable{
public T GetHeader<T>(int index);
public T GetHeader<T>(int index, XmlObjectSerializer serializer);
public T GetHeader<T>(string name, string ns);
public T GetHeader<T>(string name, string ns, params string[] actors);
public T GetHeader<T>(string name, string ns, XmlObjectSerializer serializer);
……
}
OperationContextScope類是在當前上下文不適用時,可以切換為新的上下文 。其構造函數可以將當前的上下文替換為新的上下文,而調用完成之後使用 Dispose()方法恢復原來的上下文。
public sealed class OperationContextScope : IDisposable {
// Methods
public OperationContextScope(IContextChannel channel);
public OperationContextScope(OperationContext context);
public void Dispose();
……
}
3首先我們來實現附加消息頭的邏輯
這裡為了統一我們實現了一個數據契約【StreamContext】來包含要附加的帶 外信息。這個數據契約包括三個屬性:FileName(文件名),FilePath(目標路徑) ,FileLength(文件長度,可選)。還有一個靜態的代表當前上下文的定制屬性 Current。為了簡便起見我們實現了自定義消息頭的封裝。
附加的數據契約及自定義消息頭的實現
[DataContract ]
public class StreamContext
{
[DataMember]
public string FileName;
[DataMember]
public string FilePath;
[DataMember]
public int FileLength;
//這裡實現一個消息的封閉
public StreamContext(string fileName, string filePath, int fileLength)
{
FileName = fileName;
FilePath = filePath;
FileLength = fileLength;
}
public StreamContext(string fileName, string filePath) : this(fileName, filePath,0) { }
public StreamContext(string fileName) : this(fileName, string.Empty, 0) { }
public StreamContext(StreamContext streamContext) : this(streamContext.FileName, streamContext.FilePath, streamContext.FileLength) { }
//這裡實現一個消息頭的附加邏輯
public static StreamContext Current {
get {
OperationContext context = OperationContext.Current;
if (context == null) return null;
try
{
//GetHeader<T>(string name,string ns)得 到頭消息的附加對象
return context.IncomingMessageHeaders.GetHeader<StreamContext> ("StreamContext", "http://tempuri.org");
}
catch {
return null;
}
}
set {
OperationContext context = OperationContext.Current;
Debug.Assert(context != null);
bool headerContextExist = false;
try
{//同上
context.OutgoingMessageHeaders.GetHeader<StreamContext> ("StreamContext", "http://tempuri.org");
headerContextExist = true;
}
catch (MessageHeaderException ex)
{
Debug.Assert(ex.Message == "There is not a header with name StreamContext and namespace http://tempuri.org in the message.");
}
if (headerContextExist)
throw new InvalidOperationException("相同名字 的消息頭已經存在,請試用請他的名字");
MessageHeader<StreamContext> streamContext = new MessageHeader<StreamContext>(value);
//為輸出上下文添加一個原始的MessageHeader
context.OutgoingMessageHeaders.Add (streamContext.GetUntypedHeader("StreamContext", "http://tempuri.org"));
}
}
4 服務端實現
服務類
[ServiceBehavior (InstanceContextMode=InstanceContextMode.PerCall,
ConcurrencyMode=ConcurrencyMode.Multiple)]
public class SendStreamService:ISendStreamService {
public void SendStream(System.IO.Stream stream){
int maxLength = 8192;
int length=0;
int pos=0;
byte[] buffer =new byte[maxLength];
//從附加的消息頭來得到文件路徑信息
StreamContext context = StreamContext.Current;
FileStream outStream = new FileStream (context.FilePath +"//"+context.FileName, FileMode.OpenOrCreate, FileAccess.Write);
while ((pos = stream.Read(buffer, 0, maxLength)) > 0)
{
outStream.Write(buffer, 0, pos);
length += pos;
}
System.Diagnostics.Debug.Write(string.Format("文件:{0}已經 上傳成功,文件大小為{1}", context.FileName, length / 1024 > 1 ? ((length / 1024).ToString () + "K") : length.ToString ()));
}
}
5客戶端的實現
客戶調用
string file=FileUpload1.PostedFile.FileName;
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("\\"));
StreamContext context = new StreamContext(file, path, 0);
client = new SendStreamClient("setEndpoint");
//在當前線程上建立一個與client相應的線程
using (OperationContextScope scope = new OperationContextScope (client.InnerChannel)) {//建立新的上下文
//設置消息頭附加信息
StreamContext.Current = context;
client.Open();
client.BeginSendStream(inStream, new AsyncCallback(Callback), file);
}
小結
使用消息頭的方式在向服務傳遞帶外信息不失為一種好的技術,這種方式一 般用在不宜出現在服務契約中的數據或信息。不過如果服務契約能很好地滿足我 們數據的傳遞,建議在操作契約中設定參數的形式來傳遞,畢竟這樣可以我們的 程序具有良好的閱讀性。
本文配套源碼