.NET(C#):使用UPnP來穿透NAT使內網接口對外網可見


在寫完Object 672后,軟件的一個致命問題暴露出來,如果服務器和客戶端都在內網環境下,即雙方都通過NAT來接觸外網,那么此時客戶端是無法直接和服務器交流的。

 

解決方案可以是:

 

1:把服務器部署在不存在NAT的公網環境下。

 

2:使用常見的NAT穿透方法比如UDP打洞,或者STUN協議,但是這些方法都需要另一個已知的部署在公網環境下的服務器。

 

3:就是這篇文章主要討論的方案,即不需要部署任何公網環境下的服務器,通過路由器支持的UPnP協議來把內網的接口綁定到公網接口上。

 

UPnP的一大優勢就是不會像UDP打洞那樣,內網接口不需要先向外部接口發送UDP包來把綁定的公網接口告訴NAT,而且對於對稱NAT,UDP打洞是無效的。而UPnP一旦設置成功后,內網接口完全以綁定的公網接口暴露在公網中。

 

 

 

演示程序的運行是這樣的:

 

image

 

 

 

具體過程:

 
  

1. 輸出用戶Host Name和內網IP地址。

   

2. 通過UPnP把內網IP地址,內部端口號綁定到一個外部端口號上。

   

3. 通過HTTP從外部網站獲取公網IP地址。

   

4. 在內網中創建TCP Socket服務器。

   

5. 建立另一個TCP Socket客戶端,然后嘗試連接上面獲取的公網IP和UPnP綁定的外部端口。

   

6. 如果一切沒有問題的話,此時會成功連接到服務器,並收到回應!

 

 

 

在.NET環境下使用Windows的UPnP組件需要現在工程中引用:NATUPnP 1.0 Type Library,這是一個COM類庫。

 

下面開始逐句分析源代碼,源代碼均擬用戶已加入下列命名空間:

 
  

using System.Net;                     

   

using System.Net.Sockets;            

   

using System.Text.RegularExpressions;  //提取IP時的正則

   

using System.Threading.Tasks;          //Task

   

using System.IO;                       //讀取服務器信息用到StreamReader

   

using NATUPNPLib;                      //Windows UPnP COM組件

 

 

 

首先輸出本機(也就是內網接口信息),這個很簡單了:

 
  

//獲取Host Name

   

var name =Dns.GetHostName();

   

Console.WriteLine("用戶:"+ name);

   

//從當前Host Name解析IP地址,篩選IPv4地址是本機的內網IP地址。

   

var ipv4 =Dns.GetHostEntry(name).AddressList.Where(i => i.AddressFamily ==AddressFamily.InterNetwork).FirstOrDefault();

   

Console.WriteLine("內網IP:"+ ipv4);

 

 

 

接 下來就是設置UPnP了,首先需要初始化UPnPNAT類型(他是一個接口,只不過通過CoClass特性把執行導向UPnPNATClass類型),接 着通過UPnPNAT的StaticPortMappingCollection來添加或者刪除UPnP綁定。注意在沒有路由器或者路由器的UPnP不開 啟的情況下,StaticPortMappingCollection屬性可能會返回null。

 

 

 

代碼如下:

 
  

Console.WriteLine("設置UPnP");

   

//UPnP綁定信息

   

var eport =8733;

   

var iport =8733;

   

var description ="Mgen測試";

   

 

   

//創建COM類型

   

var upnpnat =newUPnPNAT();

   

var mappings = upnpnat.StaticPortMappingCollection;

   

 

   

//錯誤判斷

   

if (mappings ==null)

   

{

   

    Console.WriteLine("沒有檢測到路由器,或者路由器不支持UPnP功能。");

   

    return;

   

}

   

 

   

//添加之前的ipv4變量(內網IP),內部端口,和外部端口

   

mappings.Add(eport, "TCP", iport, ipv4.ToString(), true, description);

   

 

   

Console.WriteLine("外部端口:{0}", eport);

   

Console.WriteLine("內部端口:{0}", iport);

 

 

 

如果成功后,你應該可以在路由器的UPnP選項中看到這些數據:

 

image

 

 

 

設置好UPnP后,開始獲取外網IP地址,可以通過這個網址(http://checkip.dyndns.org/)。此時只需要發送一個HTTP GET請求,然后把返回的HTML中的IP地址提取出來就可以了,我們用正則來提取IP地址。

 

代碼如下:

 
  

//外網IP變量

   

string eip;

   

//正則

   

var regex =@"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b";

   

using (var webclient =newWebClient())

   

{

   

    var rawRes = webclient.DownloadString("http://checkip.dyndns.org/");

   

    eip =Regex.Match(rawRes, regex).Value;

   

}

   

 

   

Console.WriteLine("外網IP:"+ eip);

 

 

 

OK,這個時候(如果一切順利的話),一切准備工作都做好了。我們有了:內網IP,內部端口,外網IP,外部端口。那么就可以做一個TCP連接做測試了。

 

 

 

直接建立一個TCP服務端,代表在NAT下的服務器,注意端口號要綁定到UPnP設置時的內部端口。

 

代碼:

 
  

//在NAT下的服務器

   

var socket =newSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

   

//綁定內網IP和內部端口

   

socket.Bind(newIPEndPoint(ipv4, iport));

   

socket.Listen(1);

   

 

   

//在另一個線程中運行客戶端Socket

   

Task.Run(() =>

   

    {

   

        Task.Delay(1000);

   

        ClientSocket(eip, eport);

   

    });

   

 

   

//成功連接

   

var client = socket.Accept();

   

//服務器向客戶端發送信息

   

client.Send(Encoding.Unicode.GetBytes("=== 歡迎來到Mgen的服務器!==="+Environment.NewLine));

   

 

   

Console.ReadKey(false);

 

 

 

上 面的ClientSocket方法就是客戶端的Socket連接執行,注意TCP協議是不保留數據邊界的,因此服務器在發送消息時,后面加了個換行符 (Environment.NewLine),然后在客戶端接受數據時,使用Socket –> NetworkStream –> StreamReader的嵌套組合,最后由StreamReader的ReadLine讀取數據,這樣確保會讀到最后的換行符。

 

 

 

ClientSocket方法的執行代碼:

 
  

//ip參數和port參數是公網的IP地址,和UPnP中的外部端口

   

staticvoid ClientSocket(string ip, int port)

   

{

   

    try

   

    {

   

        Console.WriteLine("建立客戶端TCP連接");

   

        var socket =newSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

   

        socket.Connect(newIPEndPoint(IPAddress.Parse(ip), port));

   

        using (var ns =newNetworkStream(socket))

   

        using (var sr =newStreamReader(ns, Encoding.Unicode))

   

        {

   

            Console.WriteLine("收到來自服務器的回應:");

   

            Console.WriteLine(sr.ReadLine());

   

        }

   

    }

   

    catch (Exception ex)

   

    {

   

        Console.WriteLine(ex.Message);

   

    }

   

}

 

OK。

 

 

 
  

源代碼下載     下載頁面      注意:鏈接是微軟SkyDrive頁面,下載時請用瀏覽器直接下載,用某些下載工具可能無法下載      源代碼環境:Microsoft Visual Studio Express 2012 for Windows Desktop      注意:源代碼不包含引用的外部類庫文件

作者: Mgen

本文版權歸作者所有,歡迎以網址(鏈接)的方式轉載,不歡迎復制文章內容的方式轉載,其一是為了在搜索引擎中去掉重復文章內容,其二復制后的文章往往沒有提供本博客的頁面格式和鏈接,造成文章可讀性很差。望有素質人自覺遵守上述建議。

如果一定要以復制文章內容的方式轉載,必須在文章開頭標明作者信息和原文章鏈接地址。否則保留追究法律責任的權利。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM