Moving Data across the Network



Moving Data across the Network

So far in this chapter you have seen how to move text messages efficiently from one device to another device across the network using the Socket helper classes. Certainly, for many applications this is all that is necessary; for many other applications, however, more advanced functionality is required to handle classes of data other than text such as binary data and groups of more than one type of data.

This section shows techniques for moving different types of binary data across the network. When programming to communicate with various systems, it is important that you understand how binary data is stored on a device and how it is transmitted across the network. This section also covers how to move complex datatypes, such as data elements in classes, among devices on the network.

Moving Binary Data

Whether you use TCP or UDP, sending binary data between two devices on a network is a complex topic. There are many possibilities for errors, and you must take them all into account. This section offers several suggestions for creating programs that move binary data successfully from one device to another.

Binary Data Representation

Perhaps the major issue when moving binary datatypes on a network is how they are represented. The various types of machines all represent binary datatype in their own way. You must ensure that the binary value on one machine turns out to be the same binary value on another machine.

Machines running a Microsoft Windows OS on an Intel (or compatible) processor platform store binary information using a set pattern for each datatype. It is important that you understand how this information is represented when sending binary data to a non-Windows remote host. Figure lists the binary datatypes used in C#.

Figure: C# Binary Datatypes

Datatype

Bytes

Description

sbyte

1

Signed byte integer with values from –128 to 127

byte

1

Unsigned integer with values from 0 to 255

short

2

Signed short integer with values from –32,768 to 32,767

ushort

2

Unsigned short integer with values from 0 to 65,535

int

4

Standard integer with values from –2,147,483,648 to 2,147,483,647

uint

4

Unsigned integer with values from 0 to 4,294,967,295

long

8

Large signed integer with values from –9,223,372,036,854,775,808 to 9,223,372,036,854,775,807

ulong

8

Large unsigned integer with values from 0 to 18,446,744,073,709,551,615

float

4

A floating-point decimal number with values from 1.5 x 10–45 to 3.4 x 1,038, using 7-digit precision

double

8

A floating-point decimal number with values from 5.0 x 10–324 to 1.7 x 10308, using 15–16-digit precision

decimal

16

A high-precision floating-point number with values from 1.0 x 10–28 to 7.9 x 1028, using 28–29-digit precision

Each binary datatype must be converted into a raw byte array before it can be sent using the Send() or SendTo() methods. Fortunately, the .NET library provides a class specifically for this job: the BitConverter class.

Converting Binary Datatypes

As seen in the VarTcpClient (Listing 5.8) and VarTcpSrvr (Listing 5.7) programs in Chapter 5, the .NET System class supplies the BitConverter class to convert binary datatypes into byte arrays, and vice versa. This class is crucial to accurately sending binary datatypes across the network to remote hosts.

Sending Binary Datatypes

The BitConverter method GetBytes()converts a standard binary datatype to a byte array. For example:

int test = 1990;
byte[] data = BitConverter.GetBytes(test);
newsock.Send(data, data.Length);

This simple code snippet shows the conversion of a standard 4-byte integer value into a 4-byte byte array, which is then used in a Write() or Send() method call to forward the value to a remote device.

Warning 

If you are sending binary datatypes using TCP, you cannot use the StreamWriter or StreamReader classes because they expect data to be sent in strings, not binary. Any nulls in the binary data will damage the string conversion.

Note 

When creating the byte array for the binary datatype, it is important that you allocate enough space in the byte array to contain all of the bytes of the binary datatype. If not all of the bytes of the converted binary datatype are sent, the receiving program will not be able to “reassemble” them back into the original datatype.

Receiving Binary Datatypes

As just stated in the preceding Note, the receiving program must be able to receive the byte array containing the binary datatype and convert it back into the original binary datatype. This is also done using BitConverter class methods. Figure lists the Bitconverter class’s methods for converting raw byte arrays into binary datatypes.

Figure: BitConverter Methods for Converting Data

Method

Description

ToBoolean()

Converts a 1-byte byte array to a Boolean value

ToChar()

Converts a 2-byte byte array to a Unicode character value

ToDouble()

Converts an 8-byte byte array to a double floating-point value

ToInt16()

Converts a 2-byte byte array to a 16-bit signed integer value

ToInt32()

Converts a 4-byte byte array to a 32-bit signed integer value

ToSingle()

Converts a 4-byte byte array to a single-precision floating-point value

ToString()

Converts all bytes in the byte array to a string that represents the hexadecimal values of the binary data

ToUInt16()

Converts a 2-byte byte array to a 16-bit unsigned integer value

ToUInt32()

Converts a 4-byte byte array to a 32-bit unsigned integer value

ToUing64()

Converts an 8-byte byte array to a 64-bit unsigned integer value

All the converter methods have the same format:

BitConverter.ToInt16(byte[] data, int offset)

The byte array is the first parameter, and the second parameter is the offset location within the array where the conversion is to start. Note that the methods know how many bytes to use within the array to create the appropriate binary datatype.

Once the received byte array is converted to a binary datatype, you can use it in the program as any other value of that datatype, as shown here:

double total = 0.0;
byte[] data = newsock.Receive(ref sender);
double test = BitConverter.ToDouble(data, 0);
total += test;

Sample Programs

Sending binary information using UDP packets is fairly easy, assuming that you are sending one value per message. (We’ll look at multiple-value situations in a later section.) Because UDP preserves message boundaries, you are guaranteed that if the packet arrives, there is only one data value in it. Assuming the server knows what type of binary data is in the packet, it is a snap to decode the value within the message back to its original binary value.

Listing 7.5 shows a sample UDP server program that reads binary data from the network.

Listing 7.5: The BinaryUdpSrvr.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class BinaryUdpSrvr
{
  public static void Main()
  {
   byte[] data = new byte[1024];
   IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 9050);
   UdpClient newsock = new UdpClient(ipep);
   Console.WriteLine("Waiting for a client...");
   IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
   data = newsock.Receive(ref sender);
   Console.WriteLine("Message received from {0}:", sender.ToString());
   Console.WriteLine(Encoding.ASCII.GetString(data, 0, data.Length));
   string welcome = "Welcome to my test server";
   data = Encoding.ASCII.GetBytes(welcome);
   newsock.Send(data, data.Length, sender);
   byte[] data1 = newsock.Receive(ref sender);
   int test1 = BitConverter.ToInt32(data1, 0);
   Console.WriteLine("test1 = {0}", test1);
   byte[] data2 = newsock.Receive(ref sender);
   double test2 = BitConverter.ToDouble(data2, 0);
   Console.WriteLine("test2 = {0}", test2);
   byte[] data3 = newsock.Receive(ref sender);
   int test3 = BitConverter.ToInt32(data3, 0);   
   Console.WriteLine("test3 = {0}", test3);
   byte[] data4 = newsock.Receive(ref sender);
   bool test4 = BitConverter.ToBoolean(data4, 0);
   Console.WriteLine("test4 = {0}", test4.ToString());
   byte[] data5 = newsock.Receive(ref sender);
   string test5 = Encoding.ASCII.GetString(data5);
   Console.WriteLine("test5 = {0}", test5);
   newsock.Close();
  }
}
End example

The BinaryUdpSrvr program uses the Receive() method to wait for a remote client to send a greeting message, then returns its welcome banner to the client.

Next, it expects to receive five test messages in a row from the remote client. Each message contains a different type of data. The order in which the messages appear is critical, because each message is decoded back into the appropriate datatype based on its position in the receipt order. Of course, with UDP, there’s no guarantee that the packets will arrive at all, so this is not a good real-world example. You must be careful when using this technique with UDP messages.

One important thing to note about this program is that the BitConverter class contains methods to convert raw binary data into binary datatypes, but not into a text datatype. The BitConverter method ToString()does exist, but its role is different from what you probably expect. Rather than converting the raw binary data into a printable string, it converts the raw data into a string representation of the binary data in hexadecimal. For example, this code snippet:

string data = "this is a test";
string test = BitConverter.ToString(Encoding.ASCII.GetBytes(data));
Console.WriteLine("data = '{0}'", data);
Console.WriteLine("test = '{0}'", test);

produces these results:

C:\>test
data = 'this is a test'
test = '74-68-69-73-20-69-73-20-61-20-74-65-73-74'
C:\>
Note 

The BitConverter method ToString()is a handy way to display the hexadecimal values of a byte array, but if you want to display the actual converted text string, you must use the Encoding.ASCII.GetString() method, as shown in the preceding BinaryUdpSrvr program (Listing 7.5).

The BinaryUdpClient program, Listing 7.6, is the counterpart to the BinaryUdpSrvr program. Here it sends five types of data to the server program.

Listing 7.6: The BinaryUdpClient.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class BinaryUdpClient
{
  public static void Main()
  {
   byte[] data = new byte[1024];
   string stringData;
   UdpClient server = new UdpClient("127.0.0.1", 9050);
   IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
   string welcome = "Hello, are you there?";
   data = Encoding.ASCII.GetBytes(welcome);
   server.Send(data, data.Length);
   data = new byte[1024];
   data = server.Receive(ref sender);
   Console.WriteLine("Message received from {0}:", sender.ToString());
   stringData = Encoding.ASCII.GetString(data, 0, data.Length);
   Console.WriteLine(stringData);
 
   int test1 = 45;
   double test2 = 3.14159;
   int test3 = -1234567890;
   bool test4 = false;
   string test5 = "This is a test.";
   byte[] data1 = BitConverter.GetBytes(test1);
   server.Send(data1, data1.Length);
   byte[] data2 = BitConverter.GetBytes(test2);
   server.Send(data2, data2.Length);
   byte[] data3 = BitConverter.GetBytes(test3);
   server.Send(data3, data3.Length);
   byte[] data4 = BitConverter.GetBytes(test4);
   server.Send(data4, data4.Length);
   byte[] data5 = Encoding.ASCII.GetBytes(test5);
   server.Send(data5, data5.Length);
   Console.WriteLine("Stopping client");
   server.Close();
  }
}
End example

BinaryUdpClient uses the Send() method to send a greeting banner to the server (specified by an address in the UdpClient constructor) and waits for the welcome banner to be returned. Once the welcome banner is received, the program sends a series of five messages, each containing data in a different datatype.

The output from the BinaryUdpSrvr program should look like this:

C:\>BinaryUdpSrvr
Waiting for a client...
Message received from 127.0.0.1:1252:
Hello, are you there?
test1 = 45
test2 = 3.14159
test3 = -1234567890
test4 = False
test5 = This is a test.
C:\>

Each of the binary datatypes was successfully transmitted to the server program across the network and can be used in other calculations within the server program if necessary.

WinDump and Analyzer, as usual, will let you watch the actual data packets if you are testing these programs across a network.

Note how each datatype is sent within the individual packets. The packet for the integer value contains the byte array for the integer value 45. The binary data is sent as the 4-byte value 0x2D 0x00 0x00 0x00, seen in the data section of the UDP packet.

Note the order in which the binary data value is sent in the packet. This representation format is an important feature of sending binary data that is discussed in the next section.

Communicating with Other Host Types

When sending binary datatypes between two devices that are both running a Microsoft Windows OS on an Intel microprocessor platform, you do not have to worry about how the binary data is represented. Each side of the network communications channel recognizes the binary data. The byte array produced from the BitConverter.GetBytes() method is converted to the proper binary datatype for the other machine using the BitConverter.ToInt32() method.

Of course, that’s not the end of the story. The C# language in your network programs is being ported to other operating systems running on other CPU platforms. So it is possible and entirely likely that the binary datatype representations of the client and server programs may not be the same. This section describes how to meet this challenge and make your C# network programs ready to accommodate the formats of various platforms.

Binary Datatype Representation

The problem of dueling binary datatypes arises from the fact that CPU platforms may store binary datatypes differently. Because multiple bytes are used for the datatype, they can be stored one of two ways:

  • The least significant byte first (called little endian)

  • The most significant byte first (called big endian)

It is imperative that the binary datatype is interpreted correctly on each system, sending and receiving. If the wrong datatype representation is used to convert a raw binary byte array, your programs will be working with incorrect data.

Listing 7.7 is the BinaryDataTest.cs program, which uses the BitConverter.ToString() method to demonstrate how the different binary datatypes are stored on your system.

Listing 7.7: The BinaryDataTest.cs program
Start example
using System;
using System.Net;
using System.Text;
class BinaryDataTest
{
  public static void Main()
  {
   int test1 = 45;
   double test2 = 3.14159;
   int test3 = -1234567890;
   bool test4 = false;
   byte[] data = new byte[1024];
   string output;
   data = BitConverter.GetBytes(test1);
   output = BitConverter.ToString(data);
   Console.WriteLine("test1 = {0}, string = {1}", test1, output);
   data = BitConverter.GetBytes(test2);
   output = BitConverter.ToString(data);
   Console.WriteLine("test2 = {0}, string = {1}", test2, output);
   data = BitConverter.GetBytes(test3);
   output = BitConverter.ToString(data);
   Console.WriteLine("test3 = {0}, string = {1}", test3, output);
   data = BitConverter.GetBytes(test4);
   output = BitConverter.ToString(data);
   Console.WriteLine("test4 = {0}, string = {1}", test4, output);
  }
}
End example

All that happens here is that BinaryDataTest does some simple BitConverter operations on various datatypes, and it uses the BitConverter ToString() method to display the resulting byte array values. The output from the BinaryDataTest program should look like this:

C:\>BinaryDataTest
test1 = 45, string = 2D-00-00-00
test2 = 3.14159, string = 6E-86-1B-F0-F9-21-09-40
test3 = -1234567890, string = 2E-FD-69-B6
test4 = False, string = 00
C:\>

By looking at the simple integer value (test1) you can see that the standard byte representation used on this machine is little endian (the 2D value comes before the zeros). If this were a big endian system, the integer would be stored as 00-00-00-2D instead. So when sending data to a host that uses big endian data representation, errors will occur unless your program adjusts.

Converting Binary Data Representation

The problem of using different binary datatype representations is a significant issue in the Unix environment. Because so many platforms run Unix, you can never assume that the remote system will be using the same representation as yours. The Unix world has devised a solution: sending binary datatypes in a generic method.

The network byte order representation of binary datatypes was created as intermediate storage for binary data to be transmitted across the network. The idea is for each network program to convert its own local binary data into network byte order before transmitting it. On the receiving side, the system must convert the incoming data from network byte order into its own internal byte order. This ensures that the binary data will be converted to the proper representation for the destination host. Figure illustrates the process of network-byte-order conversion.

Click To expand
Figure: Using network byte order between hosts.

The .NET library includes methods to convert integer values to network byte order, and vice versa. These methods are included in the IPAddress class, contained in the System.Net namespace. One is HostToNetworkOrder(), which converts integer datatypes to a network byte order representation. In Listing 7.8, the BinaryNetworkByteOrder.cs program demonstrates using this method on integer datatypes.

Listing 7.8: The BinaryNetworkByteOrder.cs program
Start example
using System;
using System.Net;
using System.Text;
class BinaryNetworkByteOrder
{
  public static void Main()
  {
   short test1 = 45;
   int test2 = 314159;
   long test3 = -123456789033452;
   byte[] data = new byte[1024];
   string output;
   data = BitConverter.GetBytes(test1);
   output = BitConverter.ToString(data);
   Console.WriteLine("test1 = {0}, string = {1}", test1, output);
   data = BitConverter.GetBytes(test2);
   output = BitConverter.ToString(data);
   Console.WriteLine("test2 = {0}, string = {1}", test2, output);
   data = BitConverter.GetBytes(test3);
   output = BitConverter.ToString(data);
   Console.WriteLine("test3 = {0}, string = {1}", test3, output);
   short test1b = IPAddress.HostToNetworkOrder(test1);
   data = BitConverter.GetBytes(test1b);
   output = BitConverter.ToString(data);
   Console.WriteLine("test1 = {0}, nbo = {1}", test1b, output);
   int test2b = IPAddress.HostToNetworkOrder(test2);
   data = BitConverter.GetBytes(test2b);
   output = BitConverter.ToString(data);
   Console.WriteLine("test2 = {0}, nbo = {1}", test2b, output);
   long test3b = IPAddress.HostToNetworkOrder(test3);
   data = BitConverter.GetBytes(test3b);
   output = BitConverter.ToString(data);
   Console.WriteLine("test3 = {0}, nbo = {1}", test3b, output);
  }
}
End example

The BinaryNetworkByteOrder program creates three types of integer data values and uses the HostToNetworkOrder() method to convert them to values in network byte order. The output from the BinaryNetworkByteOrder program on my machine is as follows:

C:\>BinaryNetworkByteOrder
test1 = 45, string = 2D-00
test2 = 314159, string = 2F-CB-04-00
test3 = -123456789033452, string = 14-CE-F1-79-B7-8F-FF-FF
test1 = 11520, nbo = 00-2D
test2 = 801833984, nbo = 00-04-CB-2F
test3 = 1499401231033958399, nbo = FF-FF-8F-B7-79-F1-CE-14
C:\>

You may notice something odd here. Notice that HostToNetworkOrder() returns the value in the same datatype as the original value. The byte values within the datatype are now placed in network byte order, ready for sending out on the network. Unfortunately, if the network byte order is not in the same binary representation as the local host, those data values will not be the same. For example, the value assigned to the test1 variable is 45. When test1 is converted to network byte order, it is assigned to the variable test1b. Now, the variable test1b is still a valid short integer variable, but has the value 11520. This is obviously not the same as the original value of 45. When test1b is transmitted across the network, it must be converted back to the local host order to get the original value of 45.

Warning 

Remember that when data is converted to network byte order, it may not have the same value as the original data value. The network byte order is only used for transporting the data across the network.

Before the destination host can use the data received, it must convert the data to the local binary datatype representation of the host.

Reading Data in Network Byte Order

After the integer values are converted to network byte order and sent to the remote system, they must be converted back to the host byte order representation so their original values can be used in the program. The NetworkToHostOrder() method of the IPAddress class converts data received in network byte order back to the appropriate byte order of the system running the program. Similar to HostToNetworkOrder(), the NetworkToHostOrder() method converts an integer value in network byte order to an integer value in the local host’s byte order. It is possible that both orders are the same and no conversion will be necessary, but to be on the safe side, it is always best to include this method.

Sample Programs

Listing 7.9 is the NetworkOrderClient.cs program, which demonstrates how to use the HostToNetworkOrder() and NetworkToHostOrder() methods to transmit data across the network.

Listing 7.9: The NetworkOrderClient.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class NetworkOrderClient
{
  public static void Main()
  {
   byte[] data = new byte[1024];
   string stringData;
   TcpClient server;
   try
   {
     server = new TcpClient("127.0.0.1", 9050);
   } catch (SocketException)
   {
     Console.WriteLine("Unable to connect to server");
     return;
   }
   NetworkStream ns = server.GetStream();
   int recv = ns.Read(data, 0, data.Length);
   stringData = Encoding.ASCII.GetString(data, 0, recv);
   Console.WriteLine(stringData);
   short test1 = 45;
   int test2 = 314159;
   long test3 = -123456789033452;
   short test1b = IPAddress.HostToNetworkOrder(test1);
   data = BitConverter.GetBytes(test1b);
   Console.WriteLine("sending test1 = {0}", test1);
   ns.Write(data, 0, data.Length);
   ns.Flush();
   int test2b = IPAddress.HostToNetworkOrder(test2);
   data = BitConverter.GetBytes(test2b);
   Console.WriteLine("sending test2 = {0}", test2);
   ns.Write(data, 0, data.Length);
   ns.Flush();
   long test3b = IPAddress.HostToNetworkOrder(test3);
   data = BitConverter.GetBytes(test3b);
   Console.WriteLine("sending test3 = {0}", test3);
   ns.Write(data, 0, data.Length);
   ns.Flush();
   ns.Close();
   server.Close();
  }
}
End example

The NetworkOrderClient program uses the TcpClient class to create a TCP connection to a server. It then creates a NetworkStream object to send and receive data with the remote server. Once the connection is established, it sets values for three integer datatypes and sends them in network byte order to the server.

The NetworkOrderSrvr.cs program, shown in Listing 7.10, is used to receive the data and convert it back to host byte order.

Listing 7.10: The NetworkOrderSrvr.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class NetworkOrderSrvr
{
  public static void Main()
  {
   int recv;
   byte[] data = new byte[1024];
   TcpListener server = new TcpListener(9050);
   server.Start();
   Console.WriteLine("waiting for a client...");
   TcpClient client = server.AcceptTcpClient();
   NetworkStream ns = client.GetStream();
   string welcome = "Welcome to my test server";
   data = Encoding.ASCII.GetBytes(welcome);
   ns.Write(data, 0, data.Length);
   ns.Flush();
   data = new byte[2];
   recv = ns.Read(data, 0, data.Length);
   short test1t = BitConverter.ToInt16(data, 0);
   short test1 = IPAddress.NetworkToHostOrder(test1t);
   Console.WriteLine("received test1 = {0}", test1);
   data = new byte[4];
   recv = ns.Read(data, 0, data.Length);
   int test2t = BitConverter.ToInt32(data, 0);
   int test2 = IPAddress.NetworkToHostOrder(test2t);
   Console.WriteLine("received test2 = {0}", test2);
   data = new byte[8];
   recv = ns.Read(data, 0, data.Length);
   long test3t = BitConverter.ToInt64(data, 0);
   long test3 = IPAddress.NetworkToHostOrder(test3t);
   Console.WriteLine("received test3 = {0}", test3);
   ns.Close();
   client.Close();
   server.Stop();
  }
}
End example

The NetworkOrderSrvr program uses the TcpListener class to listen on TCP port 9050 for incoming connection attempts. When a connection attempt is received, the program creates a TcpClient object. It then uses the GetStream() method to create a NetworkStream object for sending and receiving data from the remote host.

After the network connection is established and a welcome banner message is sent, the NetworkOrderSrvr expects to receive three integer datatypes from the remote host. Keep in mind that TCP is a stream-oriented communications channel and thus there is no guarantee that the three datatypes will be sent in three separate messages. To compensate for this, the NetworkOrderSrvr reads a set number of bytes from the NetworkStream for each datatype. This ensures that no matter how the data is received, it will be read from the TCP buffer in the right sizes.

Once the data is read from the TCP buffer, it is converted to the appropriate binary datatype using the BitConverter methods:

data = new byte[2];
recv = ns.Read(data, 0, data.Length);
short test1t = BitConverter.ToInt16(data, 0);

Remember, because you are not sure if the network byte order is correct for the system, the converted value should not be directly used; it is just temporary. To create the correct value, you must convert it to the host byte order:

short test1 = IPAddress.NetworkToHostOrder(test1t);

This ensures that the data value is in the correct binary representation for the host system on which the program is running.

The output from the NetworkOrderClient and NetworkOrderSrvr programs should be similar. The output from the client program should look like this:

C:\>NetworkOrderClient
Welcome to my test server
sending test1 = 45
sending test2 = 314159
sending test3 = -123456789033452
C:\>

And the output from the NetworkOrderSrvr program should be as follows:

C:\>NetworkOrderSrvr
waiting for a client...
received test1 = 45
received test2 = 314159
received test3 = -123456789033452
C:\>

The binary data values shown from the server program should be the same as those sent from the client program, showing that the data bytes were converted, sent, and reconverted to the proper order.

As usual, if you are running this program across the network, you can use the WinDump or Analyzer programs to watch the individual packets. By comparing the packet data bytes in network byte order from the BinaryNetworkOrder program against the Analyzer trace packets, you can see what data values are sent in which packets.

In my sample trace, the TCP data bytes show that the network byte order values test2 (00 04 CB 2F) and test3 (FF FF 8F B7 79 F1 CE 14) were sent in the same TCP packet. (Compare these values to those in the Listing 7.8 Binary Network ByteOrder output.) When these values are received, they are converted back to host byte order to retrieve the original values.

Moving Complex Objects

Now that you can send individual binary data values to a remote host, you may be wondering about the next step: sending groups of values across the network to a remote host. This section describes how to send groups of data as a single element to a remote device and how to decode the data back and retrieve the proper data values on the other end.

Creating a Collective Data Class

One common way to move groups of multiple data values between systems on a network is to create a class that contains all the data, along with a specific method for converting the data into a byte array. The basic class contains variables for the data elements used in the communication. For example:

class Employee
{
  public int EmployeeID;
  public string LastName;
  public string FirstName;
  public int YearsService;
  public double Salary;
  public int LastNameSize;
  public int FirstNameSize;
  public int size;
}

Here, the class Employee can be considered similar to a record, with the variables representing the fields in the record. Each instance of the class represents a record in the database.

Because the two string elements can have variable lengths, you should include additional elements to define the size of those elements. This is comparable to the variable text field methods shown in Chapter 5.

Eventually, a data element is created to hold the size of the total byte representation of the class instance—again a necessity because the class instance itself will be a variable length.

The GetBytes() Method

With the “collective” data class in place, you create a GetBytes() method for the class to help in converting all of the elements into a single byte array, suitable for sending out on the network. It looks like this:

public byte[] GetBytes()
{
  byte[] data = new byte[1024];
  int place = 0;
  Buffer.BlockCopy(BitConverter.GetBytes(EmployeeID), 0, data, place, 4);
  place += 4;
  Buffer.BlockCopy(BitConverter.GetBytes(LastName.Length), 0, data, place, 4);
  place += 4;
  Buffer.BlockCopy(Encoding.ASCII.GetBytes(LastName), 0,
    data, place, LastName.Length);
  place += LastName.Length;
  Buffer.BlockCopy(BitConverter.GetBytes(FirstName.Length),
    0, data, place, 4);
  place += 4;
  Buffer.BlockCopy(Encoding.ASCII.GetBytes(FirstName), 0,
    data, place, FirstName.Length);
  place += FirstName.Length;
  Buffer.BlockCopy(BitConverter.GetBytes(YearsService), 0, data, place, 4);
  place += 4;
  Buffer.BlockCopy(BitConverter.GetBytes(Salary), 0, data, place, 8);
  place += 8;
  size = place;
  return data;
}

