private void SendConnectionUdpMessages() { _isRunning = true; var messageToServer = new InfoMessage { Id = _id, LocalEndpoint = _localEndPoint.Convert() }; var messageToPeer = new Udp_KeepaliveMessage(); while (_isRunning) { // while we haven't got the partner's address, we will send messages to server if (_partnerPublicUdpEndPoint == null && _partnerLocalUdpEndPoint == null) { byte[] bytes = messageToServer.ToByteArray(); _udpPuncher.Send(bytes, bytes.Length, _serverUdpEndPoint); Console.WriteLine($" >>> Sent UDP to server {_serverUdpEndPoint.Address}:{_serverUdpEndPoint.Port}"); } else { // you can skip it. just a demonstration that you still can send messages to server // _udpClient.Send(messageToServer.Data, messageToServer.Data.Length, _serverUdpEndPoint); // Console.WriteLine($" >>> Sent UDP to server [ {_serverUdpEndPoint.Address} : {_serverUdpEndPoint.Port} ]"); // THIS is how we punch a hole! we expect the very first message to be dropped by the partner's NAT router // i suppose that this is good idea to send this "keep-alive" messages to peer even if you have already connected, // because AFAIK "hole" for UDP lives ~2 minutes on NAT. so "will we let it die? NEVER!" (c) byte[] bytes = messageToPeer.ToByteArray(); _udpClient.Send(bytes, bytes.Length, _partnerPublicUdpEndPoint); _udpClient.Send(bytes, bytes.Length, _partnerLocalUdpEndPoint); Console.WriteLine($" >>> Sent UDP to peer.public [ {_partnerPublicUdpEndPoint.Address} : {_partnerPublicUdpEndPoint.Port} ]"); Console.WriteLine($" >>> Sent UDP to peer.local [ {_partnerLocalUdpEndPoint.Address} : {_partnerLocalUdpEndPoint.Port} ]"); // "connected" UdpClient sends data much faster, // so if you have something that your partner cant wait for (voice, for example), send it this way if (_extraUdpClientConnected) { _extraUdpClient.Send(bytes, bytes.Length); Console.WriteLine($" >>> Sent UDP to peer.received EP"); } } Thread.Sleep(3000); } }