Moving Data, Revisited



Moving Data, Revisited

In Chapter 7 you learned how to convert complex C# classes into binary arrays that could be sent across the network to a remote system. As mentioned then, the .NET library contains classes that can do some of that work for you.

This section describes two classes that serialize data for transmission across a network, the BinaryFormatter and SoapFormatter classes. These classes can be used to easily convert class instances into a serial stream of bytes that can be sent across the network to a remote system and converted back into the original data.

Using a Serialization Class

There are three steps that are required to serialize a class and send it across the network:

  1. Create a library object for the serialized class.

  2. Write a sender program that creates instances of the serialized class and send it to a stream.

  3. Write a receiver program to read data from the stream and re-create the original serialized class data.

Creating the Serialized Class

Each data class that transports data across the network must be tagged with the [Serializable] attribute in the source code file. This indicates that, by default, all of the data elements in the class will be serialized for transit. Listing 16.1 shows how to create a serialized version of the Employee class introduced in Chapter 7.

Listing 16.1: The SerialEmployee.cs program
Start example
using System;
[Serializable]
public class SerialEmployee
{
  public int EmployeeID
  public string LastName;
  public string FirstName;
  public int YearsService;
  public double Salary;
  public SerialEmployee()
  {
   EmployeeID = 0;
   LastName = null;
   FirstName = null;
   YearsService = 0;
   Salary = 0.0;
  }
}
End example

The SerialEmployee.cs file contains a class definition for the SerialEmployee class. The class contains the data elements used to track basic employee information, along with a simple default constructor for the class. Positioned before the class definition, the [Serializable] attribute indicates that the class can be converted to a serial stream of bytes using one of the formatter classes.

To use this class to transport employee data, you must first create a library file that can be compiled into application programs:

csc /t:library SerialEmployee.cs

The output from this command will be the SerialEmployee.dll file. This file must be included as a resource in any program that uses the SerialEmployee class.

Warning 

In serialization, it is extremely important to remember that all applications that use the serialized data class must use the same data library file. When .NET creates the serialized data to send, the data stream includes the class name that defines the data. If the class name does not match when the data is read, the program will not be able to deserialize the stream into the original data class.

Creating a Sender Program

After you create the data class, you can build an application that uses instances of the new class and performs the serialization of the new data to a stream. As mentioned, the BinaryFormatter and SoapFormatter classes serialize the data.

BinaryFormatter serializes the data into a binary stream, much like the GetBytes() method of the Employee.cs program in Listing 7.11. In addition to the actual data, additional information, such as the class name and a version number, are added to the serialized data.

Alternatively, you can use the SoapFormatter class to pass the data using the XML format, similar to the technique used by the web service programs described in Chapter 14. The benefit of using XML is that it is portable between any system or application that recognizes the XML format.

First, you must create an instance of a Stream class to send the data across. This can be any type of stream, including a FileStream, MemoryStream, or of course, a NetworkStream. Next, you create an instance of the appropriate serialization class and use the Serialize() method to send the data across the Stream object:

Stream str = new FileStream(
  "testfile.bin", FileMode.Create, FileAccess.ReadWrite);
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(str, data);

The IFormatter class creates an instance of the desired serialization class (either BinaryFormatter or SoapFormatter), and the data is serialized using the Serialize() method of the formatter.

To see what information is passed on the network in a serialized data class, you can use a FileStream object to save the output to a file and view the file. Listing 16.2 shows the SoapTest.cs program, which serializes two instances of the SerialEmployee class in a file using the SoapFormatter.

Listing 16.2: The SoapTest.cs program
Start example
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;
class SoapTest
{
  public static void Main()
  {
   SerialEmployee emp1 = new SerialEmployee();
   SerialEmployee emp2 = new SerialEmployee();
   emp1.EmployeeID = 1;
   emp1.LastName = "Blum";
   emp1.FirstName = "Katie Jane";
   emp1.YearsService = 12;
   emp1.Salary = 35000.50;
   emp2.EmployeeID = 2;
   emp2.LastName = "Blum";
   emp2.FirstName = "Jessica";
   emp2.YearsService = 9;
   emp2.Salary = 23700.30;
   Stream str = new FileStream("soaptest.xml", FileMode.Create,
    FileAccess.ReadWrite);
   IFormatter formatter = new SoapFormatter();
   formatter.Serialize(str, emp1);
   formatter.Serialize(str, emp2);
   str.Close();
  }
}
End example

