Search code examples
c#socketsendpointhole-punching

Reuse UdpClient vs Disposing it


I am creating a chat application where users are behind a router (NAT). So the main problem is sending those clients a message. The server has no problem receiving messages.

This is my protocol (rules):

  1. UdpServer listens for a udp packet from client A
  2. Server receives packet from client A and replies with a packet to open the NAT
  3. Server now becomes a client (server constantly sends packets to client A and the client replies to the server notifying it that it got his message) I reverse the client and server now that the NAT is open. This is because client A can always send a message to the server through tcp.

  4. If Server sees that client A does not reply back to the packets that its constantly sending it marks client A as disconnected and stops sending packets.

This is the code

// class that holds infor about client
class ClientBehindNat
{
     public IPEndPoint EndpointListening;
     public string Id;
}

thread that listens for new clients (connections):

while(true)
{
    IPEndPoint ep = new IPEndPoint(IPAddress.Any, 1234);
    // create a new udpServer
    using(var udpServer = new UdpClient())
    {
        // allow reusing port 1234
        udpServer.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
        // wait to receive data
        var data = udpServer.Receive(ref ep);

        // analyze data make sure it is reliable....

        // maybe data is a reply from a client 
        if(data.IsReply())
        {
             // mark that client replied
             // ....
             continue;
        }
        else
        {
            // save new client
            ConnectedClients.Add(new ClientBehindNat(){EndpointListening=ep});
        }

        udpServer.Connect(ep);
        udpServer.Send( // data notifying client that we got his message)

    }
}

thread that constantly sends udp packets to client in order to keep nat open

// send packets every 15 seconds in order to leave nat open
timer.elapsed += (a,b)=> {

    foreach (var c in connectedClients)
    {
        // create a copy of udp server so that client can get our response
        using (var copySocket = new UdpClient())
        {
            // allow reusing port 1234
            copySocket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            copySocket.Client.Bind(new IPEndPoint(IPAddress.Any, 1234));
            // log that we will send this request
            // Requests[someId] = ....

            // send it
            copySocket.Send(someData, someData.Length, c.EndpointListening);

            // first thread should be responsible for receiving this response

            // dispose this client                            
        }          
    }
}

This is the question

Everything works great BUT is it bad to create a new Socket (UdpClient) per request? If I receive 100 requests I create 100 new sockets and dispose all that. When I try to reuse a udp client updating its endpoints I get an exception. I often need to send a message to Client A or maybe Client Z thats why I store its endpoint to know how to reach it. How can I reuse a udpClient updating its endpoints so that I am able to reach the client I am interested in?


Solution

  • I should recommend again about using Sockets directly, it gives you a lot of flexibility. Check out the following example that uses one socket for UDP connections for both sending and receiving:

        private static readonly List<EndPoint> clients = new List<EndPoint>();
        private static readonly SocketAsyncEventArgs receiveEventArgs = new SocketAsyncEventArgs();
        private static Socket socket;
    
        private static void Main(string[] args)
        {
            receiveEventArgs.Completed += OnReceive;
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            socket.Bind(new IPEndPoint(IPAddress.Any, 6000));
            Receive();
    
            while (true)
            {
                Thread.Sleep(3000);
                lock (clients)
                {
                    foreach (var endPoint in clients)
                        socket.SendTo(Encoding.ASCII.GetBytes("PING"), endPoint);
                }
            }
        }
    
        private static void Receive()
        {
            receiveEventArgs.SetBuffer(new byte[256], 0, 256);
            receiveEventArgs.RemoteEndPoint = new IPEndPoint(IPAddress.Any, 6000);
            socket.ReceiveFromAsync(receiveEventArgs);
        }
    
        private static void OnReceive(object sender, SocketAsyncEventArgs e)
        {
            if (e.BytesTransferred > 0)
            {
                // Is reply?
                var isReply = true/false;
                if (isReply)
                {
                    // Do domething
                }
                else
                    lock (clients)
                    {
                        clients.Add(e.RemoteEndPoint);
                    }
            }
            else
            {
                lock (clients)
                {
                    clients.Remove(e.RemoteEndPoint);
                }
            }
            Receive();
        }