none
使用C#基于UPnP协议进行端口映射时无反馈 RRS feed

  • 问题

  • 网络环境:几台终端由TP-LINK路由器接入公网,我们用的TP-LINK支持UPnP

    调试一段用C#编程实现的UPnP映射代码时,没有任何反馈,然后就抛出了超时异常。下面是代码:

    using System;
    using System.Text;
    using System.Diagnostics;
    using System.Net;
    using System.Net.NetworkInformation;
    using System.Net.Sockets;
    using System.Windows.Forms;
    
    namespace UPNPTest
    {
        public class UPnP
        {
            public UPnP() { }
    
            //使用下面这个方法用指定端口穿越NAT
            //
            public static void OpenFirewallPort(int port)
            {
                NetworkInterface[] nics = NetworkInterface.GetAllNetworkInterfaces();
    
                //for each nic in computer
                foreach (NetworkInterface nic in nics)
                {
                    try
                    {
                        string machineIP = nic.GetIPProperties().UnicastAddresses[0].Address.ToString();
                        //send msg to each gateway configured on this nic
                        foreach (GatewayIPAddressInformation gwInfo in nic.GetIPProperties().GatewayAddresses)
                        {
                            try
                            {
                                OpenFirewallPort(machineIP, gwInfo.Address.ToString(), port);
                            }
                            catch { }
                        }
                    }
                    catch { }
                }
    
            }
    
            public static void OpenFirewallPort(string machineIP, string firewallIP, int openPort)
            {
                string svc = getServicesFromDevice(firewallIP);
                openPortFromService(svc, "urn:schemas-upnp-org:service:WANIPConnection:1", machineIP, firewallIP, 80, openPort);
                openPortFromService(svc, "urn:schemas-upnp-org:service:WANPPPConnection:1", machineIP, firewallIP, 80, openPort);
            }
    
            private static string getServicesFromDevice(string firewallIP)
            {
                //向地址:239.255.255.250:1900发出多播数据报,否则把下面第二行注释掉
                string queryResponse = "";
                firewallIP = "239.255.255.250";
                try
                {
                    string query = "M-SEARCH * HTTP/1.1\r\n" +
                    "Host:" + firewallIP + ":1900\r\n" +
                    "Man:\"ssdp:discover\"\r\n" +
                    "MX:3\r\n" +
                    "ST:upnp:rootdevice\r\n" +
                    "\r\n";
    
                    //为了更容易地设置超时我们用Socket原型代替UDP,性质相同
                    Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
                    IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(firewallIP), 1900);
    
                    //设置1.5s超时是因为路由器回传通过同网段,速度很快
                    client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 1500);
    
                    byte[] q = Encoding.ASCII.GetBytes(query);
                    client.SendTo(q, q.Length, SocketFlags.None, endPoint);
                    IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
                    EndPoint senderEP = (EndPoint)sender;
    
                    byte[] data = new byte[1024];
                    int recv = client.ReceiveFrom(data, ref senderEP);
                    queryResponse = Encoding.ASCII.GetString(data);
                    MessageBox.Show(queryResponse);
                }
                catch(Exception ex) { MessageBox.Show("Error\n" + ex.Message); }
    
                if (queryResponse.Length == 0)
                {
                    return "";
                }
    
    
                /* QueryResult is somthing like this:
                *
                HTTP/1.1 200 OK
                Cache-Control:max-age=60
                Location:http://10.10.10.1:80/upnp/service/des_ppp.xml
                Server:NT/5.0 UPnP/1.0
                ST:upnp:rootdevice
                EXT:
    
                USN:uuid:upnp-InternetGatewayDevice-1_0-00095bd945a2::upnp:rootdevice
                */
    
                string location = "";
                string[] parts = queryResponse.Split(new string[] { System.Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string part in parts)
                {
                    if (part.ToLower().StartsWith("location"))
                    {
                        location = part.Substring(part.IndexOf(':') + 1);
                        break;
                    }
                }
                if (location.Length == 0)
                {
                    return "";
                }
    
                //然后使用本地URL,我们可以获得更多的信息
    
                System.Net.WebClient webClient = new WebClient();
                try
                {
                    string ret = webClient.DownloadString(location);
                    Debug.WriteLine(ret);
                    return ret;//return services
                }
                catch (System.Exception ex)
                {
                    Debug.WriteLine(ex.Message);
                }
                finally
                {
                    webClient.Dispose();
                }
                return "";
            }
    
            private static void openPortFromService(string services, string serviceType, string machineIP, string firewallIP, int gatewayPort, int portToForward)
            {
                if (services.Length == 0)
                    return;
                int svcIndex = services.IndexOf(serviceType);
                if (svcIndex == -1)
                    return;
                string controlUrl = services.Substring(svcIndex);
                string tag1 = "<controlURL>";
                string tag2 = "</controlURL>";
                controlUrl = controlUrl.Substring(controlUrl.IndexOf(tag1) + tag1.Length);
                controlUrl = controlUrl.Substring(0, controlUrl.IndexOf(tag2));
    
    
                string soapBody = "<s:Envelope " +
                "xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/ \" " +
    
                "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/ \">" +
                "<s:Body>" +
                "<u:AddPortMapping xmlns:u=\"" + serviceType + "\">" +
                "<NewRemoteHost></NewRemoteHost>" +
                "<NewExternalPort>" + portToForward.ToString() +
                "</NewExternalPort>" +
                "<NewProtocol>TCP</NewProtocol>" +
                "<NewInternalPort>" + portToForward.ToString() +
                "</NewInternalPort>" +
                "<NewInternalClient>" + machineIP +
                "</NewInternalClient>" +
                "<NewEnabled>1</NewEnabled>" +
                "<NewPortMappingDescription>SynciumNetwork</NewPortMappingDescription>" +
                "<NewLeaseDuration>0</NewLeaseDuration>" +
                "</u:AddPortMapping>" +
                "</s:Body>" +
                "</s:Envelope>";
    
                byte[] body = System.Text.UTF8Encoding.ASCII.GetBytes(soapBody);
    
                string url = "http://" + firewallIP + ":" + gatewayPort.ToString() + controlUrl;
                System.Net.WebRequest wr = System.Net.WebRequest.Create(url);//+ controlUrl);
                wr.Method = "POST";
                wr.Headers.Add("SOAPAction", "\"" + serviceType + "#AddPortMapping\"");
                wr.ContentType = "text/xml;charset=\"utf-8\"";
                wr.ContentLength = body.Length;
    
                System.IO.Stream stream = wr.GetRequestStream();
                stream.Write(body, 0, body.Length);
                stream.Flush();
                stream.Close();
    
                WebResponse wres = wr.GetResponse();
                System.IO.StreamReader sr = new System.IO.StreamReader(wres.GetResponseStream());
                string ret = sr.ReadToEnd();
                sr.Close();
    
                Debug.WriteLine("Setting port forwarding:" + portToForward.ToString() + "\r\r" + ret);
            }
        }
    }
    
    正常情况是收到http报文,可是我什么也没有收到,请教各位是代码有问题还是别的情况?
    2010年3月24日 18:23

