четверг, 5 апреля 2012 г.

UDP


MSDN Magazine > Issues and Downloads > 2006 > February >  UDP Delivers: Take Total Control Of Your Networ...
UDP Delivers
Take Total Control Of Your Networking With .NET And UDP
Yaniv Pessach

This article discusses:
  • The pros and cons of TCP and UDP
  • Building UDP into your applications
  • Reliability protocols and security issues
  • UDP support in Windows Communication Foundation
This article uses the following technologies:
.NET Framework, C#
Code download available at: UDP.exe (136 KB) 
Browse the Code Online 
You've probably made use of Transmission Control Protocol (TCP) in your applications at some point, whether directly or indirectly—after all, HTTP runs on top of TCP, so every Web application is built atop it. But maybe you've wondered whether its brother User Datagram Protocol (UDP) might be a better fit for your intended solution.
Most Internet traffic utilizes TCP and UDP, running on top of Internet Protocol (IP), the low-level protocol used by all traffic on the Internet. While TCP is the more familiar of the two, accounting for as much as 75 percent of Internet traffic, UDP holds second place with approximately 20 percent of sent packets. All other low-level protocols combined, including raw Internet Control Message Protocol (ICMP), account for less than 5 percent of Internet traffic.
UDP is used for such important and common tasks as DNS resolution, Simple Network Management Protocol (SNMP) network status, Windows® Internet Naming Service (WINS) NetBIOS name resolution, Trivial FTP (TFTP), Kerberos security, server discovery, digital media streaming, voice over IP (VoIP) using the International Telecommunications Union (ITU) H.323 protocol, and online gaming.
Trying to design an efficient, quick, and responsive network application is hard work, so using the right tools can help a lot. Sometimes, choosing UDP as the low-level network protocol can give you the necessary flexibility to use fewer resources, support more clients, reduce latency, increase throughput, or implement services that are not otherwise practical over TCP. These benefits aren't free, however. Writing UDP code is relatively simple, but using UDP in a useful, safe, and efficient manner requires that you implement the application protocol yourself.
In this article, I will discuss the pros and cons of using UDP in your own applications. I'll take a look at the design considerations for UDP-based applications, including the details and pitfalls of implementing those applications in C#. I will also offer a preview of exposing UDP Web services through Windows Communication Foundation.

TCP Versus UDP
Since their inception, the TCP and UDP protocols have taken very different evolutionary paths. TCP is based on connections, maintains a session, and guarantees a degree of reliability and standardized flow control. UDP provides no such features and relies upon the application layer to provide such services.
UDP allows you to send a packet, with or without a checksum, and redirects the packet (multiplexes it) to a listening application based on the port number the packet was sent to. A single packet can be sent to multiple machines at once by using multicast and broadcast transmission.
With UDP, no connection is maintained—each packet sent is treated independently. UDP makes no attempt to control transmission speeds based on congestion. If a packet is lost, the application must detect and remedy the situation. If a packet arrives out of order, you're on your own again.
UDP also does not provide the security features that you can expect to find with TCP. This includes the three-way handshake that TCP uses to ensure the validity of the claimed source address.
So what can UDP do for your application that TCP cannot? To start, UDP supports multicast—sending a single packet to multiple machines. Multicast can be used to send a message even when you do not know the IP address or DNS name of the target machines. This is used, for example, to discover the servers within a local network. Using multicast can also save bandwidth. If you want to play a presentation to 20 local machines, by using multicast the server would only need to send each packet once. UDP multicast is used by UPnP and the Windows XP My Network Places feature.
UDP is also useful when the overhead of TCP is not acceptable, especially when only one request/response is needed. Establishing a TCP connection requires exchanging at least three packets (see Figure 1). If it takes 150ms for a packet to go one way from Seattle to Sydney, and if the client (the sender) makes only one request at a time from the server (the receiver), then connection establishment and a response would take at least 600ms. On the other hand, if you sent the request over UDP, it could shave 300ms off this wait. By using UDP, you also spare the server the resources it needs to manage a TCP connection, thus enabling the server to process more requests.
Figure 1 UDP and TCP Request/Response Models 
UDP can help if your application can use a different packet-loss recovery mechanism. Since TCP guarantees that data will be processed by the server in the order it was sent, packet loss on a TCP connection prevents the processing of any later data until the lost packet is received successfully. For some applications this behavior is not acceptable, but others can proceed without the missing packet. For example, the loss of one packet in a broadcast video should not cause a delay because the application should just play the next frame.
UDP can also help if you want to control or fine-tune communication parameters such as the outgoing buffer size, minimize network traffic, or use different congestion-avoidance logic than TCP provides. Specifically, TCP assumes that packet loss indicates congestion. Therefore, when packet-loss is detected, TCP slows down the rate of outgoing information on a connection. In that respect, TCP uses a "considerate" algorithm to optimize throughput. However, this can result in slow transmission rates on networks with high packet loss. In some cases, you may want to deliberately choose a less-considerate approach to achieve higher throughput.
On the other hand, TCP has distinct advantages in simplicity of implementation. Many security threats are resolved in the TCP stack so you don't have to worry about them. Also, since UDP does not have a connection, firewalls cannot easily identify and manage UDP traffic. System administrators would rather allow an outgoing TCP connection than open their firewalls.

