< VB .NET Multicast Program Example | Main | C++ & C# TcpListener Examples >

 


 

Chapter 8 Part 27:

Client (and Server) Sockets Communication

 

 

What do we have in this chapter 8 Part 27?

  1. Raw Sockets

  2. Sending with Raw Sockets

  3. Receiving with Raw Sockets

  4. Sample: IPv4 Raw Socket and ICMP

  5. Using the HeaderIncluded Option

  6. Computing Protocol Checksums

  7. Simplified Socket Classes

  8. TcpClient

  9. TcpListener

  10. UdpClient

 

 

Raw Sockets

 

Another very useful socket type is SocketType.Raw, which is used by applications that need to build custom protocol headers encapsulated in the given transport protocol header, which in our case can either be an IPv4 or IPv6 header. A raw socket is created by creating a Socket object and specifying either IPv4 or IPv6, SocketType.Raw, and the ProtocolType of the protocol being built by the application. For example, if ProtocolType.Icmp is specified, any data sent on that socket is expected to have a valid ICMP protocol header followed by the data payload. Both the IPv4 and IPv6 headers contain an 8-bit value that identifies the next upper-layer protocol header that’s set by the ProtocolType parameter to the Socket constructor. The ICMP and ICMPv6 protocols are defined in RFCs 792 and 2463, respectively.

After the raw socket is created, it must be bound to an explicit local interface before any operation can be performed on it. Once bound, it can be used to send and receive protocol-specific data, which we’ll cover in the following sections.

 

Sending with Raw Sockets

 

To send data, the protocol header must be built correctly. A protocol header is defined by a series of bit fields that have distinct values with specific meanings. Figure 8-2 shows the ICMP header for the IPv4 protocol. Each field must be set to valid values for the remote host to respond. For example, to issue an ICMP echo request (otherwise known as a ping), the Type field is set to 8 while the Code field is set to 0. Also, the Checksum field must be correctly calculated. Protocol headers typically require the 16-bit one’s complement checksum of the protocol header and any payload. When a remote machine receives this packet, it sends back an ICMP echo response packet (Type and Code both 0).

 

The ICMP protocol header illustration

 

Figure 8-2: ICMP protocol header

 

When building the protocol header, it’s important to remember that all data must be converted to network byte order. Additionally, most values used by the Socket class that are related to the protocol (such as the IP address and port) are also subject to this rule. However, this conversion is hidden from the application because the constructors for these various object (such as IPAddress and IPEndPoint) implicitly perform the necessary byte order conversion when getting and setting the values, that is, when a value is assigned into the object, it’s converted to network byte order, and when the value is retrieved, it’s converted into host byte order.

An application can perform byte order conversion by using the static methods IPAddress.NetworkToHostOrder and IPAddress.HostToNetworkOrder. Each of these methods is overloaded to perform conversion on different basic types. The inputs supported are short, int, and long (or Short, Integer, and Long in Visual Basic .NET). Take note that the unsigned variables passed into a byte order conversion function should be cast as the signed equivalent and cast back to the original unsigned type. Otherwise, the method mangles the resulting value. Additionally, the Unchecked operator may be used in C#.

 

Receiving with Raw Sockets

 

When receiving data on a raw socket, there are a few behavioral differences between IPv4 and IPv6 raw sockets. With IPv4, data received on the socket will include the IPv4 header, all subsequent headers, and the packet payload. With IPv6, data received on the raw socket will start with the protocol header following the IPv6 header; that is, a raw IPv6 socket will never receive the IPv6 header. Also, don’t forget that the protocol headers returned from the receive call will be in network byte order and will require manual translation to host byte order when decoding the header.

 

Sample: IPv4 Raw Socket and ICMP

 

Let’s look at how to create a raw socket and build the protocol header (the full examples have been demonstrated in the previous sections). In this example, we’ll create an IPv4/ICMP echo request packet, which is the same thing ping.exe does to determine if a remote computer is alive and on the network. The associated code sample also performs IPv6/ICMPv6 echo requests.

 

1.      Create a raw IPv4 socket and specify the ICMP protocol, as shown here:

 

C#

 

Socket  rawSocket = new Socket(

    AddressFamily.InterNetwork,

    SocketType.Raw,

    ProtocolType.Icmp

    );

 

Visual Basic .NET

 

Dim rawSocket As Socket = New Socket( _

    AddressFamily.InterNetwork, _

    SocketType.Raw, _

    ProtocolType.Icmp _

    )

 

