| ||
|
Get the sample source code! SOAP Extension Compression Sample The sample show how to call XML Web Services that use SOAP Extensions to do compression in a Pocket PC application. This sample application is built for .NET CF with Visual Studio (the download include source for VS2003/CF1/WM2003 and VS2005/CF2/WM5) and it looks like this: ![]() Figure 1. SOAP Extension Compression Sample The sample allows a customer table (from the Northwind sample database) to be checked out (downloaded to the device) and later checked in (uploaded to the server) again. This is a common business scenario where a part of the data in a database is used exclusively by a client for a limited period of time until it is returned to the server again. Code Walkthrough To start on the server side, the following is the code for the ASP.NET Web Service method to check out the customers:
[WebMethod]
public DataSet CheckOutCustomers()
{
DataSet ds = new DataSet();
using(SqlConnection cn = new SqlConnection(connectionString))
{
cn.Open();
ds = SqlHelper.ExecuteDataset(cn, CommandType.Text,
"SELECT * FROM Customers", "Customers");
}
return ds;
}
In a real-world application, this method would also mark the customers as checked
out somehow. Note that the above code uses a slightly enhanced second version of
the Data Access Application Block from Microsoft. The code on the
client that calls the above method looks like this:
WebServices.DataService ws = new WebServices.DataService();
DataSet ds = ws.CheckOutCustomers();
xmlTextBox.Text = ds.GetXml();
MessageBox.Show("Checked out!", "Customers");
The customer check-in code looks like this:
[WebMethod]
public void CheckInCustomers(DataSet clientDataSet)
{
using(SqlConnection cn = new SqlConnection(connectionString))
{
cn.Open();
DataSet serverDataSet = SqlHelper.ExecuteDataset(cn,
CommandType.Text, "SELECT * FROM Customers", "Customers");
foreach(DataRow dr in clientDataSet.Tables["Customers"].Rows)
{
DataRow[] drs = serverDataSet.Tables["Customers"].Select(
"CustomerID='" + dr["CustomerID"].ToString() + "'");
if(drs.Length > 0) // Update
for(int i = 0; i < dr.ItemArray.Length; i++)
drs[0][i] = dr[i];
else // Insert
serverDataSet.Tables["Customers"].Rows.Add(dr.ItemArray);
} // Delete
foreach(DataRow dr in serverDataSet.Tables["Customers"].Rows)
if(clientDataSet.Tables["Customers"].Select("CustomerID='" +
dr["CustomerID"].ToString() + "'").Length < 1)
dr.Delete();
SqlHelper.UpdateDataset(cn, serverDataSet, "Customers");
}
}
The server database is updated with the data passed in the DataSet from the
client (clientDataSet). This code is called by the client like this:
WebServices.DataService ws = new WebServices.DataService();
ws.CheckInCustomers(ds);
xmlTextBox.Text = string.Empty;
MessageBox.Show("Checked in!", "Customers");
This is all well and works as a standard scenario. However, now we want to
compress the data sent both to and from the server, and we do that using SOAP
Extensions that allow the possibility to intercept the SOAP message in various
points of the serialization process. That way the compression/decompression of
the messages can be separated from the implementation of our application logic.
For details on how this works, see the section
Altering the SOAP Message Using SOAP Extensions in the
.NET Framework Developer's Guide.
To be able to mark each of the methods in the Web reference, a class (CompressionSoapExtensionAttribute) that inherits from the class SoapExtensionAttribute (in the System.Web.Services.Protocols namespace) is created like this:
[AttributeUsage(AttributeTargets.Method)]
public class CompressionSoapExtensionAttribute : SoapExtensionAttribute
{
private int priority;
public override Type ExtensionType
{
get { return typeof(CompressionSoapExtension); }
}
public override int Priority
{
get { return priority; }
set { priority = value; }
}
}
The ExtensionType property returns the type that implements the logic of the
extension (CompressionSoapExtension, see below for details). The .NET Compact
Framework will retrieve this property to know what type to instantiate. The Priority
property indicates the order of processing when there are several extensions simultaneously.
The implementation of the actual extension logic is a class that inherits from the class
SoapExtension (in the System.Web.Services.Protocols namespace) and looks
like this:
public class CompressionSoapExtension : SoapExtension
{
Stream oldStream;
Stream newStream;
public override Stream ChainStream( Stream stream )
{
oldStream = stream;
newStream = new MemoryStream();
return newStream;
}
public override object GetInitializer(LogicalMethodInfo methodInfo,
SoapExtensionAttribute attribute)
{
return attribute;
}
public override object GetInitializer(Type type)
{
return typeof(CompressionSoapExtension);
}
public override void Initialize(object initializer)
{
CompressionSoapExtensionAttribute attribute =
(CompressionSoapExtensionAttribute)initializer;
}
public override void ProcessMessage(SoapMessage message)
{
Byte[] buffer = new Byte[2048];
int size;
switch(message.Stage)
{
case SoapMessageStage.AfterSerialize:
newStream.Seek(0, SeekOrigin.Begin);
GZipOutputStream zipOutputStream =
new GZipOutputStream(oldStream);
size = 2048;
while(true)
{
size = newStream.Read(buffer, 0, buffer.Length);
if (size > 0)
zipOutputStream.Write(buffer, 0, size);
else
break;
}
zipOutputStream.Flush();
zipOutputStream.Close();
break;
case SoapMessageStage.BeforeDeserialize:
GZipInputStream zipInputStream =
new GZipInputStream(oldStream);
size = 2048;
while(true)
{
size = zipInputStream.Read(buffer, 0, buffer.Length);
if (size > 0)
newStream.Write(buffer, 0, size);
else
break;
}
newStream.Flush();
newStream.Seek(0, SeekOrigin.Begin);
break;
}
}
}
First, the ChainStream method is called with the stream that contains the data and
allowing the opportunity to return a new stream for the data after the custom processing.
The input stream is stored in memory and a new stream is returned that will be use to store
the result of the compression and decompression. Then, the main method,
ProcessMessage, is called on each stage of the processing of the SOAP message.
In our case we are interested only in the AfterSerialize and BeforeDeserialize
stages. The AfterSerialize stage indicates that a message has been serialized and is
ready to be sent, and this is where the serialized data need to be compressed. The
BeforeDeserialize stage indicates that a message has arrived and is about to be
de-serialized, and this is where the yet not serialized data need to be uncompressed. The
valuable SharpZipLib
library is used to do the actual compression and decompression.
With the above two classes in place, each method in the XML Web Service implementation (on the server) can be marked with an attribute like this:
[WebMethod]
[CompressionSoapExtension]
public DataSet CheckOutCustomers()
{ ... }
[WebMethod]
[CompressionSoapExtension]
public void CheckInCustomers(DataSet clientDataSet)
{ ... }
Each method also needs to be marked in the same way in the Web reference (on the client)
like this:
[SoapDocumentMethodAttribute...]
[CompressionSoapExtension]
public DataSet CheckOutCustomers()
{ ... }
[SoapDocumentMethodAttribute...]
[CompressionSoapExtension]
public void CheckInCustomers(DataSet clientDataSet)
{ ... }
An advantage of this approach is that you can select which methods you want to compress.
If the method has a very small payload, the compression may only create overhead, and such
methods can then be left uncompressed.
An interesting alternative to the above described solution is the Plumbwork Orange project that has a more extensive implementation of what could be very similar to a future WS-Compression specification. For a more complete example, see the SOAP Extension Compression source code. Conclusion Using the standard extendibility of SOAP, a lot of bandwidth can be saved along with significant reduction of communication costs. As the impact on new or existing code is minimal, there are really no good reasons preventing you from compressing your XML Web Service calls where appropriate (on medium and large payloads). Any comments? |
||||||||||||||||||||||||||
| ©2001-2009 Christian Forsberg & Andreas Sjöström |
||