A Custom UDP Solution
To illustrate the issues surrounding a real-world UDP implementation, I'll start by building simple UDP client and server apps modeled on an online gaming scenario. The application will send a player's information (ID, X coordinate, and Y coordinate) from the client to a server:
public class PlayerInfo 
{
    public byte playerID, locationX, locationY;
}
Before sending any information on the wire, I first have to decide on a wire format and how to serialize and deserialize the message—that is, how to translate PlayerInfo into an array of bytes and then back. In this article, I use a couple of simple methods to do this (see Figure 2).
public int ToBuffer(byte[] buffer, int pos)
{
    int newPos = pos;
    buffer[newPos++] = playerID;
    buffer[newPos++] = locationX;
    buffer[newPos++] = locationY;
    return newPos - pos;
}

public void FromBuffer(byte[] buffer, int pos)
{
    playerID = buffer[pos++];
    locationX = buffer[pos++];
    locationY = buffer[pos++];
}
The client code should initialize a Socket, serialize the data from PlayerInfo, and send the data. The data will be sent to the IP address 127.0.0.1, which is always the local computer:
const int ProtocolPort = 3001;
Socket sendSocket = new Socket(AddressFamily.InterNetwork, 
    SocketType.Dgram, ProtocolType.Udp);
IPAddress sendTo = IPAddress.Parse("127.0.0.1");
EndPoint sendEndPoint = new IPEndPoint(sendTo, ProtocolPort);
byte[] buffer = new byte[PlayerInfo.MaxWireSize];
int bufferUsed = player.ToBuffer(buffer, 0);
sendSocket.SendTo(buffer, bufferUsed, SocketFlags.None, 
    sendEndPoint);
Production code would also add framing—in the form of headers—to the actual data. In this example, I chose to send each PlayerInfo in a separate UDP packet.
The Socket.SendTo method sends the number of bytes specified by bufferUsed to the address in sendEndPoint. Since UDP does not implement a flow control protocol and there isn't a guaranteed response for a UDP packet, SendTo does not generally need to block. This is unlike sending with TCP, which would block until the point when too much data has been sent but not yet acknowledged by the receiver. It is safe to assume that no long delays will happen when using SendTo, which is why I did not use the asynchronous version Socket.BeginSendTo.
If you have the machine name rather than an IP address, you can modify the code to resolve the machine name to an IP address:
IPAddress FirstDnsEntry(string hostName)
{
    IPHostEntry IPHost = Dns.Resolve(hostName);
    IPAddress[] addr = IPHost.AddressList;
    if (addr.Length == 0) throw new Exception("No IP addresses");
    return addr[0];
}
Note that the DNS protocol, used in Dns.Resolve, may use either UDP or TCP. When I call FirstDnsEntry with a remote machine name, I can see a UDP packet sent from my machine to port 53. Traffic analyzers, such as Netmon.exe, can help to debug your applications by showing exactly which packets are being sent and received on your machine.
To implement the server side of this communication, I use the asynchronous Socket.BeginReceiveFrom method, which will call a delegate when a UDP packet is received (see the code in Figure 3). You have to call again if you want to receive additional packets, so a good practice to follow is to always call BeginReceiveFrom from the BeginReceiveFrom delegate.
Socket receiveSocket = new Socket(AddressFamily.InterNetwork, 
    SocketType.Dgram, ProtocolType.Udp);
EndPoint bindEndPoint = new IPEndPoint(IPAddress.Any, ProtocolPort);
byte[] recBuffer = new byte[PlayerInfo.MaxWireSize];
receiveSocket.Bind(bindEndPoint);
receiveSocket.BeginReceiveFrom(recBuffer, 0, recBuffer.Length, 
    SocketFlags.None, ref bindEndPoint, 
    new AsyncCallback(MessageReceivedCallback), (object)this);

void MessageReceivedCallback(IAsyncResult result)
{
    EndPoint remoteEndPoint = new IPEndPoint(0, 0);
    try 
    {
        int bytesRead = receiveSocket.EndReceiveFrom(result, 
            ref remoteEndPoint);
        player.FromBuffer(recBuffer, 0, Math.Min(recBuffer.Length, 
            bytesRead));
        Console.WriteLine("ID:{0} X:{1} Y:{2}", player.playerID, 
            player.locationX, player.locationY);
    }
    catch (SocketException e) 
    {
        Console.WriteLine("Error: {0} {1}", e.ErrorCode, e.Message);
    }

    receiveSocket.BeginReceiveFrom(recBuffer, 0, recBuffer.Length, 
        SocketFlags.None, ref bindEndPoint, 
        new AsyncCallback(MessageReceivedCallback), (object)this);
}

