在寫完Object 672後,軟件的一個致命問題暴露出來,若是服務器和客戶端都在內網環境下,即雙方都經過NAT來接觸外網,那麼此時客戶端是沒法直接和服務器交流的。html
解決方案能夠是:web
1:把服務器部署在不存在NAT的公網環境下。瀏覽器
2:使用常見的NAT穿透方法好比UDP打洞,或者STUN協議,可是這些方法都須要另外一個已知的部署在公網環境下的服務器。服務器
3:就是這篇文章主要討論的方案,即不須要部署任何公網環境下的服務器,經過路由器支持的UPnP協議來把內網的接口綁定到公網接口上。app
UPnP的一大優點就是不會像UDP打洞那樣,內網接口不須要先向外部接口發送UDP包來把綁定的公網接口告訴NAT,並且對於對稱NAT,UDP打洞是無效的。而UPnP一旦設置成功後,內網接口徹底以綁定的公網接口暴露在公網中。socket
演示程序的運行是這樣的:工具
具體過程:post
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組件
首先輸出本機(也就是內網接口信息),這個很簡單了:
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選項中看到這些數據:
設置好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 注意:源代碼不包含引用的外部類庫文件
出處:http://www.cnblogs.com/cuihongyu3503319/archive/2013/02/05/2892764.html