答案

  • 首先,我必须感谢大家对这个问题表现的关心。

    自从问题提出后若干天,我一直查阅各种论文、资源期望解决这个问题。到现在我已经可以使用UPnP做静态端口映射,但不是通过使用协议本身编程去实现的。

    不得不说,微软的确不会放过任何一种新兴的技术方向,尤其是UPnP这个微软自己都极力推动的。我做UPnP静态端口映射的最终实现也是用微软提供的COM支持。

    问题已经过去,但是我想把我在调试过程中出现的次代问题贴出来,包含一些我个人的理解,希望对大家有用。

    1. 某些异常不会被轻易捕获并搞清楚类型,我们经常会粗暴地捕获这个异常后,弹出个提示的消息,把问题踢给用户。
    但是对于一些代码,抛出异常的同时可能已经获得了我们期望的执行效果,可能只是执行过程不佳。如果放任这种潜在异常不管,可能会被运行时莫名其妙的处理后终止执行后面的代码。
    我在调试这段代码的时候遇到过这种情况三次。一次是Socket通信时,Socket已经收到了Response,但是如果不尝试着捕获,程序会没有任何异常表现地跳过后面大段代码,这令我百思不得其解,直到现在依然不明就里。不过我解决了这个问题,就是把Socket通信的部分try{}catch{},catch后不做任何处理,代码就可以正常执行了。另外两种情况一种是操作XML(我后来把SOAP的那段从字符串分析改成了XML分析)时和获取网关信息时,处理方法同上,就是try起来不做任何处理。

    当然,如果哪位朋友能告诉我这是为什么我会非常感激,而上面的方法您可以参考。

    2. 尽量避免这种直接操作协议的问题,我们真的没有必要总是纠结协议本身,大部分时候Microsoft会帮我们实现的很好,我们需要做的是掌握调用操作系统功能的方法。
    我在查阅资料时看到BitComet自己处理不了的UPnP静态端口映射被一个第三方工具解决了,而通过使用Omni可以发现两者的网络通讯过程一模一样,这就是问题,为什么同样环境同样操作结果不同,当然提出问题的人最后也没给出自己的答案。
    我要说的就是避免总是纠缠底层操作,虽然那样灵活性更大功能更强。

    3. 最后给出我的UPnP静态端口映射方法(与网络上其他资源雷同)

    <1>添加引用COM组件“NATUPNP 1.0 Type Library(hnetcfg.dll)”
    <2>在代码中引入命名空间“NATUPNPLib”
    <3>声明静态对象:static UPnPNATClass nat = new UPnPNATClass();
    <4>操作相关功能(可以用对象浏览器研究),比如添加静态端口映射:
    nat.StaticPortMappingCollection.Add(ExternalPort, ProtocalType, InternalPort, MachineIP, IfEnableThePort, Description);
    注意一定要try,如果捕获了异常,我可以负责任地说十有八九是防火墙把通信拦截了。


    最后,祝大家C#越用越开心,别搞得跟我一样打开IDE都有点反胃。

    • 已标记为答案 sartrey 2010年3月27日 18:18
    2010年3月27日 18:18