The SoapFormatter class is found in the System.Runtime.Serialization.Formatters.Soap namespace, so it must be declared with a using statement. If you want to use the BinaryFormatter class instead, that is found in the System.Runtime.Serialization.Formatters.Binary namespace. The IFormatter interface is in the System.Runtime.Serialization namespace, so that must also be included. After creating two instances of the SerialEmployee class, the program generates a FileStream object pointing to a file to store the output in, and then uses the Serialize() method to save the two instances to the stream.

To compile the program, remember to include the SerialEmployee.dll file as a resource:

csc /r:SerialEmployee.dll SoapTest.cs

After running the SoapTest.exe program, you can examine the soaptest.xml file that is generated (Listing 16.3).

Listing 16.3: The soaptest.xml file
Start example
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Â
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC= Â
"http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV= Â
"http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr= Â
"http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle= Â
"http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:SerialEmployee id="ref-1" xmlns:a1= Â
"http://schemas.microsoft.com/clr/assem/SerialEmployee%2C%20Version%3D0.Â
0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<EmployeeID>1</EmployeeID>
<LastName id="ref-3">Blum</LastName>
<FirstName id="ref-4">Katie Jane</FirstName>
<YearsService>12</YearsService>
<Salary>35000.5</Salary>
</a1:SerialEmployee>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Â
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC= Â
"http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV= Â
"http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr= Â
"http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle= Â
"http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:SerialEmployee id="ref-1" xmlns:a1= Â
"http://schemas.microsoft.com/clr/assem/SerialEmployee%2C%20Version%3D0.Â
0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<EmployeeID>2</EmployeeID>
<LastName id="ref-3">Blum</LastName>
<FirstName id="ref-4">Jessica</FirstName>
<YearsService>9</YearsService>
<Salary>23700.3</Salary>
</a1:SerialEmployee>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
End example

By looking over the soaptest.xml file, you can see how SOAP defines each data element in the serialized class. One important feature of the XML data to notice are the following lines:

<a1:SerialEmployee id="ref-1" xmlns:a1= Â
"http://schemas.microsoft.com/clr/assem/SerialEmployee%2C%20Version%3D0.Â
0.0.0.%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">

Here, the actual class name for the serialized data class is used within the XML definition data. This is important If the receiving program uses a different class name to define the same data class, it will not match with the XML data read from the stream. The classes must match or the read will fail.

Note 

As demonstrated in Listing 16.3, though the SoapFormatter adds the ability to communicate class information with other systems, it can greatly increase the amount of data sent in the transmission.

Now that you have seen how the data is serialized, you can write a network application that serializes the class data and sends it to a program running on a remote device. Listing 16.4 shows the BinaryDataSender.cs program, which uses the BinaryFormatter class to send the SerialEmployee data.

Listing 16.4: The BinaryDataSender.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
class BinaryDataSender
{
  public static void Main()
  {
   SerialEmployee emp1 = new SerialEmployee();
   SerialEmployee emp2 = new SerialEmployee();
   emp1.EmployeeID = 1;
   emp1.LastName = "Blum";
   emp1.FirstName = "Katie Jane";
   emp1.YearsService = 12;
   emp1.Salary = 35000.50;
   emp2.EmployeeID = 2;
   emp2.LastName = "Blum";
   emp2.FirstName = "Jessica";
   emp2.YearsService = 9;
   emp2.Salary = 23700.30;
   TcpClient client = new TcpClient("127.0.0.1", 9050);
   IFormatter formatter = new BinaryFormatter();
   NetworkStream strm = client.GetStream();
   formatter.Serialize(strm, emp1);
   formatter.Serialize(strm, emp2);
   strm.Close();
   client.Close();
  }
}
End example

The BinaryDataSender program uses the BinaryFormatter to serialize two instances of the SerialEmployee class and sends it to a remote device specified in the TcpClient object. After the two instances are sent, both the Stream and the TcpClient objects are closed.

Note 

Because both the BinaryFormatter and SoapFormatter classes require a Stream object to send the serialized data, you must use either a TCP Socket object, or a TcpClient object to send the data. You cannot directly use UDP with the serializers.

Creating a Receiver Program

The third and final step in moving the class data across the network is to build a program that can read the data from the stream and assemble it back into the original class data elements. Again, this is done using either the BinaryFormatter or SoapFormatter classes. Obviously, you must use the same class that was used to serialize the data on the sender.

When the formatter classes deserialize the data, the data elements are extracted into a generic Object object. You must typecast the output to the appropriate class to extract the data elements:

IFormatter formatter = new BinaryFormatter();
SerialEmployee emp1 = (SerialEmployee)formatter.Deserialize(str);

When the data is received from the stream, it is reassembled into the appropriate data class elements. You should take care to ensure that the proper amount of data is present to reconstruct the data class.

Listing 16.5 shows the BinaryDataRcvr.cs program, which accepts the serialized data from the BinaryDataSender program and re-creates the original data classes.

Listing 16.5: The BinaryDataRcvr.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
class BinaryDataRcvr
{
  public static void Main()
  {
   TcpListener server = new TcpListener(9050);
   server.Start();
   TcpClient client = server.AcceptTcpClient();
   NetworkStream strm = client.GetStream();
   IFormatter formatter = new BinaryFormatter();
   SerialEmployee emp1 = (SerialEmployee)formatter.Deserialize(strm);
   Console.WriteLine("emp1.EmployeeID = {0}", emp1.EmployeeID);
   Console.WriteLine("emp1.LastName = {0}", emp1.LastName);
   Console.WriteLine("emp1.FirstName = {0}", emp1.FirstName);
   Console.WriteLine("emp1.YearsService = {0}", emp1.YearsService);
   Console.WriteLine("emp1.Salary = {0}\n", emp1.Salary);
   SerialEmployee emp2 = (SerialEmployee)formatter.Deserialize(strm);
   Console.WriteLine("emp2.EmployeeID = {0}", emp2.EmployeeID);
   Console.WriteLine("emp2.LastName = {0}", emp2.LastName);
   Console.WriteLine("emp2.FirstName = {0}", emp2.FirstName);
   Console.WriteLine("emp2.YearsService = {0}", emp2.YearsService);
   Console.WriteLine("emp2.Salary = {0}", emp2.Salary);
   strm.Close();
   server.Stop();
  }
}
End example

The BinaryDataRcvr program creates a TcpListener object bound to a specific port and waits for a connection attempt from the BinaryDataSender program. When the connection is established, the Deserialize() method converts the received data stream back into the original data class.

Note 

This example used simple datatypes, but you can easily modify it to add more complex datatypes, such as an employee start date. Just add the new data elements to the SerialEmployee.cs file, re-create the DLL library file, and rebuild the sender and receiver programs.

Problems with Serialization

While the serializing examples so far show a simple technique for serializing and transmitting complex data classes, in the real world, on real networks, it is not often this easy. You may have noticed that the BinaryDataSender and BinaryDataRcvr programs assumed one important thing: they both expected all of the data to arrive at the receiver for the BinaryFormatter to deserialize the stream into the original class data. As you probably know by now, this is not necessarily what occurs on a real network.

If not all of the data is received on the stream before the Deserialize() method is performed, there will be a problem. When the Deserialize() method does not have enough bytes to complete the reassembly, it produces an Exception, and the data class is not properly created. The solution to this is to use a hybrid technique, combining the serialization classes presented here with the data sizing methods demonstrated back in Chapter 7. If you send the size of each serialized data object before the actual object, the receiver can determine how many bytes of data to receive before attempting to deserialize the data.

The easiest way to do this is to serialize the data coming from the stream into a MemoryStream object. The MemoryStream object holds all of the serialized data in a memory buffer. It allows you to easily determine the total size of the serialized data. When the size of the data stream is determined, both the size value and the serialized data buffer can be sent out the NetworkStream to the remote device.

Listing 16.6 shows the BetterDataSender.cs program, which uses this technique to transmit two instances of the SerialEmployee data class to a remote device.