Improving the Code
While responding to a UDP packet is normally up to the receiving application, a special case exists when there is no application listening on the port. The UDP stack then will send (in most configurations, and unless a firewall is configured to prevent it) an ICMP port unreachable response. This will be visible to the sending application if it is listening on the socket as a SocketException with an ErrorCode value of 10054 (connection reset by peer). Adding this code after socket initialization would catch the exception:
EndPoint bindEndPoint = new IPEndPoint(IPAddress.Any, 0);
sendSocket.Bind(bindEndPoint);
byte[] recBuffer = new byte[PlayerInfo.MaxWireSize];
sendSocket.BeginReceiveFrom(recBuffer, 0, recBuffer.Length, 
    SocketFlags.None, ref reponseEndPoint, 
    new AsyncCallback(CheckForFailuresCallback), 
    (object)this);
I react to this exception in the callback:
void CheckForFailuresCallback(IAsyncResult result)
{
    EndPoint remoteEndPoint = new IPEndPoint(0, 0);
    try 
    {
        int bytesRead = sendSocket.EndReceiveFrom(result, 
            ref remoteEndPoint);
    }
    catch (SocketException e) 
    {
        if (e.ErrorCode == 10054) serviceMissing = true;
    }
}
Incidentally, the behavior I just relied on—detecting whether any application is listening on a UDP port—can let people identify which UDP ports a machine is listening on, a process known as UDP port scanning. The same can be done for TCP, and the recommended cure is firewall configuration.
UDP is not a session-based protocol, and responses to a message, if sent at all, are treated as independent of the original message. The sender of a response must specify both the IP address and the port the response is sent to, and must use the SendTo method.
There are several ways for a sender and a receiver to agree on a return address and port. The sender IP address (as claimed on the packet) can be retrieved from the received message, along with the source port. Sending a message to the source port implies that the same socket is used by the original sender to both send the original request and listen to responses. An alternative approach is to specify the response port in the request:
bytesRead = socket.EndReceiveFrom(result, ref remoteEndPoint);
EndPoint endPointDestination = 
    new IPEndPoint(((IPEndPoint)remoteEndPoint).Address, 
    ((IPEndPoint)remoteEndPoint).Port);
socket2.SendTo(buffer, length, SocketFlags.None, endPointDestination);
The port a server is listening on can either be fixed (a well-known value used by a protocol and hardcoded in all clients and servers) or determined at run time based on which ports are in use. Since, in general, only one socket and one application can listen on a specific port number at a time, determining the port at run time allows multiple instances of a receiver to run on a machine. Run-time port selection is also useful when a server should send a message or messages back to clients.
To find a free port number, the receiver should bind to port 0. The actual port it is listening on can be found using the LocalEndPoint property, as shown here:
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 
    ProtocolType.Udp);
EndPoint endPoint = new IPEndPoint(IPAddress.Any, 0);
socket.Bind(endPoint);
listeningPort = ((IPEndPoint)(socket.LocalEndPoint)).Port;

Reliability Protocols
While the techniques I've demonstrated so far work most of the time, they don't help address the reliability issues of UDP. Packets can be lost in transit, and no corrective action will be taken. UDP will drop packets on the send side before sending them if sending happens fast enough and the internal buffers are exhausted. UDP will also drop packets upon receipt if, even momentarily, BeginReceiveFrom is not active on the socket.
The last problem is the one most easily solved. In my sample receiver code, there's a short span of time between acceptance of a packet and calling another BeginReceiveFrom. Even if this call were the first one in MessageReceivedCallback, there's a still short period when the app isn't listening. One improvement would be activating several instances of BeginReceiveFrom, each with a separate buffer to hold received packets.
Also consider the impact of the UDP packet sizes. While UDP packets can be up to 64 kilobytes (KB), the underlying IP transport can only transport packets up to the Maximum Transmission Unit (MTU) of the link. When a packet bigger than an MTU comes along, IP fragmentation occurs—the IP protocol divides the sent packet into fragments smaller than MTU, and reassembles them at the destination, but only if all fragments were received.
Not relying on IP fragmentation, but instead limiting your packets to MTU, increases the reliability of the transmission and reduces packet loss. Ethernet MTU is usually 1500 bytes, but some connections (such as dial-up and some tunneling protocols) have an MTU of 1280 bytes. Practically speaking, it is hard to discover the minimum MTU of a link, so choosing a packet size of less than 1280 (or 1500) will increase reliability.
Another concern is that sending UDP packets too fast will result in some packets being silently discarded. Earlier I pointed out that SendTo calls do not block. This can surprise developers who are accustomed to the TCP behavior of blocking the Send call until the data can be sent.
What constitutes too fast? A few packets a second are not an issue; hundreds or thousands may be an issue, depending on the computer and network.
All those factors show that even on a local network with perfect connectivity, you cannot rely on all sent UDP packets being received. And for applications using the Internet, packet loss can be a major issue, reaching more than 5 percent in some areas, with worse packet loss experienced on specific links or at specific times.
Most UDP-based applications need to implement some kind of reliability mechanism. Reliability is often seen in the form of guarantees. The three guarantees you may be familiar with in TCP are: non-duplication (each packet is received once at most); transmission (each packet is received at least once or an error is reported to the sender); and packet order (if packet A was sent before packet B on the same TCP connection, it will be read before packet B). Figure 4 shows an example of the problems caused by dropped and out-of-order packets.
Figure 4 Dropped and Out-of-Order Packets 
With UDP, each of those guarantees, if desired, needs to be implemented by the application. If a specific guarantee is not needed or can be relaxed, you have more freedom to write a more efficient protocol.
In my version of the protocol, I deal with dropped packets by resending them—but I send only the dropped packets to conserve bandwidth. To do that, the application needs to detect which packets were dropped and which arrived successfully.
The common method (which is also used by TCP) is for the receiver to send an acknowledgment (ACK) for packets received. The protocol can save bandwidth if the receiver sends a single ACK for several received packets. To facilitate this, each sent packet will normally have a sequence number or a unique identifier.
Different packet-loss handling protocols select different ACK methods and timeouts for handling a packet that did not receive an ACK. They also differ in their handling of out-of-order packets.
To help test the UDP samples presented in this article, I wrote a utility called UDPForward that can simulate packet drops and delays. UDPForward is available in the code download for this article. If you try the samples in this article with simulated packet loss, you can see how packet loss and delay affects the receiver of messages, and how the reliability protocol I implemented resolves this issue. You can also use UDPForward to test your own UDP applications and get a feel for their behavior in the presence of packet loss.

Sample Protocol Implementation
To demonstrate how an application can use UDP to tailor a specific protocol for specific needs, I will implement a sample protocol for transmitting the current location of a PlayerInfo object. Using UDP, I will choose suitable ACK behaviors as well as control and communication parameters such as buffer sizes and timeouts.
The receiver of a sequence of PlayerInfo packets normally cares only about the last known or current state. Using this property, which is applicable to other applications such as streaming audio and video, I can better handle a dropped packet. If a newer update to the same information becomes available, I can avoid the ACK and the delay until the packet is resent. This approach results in better bandwidth utilization and lower delay.
Why would you ever resend a packet if you can constantly receive updated information? For the PlayerInfo application, I'm willing to accept a momentary stale display, but not a permanent one. What if the packet notifying the movement of player 7 was lost and therefore player 7 was not moved afterwards? Without an ACK, I will never know. In addition, the sender can determine when the receiver is no longer receiving any packets. And since the sender may send a packet twice, the receiver implements a mechanism to drop duplicate and stale copies of the packet received.
The important point is that this is an application-specific decision. In some applications, including the PlayerInfoBroadcaster example shown later and some voice streaming implementations, you can choose a different tradeoff.
The protocol I chose to implement optimizes sending PlayerInfo data by only requiring one ACK per PlayerInfo send if no newer PlayerInfo data was received. The sender will send one packet per update of PlayerInfo data, together with a constantly-increasing sequence number. The sender maintains a list of "waiting for ACK" data, but the sender would only process an ACK for the last update sent (so old ACKs are discarded). The sender also occasionally checks the waiting for ACK list and resends the PlayerInfo data for any item that has been waiting too long.
The receiver needs to perform two duties: duplicate detection and ACK sending. This implementation sends ACKs immediately for new data as well as for duplicate data, but duplicates are detected (since I maintain the last sequence received for this PlayerID) and will be dropped before they are passed to the application.
The application also detects a broken connection if too many attempts to resend data fail. In the real world, the user may be informed or some error recovery could be attempted.
I first define PacketInfo:
public struct PacketInfo
{
    public int sequenceNumber;
    public long sentTicks;
    public int retryCount;
}
The sender initiates the socket and listens for ACKs (see Figure 5). The sender maintains a list of ACKs that are needed and will handle incoming ACKs by retrieving the PlayerID and sequenceNumber from the ACK packet, then removing an entry from the ACK-needed list if the sequence number matches (seeFigure 6).
static void OnReceiveAck(IAsyncResult result) 
{
    UDP_protocol uf = (UDP_protocol)result.AsyncState;
    EndPoint remoteEndPoint = new IPEndPoint(0, 0);
    int bytesRead = uf.senderSocket.EndReceiveFrom(
        result, ref remoteEndPoint);
    uf.ProcessIncomingAck(uf.bufferAck, bytesRead);
    uf.ListenForAcks();
}

void ProcessIncomingAck(byte[] packetData, int size)  
{
    ...
    // remove from 'needing ACK' list if ACK for newest info
    if (sentPacketInfo[playerID].sequenceNumber == sequenceNumber) 
    {
        Console.WriteLine("Received current ACK on {0} {1}", 
            playerID, sequenceNumber);
        ProcessPacketAck(playerID, sequenceNumber);
    }
    ...
}
void OpenSenderSocket(IPAddress ip, int port)
{
    senderEndPoint = new IPEndPoint(ip, port);
    senderSocket = new Socket(AddressFamily.InterNetwork, 
        SocketType.Dgram, ProtocolType.Udp);
    EndPoint bindEndPoint = new IPEndPoint(IPAddress.Any, 0);
    senderSocket.Bind(bindEndPoint); // any free port
    ListenForAcks();

    // start 'ack waiting' thread
    ThreadPool.QueueUserWorkItem(CheckPendingAcks, 0);
}

public void ListenForAcks()
{
    EndPoint endPoint = new IPEndPoint(0, 0);
    senderSocket.BeginReceiveFrom (bufferAck, 0, SizeAck, 
        SocketFlags.None, ref endPoint, 
        onReceiveAck, (object)this);
}

public void ListenForAcks()
{
    EndPoint endPoint = new IPEndPoint(0, 0);
    senderSocket.BeginReceiveFrom (bufferAck, 0, SizeAck, 
        SocketFlags.None, ref endPoint, 
        onReceiveAck, (object)this);
}
When sending data, the sequenceNumber and PlayerID are written to the packet, the packet is sent, and the list of ACKs that are needed is updated, including current time, which will be used to decide if a resend is needed:
void SendPlayerInfoData(byte playerID, byte locationX, byte locationY)
{
    sequenceNumber++;
    ...
    SendPlayerInfo(info, sequence, false);
}

void SendPlayerInfo(PlayerInfo info, int sequenceNumber, bool retry) 
{
    ...
    UpdatePlayerInfo(info, sequenceNumber, retry);
    senderSocket.SendTo(packetData, SizeData, SocketFlags.None, 
        senderEndPoint);
}
Occasionally, the sender will resend packets that were not ACKed. If too many resends are attempted, then the sender will decide that the connection is broken (see Figure 7).
public void CheckPendingAcks(object o) 
{
    Console.WriteLine("Checking for missing ACKs");
    byte currentPosition = 0;
    while(!closingSender) 
    {
        Thread.Sleep(10);
        ResendNextPacket(ref currentPosition, 10, 
            DateTime.Now.Ticks - TimeSpan.TicksPerSecond / 10);
    }
}

void ResendPlayerInfo(byte playerID) 
{
    SendPlayerInfo(sentPlayerInfo[playerID], 
        sentPacketInfo[playerID].sequenceNumber, true);
    sentPacketInfo[playerID].retryCount++;
    sentPacketInfo[playerID].sentTicks = DateTime.Now.Ticks;

    Console.WriteLine("Resending packet {0} {1} {2}", playerID, 
        sentPacketInfo[playerID].sequenceNumber, 
        sentPacketInfo[playerID].retryCount);
}

void ResendNextPacket(ref byte currentPosition, 
    byte maxPacketsToSend, long olderThan) 
{
    ...
    if ((sentPacketInfo[newPosition].sequenceNumber = 0;
        sentPacketInfo[newPosition].sentTicks < olderThan)
    {
        if (sentPacketInfo[newPosition].retryCount > 4) 
        {
            Console.WriteLine("Too many retries, should fail connection");
        } 
        else 
        {
            ResendPlayerInfo(newPosition);
            packetsSent++;
        }
    }
}
The receiver will just listen for packets and check for duplicate or old data (see Figure 8). An ACK here is sent immediately. More elaborate implementations could reduce the number of ACKs by sending one ACK for several packets.
void OpenReceiverSocket(int port) 
{
    EndPoint receiverEndPoint = new IPEndPoint(IPAddress.Any, port);
    receiverSocket = new Socket(AddressFamily.InterNetwork, 
        SocketType.Dgram, ProtocolType.Udp);
    receiverSocket.Bind(receiverEndPoint); 
    ListenForData();
}

public void ListenForData()
{
    EndPoint endPoint = new IPEndPoint(0, 0);
    receiverSocket.BeginReceiveFrom (bufferData, 0, SizeData,
        SocketFlags.None, ref endPoint, onReceiveData, (object)this);
}

static void OnReceiveData(IAsyncResult result) 
{
    UDP_protocol uf = (UDP_protocol)result.AsyncState;
    EndPoint remoteEndPoint = new IPEndPoint(0, 0);
    int bytesRead = uf.receiverSocket.EndReceiveFrom(result, 
        ref uf.receiverAckEndPoint);
    uf.ProcessIncomingPlayerInfo(uf.bufferData, bytesRead);
    uf.ListenForData();
}

void SendAck(byte playerID, int sequenceNumber) 
{
    ... // create packet with playerID and sequenceNumber
    receiverSocket.SendTo(packetData, SizeAck, SocketFlags.None, 
        receiverAckEndPoint);
}

void ProcessIncomingPlayerInfo(byte[] packetData, int size)
{
    ... // create packet with playerID and sequenceNumber
    if (sequenceNumber >= recvSequenceNumber[info.playerID])
        SendAck(info.playerID, sequenceNumber);

    if (sequenceNumber > recvSequenceNumber[info.playerID])
    {
        ... // update internal data and process packet
        Console.WriteLine("Received update: {0} ({1},{2})", 
            info.playerID, info.locationX, info.locationY);
    } 
    // else older packet, don't process or send ACK
}

Broadcast and Multicast
A crucial feature supported by UDP but not TCP is sending a single message to multiple destinations. UDP supports both broadcast and multicast communication. Broadcasting a packet makes it available within a subnet mask. Multicasting is subscription-based in the sense that listeners have to join a multicast group to receive messages sent to that group.
A multicast group uses an IP address in the range of 224.0.0.0 through 239.255.255.255. To listen on a multicast group, you need to indicate that you want multicast messages from a specific multicast group by setting the add membership SocketOption after calling Bind, but before calling BeginReceiveFrom:
IPAddress multicastGroup = IPAddress.Parse("239.255.255.19");
socket.Bind(...);
socket.SetSocketOption(SocketOptionLevel.IP,   
    SocketOptionName.AddMembership, 
    new MulticastOption(multicastGroup));
socket.BeginReceiveFrom(...);
You do not need to set SocketOption in order to send to a multicast group, as shown here:
const int ProtocolPort = 3001;
Socket sendSocket = new Socket(AddressFamily.InterNetwork, 
    SocketType.Dgram, ProtocolType.Udp);
EndPoint sendEndPoint = new IPEndPoint(multicastGroup, ProtocolPort);
sendSocket.SendTo(buffer, bufferUsed, SocketFlags.None, sendEndPoint);
If your application does not know the IP address or machine name of, say, a game server, that server can listen on a well-known multicast address, wait for a specific request, and respond directly to the application that made the inquiry.
In the sample I built that follows, the inquiring application will also use a dynamic port to receive the responses from zero, one, or more game servers. It also shows serialization of more complex data types. First I'll define the classes representing FindRequest and FindResult, as shown in the following:
class FindRequest {
    public int serviceID;
    public int responsePort;
    public int SerializeToPacket(byte[] packet) {...}
    public FindRequest(byte[] packet) {...}
}

class FindResult {
    public int serviceID;
    public int SerializeToPacket(byte[] packet) {...} 
    public FindResult(byte[] packet) {...}
}
To advertise itself, a server would open a socket, set multicast options on it, and process any requests, as shown in Figure 9.
public AsyncCallback onReceiveRequest = 
    new AsyncCallback(OnReceiveRequest);
IPAddress multicastGroup = IPAddress.Parse("239.255.255.19");

public void FindMe(int blockFor, int serviceID) 
{
    responseSocket = new Socket(AddressFamily.InterNetwork, 
        SocketType.Dgram, ProtocolType.Udp);
    EndPoint responseEndPoint = new IPEndPoint(IPAddress.Any, 
        multicastPort);
    responseSocket.Bind(responseEndPoint);
    responseSocket.SetSocketOption(SocketOptionLevel.IP, 
        SocketOptionName.AddMembership, 
        new MulticastOption(multicastGroup));
    ListenForRequests();
}

public void ListenForRequests() 
{
    EndPoint endPoint = new IPEndPoint(0, 0);
    responseSocket.BeginReceiveFrom(findRequestBuffer, 0, 12, 
        SocketFlags.None, ref endPoint, 
        onReceiveRequest, (object)this);
}
When a request arrives, the server identifies the sender of the request either by looking at the remoteEndPoint obtained or by including this data in the packet. The server then sends a response announcing that it is present and active (see Figure 10). The client looking for a server would send a request, then wait a given amount of time for a response as shown in Figure 11.
public void Finder(int waitFor, int serviceID)  
{
    // start listening for responses before sending the request
    responseSocket = new Socket(AddressFamily.InterNetwork, 
        SocketType.Dgram, ProtocolType.Udp);
    EndPoint responseEndPoint = new IPEndPoint(IPAddress.Any, 0);
    responseSocket.Bind(responseEndPoint);
    responsePort = ((IPEndPoint)(responseSocket.LocalEndPoint)).Port;

    ListenForResponses();

    // prepare request
    FindRequest fr = new FindRequest();
    fr.serviceID = serviceID; fr.responsePort = responsePort;
    int requestLength = fr.SerializeToPacket(findRequestBuffer);

    //send request
    Socket requestSocket = new Socket(AddressFamily.InterNetwork, 
        SocketType.Dgram, ProtocolType.Udp);
    EndPoint requestEndPointDestination = 
        new IPEndPoint(multicastGroup, multicastPort);
    requestSocket.SendTo(findRequestBuffer, requestLength, 
        SocketFlags.None, requestEndPointDestination);

    requestSocket.Close();

    //wait for responses
    Thread.Sleep(waitFor);
}

public void ListenForResponses() 
{
    EndPoint endPoint = new IPEndPoint(0, 0);
    responseSocket.BeginReceiveFrom(findResultBuffer, 0, 8, 
        SocketFlags.None, ref endPoint, 
        onReceiveResponse, (object)this);
}

static void OnReceiveResponse(IAsyncResult result) 
{
    UDP_finder uf = (UDP_finder)result.AsyncState;
    EndPoint remoteEndPoint = new IPEndPoint(0, 0);
    int bytesRead = uf.responseSocket.EndReceiveFrom(result, 
        ref remoteEndPoint);
    FindResult response = new FindResult(uf.findResultBuffer);

    uf.ListenForResponses();

    Console.WriteLine(„Found service {0}", response.serviceID);
}
static void OnReceiveRequest(IAsyncResult result) 
{
    UDP_finder uf = (UDP_finder)result.AsyncState;
    EndPoint remoteEndPoint = new IPEndPoint(0, 0);
    int bytesRead = uf.responseSocket.EndReceiveFrom(result, 
        ref remoteEndPoint);
    FindRequest request = new FindRequest(uf.findRequestBuffer);

    // prepare result
    FindResult fr = new FindResult();
    fr.serviceID = uf.currentServiceID; 
    int requestLength = fr.SerializeToPacket(uf.findResultBuffer);

    //send result
    Socket requestSocket = new Socket(AddressFamily.InterNetwork, 
        SocketType.Dgram, ProtocolType.Udp);
    EndPoint requestEndPointDestination = 
        new IPEndPoint(((IPEndPoint)remoteEndPoint).Address, 
        request.responsePort);
    requestSocket.SendTo(uf.findResultBuffer, requestLength, 
        SocketFlags.None, requestEndPointDestination);

    requestSocket.Close();

    uf.ListenForRequests();
}
Another purpose is reducing traffic and server load by sending a message to multiple senders, since one packet sent can be read by many machines on the local network, when multiple machines are interested in the same data. Copying the same file to many machines can take advantage of multicast and save bandwidth and reduce CPU usage for the sending server.

Security
When building a UDP (or any) server, security is an important consideration. The first concern is a denial of service attack, where an attacker sends many requests and spends too much of the server's resources, especially CPU time. It is common to implement throttling so that, if messages are received too quickly, some of them will be rejected or ignored.
A more difficult problem to solve is authenticating incoming messages. Since an attacker can possibly send messages as well as observe sent messages, you can use standard cryptographic methods of verifying an identity, including signing and encrypting packets. Signing a message can prevent tampering and may contain some form of authorization. Encryption can prevent third parties from reading the information.
However, UDP messages pose the additional problem of controlling the response address. You can find out the source IP address by examining the EndPoint class passed to EndReceiveFrom, but this source address is not verified by the UDP stack. It is just the claimed source address—an attacker can craft UDP packets that fake this. An attacker can also copy a valid request spotted on the network, change the source address, and send the modified copy. Often, the application will either validate the claimed source address against the message authentication or ignore the claimed source address altogether and pass the reply information in the packet, where tampering is prevented by message signing.
Sometimes you may choose to encrypt a message rather than just sign it. Encryption can use symmetric algorithms such as Data Encryption Standard (DES) or asymmetric algorithms (using public/private key cryptography). Note that for messages of several kilobytes or larger, compressing the message before encrypting it can not only reduce bandwidth, but also reduce encryption time. That is because encryption is a CPU-intensive operation and the time it takes is a function of the number of bytes (or blocks of bytes) to encrypt. If the compression reduced the data size by 75 percent, the encryption stage will be reduced similarly.

Windows Communication Foundation
Windows Communication Foundation (WCF), formerly codenamed "Indigo," provides a new way to exchange messages between distributed applications. WCF can be used to expose Web services, a standardized way to exchange messages and expose services using SOAP and XML, and allow an easier interoperability experience. In the past, Web services were usually exposed over HTTP, but specifications for SOAP over UDP are available at soap-over-udp.pdf.
WCF exposes multiple network protocols (most notably TCP) and is extensible to other network protocols by writing a WCF transport channel. WCF supports UDP through its extensibility model, and the code for a basic UDP channel will be provided as a code sample in the WinFX® SDK.
WCF sends messages as XML representations of data passed between applications. It also provides a programming model that makes exchanging messages between applications easy. In addition to one-way messages, which are the equivalent of sending a UDP packet in the code samples, support is provided for duplex (two-way) and request-reply message exchange.
Although UDP does not provide reliability features out of the box, WCF supports a standard-based reliability protocol through WS-ReliableMessaging. The WS-ReliableMessaging protocol can work with any WCF transport channel and defines a standard method to send and request ACKs, resend a dropped message, and ensure message delivery. WS-ReliableMessaging exposes three different guarantees: AtMostOnce, AtLeastOnce, and InOrder. Using this protocol layer implementation can save you from hand-coding your own reliability layer.
The code to send a message using UDP and WS-ReliableMessaging isn't very complex. I first define an address, binding, and contract. The contract outlines the data that I want passed in the message. I will define a one-way contract, similar to the one implemented previously in this article:
[ServiceContract ]
public interface IUpdatePlayerInfoContract {
    [OperationContract(IsOneWay = true)]
    void Update(byte playerID, byte locationX, byte locationY);
}
You could also choose to implement a request-reply contract by making the Update method return a value and removing the one-way reference.
The binding is the link between the transfer protocol and the data. I have to specify that I want to use the UDP transport, and that I want to use a ReliableMessaging channel.
The WCF-based implementation of UDP is a one-way channel, but I need both a listener and a sender on each side—after all, the receiver (server) needs a return address to be able to send back ACKs. Support for a return address feature in an inherently one-way transport channel is provided by the CompositeDuplex channel, as shown in this code snippet:
BindingElementCollection bindingElements = 
    new BindingElementCollection();
ReliableSessionBindingElement session = 
    new ReliableSessionBindingElement();
session.Ordered = true;
bindingElements.Add(session);
bindingElements.Add(new CompositeDuplexBindingElement());
bindingElements.Add(new UdpTransportBindingElement());
Binding playerInfoBinding = new CustomBinding(bindingElements);
Had I opted for no reliability, the BindingElementCollection would be comprised of only the UdpTransportBindingElement, but the rest of the code would stay the same.
The address is simply the address the UDP server will listen on. In the case of the UDP transport, this address includes only a machine name and port:
Uri playerInfoAddress = new Uri( "soap.udp://localhost:16000/");
The code on the server should handle a change in the player information, like so:
class playerInfoService : IUpdatePlayerInfoContract
{
    public void Update(byte playerID, byte locationX, byte locationY)
    {
        Console.WriteLine("Player {0}: {1},{2}", playerID, 
            locationX, locationY);
    }
}
The server code will also include something like this to set up the endpoint and start listening:
using(ServiceHost service = new ServiceHost(typeof(playerInfoService)))
{
    service.AddServiceEndpoint(typeof(IUpdatePlayerInfoContract), 
        playerInfoBinding, playerInfoAddress);
    service.Open();
    ... // more service code here
}
The client would need to define a proxy—a class that will be used to call the server method:
public class PlayerInfoProxy : ClientBase<IUpdatePlayerInfoContract>, 
    IUpdatePlayerInfoContract
{
    public PlayerInfoProxy(Binding binding, EndpointAddress address) :
        base(binding, address) {}

    public void Update(byte playerID, byte locationX, byte locationY)
    {
        base.InnerProxy.Update(playerID, locationX, locationY);
    }
}
And with an instance of a proxy, the client can send a message to update the PlayerInfo:
using(PlayerInfoProxy playerInfoProxy = new PlayerInfoProxy(
    playerInfoBinding, new EndpointAddress(playerInfoAddress)))
{
    for (byte i = 0; i < 10; ++i) 
    {
        playerInfoProxy.Update(1, i, i);
        Console.WriteLine("Sent");
    }
}
You can also control the ReliableMessage parameters. For example, if message ordering is not important, a more efficient exchange would take place if you set the session's Ordered property to false:
session.Ordered = false;
You can easily configure the channel selection using a configuration file and avoid the code setting bindingElements, allowing an administrator to change the transport selection, the port, address, or even protocol, and replace UDP with TCP without changing the program source.
You can configure security on the exchange in a similar manner; WCF supports authentication either using cryptographic signatures (x.509 certificates) or Windows Authentication. It also allows for validating, signing, and encrypting messages.
If the provided reliability mechanism does not fit your application, you can use WCF extensibility to implement your own reliability protocol on top of UDP by implementing a custom reliability channel. Similarly, you can support other transport protocol and security mechanisms.

Conclusion
In this article, I've discussed both the benefits and challenges of implementing UDP communication in your own apps. You've seen two ways to use UDP in your .NET-based applications. Using the Socket class will be useful if you want to provide a full implementation of the application protocol and do the bookkeeping yourself. Meanwhile, using the Windows Communication Foundation classes will enable you to rely on a standardized implementation, reducing the code you need to write, optimize, and test. Either way, with UDP in your toolbox, you have more options available when designing your next networked application.
Note that the code samples shown here use IPv4, which is the version of IP most commonly used. Converting the sample code to use IPv6 is usually simple, and requires you to specify AddressFamily.InterNetworkV6 instead of AddressFamily.InterNetwork, IPAddress.IPv6Any instead of IPAddress.IPAny, and SocketOptionLevel.IPv6 instead of SocketOptionLevel.IP. You must also replace IPv4 addresses with IPv6 addresses. For simplicity, I skipped most error checking (such as bounds checking) in the code samples. For safety and stability, don't forget these details in your own code.

Yaniv Pessach is a Software Design Engineer at Microsoft working on Windows Communication Foundation Core Messaging. He contributed to the WS-Discovery and SOAP-over-UDP specifications, and is co-author of the upcoming book An Introduction to Windows Communication Foundation (Addison-Wesley, 2006). Reach Yaniv at www.yanivpessach.com.



Комментариев нет:

Отправить комментарий