W moim poprzednim poście pokazałem, jak udało włączyć się kompresję w kierunku serwer-klient. Teraz pojawił się problem, jak dla niektórych operacji zrobić kompresowanie niektórych operacji w kierunku klient-serwer. Opcją, jak wcześniej mówiłem, jest użycie kompresji z sdk wcf, ale to przykrywa cały kanał poprzez modyfikację atrybutu TextMessageEncoding i wymaga to użycia CustomBinding
W przypadku wsHttpBinding, który często jest domyślny w rozwiązaniach wcf i w przypadku zaawansowanych, często uruchomionych już projektów nie mamy za bardzo możliwości go zmienić na inne. Pomysłem jest zastosowanie BehaviorExtensions i zastosowanie kompresji na poziomie wiadomości. Do naszego projektu dodajemy kolejny o nazwie np. zip typu Custom Library. Ważne jest według mnie nazwanie projektu tak samo jak nazwa głównej klasy (miałem kłopoty z ładowaniem assembly, gdy nazwy były różne – pewnie wynikało to również z jakiejś mojej niewiedzy w tym zakresie, ale nie miałem czasu zagłębiać się w to bardziej). Do projektu dodajemy też klasę ZipEncoder, która będzie dokonywać całej pracy. Klasa zip naszego projektu musi implementować dwa interfejsy:
public class zip : BehaviorExtensionElement, IEndpointBehavior { #region IEndpointBehavior Members public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime) { ZipEncoder enc = new ZipEncoder(); clientRuntime.MessageInspectors.Add(enc); } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher) { ZipEncoder enc = new ZipEncoder(); endpointDispatcher.DispatchRuntime.MessageInspectors.Add(enc); } public void Validate(ServiceEndpoint endpoint) { } #endregion public override Type BehaviorType { get { return typeof(zip); } } protected override object CreateBehavior() { return new zip(); } }
Z powyższego kodu widać, że nasz dll będzie coś robił z wiadomościami po stronie klienta i serwera. Projekt musi być widoczny po stronie klienta oraz serwera, czyli w kliencie i serwerze musimy dodać referencję do naszej dll’ki. Następnie w pliku app.config, w gałęzi system.serviceModel dodajemy następujący wpis:
<extensions> <behaviorextensions> <add type="zip.zip, zip, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" name="MessageEnc" /> </behaviorextensions> </extensions>
oraz w endpoint behavior dodajemy:
<behaviors> <endpointbehaviors> <behavior name="ZipBehavior"> <MessageEnc /> </behavior> </endpointbehaviors> </behaviors>
oczywiście pamietamy, żeby w tagu client oraz endpoint dodać wskazanie w behaviorConfiguration=”ZipBehavior”
Podobną operację wykonujemy po stronie serwera – następnie wracamy do naszej klasy ZipEncoder, która implementuje dwa interfejsy:
public class ZipEncoder : IClientMessageInspector, IDispatchMessageInspector { #region IClientMessageInspector Members public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState) { } public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel) { //skopiuj całą wiadomość MessageBuffer mb = request.CreateBufferedCopy(int.MaxValue); Message tmpMessage = mb.CreateMessage(); //odczytaj body soapa - czyli request z aplikacji XmlDictionaryReader xdr = tmpMessage.GetReaderAtBodyContents(); XmlDocument xDoc = new XmlDocument(); xDoc.Load(xdr); xdr.Close(); //wczytaj zserializowanego xmla do stringa - RAZEM Z TAGAMI StringBuilder sbString = new StringBuilder(xDoc.InnerXml); XElement el = null; //jezeli rozmiar jest co najmniej 1000, to wtedy sie to oplaca if (sbString.Length > 1000) { //wiemy, że używamy utf8 byte[] bufor = Encoding.UTF8.GetBytes(sbString.ToString()); //skompresuj do zipa MemoryStream ms = new MemoryStream(); using (GZipStream gs = new GZipStream(ms, CompressionMode.Compress, true)) { gs.Write(bufor, 0, bufor.Length); } ms.Position = 0; //wczytaj skompresowane dane do bufora byte[] skompresowane = new byte[ms.Length]; ms.Read(skompresowane, 0, skompresowane.Length); byte[] zipBufor = new byte[skompresowane.Length + 4]; Buffer.BlockCopy(skompresowane, 0, zipBufor, 4, skompresowane.Length); Buffer.BlockCopy(BitConverter.GetBytes(bufor.Length), 0, zipBufor, 0, 4); //przedstaw jako base64, żeby można było słać przez sieć StringBuilder sbSkompresowane = new StringBuilder(Convert.ToBase64String(zipBufor)); //XNamespace nam = XNamespace.Get("tempuri"); //stwórz małego xmla, który będzie w soap body i wpisz skompresowanego stringa do niego //el = new XElement(nam+"GetData"); el = new XElement("k"); el.Value = sbSkompresowane.ToString(); } else { el = XElement.Parse(sbString.ToString()); } //odtwórz wiadomość - trzeba tak robić, bo jakbyś robił bezpośrednio na reqescie to by wyjątek rzucało, że nie można modyfikować Message nowaWiadomosc = Message.CreateMessage(request.Version, null, el); nowaWiadomosc.Headers.CopyHeadersFrom(request); nowaWiadomosc.Properties.CopyProperties(request.Properties); //przypisz nową wiadomość do wysyłanego requesta request = nowaWiadomosc; return null; } #endregion #region IDispatchMessageInspector Members public object AfterReceiveRequest(ref Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext) { MessageBuffer mb = request.CreateBufferedCopy(int.MaxValue); Message tmpMessage = mb.CreateMessage(); //odczytaj body soapa - czyli request z aplikacji XmlDictionaryReader xdr = tmpMessage.GetReaderAtBodyContents(); XmlDocument xDoc = new XmlDocument(); xDoc.Load(xdr); xdr.Close(); //wczytaj zserializowanego xmla do stringa - RAZEM Z TAGAMI!! StringBuilder sbString = new StringBuilder(xDoc.InnerXml); string odkodowanaWiadomosc = string.Empty; if (sbString.Length > 0) { if (sbString.ToString().StartsWith("<k>")) { sbString = sbString.Remove(0, 3); sbString = sbString.Remove(sbString.Length - 4, 4); byte[] gzBufor = Convert.FromBase64String(sbString.ToString()); using (MemoryStream ms = new MemoryStream()) { int dlugoscWiadomosci = BitConverter.ToInt32(gzBufor, 0); ms.Write(gzBufor, 4, gzBufor.Length - 4); byte[] bufor = new byte[dlugoscWiadomosci]; ms.Position = 0; using (GZipStream gs = new GZipStream(ms, CompressionMode.Decompress)) { gs.Read(bufor, 0, bufor.Length); } odkodowanaWiadomosc = Encoding.UTF8.GetString(bufor); } } else { odkodowanaWiadomosc = xDoc.InnerXml; } XElement el = XElement.Parse(odkodowanaWiadomosc); //odtwórz wiadomość - trzeba tak robić, bo jakbyś robił bezpośrednio na reqescie to by wyjątek rzucało, że nie można modyfikować Message nowaWiadomosc = Message.CreateMessage(request.Version, null, el); nowaWiadomosc.Headers.CopyHeadersFrom(request); nowaWiadomosc.Properties.CopyProperties(request.Properties); //przypisz nową wiadomość do wysyłanego requesta request = nowaWiadomosc; } return null; } #endregion
powyższy kod do tworzenia zipa ze stringa znalazłem na blogu: http://www.csharphelp.com/2007/09/compress-and-decompress-strings-in-c/
Ważne w powyższym przykładzie jest to, że na wiadomości możesz operować jedynie jak zrobisz jej kopię.
Tak na marginesie, to w powyższym przykładzie mogą wystąpić problemy, bo przy konfiguracji wcf może rzucać wyjątek, gdy modyfikujemy wiadomość przed wysłaniem – u mnie przy security ustawionym na transport nie było tego problemu.
PS. Polecam też analizę na fiddlerze, bo przy niewielkim rozmiarze wiadomości, po kompresji może być więcej wiadomości do przesłania!!