The GetBytes() method performs three functions:

  • It converts each element of the class to a byte array.

  • It places all of the individual byte arrays into a single-byte array.

  • It calculates the total size of the byte array.

By now you are familiar with the BitConverter methods to convert the various binary datatypes to byte arrays. What you may not be familiar with is the Buffer class’s BlockCopy()method. The BlockCopy() method allows you to copy an entire byte array into a location within another byte array. Here is the format of this method:

BlockCopy(byte[] array1, int start, byte[] array2, int offset, int size)

The array1 parameter is the array to copy to array2. The starting location of the copy within array1 is always the first byte. The offset within the second array changes after each item is added to the array. Each short integer value added takes up 2 bytes, and the double floating-point value takes up 8 bytes.

The unknowns are the two variable-length string values. This is where string-size elements from the class come in handy. Because you know how long the string instance is, you can use that value when placing it in the byte array, as illustrated in Figure.

Click To expand
Figure: Placing data values within a byte array

The idea is to drop each byte array into its proper place in the data array. Care must be taken when calculating the location to ensure that each value is placed in the correct order in the byte array. Once the byte array is completed, it is ready to be sent across the network.

The Constructors

There should be two constructor formats for the data class. One is the default constructor, used to manually enter values into the data elements. The other reads a byte array produced from the GetBytes() method and converts it back into a class instance:

public Employee()
{
}
public Employee(byte[] data)
{
  int place = 0;
  EmployeeID = BitConverter.ToInt32(data, place);
  place += 4;
  LastNameSize = BitConverter.ToInt32(data, place);
  place += 4;
  LastName = Encoding.ASCII.GetString(data, place, LastNameSize);
  place = place + LastNameSize;
  FirstNameSize = BitConverter.ToInt32(data, place);
  place += 4;
  FirstName = Encoding.ASCII.GetString(data, place, FirstNameSize);
  place += FirstNameSize;
  YearsService = BitConverter.ToInt32(data, place);
  place += 4;
  Salary = BitConverter.ToDouble(data, place);
  }

The default constructor allows you to manually specify each of the data elements for a class instance, storing values in the instance. The second constructor format walks through the byte array and extracts each data element value. The variable-length string fields require the size parameters to help determine how many bytes are allocated for each string. Because the sizes were embedded into the byte array by the GetBytes() method, it is important to read each one and extract the proper number of bytes for the string.

The Whole Class Program

Putting all of the elements and methods together produces the Employee class file, Employee.cs, shown in Listing 7.11.

Listing 7.11: The Employee.cs program
Start example
using System;
using System.Text;
class Employee
{
  public int EmployeeID;
  private int LastNameSize;
  public string LastName;
  private int FirstNameSize;
  public string FirstName;
  public int YearsService;
  public double Salary;
  public int size;
  public Employee()
  {
  }
  public Employee(byte[] data)
  {
 
   int place = 0;
   EmployeeID = BitConverter.ToInt32(data, place);
   place += 4;
   LastNameSize = BitConverter.ToInt32(data, place);
   place += 4;
   LastName = Encoding.ASCII.GetString(data, place, LastNameSize);
   place = place + LastNameSize;
   FirstNameSize = BitConverter.ToInt32(data, place);
   place += 4;
   FirstName = Encoding.ASCII.GetString(data, place, FirstNameSize);
   place += FirstNameSize;
   YearsService = BitConverter.ToInt32(data, place);
   place += 4;
   Salary = BitConverter.ToDouble(data, place);
  }
  public byte[] GetBytes()
  {
   byte[] data = new byte[1024];
   int place = 0;
   Buffer.BlockCopy(BitConverter.GetBytes(EmployeeID), 0, data, place, 4);
   place += 4;
   Buffer.BlockCopy(BitConverter.GetBytes(
      LastName.Length), 0, data, place, 4);
   place += 4;
   Buffer.BlockCopy(Encoding.ASCII.GetBytes(
     LastName), 0, data, place, LastName.Length);
   place += LastName.Length;
   Buffer.BlockCopy(BitConverter.GetBytes(
      FirstName.Length), 0, data, place, 4);
   place += 4;
   Buffer.BlockCopy(Encoding.ASCII.GetBytes(
      FirstName), 0, data, place, FirstName.Length);
   place += FirstName.Length;
   Buffer.BlockCopy(BitConverter.GetBytes(YearsService), 0, data, place, 4);
   place += 4;
   Buffer.BlockCopy(BitConverter.GetBytes(Salary), 0, data, place, 8);
   place += 8;
   size = place;
   return data;
  }
}
End example