2.      Bind the raw socket to an explicit local interface. In this example, the interface is 10.10.10.1. Because the ICMP protocol does not contain port fields, the IPEndPoint is created by specifying zero for the port.

 

C#

 

IPEndPoint  bindEndPoint = new IPEndPoint(

    IPAddress.Parse("10.10.10.1"),

    0

    );

rawSocket.Bind( bindEndPoint );

 

Visual Basic .NET

 

Dim bindEndPoint As IPEndPoint = New IPEndPoint( _

    IPAddress.Parse("10.10.10.1"), _

    0 _

    )

rawSocket.Bind(bindEndPoint)

 

3.      Build the ICMP and echo request header. A byte array is allocated to contain the 4-byte ICMP base header plus another 4 bytes for the echo request header followed by a 32-byte payload. The Checksum field is initially set to 0. For the Identifier field, we use the process ID, which is used to verify that any responses received are responses to our requests (because the echo reply will use the same identifier).

 

C#

 

byte [ ]  icmpData = new byte [ 40 ], byteArray;

int      offset = 0;

short    icmpIdentifier = 0,

icmpSequence = IPAddress.HostToNetworkOrder( (short) 1 );

System.Diagnostics.Process proc = System.Diagnostics.Process.GetCurrentProcess();

 

icmpIdentifier = IPAddress.HostToNetworkOrder( (short) proc.Id );

icmpData[ offset++ ] = (byte) 8; // ICMP echo request type

icmpData[ offset++ ] = (byte) 0; // ICMP echo request code

icmpData[ offset++ ] = 0;        // Checksum set to zero

icmpData[ offset++ ] = 0;

byteArray = BitConverter.GetBytes(IPAddress.HostToNetworkOrder( icmpIdentifier ) );

Array.Copy(byteArray, 0, icmpData, offset, byteArray.Length);

 

offset += byteArray.Length;

 

byteArray = BitConverter.GetBytes(IPAddress.HostToNetworkOrder( icmpSequence ) );

Array.Copy(byteArray, 0, icmpData, offset, byteArray.Length);

 

for( ; offset < icmpData.Length; offset++)

    icmpData[ offset ] = (byte) 'e';

 

Visual Basic .NET

 

Dim icmpData(40) As Byte

Dim byteArray() As Byte

Dim offset As Integer = 0

Dim icmpIdentifier As Short = 0

Dim icmpSequence As Short = IPAddress.HostToNetworkOrder(CShort(1))

Dim proc As System.Diagnostics.Process = System.Diagnostics.Process.GetCurrentPRocess()

 

icmpIdentifier = IPAddress.HostToNetworkOrder(CShort(proc.Id))

icmpData(offset) = 8     ' ICMP echo request type

offset = offset + 1

icmpData(offset) = 0     ' ICMP echo request code

offset = offset + 1

icmpData(offset) = 0     ' Checksum set to zero

offset = offset + 1

icmpData(offset) = 0

offset = offset + 1

byteArray = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(icmpIdentifier))

Array.Copy(byteArray, 0, icmpData, offset, byteArray.Length)

 

offset = offset + byteArray.Length

 

byteArray = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(icmpSequence))

Array.Copy(byteArray, 0, icmpData, offset, byteArray.Length)

offset = offset + byteArray.Length

 

4.      Compute the checksum. The following method computes the 16-bit one’s complement checksum on a byte array. Assume that this static method is a member of the class ProtocolHeader.

 

C#

 

static public ushort ComputeChecksum( byte [] payLoad )

{

    uint    xsum = 0;

    ushort  shortval = 0, hiword = 0, loword = 0;

 

    // Sum up the 16-bits

    for(int i=0; i < payLoad.Length / 2; i++)

    {

        hiword = (ushort) (((ushort) payLoad[i*2]) << 8);

        loword = (ushort) payLoad[ ( i*2 ) + 1 ];

        shortval = (ushort) (hiword | loword);

        xsum = xsum + (uint)shortval;

    }

    // Pad the last byte if necessary

    if ( ( payLoad.Length % 2 ) != 0 )

    {

        xsum += (uint) payLoad[ payLoad.Length-1 ];

    }

 

    xsum =  ( ( xsum >> 16 ) + ( xsum & 0xFFFF ) );

    xsum =  ( xsum + ( xsum >> 16 ) );

    shortval = (ushort) (~xsum);

 

    return shortval;

}

 

 

5.      Call the checksum routine, and assign the value into the ICMP packet.

 

C#

 

