在流模式下利用消息头传输带外信息
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);
……
}
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);
}
小结
使用消息头的方式在向服务传递带外信息不失为一种好的技术,这种方式一般用在不宜出现在服务契约中的数据或信息。不过如果服务契约能很好地满足我们数据的传递,建议在操作契约中设定参数的形式来传递,毕竟这样可以我们的程序具有良好的阅读性。
点击下载范例Code