全部回复

  • 超时说明网络有问题

    检查网络是否正常,端口是否打开,是否有防火墙等。

     


    family as water
    2010年3月25日 1:24
  • 我起初也觉得是网络的问题,所以把Windows防火墙和第三方防火墙都禁用了,这个时候迅雷还能用呢(迅雷不是也用UPnP做端口映射简化TCP穿越NAT么)。

    后来看网上说检查服务,于是把SSDP Discover检查了,也是自动启动的,可是就是接收不到一点信息。

    顺便说,我还下了一个.NET的UPnP设备搜索实用工具,也可以使用(可是做了保护,不能反编译)。所以,我觉得一定是代码的问题,但是不知道究竟哪有问题。

    单步调试到

    ...
                    int recv = client.ReceiveFrom(data, ref senderEP);
                    queryResponse = Encoding.ASCII.GetString(data);
    ...

    的时候发生异常,“主机没有反应”。

    我没有更多手段发现问题了,请大家帮帮我,谢谢!

    2010年3月25日 8:52
  • 您确定已经在 UPnP 设备上打开了 UPnP?一般情况下,TP-LINK 的 UPnP 功能是关闭的。需要用 telnet 上去或者 http 去访问路由器/交换机的默认网站进行设置。

    主机没有响应的问题可能会有很多种情况,如网络,端口,防火墙,代理,NAT 等。

    您先确定您打开了 UPnP,然后我们再进一步分析。


    Mark Zhou
    2010年3月25日 11:34
  • 我又检查了TP-LINK的配置页,UPnP设置显示“已启动”。

    应该不是路由器本身的问题吧,如果您觉得还有什么情况, 我再验证验证。

    2010年3月25日 14:08
  • 首先,我必须感谢大家对这个问题表现的关心。

    自从问题提出后若干天,我一直查阅各种论文、资源期望解决这个问题。到现在我已经可以使用UPnP做静态端口映射,但不是通过使用协议本身编程去实现的。

    不得不说,微软的确不会放过任何一种新兴的技术方向,尤其是UPnP这个微软自己都极力推动的。我做UPnP静态端口映射的最终实现也是用微软提供的COM支持。

    问题已经过去,但是我想把我在调试过程中出现的次代问题贴出来,包含一些我个人的理解,希望对大家有用。

    1. 某些异常不会被轻易捕获并搞清楚类型,我们经常会粗暴地捕获这个异常后,弹出个提示的消息,把问题踢给用户。
    但是对于一些代码,抛出异常的同时可能已经获得了我们期望的执行效果,可能只是执行过程不佳。如果放任这种潜在异常不管,可能会被运行时莫名其妙的处理后终止执行后面的代码。
    我在调试这段代码的时候遇到过这种情况三次。一次是Socket通信时,Socket已经收到了Response,但是如果不尝试着捕获,程序会没有任何异常表现地跳过后面大段代码,这令我百思不得其解,直到现在依然不明就里。不过我解决了这个问题,就是把Socket通信的部分try{}catch{},catch后不做任何处理,代码就可以正常执行了。另外两种情况一种是操作XML(我后来把SOAP的那段从字符串分析改成了XML分析)时和获取网关信息时,处理方法同上,就是try起来不做任何处理。

    当然,如果哪位朋友能告诉我这是为什么我会非常感激,而上面的方法您可以参考。

    2. 尽量避免这种直接操作协议的问题,我们真的没有必要总是纠结协议本身,大部分时候Microsoft会帮我们实现的很好,我们需要做的是掌握调用操作系统功能的方法。
    我在查阅资料时看到BitComet自己处理不了的UPnP静态端口映射被一个第三方工具解决了,而通过使用Omni可以发现两者的网络通讯过程一模一样,这就是问题,为什么同样环境同样操作结果不同,当然提出问题的人最后也没给出自己的答案。
    我要说的就是避免总是纠缠底层操作,虽然那样灵活性更大功能更强。

    3. 最后给出我的UPnP静态端口映射方法(与网络上其他资源雷同)

    <1>添加引用COM组件“NATUPNP 1.0 Type Library(hnetcfg.dll)”
    <2>在代码中引入命名空间“NATUPNPLib”
    <3>声明静态对象:static UPnPNATClass nat = new UPnPNATClass();
    <4>操作相关功能(可以用对象浏览器研究),比如添加静态端口映射:
    nat.StaticPortMappingCollection.Add(ExternalPort, ProtocalType, InternalPort, MachineIP, IfEnableThePort, Description);
    注意一定要try,如果捕获了异常,我可以负责任地说十有八九是防火墙把通信拦截了。


    最后,祝大家C#越用越开心,别搞得跟我一样打开IDE都有点反胃。

    • 已标记为答案 sartrey 2010年3月27日 18:18
    2010年3月27日 18:18
  • c#里面最让人头痛的就是会throw的函数,而绝大部分函数都会throw,所以造成try catch满天飞,而且大部分时候是在做无用功(try catch会大大降低性能)。

    大多数情况下需要对所有会throw的函数进行一下try,有些时候干脆对整个函数体进行try,当然这并不是好办法,因为这破坏了程序结构。

    相对比较好的做法是对操作进行封装,比如数据层,所有操作统一可以throw,或者都不throw,这样后面的操作就可以方便许多,业务层也如此;总之尽量让同一层的操作行为统一。

    至于空的catch,理论上很不推荐,因为可能错过一些致命问题,并且导致后面的操作产生不可预料的后果。所以有必要在catch里对上面的操作结果做一下验证,或者付一个可以接受的值(比如int.parse(string),往往需要一大堆try-catch,并在catch里弄成0)。

    从设计上来讲,try-catch-throw提供了一个跨层的错误(其实未必是错误,也可以是其他信息)处理机制,这是很不错的东西,而只是往往设计上不需要这么复杂(一般有三层就足够了),所以她的优势就变成了麻烦……


    霸王
    2010年3月28日 10:28
  • 学习了。感谢您的分享!
    共同努力,共同提高
    kaedei#live.cn My BLOG
    2010年3月29日 13:55