ushort  checksumValue;

// Compute the checksum

checksumValue = ProtocolHeader.ComputeChecksum( icmpData );

// Assign value back into ICMP packet

byteArray = BitConverter.GetBytes(IPAddress.HostToNetworkOrder( checksumValue ) );

Array.Copy( byteArray, 0, icmpData, 3, byteArray.Length );

 

Visual Basic .NET

 

Dim checksumValue As UInt16

' Compute the checksum

checksumValue = ProtocolHeader.ComputeChecksum(icmpData)

' Assign value back into the ICMP packet

byteArray = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(checksumValue))

Array.Copy(byteArray, 0, icmpData, 0, byteArray.Length)

 

6.      Send the packet with a simple call to SendTo() with the icmpData buffer built earlier that contains the ICMP header and payload.

 

C#

 

IPEndPoint  pingDestination = new IPEndPoint(IPAddress.Parse("10.10.10.1"), 0);

rawSocket.SendTo( icmpData, pingDestination );

 

Visual Basic .NET

 

Dim pingDestination As IPEndPoint = New IPEndPoint(IPAddress.Parse("10.10.10.1"), 0)

rawSocket.SendTo(icmpData, pingDestination)

 

7.      Wait for a response. After the ICMP echo request is sent, the response needs to be received, which is a simple call to ReceiveFrom().

 

Using the HeaderIncluded Option

 

Raw sockets are ideal in cases where the custom protocol being created is encapsulated in IPv4 or IPv6, but what if an application needs to modify the fields of the IPv4 or IPv6 header itself? In this situation, the SocketOptionName.HeaderIncluded option is useful. When this option is set on a raw socket, the application must build the IPv4 or IPv6 header itself and pass it along with all encapsulated headers and data payload to a send method (Send() or SendTo()). The HeaderIncluded option is a simple Boolean value that must be set on the socket before binding.

In the ping example, if the option was set, an IPv4 header would need to have been built. The byte array passed to SendTo() would be 28 bytes plus any payload, and would be composed of the following entries:

 

1.      20-byte IPv4 header.

2.      8-byte ICMP echo request header.

3.      n bytes for the ICMP echo payload.

 

Setting the HeaderIncluded option does not affect how data is received on the raw socket. For IPv4 raw sockets, the IPv4 header will still be part of any data received on the socket, and for IPv6, the IPv6 header will not be returned with received data (as is the case for raw sockets when HeaderIncluded isn’t set).

 

Computing Protocol Checksums

 

The ping sample earlier illustrated the method that computes a checksum over an arbitrary byte array, which is a fairly simple algorithm. However, depending on the protocol, the checksum might need to be calculated over different parts of the protocol packet. Many protocols, such as IPv4 and ICMP, require the checksum to be calculated over its own header and any payload.

On the other hand, protocols such as UDP and ICMPv6 require what is known as a pseudoheader checksum. This is a checksum that’s calculated over parts of multiple headers. Figure 8-3 shows the pseudoheader checksum for IPv4/UDP.

 

The IPv4/UDP pseudoheader checksum fields

 

Figure 8-3: IPv4/UDP pseudoheader checksum fields

 

You’ll notice that checksum is calculated over parts of the encompassing IPv4 header as well as parts of the UDP header followed by whatever UDP payload is present. If an application was using raw sockets with the HeaderIncluded option set to build IPv4/UDP packets, the UDP header would be built and the checksum would be calculated according to the pseudoheader in Figure 8-3. Once it is calculated, the IPv4 checksum is calculated over the IPv4 header, the UDP header (with its checksum field already calculated), and the payload.

The IPv6 protocol is a bit different from IPv4. The IPv6 protocol header does not contain a checksum field. It relies on the upper-level protocols such as UDP or ICMPv6 to perform any checksum calculations. Therefore, IPv6 also states that these encapsulated protocols must calculate checksums over a pseudoheader that includes portions of the IPv6 protocol. Figure 8-4 shows the IPv6/UDP pseudoheader checksum.

 

The IPv6/UDP pseudoheader checksum fields

 

Figure 8-4: IPv6/UDP pseudoheader checksum fields

 

Simplified Socket Classes

 

