如何讓兩臺處在不同內(nèi)網(wǎng)的主機(jī)直接互連?你需要內(nèi)網(wǎng)穿透!
上圖是一個非完整版內(nèi)外網(wǎng)通訊圖由內(nèi)網(wǎng)端先發(fā)起,內(nèi)網(wǎng)設(shè)備192.168.1.2:6677發(fā)送數(shù)據(jù)到外網(wǎng)時候必須經(jīng)過nat會轉(zhuǎn)換成對應(yīng)的外網(wǎng)ip+端口,然后在發(fā)送給外網(wǎng)設(shè)備,外網(wǎng)設(shè)備回復(fù)數(shù)據(jù)也是發(fā)給你的外網(wǎng)ip+端口。
這只是單向的內(nèi)去外,那反過來,如果外網(wǎng)的設(shè)備需要主動訪問我局域網(wǎng)里的某一個設(shè)備是無法訪問的,因?yàn)檫@個時候還沒做nat轉(zhuǎn)換所以外網(wǎng)不知道你內(nèi)網(wǎng)設(shè)備的應(yīng)用具體對應(yīng)的是哪個端口,這個時候我們就需要內(nèi)網(wǎng)穿透了,內(nèi)網(wǎng)穿透也叫NAT穿透;
穿透原理
如上圖所示經(jīng)NAT轉(zhuǎn)換后的內(nèi)外網(wǎng)地址+端口,會緩存一段時間,在這段時間內(nèi)192.168.1.2:6677和112.48.69.2020:8899的映射關(guān)系會一直存在,這樣你的內(nèi)網(wǎng)主機(jī)就得到一個外網(wǎng)地址,這個對應(yīng)關(guān)系又根據(jù)NAT轉(zhuǎn)換方法類型的不同,得用對應(yīng)的方式實(shí)現(xiàn)打洞,NAT轉(zhuǎn)換方法類型有下列幾種(來源百度百科NAT):
(1)Full cone NAT:即著名的一對一(one-to-one)NAT。
一旦一個內(nèi)部地址(iAddr:port1)映射到外部地址(eAddr:port2),所有發(fā)自iAddr:port1的包都經(jīng)由eAddr:port2向外發(fā)送。任意外部主機(jī)都能通過給eAddr:port2發(fā)包到iAddr:port1(純天然不用打洞?。?/span>
(2)Address-Restricted cone NAT :限制地址,即只接收曾經(jīng)發(fā)送到對端的IP地址來的數(shù)據(jù)包。
一旦一個內(nèi)部地址(iAddr:port1)映射到外部地址(eAddr:port2),所有發(fā)自iAddr:port1的包都經(jīng)由eAddr:port2向外發(fā)送。
任意外部主機(jī)(hostAddr:any)都能通過給eAddr:port2發(fā)包到達(dá)iAddr:port1的前提是:iAddr:port1之前發(fā)送過包到hostAddr:any. "any"也就是說端口不受限制(只需知道某個轉(zhuǎn)換后的外網(wǎng)ip+端口即可。)
(3)Port-Restricted cone NAT:類似受限制錐形NAT(Restricted cone NAT),但是還有端口限制。
一旦一個內(nèi)部地址(iAddr:port1)映射到外部地址(eAddr:port2),所有發(fā)自iAddr:port1的包都經(jīng)由eAddr:port2向外發(fā)送。一個外部主機(jī)(hostAddr:port3)能夠發(fā)包到達(dá)iAddr:port1的前提是:iAddr:port1之前發(fā)送過包到hostAddr:port3.(雙方需要各自知道對方轉(zhuǎn)換后的外網(wǎng)ip+端口,然后一方先發(fā)一次嘗試連接,另一方在次連接過來的時候就能直接連通了。)
(4)Symmetric NAT(對稱NAT)
每一個來自相同內(nèi)部IP與port的請求到一個特定目的地的IP地址和端口,映射到一個獨(dú)特的外部來源的IP地址和端口。
同一個內(nèi)部主機(jī)發(fā)出一個信息包到不同的目的端,不同的映射使用外部主機(jī)收到了一封包從一個內(nèi)部主機(jī)可以送一封包回來(只能和Full cone NAT連,沒法打洞,手機(jī)流量開熱點(diǎn)就是,同一個本地端口連接不同的服務(wù)器得到的外網(wǎng)第地址和IP不同!)
例子:
下面用一個例子演示下“受限制錐形NAT”的打洞,實(shí)現(xiàn)了這個它前面兩個類型也能通用。對稱型的話不考慮,打不了洞。
我們知道要實(shí)現(xiàn)兩臺“受限制錐形NAT”互連重點(diǎn)就是要知道對方轉(zhuǎn)換后的外網(wǎng)IP+端口,這樣我們可以:
1、準(zhǔn)備一臺Full cone NAT 類型的外網(wǎng)服務(wù)端,接受來自兩個客戶端的連接,并對應(yīng)告知對方ip+端口;
2、知道了對方ip+端口 需要設(shè)置socke:Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);這樣才能端口復(fù)用;目的就是讓連接對外的端口一致;
3、最后,我們可以讓兩臺客戶端互相連接,或者一臺先發(fā)一個請求,打個洞;另一個在去連接;
代碼:
1、TCP+IOCP方式,相對 “面向?qū)ο蟆钡貙?shí)現(xiàn)穿透!
服務(wù)端 ServerListener類,用SocketAsyncEventArgs:
/// <summary>
/// 打洞服務(wù)端,非常的簡單,接收兩個連接并且轉(zhuǎn)發(fā)給對方;
/// </summary>
public class ServerListener : IServerListener
{
IPEndPoint EndPoint { get; set; }
//消息委托
public delegate void EventMsg(object sender, string e);
public static object obj = new object();
//通知消息
public event EventMsg NoticeMsg;
//接收事件
public event EventMsg ReceivedMsg;
/// <summary>
/// 上次鏈接的
/// </summary>
private Socket Previous;
public ServerListener(IPEndPoint endpoint)
{
this.EndPoint = endpoint;
}
private Socket listener;
public void Start()
{
this.listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
var connectArgs = new SocketAsyncEventArgs();
listener.Bind(EndPoint);
listener.Listen(2);
EndPoint = (IPEndPoint)listener.LocalEndPoint;
connectArgs.Completed += OnAccept;
//是否同步就完成了,同步完成需要自己觸發(fā)
if (!listener.AcceptAsync(connectArgs))
OnAccept(listener, connectArgs);
}
byte[] bytes = new byte[400];
private void OnAccept(object sender, SocketAsyncEventArgs e)
{
Socket socket = null;
try
{
var remoteEndPoint1 = e.AcceptSocket.RemoteEndPoint.ToString();
NoticeMsg?.Invoke(sender, $"客戶端:{remoteEndPoint1}連接上我了!\r\n");
SocketAsyncEventArgs readEventArgs = new SocketAsyncEventArgs();
readEventArgs.Completed += OnSocketReceived;
readEventArgs.UserToken = e.AcceptSocket;
readEventArgs.SetBuffer(bytes, 0, 400);
if (!e.AcceptSocket.ReceiveAsync(readEventArgs))
OnSocketReceived(e.AcceptSocket, readEventArgs);
lock (obj)
{
socket = e.AcceptSocket;
//上次有鏈接并且鏈接還”健在“
if (Previous == null||! Previous.Connected)
{
Previous = e.AcceptSocket;
}
else
{
//Previous.SendAsync()..?
Previous.Send(Encoding.UTF8.GetBytes(remoteEndPoint1 + "_1"));
socket.Send(Encoding.UTF8.GetBytes(Previous.RemoteEndPoint.ToString() + "_2"));
NoticeMsg?.Invoke(sender, $"已經(jīng)通知雙方!\r\n");
Previous = null;
}
}
e.AcceptSocket = null;
if (e.SocketError != SocketError.Success)
throw new SocketException((int)e.SocketError);
if(!listener.AcceptAsync(e))
OnAccept(listener, e);
}
catch
{
socket?.Close();
}
}
public void Close()
{
using (listener)
{
// listener.Shutdown(SocketShutdown.Both);
listener.Close();
}
//throw new NotImplementedException();
}
/// <summary>
/// 此處留有一個小BUG,接收的字符串大于400的時候會有問題;可以參考客戶端修改
/// </summary>
public void OnSocketReceived(object sender, SocketAsyncEventArgs e)
{
Socket socket = e.UserToken as Socket;
var remoteEndPoint = socket.RemoteEndPoint.ToString();
try
{
if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success)
{
ReceivedMsg?.Invoke(sender, $"收到:{remoteEndPoint}發(fā)來信息:{Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred)}\r\n");
}
else
{
socket?.Close();
NoticeMsg?.Invoke(sender, $"鏈接:{remoteEndPoint}釋放啦!\r\n");
return;
}
if (!socket.ReceiveAsync(e))
OnSocketReceived(socket, e);
}
catch
{
socket?.Close();
}
//{
// if (!((Socket)sender).AcceptAsync(e))
// OnSocketReceived(sender, e);
//}
//catch
//{
// return;
//}
}
}
2、客戶端類 PeerClient用BeginReceive和EndReceive實(shí)現(xiàn)異步;
public class StateObject
{
public Socket workSocket = null;
public const int BufferSize = 100;
public byte[] buffer = new byte[BufferSize];
public List<byte> buffers = new List<byte>();
//是不是和服務(wù)器的鏈接
public bool IsServerCon = false;
}
/// <summary>
/// 打洞節(jié)點(diǎn)客戶端 實(shí)現(xiàn)的功能:
/// 連接服務(wù)器獲取對方節(jié)點(diǎn)ip
/// 請求對方ip(打洞)
/// 根據(jù)條件判斷是監(jiān)聽連接還是監(jiān)聽等待連接
/// </summary>
public class PeerClient : IPeerClient
{
//ManualResetEvent xxxxDone = new ManualResetEvent(false);
//Semaphore
/// <summary>
/// 當(dāng)前鏈接
/// </summary>
public Socket Client { get;private set; }
#region 服務(wù)端
public string ServerHostName { get;private set; }
public int ServerPort { get; private set; }
#endregion
#region 接收和通知事件
public delegate void EventMsg(object sender, string e);
//接收事件
public event EventMsg ReceivedMsg;
//通知消息
public event EventMsg NoticeMsg;
#endregion
//本地綁定的節(jié)點(diǎn)
private IPEndPoint LocalEP;
public PeerClient(string hostname, int port)
{
Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
this.ServerHostName = hostname;
this.ServerPort = port;
}
/// <summary>
/// 初始化客戶端(包括啟動)
/// </summary>
public void Init()
{
try
{
Client.Connect(ServerHostName, ServerPort);
}
catch (SocketException ex)
{
NoticeMsg?.Invoke(Client, $"連接服務(wù)器失??!{ex}!\r\n");
throw;
}
catch (Exception ex)
{
NoticeMsg?.Invoke(Client, $"連接服務(wù)器失?。?span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; color: rgb(224, 108, 117);">{ex}!\r\n");
throw;
}
NoticeMsg?.Invoke(Client, $"連接上服務(wù)器了!\r\n");
var _localEndPoint = Client.LocalEndPoint.ToString();
LocalEP = new IPEndPoint(IPAddress.Parse(_localEndPoint.Split(':')[0])
, int.Parse(_localEndPoint.Split(':')[1]));
Receive(Client);
}
private void Receive(Socket client)
{
try
{
StateObject state = new StateObject();
state.workSocket = client;
state.IsServerCon = true;
client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
new AsyncCallback(ReceiveCallback), state);
}
catch (Exception e)
{
NoticeMsg?.Invoke(Client, $"接收消息出錯了{e}!\r\n");
}
}
private void ReceiveCallback(IAsyncResult ar)
{
try
{
var state = (StateObject)ar.AsyncState;
Socket _client = state.workSocket;
//因?yàn)榈竭@邊的經(jīng)常Connected 還是true
//if (!_client.Connected)
//{
// _client.Close();
// return;
//}
SocketError error = SocketError.Success;
int bytesRead = _client.EndReceive(ar,out error);
if (error == SocketError.ConnectionReset)
{
NoticeMsg?.Invoke(Client, $"鏈接已經(jīng)釋放!\r\n");
_client.Close();
_client.Dispose();
return;
}
if (SocketError.Success!= error)
{
throw new SocketException((int)error);
}
var arr = state.buffer.AsQueryable().Take(bytesRead).ToArray();
state.buffers.AddRange(arr);
if (bytesRead >= state.buffer.Length)
{
_client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
new AsyncCallback(ReceiveCallback), state);
////state.buffers.CopyTo(state.buffers.Count, state.buffer, 0, bytesRead);
//_client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
// new AsyncCallback(ReceiveCallback), state);
}
else
{
var _msg = Encoding.UTF8.GetString(state.buffers.ToArray());
ReceivedMsg?.Invoke(_client, _msg);
if (state.IsServerCon)
{
_client.Shutdown(SocketShutdown.Both);
_client.Close();
int retryCon = _msg.Contains("_1") ? 1 : 100;
_msg = _msg.Replace("_1", "").Replace("_2", "");
TryConnection(_msg.Split(':')[0], int.Parse(_msg.Split(':')[1]), retryCon);
return;
}
state = new StateObject();
state.IsServerCon = false;
state.workSocket = _client;
_client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
new AsyncCallback(ReceiveCallback), state);
}
}
catch (SocketException ex)
{
//10054
NoticeMsg?.Invoke(Client, $"鏈接已經(jīng)釋放!{ex}!\r\n");
}
catch (Exception e)
{
NoticeMsg?.Invoke(Client, $"接收消息出錯了2{e}!\r\n");
}
}
/// <summary>
/// 打洞或者嘗試鏈接
/// </summary>
private void TryConnection(string remoteHostname, int remotePort,int retryCon)
{
Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
var _iPRemotePoint = new IPEndPoint(IPAddress.Parse(remoteHostname), remotePort);
Client.Bind(LocalEP);
System.Threading.Thread.Sleep(retryCon==1?1:3*1000);
for (int i = 0; i < retryCon; i++)
{
try
{
Client.Connect(_iPRemotePoint);
NoticeMsg?.Invoke(Client, $"已經(jīng)連接上:{remoteHostname}:{remotePort}!\r\n");
StateObject state = new StateObject();
state.workSocket = Client;
state.IsServerCon = false;
Client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
new AsyncCallback(ReceiveCallback), state);
return;
}
catch
{
NoticeMsg?.Invoke(Client, $"嘗試第{i+1}次鏈接:{remoteHostname}:{remotePort}!\r\n");
}
}
if (retryCon==1)
{
Listening(LocalEP.Port);
return;
}
NoticeMsg?.Invoke(Client, $"嘗試了{retryCon}次都沒有辦法連接到:{remoteHostname}:{remotePort},涼了!\r\n"); }
/// <summary>
/// 如果連接不成功,因?yàn)槭孪扔写蚨催^了,根據(jù)條件監(jiān)聽 等待對方連接來
/// </summary>
private void Listening(int Port)
{
try
{
Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
Client.Bind(new IPEndPoint(IPAddress.Any, Port));Client.Listen((int)SocketOptionName.MaxConnections);
NoticeMsg?.Invoke(Client, $"開始偵聽斷開等待鏈接過來!\r\n");
StateObject state = new StateObject();
state.IsServerCon = false;
var _socket = Client.Accept();//只有一個鏈接 不用BeginAccept
Client.Close();//關(guān)系現(xiàn)有偵聽
Client = _socket;
state.workSocket = Client;
NoticeMsg?.Invoke(Client, $"接收到來自{Client.RemoteEndPoint}的連接!\r\n");
Client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
new AsyncCallback(ReceiveCallback), state);
}
catch (Exception ex)
{
NoticeMsg?.Invoke(Client, $"監(jiān)聽出錯了{ex}涼了!\r\n");
}
//scoket.send
}
/// <summary>
/// 本例子只存在一個成功的鏈接,對成功的連接發(fā)送消息!
/// </summary>
/// <param name="strMsg"></param>
public void Send(string strMsg)
{
byte[] bytes = Encoding.UTF8.GetBytes(strMsg);
Client.BeginSend(bytes, 0, bytes.Length, 0,
new AsyncCallback(SendCallback), Client);
}
private void SendCallback(IAsyncResult ar)
{
try
{
Socket _socket = (Socket)ar.AsyncState;
//if(ar.IsCompleted)
_socket.EndSend(ar);
}
catch (Exception e)
{
NoticeMsg?.Invoke(Client, $"發(fā)送消息出錯了{e}!\r\n");
}
}
}
完整代碼:https://gitee.com/qqljcn/zsg_-peer-to-peer
二、面向過程方式
Task+(TcpClient+TcpListener )|(UdpClient)實(shí)現(xiàn) tcp|udp的打洞!這個就不貼代碼了直接放碼云連接:
https://gitee.com/qqljcn/zsg_-peer-to-peer_-lite
三、說明
1、代碼僅供參考,都是挺久以前寫的也沒有經(jīng)過嚴(yán)格的測試僅能演示這個例子,有不成熟的地方,煩請各位大神海涵指教;
2、不要都用本機(jī)試這個例子,本機(jī)不走nat;
3、然后udp因?yàn)槭菬o連接的所以打孔成功后不要等太久再發(fā)消息,nat緩存一過就失效了!
4、確定自己不是對稱型nat的話,如果打洞不成功,那就多試幾次!
5 、我這個例子代碼名字叫 PeerToPeer 但不是真的p2p, 微軟提供了p2p的實(shí)現(xiàn) 在using System.Net.PeerToPeer命名空間下。
以上是通過nat的方式,另外還有一種方式是,通過一個有外網(wǎng)ip的第三方服務(wù)器轉(zhuǎn)發(fā),像花生殼、nat123這類軟件,也有做個小程序,并且自己在用以后演示。
- EOF -
該文章在 2024/4/9 23:34:52 編輯過