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!!