Introduction
Recently I coded a prototype (for the Cebit 2002) with the release version
of Visual Studio.NET. The program had to communicate with an existing server
(coded in C++) via a proprietary protocol based on UDP. While doing so, I
experienced some strange errors, which finally led me to a major bug in the
UdpClient implementation.
Description
When more than one UDP packet is enqueued on a
socket, UdpClient.Receive will receive packets with the
wrong size. The size
of the packet is the sum of the sizes of all packets waiting for receive.
The received packet will contain correctly the data of the first waiting
packet plus so many null bytes as data is available. If you are working
with binary data, you have no possibilty to determine the real size of the
received packet.
Reproduction
Simply download and execute the UdpClientBug
example. It will send four packets to a listening socket, and then receive
those packets. You should see the following output:
UdpClient.Receive Bug
Sending 'One' (3 bytes)
Sending 'Two' (3 bytes)
Sending 'Three' (5 bytes)
Sending 'Four' (4 bytes)
Received 15 bytes, s = 'One ', s.Length = 15
Received 12 bytes, s = 'Two ', s.Length = 12
Received 9 bytes, s = 'Three ', s.Length = 9
Received 4 bytes, s = 'Four', s.Length = 4
The first packet received has a size of 15 bytes, which is the sum of all send
bytes (3+3+5+4 = 15). When reconverted to strings the received packets are
unequal to packets which were send.
Explanation
Digging deeper, I have found an explanation what
has gone wrong. The UdpClient.Receive() method returns an array of bytes. The
implementation of UdpClient has to create this array with an explicit size
before it can call Socket.Receive(). I guess that the implementation uses the
property Socket.Available to determine the size to reserve for the array.
To cite the documentation, "If you are using a message-oriented Socket type
such as Dgram (UDP) the available data is the first message in the input
queue.". This is wrong! Socket.Available always
returns the number of bytes of all data waiting for receive. To prove
this, I
have written a second method DemonstrateSocketAvailableBug. The difference
is, that now I am using a basic Socket to query the Socket.Available property
and receive the data. It produces the following output:
Socket.Available Bug
Sending 'One' (3 bytes)
Sending 'Two' (3 bytes)
Sending 'Three' (5 bytes)
Sending 'Four' (4 bytes)
Available: 15 bytes
Received: 3 bytes, s = 'One', s.Length = 3
I guess that Socket.Available uses ioctlsocket with the FIONREAD
option. A quote from the winsock documentation:
FIONREAD returns the
amount of data that can be read in a single call to the recv function,
which may not be the same as the total amount of data queued on the socket.
If s
is message oriented (for example, type SOCK_DGRAM), FIONREAD still
returns the amount of pending data in the network buffer, however, the
amount that can actually be read in a single call to the recv function
is limited to the data size written in the send or sendto function
call.
Conclusion
In conjunction with the impossibility to set a
timeout while receiving data, the UdpClient class is pretty useless for
receiving UDP packets. You cannot determine the size of an
received packet and if a packet is lost on the network, your code will
block for ever. As loss of packets is an Udp 'feature' and you have
to deal with this situation. So do yourself a favor, don't
use UdpClient, but use the basic socket implementation instead. But
be aware, the documentation for the Socket.Available property is wrong
too.
Code Example
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace UdpClientBug
{
class Class1
{
[STAThread]
static void Main(string[] args)
{
Bugs bugs = new Bugs();
bugs.DemonstrateUdpClientBug();
bugs.DemonstrateSocketAvailableBug();
Console.WriteLine(); Console.WriteLine("Press enter to continue ...");
Console.ReadLine();
}
}
class Bugs
{
private IPEndPoint listenerIP = new IPEndPoint(IPAddress.Loopback, 4201);
public void DemonstrateUdpClientBug()
{
Console.WriteLine(); Console.WriteLine("UdpClient.Receive Bug");
UdpClient listener = new UdpClient(listenerIP); UdpClient sender = new UdpClient();
sender.Connect(listenerIP);
Send(sender, "One");
Send(sender, "Two");
Send(sender, "Three");
Send(sender, "Four");
Receive(listener);
Receive(listener);
Receive(listener);
Receive(listener);
listener.Close();
sender.Close();
}
void Send(UdpClient sender, string s)
{
byte[] dgram = Encoding.ASCII.GetBytes(s);
Console.WriteLine("Sending '" + s + "' (" + dgram.Length.ToString()
+ " bytes)");
sender.Send(dgram, dgram.Length);
}
void Receive(UdpClient listener)
{
IPEndPoint from = new IPEndPoint(IPAddress.Any, 0);
byte[] dgram = listener.Receive(ref from);
string s = Encoding.ASCII.GetString(dgram, 0, dgram.Length);
Console.WriteLine
(
"Received {0} bytes, s = '{1}', s.Length = {2}",
dgram.Length, s, s.Length
);
}
public void DemonstrateSocketAvailableBug()
{
Console.WriteLine(); Console.WriteLine("Socket.Available Bug");
Socket listener = new Socket(AddressFamily.InterNetwork,
SocketType.Dgram, ProtocolType.IP);
listener.Bind(listenerIP);
UdpClient sender = new UdpClient();
sender.Connect(listenerIP);
Send(sender, "One");
Send(sender, "Two");
Send(sender, "Three");
Send(sender, "Four");
Console.WriteLine("Available: {0} bytes", listener.Available);
byte[] dgram = new byte[50];
int nReceived = listener.Receive(dgram);
string s = Encoding.ASCII.GetString(dgram, 0, nReceived);
Console.WriteLine("Received: {0} bytes, s = '{1}',
s.Length = {2}", nReceived, s, s.Length);
}
}
}