WCF–kompresja klient

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 &gt; 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!!

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

*

Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">