The .NET Framework offers several simplified socket-oriented classes for the TCP and UDP protocols: TcpClient, TcpListener, and UdpClient. Typically, these classes offer multiple constructors that take most of the common parameters used to set up the socket and thereby remove a few steps from the creation process when compared to using the Socket class. Each class exposes the underlying Socket object as a protected property that can be used to perform the more powerful actions that the Socket class offers, which is only useful for derived classes. Note that each of these simplified classes offers constructors that have become obsolete because they were designed with IPv4 in mind. However, they have been extended and brought up-to-date in version 1.1 of the .NET Framework to support IPv6 as well. The next sections will look at each of these three classes in more detail.

 

TcpClient

 

The TcpClient class is a simplified interface for a TCP socket that connects to a server. This connection is typically accomplished by calling the TcpClient constructor that takes the string server name and port to establish a connection to. If a host name is specified in the constructor, only the first DNS resolved name is attempted. If that connection fails, an exception is thrown. This behavior is not desirable because, for example, the DNS server might return an IPv4 address but the server might be listening on IPv6. The TcpClient class also offers a Connect() method if a connection is not established as part of the constructor. This method allows a client to be more robust by performing DNS resolution itself and attempting to connect to each address returned. Because the underlying socket object is not public, TcpClient exposes a number of properties (listed in Table 8-5) that map directly to socket options but remove the necessity of the application making the call to SetSocketOption.

 

Table 8-5: TcpClient Properties and Equivalent Socket Options

 

Property

Socket Option Equivalent

LingerState

SocketOptionName.Linger

NoDelay

SocketOptionName.NoDelay

ReceiveBufferSize

SocketOptionName.ReceiveBuffer

ReceiveTimeout

SocketOptionName.ReceiveTimeout

SendBufferSize

SocketOptionName.SendBuffer

SendTimeout

SocketOptionName.SendTimeout

 

Once a connection is established, the TcpClient class does not expose a send or receive method. Instead, it offers a NetworkStream object that’s obtained via the GetStream() method. The NetworkStream class was introduced in Chapter 2. To send and receive data, the NetworkStream object exposes Read() and Write() methods as well as asynchronous versions.

Finally, TcpClient does not offer a method to shut down the connection, but the Close() method should be called when the connection is done to release the underlying network resources. It’s questionable that the TcpClient by itself greatly simplifies a TCP client because an application running on an IPv4- or IPv6-enabled network will most likely have to implement DNS resolution to connect to a server.

 

TcpListener

 

The TcpListener class is very simple, but then again, setting up a TCP listening socket isn’t too difficult to begin with. An instance of TcpListener is created by specifying an IPEndPoint or an IPAddress and a port on which to listen for incoming TCP connection requests. Once created, the TCP server must be started by invoking the Start() method, which actually puts the server into the listening mode.

Once TcpListener is listening for client connections, there are two methods for accepting connections: AcceptSocket() and AcceptTcpClient(). The former accepts a connection and returns it as a Socket object, and the latter accepts the connection and returns it as a TcpClient object. When finished with the TcpListener, the Stop() method will close the underlying listening socket and network resources. The TcpListener class is ideal for simple applications that don’t expect a high volume of client connections because there’s no control over the listen backlog as well as no asynchronous method for accepting a connection. Chapter 9 will cover more details of accepting client connections.

 

UdpClient

 

The UdpClient class is a simplified interface for sending and receiving UDP data over IPv4 or IPv6. It differs from the Socket class in that the binding step is removed. The UdpClient object can be constructed two different ways: by specifying the local address and port to bind to or by specifying the remote destination to connect the UDP socket to. If UdpClient is to be used as a receiver, it should not be connected.

In addition to simplifying the socket creation step, UdpClient has a simplified interface for joining multicast groups. The JoinMulticastGroup method is overloaded to allow joining both IPv4 and IPv6 multicast groups. However, when joining IPv4 groups, the method does not allow you to specify the local interface to join, unlike the IPv6 version, which also takes the interface index on which the group is joined. This functionality makes the UdpClient class unsuitable for IPv4 multicasting because on a multihomed machine, the interface on which the group is actually joined can’t be determined unless the routing table is consulted.

Once the socket is set up, the Send and Receive methods can be used for sending and receiving data. If UdpClient is not connected to an endpoint, the Send() method is overloaded and offers an instance that specifies the destination host name and port. Of course, this class suffers the same problem as the TcpClient class in that the first DNS resolved name is used as the destination. This name might not correspond to the address family the receiver is on.

In truth, the UdpClient doesn’t save too many steps over a direct Socket implementation, but it’s useful when a quick-and-dirty UDP sender or receiver is required (but multicasting is not).

 

 

 


 

< VB .NET Multicast Program Example | Main | C++ & C# TcpListener Examples >