Because it is just a data container and can’t run by itself, the Employee.cs program does not contain a Main() method. You can’t compile the Employee.cs program by itself with the csc command. Instead, it must be compiled along with whatever programs use the Employee class.

Using Data Classes

Once the Employee.cs program is created, it’s a snap to put it to work in client and server programs. Listing 7.12 shows a sample TCP client program that uses the Employee class to send employee information to the server.

Listing 7.12: The EmployeeClient.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
class EmployeeClient
{
  public static void Main()
  {
   Employee emp1 = new Employee();
   Employee emp2 = new Employee();
   TcpClient client;
   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;
   try
   {
     client = new TcpClient("127.0.0.1", 9050);
   } catch (SocketException)
   {
     Console.WriteLine("Unable to connect to server");
     return;
   }
   NetworkStream ns = client.GetStream();
   byte[] data = emp1.GetBytes();
   int size = emp1.size;
   byte[] packsize = new byte[2];
   Console.WriteLine("packet size = {0}", size);
   packsize = BitConverter.GetBytes(size);
   ns.Write(packsize, 0, 2);
   ns.Write(data, 0, size);
   ns.Flush();
   data = emp2.GetBytes();
   size = emp2.size;
   packsize = new byte[2];
   Console.WriteLine("packet size = {0}", size);
   packsize = BitConverter.GetBytes(size);
   ns.Write(packsize, 0, 2);
   ns.Write(data, 0, size);
   ns.Flush();
 
   ns.Close();
   client.Close();  
  }
}
End example

After two instances of the Employee class are created, data is entered into the data elements. The GetByte() method then converts the data into a byte array to send to the server. Before the byte array is sent, the size of the array is sent so the server knows how many bytes of data to read to complete the data package.

Similarly, the EmployeeSrvr.cs program, Listing 7.13, performs the server function using the Employee class.

Listing 7.13: The EmployeeSrvr.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
class EmployeeSrvr
{
  public static void Main()
  {
   byte[] data = new byte[1024];
   TcpListener server = new TcpListener(9050);
   server.Start();
   TcpClient client = server.AcceptTcpClient();
   NetworkStream ns = client.GetStream();
   byte[] size = new byte[2];
   int recv = ns.Read(size, 0, 2);
   int packsize = BitConverter.ToInt16(size, 0);
   Console.WriteLine("packet size = {0}", packsize);
   recv = ns.Read(data, 0, packsize);
   Employee emp1 = new Employee(data);
   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);
   size = new byte[2];
   recv = ns.Read(size, 0, 2);
   packsize = BitConverter.ToInt16(size, 0);
   data = new byte[packsize];
   Console.WriteLine("packet size = {0}", packsize);
   recv = ns.Read(data, 0, packsize);
   Employee emp2 = new Employee(data);
   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);
   ns.Close();
   client.Close();
   server.Stop();
  }
}
End example

The EmployeeSrvr program reads 2 bytes from the network, then converts them into an integer size value. The size value represents how many bytes to read for the data package. Once the data package is read, it can be converted to an Employee class instance using the Employee constructor.

To compile both the EmployeeClient.cs and EmployeeSrvr.cs programs, you must also include the Employee.cs file:

csc EmployeeClient.cs Employee.cs
csc EmployeeSrvr.cs Employee.cs

After compiling the two programs, you can test them out by starting EmployeeSrvr in a command-prompt window and EmployeeClient program in either a separate command-prompt window or on a separate network client. The output from the EmployeeSrvr program should look like this:

C:\>EmployeeSrvr
packet size = 30
emp1.EmployeeID = 1
emp1.LastName = Blum
emp1.FirstName = Katie Jane
emp1.YearsService = 12
emp1.Salary = 35000.5
packet size = 27
emp2.EmployeeID = 2
emp2.LastName = Blum
emp2.FirstName = Jessica
emp2.YearsService = 9
emp2.Salary = 23700.3
C:\>

The client program successfully transferred the employee data for each instance to the server program.

Note 

You may have noticed that the Employee examples did not use the network byte order to transfer the data values. These programs will only work on like machines on the network. You can experiment with converting the byte values to network byte order in order to make the examples run on any platform on the network.

Note 

Note: What we just wrote is a serializer—it serializes a class into a binary stream. .NET offers its own serializers, and we’ll cover them in Chapter 16, “Using .NET Remoting.”

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