Listing 16.6: The BetterDataSender.cs program
Start example
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;
class BetterDataSender
{
  public void SendData (NetworkStream strm, SerialEmployee emp)
  {
   IFormatter formatter = new SoapFormatter();
   MemoryStream memstrm = new MemoryStream();
   formatter.Serialize(memstrm, emp);
   byte[] data = memstrm.GetBuffer();
   int memsize = (int)memstrm.Length;
   byte[] size = BitConverter.GetBytes(memsize);
   strm.Write(size, 0, 4);
   strm.Write(data, 0, memsize);
   strm.Flush();
   memstrm.Close();
  }
  public BetterDataSender()
  {
   SerialEmployee emp1 = new SerialEmployee();
   SerialEmployee emp2 = new SerialEmployee();
   emp1.EmployeeID = 1;
   emp1.LastName = "Blum";
   emp1.FirstName = "Katie Jane";
   emp1.YearsService = 12;
   emp1.Salary = 35000.50;
   emp2.EmployeeID = 2;
   emp2.LastName = "Blum";
   emp2.FirstName = "Jessica";
   emp2.YearsService = 9;
   emp2.Salary = 23700.30;
   TcpClient client = new TcpClient("127.0.0.1", 9050);
   NetworkStream strm = client.GetStream();
   SendData(strm, emp1);
   SendData(strm, emp2);
   strm.Close();
   client.Close();
  }
  public static void Main()
  {
   BetterDataSender bds = new BetterDataSender();
  }
}
End example

The BetterDataSender program uses the SendData() method to create a MemoryStream object with the serialized data to send to the remote device. From the MemoryStream object, the size of the data is determined and sent to the remote receiver. Then the serialized data MemoryStream buffer is sent to the remote receiver.

Now take a look at the BetterDataRcvr.cs program, Listing 16.7, which demonstrates how to reassemble the received data size and serialized data from the sender.

Listing 16.7: The BetterDataRcvr.cs program
Start example
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;
class BetterDataRcvr
{
  private SerialEmployee RecvData (NetworkStream strm)
  {
   MemoryStream memstrm = new MemoryStream();
   byte[] data = new byte[4];
   int recv = strm.Read(data, 0, 4);
   int size = BitConverter.ToInt32(data, 0);
   int offset = 0;
   while(size > 0)
   {
     data = new byte[1024];
     recv = strm.Read(data, 0, size);
     memstrm.Write(data, offset, recv);
     offset += recv;
     size -= recv;
   }
   IFormatter formatter = new SoapFormatter();
   memstrm.Position = 0;
   SerialEmployee emp = (SerialEmployee)formatter.Deserialize(memstrm);
   memstrm.Close();
   return emp;
  } 
  public BetterDataRcvr()
  {
   TcpListener server = new TcpListener(9050);
   server.Start();
   TcpClient client = server.AcceptTcpClient();
   NetworkStream strm = client.GetStream();
   SerialEmployee emp1 = RecvData(strm);
   Console.WriteLine("emp1.EmployeeID = {0}", emp1.EmployeeID);
   Console.WriteLine("emp1.LastName = {0}", emp1.LastName);
   Console.WriteLine("emp1.FirstName = {0}", emp1.FirstName);
   Console.WriteLine("emp1.YearsService = {0}", emp1.YearsService);
   Console.WriteLine("emp1.Salary = {0}\n", emp1.Salary);
   SerialEmployee emp2 = RecvData(strm);
   Console.WriteLine("emp2.EmployeeID = {0}", emp2.EmployeeID);
   Console.WriteLine("emp2.LastName = {0}", emp2.LastName);
   Console.WriteLine("emp2.FirstName = {0}", emp2.FirstName);
   Console.WriteLine("emp2.YearsService = {0}", emp2.YearsService);
   Console.WriteLine("emp2.Salary = {0}", emp2.Salary);
   strm.Close();
   server.Stop();
  }
  public static void Main()
  {
   BetterDataRcvr bdr = new BetterDataRcvr();
  }
}
End example

The BetterDataRcvr program uses the RecvData() method to obtain the incoming data from the sender. First, a 4-byte size value come in. This value indicates how many bytes to expect in the serialized data portion of the transmission. The RecvData() method then goes into a loop until all of the expected bytes are received from the NetworkStream. As the bytes arrive, they are added to a MemoryStream object. When all of the bytes have been received, the MemoryStream object creates the data class instance.

Warning 

It is important to remember that the MemoryStream object must be reset to point to the start of the stream before being used in the Deserialize() method.

To test the BetterDataSender and BetterDataRcvr programs, you must compile them with the same SerialEmployee.dll file.

Note 

The BinaryFormatter and SoapFormatter take care of any network byte order issues when transferring the data to the remote host.

 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows