Редактировать: После гораздо большего тестирования это, кажется, не работает для меня, если я не включаю UPnP.Поэтому многие вещи, которые я написал здесь, могут оказаться полезными, но у многих людей не включен UPnP (потому что это угроза безопасности), поэтому он не будет работать для них.
Вот некоторый код, использующий PubNubв качестве сервера ретрансляции :).Я не рекомендую использовать этот код без тестирования, потому что он не идеален (я не уверен, является ли он даже безопасным или правильным способом? Idk, я не специалист по сетевым технологиям), но он должен дать вам представлениечто делатьПо крайней мере, это сработало для меня до сих пор в хобби-проекте.Вот что ему не хватает:
- Проверка, находится ли клиент в вашей локальной сети.Я просто отправляю оба, которые работают для вашей локальной сети и устройства в другой сети, но это очень неэффективно.
- Тестирование, когда клиент перестает слушать, если, например, он закрыл программу.Поскольку это UDP, он не имеет состояния, поэтому не имеет значения, отправляем ли мы сообщения в пустоту, но мы, вероятно, не должны этого делать, если никто их не получает
- Я использую Open.NAT для программной переадресации портов, но это может не работать на некоторых устройствах.В частности, он использует UPnP, который немного небезопасен и требует, чтобы порт UDP 1900 был перенаправлен вручную.Как только они это делают, это поддерживается большинством маршрутизаторов, но многие еще этого не сделали.
Итак, прежде всего, вам нужен способ получения ваших внешних и локальных IP-адресов.Вот код для получения вашего локального IP:
// From /4220139/poluchit-lokalnyi-ip-adres
public string GetLocalIp()
{
var host = Dns.GetHostEntry(Dns.GetHostName());
foreach (var ip in host.AddressList)
{
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
return ip.ToString();
}
}
throw new Exception("Failed to get local IP");
}
А вот код для получения вашего внешнего IP с помощью нескольких веб-сайтов, которые предназначены для возврата вашего внешнего IP
public string GetExternalIp()
{
for (int i = 0; i < 2; i++)
{
string res = GetExternalIpWithTimeout(400);
if (res != "")
{
return res;
}
}
throw new Exception("Failed to get external IP");
}
private static string GetExternalIpWithTimeout(int timeoutMillis)
{
string[] sites = new string[] {
"http://ipinfo.io/ip",
"http://icanhazip.com/",
"http://ipof.in/txt",
"http://ifconfig.me/ip",
"http://ipecho.net/plain"
};
foreach (string site in sites)
{
try
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(site);
request.Timeout = timeoutMillis;
using (var webResponse = (HttpWebResponse)request.GetResponse())
{
using (Stream responseStream = webResponse.GetResponseStream())
{
using (StreamReader responseReader = new System.IO.StreamReader(responseStream, Encoding.UTF8))
{
return responseReader.ReadToEnd().Trim();
}
}
}
}
catch
{
continue;
}
}
return "";
}
Теперь нам нужно найти открытый порт и перенаправить его на внешний порт.Как упоминалось выше, я использовал Open.NAT .Во-первых, вы составляете список портов, которые, по вашему мнению, будут разумно использовать для вашего приложения, после просмотра зарегистрированных портов UDP .Вот несколько примеров:
public static int[] ports = new int[]
{
5283,
5284,
5285,
5286,
5287,
5288,
5289,
5290,
5291,
5292,
5293,
5294,
5295,
5296,
5297
};
Теперь мы можем просмотреть их и, надеюсь, найти тот, который не используется для переадресации портов на:
public UdpClient GetUDPClientFromPorts(out Socket portHolder, out string localIp, out int localPort, out string externalIp, out int externalPort)
{
localIp = GetLocalIp();
externalIp = GetExternalIp();
var discoverer = new Open.Nat.NatDiscoverer();
var device = discoverer.DiscoverDeviceAsync().Result;
IPAddress localAddr = IPAddress.Parse(localIp);
int workingPort = -1;
for (int i = 0; i < ports.Length; i++)
{
try
{
// You can alternatively test tcp with nc -vz externalip 5293 in linux and
// udp with nc -vz -u externalip 5293 in linux
Socket tempServer = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
tempServer.Bind(new IPEndPoint(localAddr, ports[i]));
tempServer.Close();
workingPort = ports[i];
break;
}
catch
{
// Binding failed, port is in use, try next one
}
}
if (workingPort == -1)
{
throw new Exception("Failed to connect to a port");
}
int localPort = workingPort;
// You could try a different external port if the below code doesn't work
externalPort = workingPort;
// Mapping ports
device.CreatePortMapAsync(new Open.Nat.Mapping(Open.Nat.Protocol.Udp, localPort, externalPort));
// Bind a socket to our port to "claim" it or cry if someone else is now using it
try
{
portHolder = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
portHolder.Bind(new IPEndPoint(localAddr, localPort));
}
catch
{
throw new Exception("Failed, someone is now using local port: " + localPort);
}
// Make a UDP Client that will use that port
UdpClient udpClient = new UdpClient(localPort);
return udpClient;
}
Теперь для PubNubкод сервера ретрансляции (P2PPeer будет определен позже ниже).Здесь много чего, поэтому я не собираюсь объяснять это, но, надеюсь, код достаточно ясен, чтобы помочь вам понять, что происходит
public delegate void NewPeerCallback(P2PPeer newPeer);
public event NewPeerCallback OnNewPeerConnection;
public Pubnub pubnub;
public string pubnubChannelName;
public string localIp;
public string externalIp;
public int localPort;
public int externalPort;
public UdpClient udpClient;
HashSet<string> uniqueIdsPubNubSeen;
object peerLock = new object();
Dictionary<string, P2PPeer> connectedPeers;
string myPeerDataString;
public void InitPubnub(string pubnubPublishKey, string pubnubSubscribeKey, string pubnubChannelName)
{
uniqueIdsPubNubSeen = new HashSet<string>();
connectedPeers = new Dictionary<string, P2PPeer>;
pubnub = new Pubnub(pubnubPublishKey, pubnubSubscribeKey);
myPeerDataString = localIp + " " + externalIp + " " + localPort + " " + externalPort + " " + pubnub.SessionUUID;
this.pubnubChannelName = pubnubChannelName;
pubnub.Subscribe<string>(
pubnubChannelName,
OnPubNubMessage,
OnPubNubConnect,
OnPubNubError);
return pubnub;
}
//// Subscribe callbacks
void OnPubNubConnect(string res)
{
pubnub.Publish<string>(pubnubChannelName, connectionDataString, OnPubNubTheyGotMessage, OnPubNubMessageFailed);
}
void OnPubNubError(PubnubClientError clientError)
{
throw new Exception("PubNub error on subscribe: " + clientError.Message);
}
void OnPubNubMessage(string message)
{
// The message will be the string ["localIp externalIp localPort externalPort","messageId","channelName"]
string[] splitMessage = message.Trim().Substring(1, message.Length - 2).Split(new char[] { ',' });
string peerDataString = splitMessage[0].Trim().Substring(1, splitMessage[0].Trim().Length - 2);
// If you want these, I don't need them
//string peerMessageId = splitMessage[1].Trim().Substring(1, splitMessage[1].Trim().Length - 2);
//string channelName = splitMessage[2].Trim().Substring(1, splitMessage[2].Trim().Length - 2);
string[] pieces = peerDataString.Split(new char[] { ' ', '\t' });
string peerLocalIp = pieces[0].Trim();
string peerExternalIp = pieces[1].Trim();
string peerLocalPort = int.Parse(pieces[2].Trim());
string peerExternalPort = int.Parse(pieces[3].Trim());
string peerPubnubUniqueId = pieces[4].Trim();
pubNubUniqueId = pieces[4].Trim();
// If you are on the same device then you have to do this for it to work idk why
if (peerLocalIp == localIp && peerExternalIp == externalIp)
{
peerLocalIp = "127.0.0.1";
}
// From me, ignore
if (peerPubnubUniqueId == pubnub.SessionUUID)
{
return;
}
// We haven't set up our connection yet, what are we doing
if (udpClient == null)
{
return;
}
// From someone else
IPEndPoint peerEndPoint = new IPEndPoint(IPAddress.Parse(peerExternalIp), peerExternalPort);
IPEndPoint peerEndPointLocal = new IPEndPoint(IPAddress.Parse(peerLocalIp), peerLocalPort);
// First time we have heard from them
if (!uniqueIdsPubNubSeen.Contains(peerPubnubUniqueId))
{
uniqueIdsPubNubSeen.Add(peerPubnubUniqueId);
// Dummy messages to do UDP hole punching, these may or may not go through and that is fine
udpClient.Send(new byte[10], 10, peerEndPoint);
udpClient.Send(new byte[10], 10, peerEndPointLocal); // This is if they are on a LAN, we will try both
pubnub.Publish<string>(pubnubChannelName, myPeerDataString, OnPubNubTheyGotMessage, OnPubNubMessageFailed);
}
// Second time we have heard from them, after then we don't care because we are connected
else if (!connectedPeers.ContainsKey(peerPubnubUniqueId))
{
//bool isOnLan = IsOnLan(IPAddress.Parse(peerExternalIp)); TODO, this would be nice to test for
bool isOnLan = false; // For now we will just do things for both
P2PPeer peer = new P2PPeer(peerLocalIp, peerExternalIp, peerLocalPort, peerExternalPort, this, isOnLan);
lock (peerLock)
{
connectedPeers.Add(peerPubnubUniqueId, peer);
}
// More dummy messages because why not
udpClient.Send(new byte[10], 10, peerEndPoint);
udpClient.Send(new byte[10], 10, peerEndPointLocal);
pubnub.Publish<string>(pubnubChannelName, connectionDataString, OnPubNubTheyGotMessage, OnPubNubMessageFailed);
if (OnNewPeerConnection != null)
{
OnNewPeerConnection(peer);
}
}
}
//// Publish callbacks
void OnPubNubTheyGotMessage(object result)
{
}
void OnPubNubMessageFailed(PubnubClientError clientError)
{
throw new Exception("PubNub error on publish: " + clientError.Message);
}
А вот P2PPeer
public class P2PPeer
{
public string localIp;
public string externalIp;
public int localPort;
public int externalPort;
public bool isOnLan;
P2PClient client;
public delegate void ReceivedBytesFromPeerCallback(byte[] bytes);
public event ReceivedBytesFromPeerCallback OnReceivedBytesFromPeer;
public P2PPeer(string localIp, string externalIp, int localPort, int externalPort, P2PClient client, bool isOnLan)
{
this.localIp = localIp;
this.externalIp = externalIp;
this.localPort = localPort;
this.externalPort = externalPort;
this.client = client;
this.isOnLan = isOnLan;
if (isOnLan)
{
IPEndPoint endPointLocal = new IPEndPoint(IPAddress.Parse(localIp), localPort);
Thread localListener = new Thread(() => ReceiveMessage(endPointLocal));
localListener.IsBackground = true;
localListener.Start();
}
else
{
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(externalIp), externalPort);
Thread externalListener = new Thread(() => ReceiveMessage(endPoint));
externalListener.IsBackground = true;
externalListener.Start();
}
}
public void SendBytes(byte[] data)
{
if (client.udpClient == null)
{
throw new Exception("P2PClient doesn't have a udpSocket open anymore");
}
//if (isOnLan) // This would work but I'm not sure how to test if they are on LAN so I'll just use both for now
{
client.udpClient.Send(data, data.Length, new IPEndPoint(IPAddress.Parse(localIp), localPort));
}
//else
{
client.udpClient.Send(data, data.Length, new IPEndPoint(IPAddress.Parse(externalIp), externalPort));
}
}
// Encoded in UTF8
public void SendString(string str)
{
SendBytes(System.Text.Encoding.UTF8.GetBytes(str));
}
void ReceiveMessage(IPEndPoint endPoint)
{
while (client.udpClient != null)
{
byte[] message = client.udpClient.Receive(ref endPoint);
if (OnReceivedBytesFromPeer != null)
{
OnReceivedBytesFromPeer(message);
}
//string receiveString = Encoding.UTF8.GetString(message);
//Console.Log("got: " + receiveString);
}
}
}
Наконец, вот все, что я могу использовать:
using PubNubMessaging.Core; // Get from PubNub GitHub for C#, I used the Unity3D library
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
Я открыт для комментариев и вопросов, не стесняйтесь оставлять отзывы, если что-то здесь является плохой практикой или не работает.В переводе из моего кода было внесено несколько ошибок, которые я, в конце концов, здесь исправлю, но это должно, по крайней мере, дать вам представление о том, что делать.