必须填写至少10个字的日志
nnhy 编写于 2012-07-27 18:48:21
X
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using NewLife.Configuration;
using NewLife.Net.Sockets;

#if NET4
using System.Linq;
#else
using NewLife.Linq;
#endif

namespace NewLife.Net.Stun
{
    /// <summary>Stun客户端。Simple Traversal of UDP over NATs,NAT 的UDP简单穿越。RFC 3489</summary>
    /// <remarks>
    /// <a target="_blank" href="http://baike.baidu.com/view/884586.htm">STUN</a>
    /// 
    /// 国内STUN服务器:220.181.126.73、220.181.126.74,位于北京电信,但不清楚是哪家公司
    /// </remarks>
    /// <example>
    /// <code>
    /// var result = new StunClient().Query();
    /// if(result.Type != StunNetType.UdpBlocked){
    ///     
    /// }
    /// else{
    ///     var publicEP = result.Public;
    /// }
    /// </code>
    /// </example>
    public class StunClient : Netbase
    {
        #region 工作原理
        /*
        In test I, the client sends a STUN Binding Request to a server, without any flags set in the
        CHANGE-REQUEST attribute, and without the RESPONSE-ADDRESS attribute. This causes the server 
        to send the response back to the address and port that the request came from.
            
        In test II, the client sends a Binding Request with both the "change IP" and "change port" flags
        from the CHANGE-REQUEST attribute set.  
              
        In test III, the client sends a Binding Request with only the "change port" flag set.
                          
                            +--------+
                            |  Test  |
                            |   I    |
                            +--------+
                                    |
                                    |
                                    V
                                   /\              /\
                                N /  \ Y          /  \ Y             +--------+
                UDP      <-------/Resp\--------->/ IP \------------->|  Test  |
                Blocked          \ ?  /          \Same/              |   II   |
                                  \  /            \? /               +--------+
                                   \/              \/                    |
                                                    | N                  |
                                                    |                    V
                                                    V                    /\
                                                +--------+  Sym.      N /  \
                                                |  Test  |  UDP    <---/Resp\
                                                |   II   |  Firewall   \ ?  /
                                                +--------+              \  /
                                                    |                    \/
                                                    V                    |Y
                        /\                         /\                    |
         Symmetric  N  /  \       +--------+   N  /  \                   V
            NAT  <--- / IP \<-----|  Test  |<--- /Resp\               Open
                      \Same/      |   I    |     \ ?  /               Internet
                       \? /       +--------+      \  /
                        \/                         \/
                        |                           |Y
                        |                           |
                        |                           V
                        |                           Full
                        |                           Cone
                        V              /\
                    +--------+        /  \ Y
                    |  Test  |------>/Resp\---->Restricted
                    |   III  |       \ ?  /
                    +--------+        \  /
                                       \/
                                       |N
                                       |       Port
                                       +------>Restricted

    */
        #endregion

        #region 服务器
        static String[] servers = new String[] { "stun.nnhy.org", "stun.sipgate.net:10000", "stunserver.org", "stun.xten.com", "stun.fwdnet.net", "stun.iptel.org", "220.181.126.73" };
        private List<String> _Servers;
        /// <summary>Stun服务器</summary>
        public List<String> Servers
        {
            get
            {
                if (_Servers == null)
                {
                    var list = new List<String>();
                    var ss = Config.GetConfigSplit<String>("NewLife.Net.StunServers", null);
                    if (ss != null && ss.Length > 0) list.AddRange(ss);
                    list.AddRange(servers);
                    _Servers = list;
                }
                return _Servers;
            }
        }
        #endregion

        #region 属性
        private ISocket _Socket;
        /// <summary>套接字</summary>
        public ISocket Socket { get { return _Socket; } set { _Socket = value; } }

        private ISocket _Socket2;
        /// <summary>用于测试更换本地套接字的第二套接字</summary>
        public ISocket Socket2 { get { return _Socket2; } set { _Socket2 = value; } }

        private ProtocolType _ProtocolType = ProtocolType.Udp;
        /// <summary>协议,默认Udp</summary>
        public ProtocolType ProtocolType { get { return _ProtocolType; } set { _ProtocolType = value; } }

        private Int32 _Port;
        /// <summary>本地端口</summary>
        public Int32 Port { get { return _Port; } set { _Port = value; } }

        private Int32 _Timeout = 2000;
        /// <summary>超时时间,默认2000ms</summary>
        public Int32 Timeout { get { return _Timeout; } set { _Timeout = value; } }
        #endregion

        #region 构造
        /// <summary>实例化</summary>
        public StunClient() { }

        /// <summary>在指定协议上执行查询</summary>
        /// <param name="protocol"></param>
        /// <returns></returns>
        public StunClient(ProtocolType protocol) : this(protocol, 0) { }

        /// <summary>在指定协议和本地端口上执行查询</summary>
        /// <param name="protocol"></param>
        /// <param name="port"></param>
        /// <returns></returns>
        public StunClient(ProtocolType protocol, Int32 port)
        {
            ProtocolType = protocol;
            Port = port;
        }

        /// <summary>在指定套接字上执行查询</summary>
        /// <param name="socket"></param>
        /// <returns></returns>
        public StunClient(ISocket socket) { Socket = socket; }

        // 如果是外部传进来的Socket,也销毁,就麻烦大了
        ///// <summary>子类重载实现资源释放逻辑时必须首先调用基类方法</summary>
        ///// <param name="disposing">从Dispose调用(释放所有资源)还是析构函数调用(释放非托管资源)</param>
        //protected override void OnDispose(bool disposing)
        //{
        //    base.OnDispose(disposing);

        //    if (_Socket != null)
        //    {
        //        _Socket.Dispose();
        //        _Socket = null;
        //    }
        //    if (_Socket2 != null)
        //    {
        //        _Socket2.Dispose();
        //        _Socket2 = null;
        //    }
        //}
        #endregion

        #region 方法
        void EnsureSocket()
        {
            if (_Socket == null)
            {
                var client = NetService.Resolve<ISocketClient>(ProtocolType);
                client.Port = Port;
                client.Client.SendTimeout = Timeout;
                client.Client.ReceiveTimeout = Timeout;
                client.Bind();
                _Socket = client;
            }
        }

        void EnsureSocket2()
        {
            if (_Socket2 == null)
            {
                var socket = Socket.Socket;
                var ep = socket.LocalEndPoint as IPEndPoint;
                var sto = socket.SendTimeout;
                var rto = socket.ReceiveTimeout;

                // 如果原端口没有启用地址重用,则关闭它
                Object value = socket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress);
                if (!Convert.ToBoolean(value)) socket.Close();

                var sk = NetService.Resolve<ISocketClient>(socket.ProtocolType);
                sk.Address = ep.Address;
                sk.Port = ep.Port;
                sk.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                sk.Bind();
                sk.Client.SendTimeout = sto;
                sk.Client.ReceiveTimeout = rto;

                _Socket2 = sk;
            }
        }
        #endregion

        #region 查询
        /// <summary>按服务器列表执行查询</summary>
        /// <returns></returns>
        public StunResult Query()
        {
            foreach (var result in QueryByServers())
            {
                if (result != null && result.Type != StunNetType.Blocked) return result;
            }
            return null;
        }

        IEnumerable<StunResult> QueryByServers()
        {
            // 如果是被屏蔽,很有可能是因为服务器没有响应,可以通过轮换服务器来测试
            StunResult result = null;
            foreach (var item in Servers)
            {
                WriteLog("使用服务器:{0}", item);

                Int32 p = item.IndexOf(":");
                if (p > 0)
                    result = QueryWithServer(item.Substring(0, p), Int32.Parse(item.Substring(p + 1)));
                else
                    result = QueryWithServer(item, 3478);

                yield return result;
            }
        }

        /// <summary>在指定服务器上执行查询</summary>
        /// <param name="host"></param>
        /// <param name="port"></param>
        /// <returns></returns>
        public StunResult QueryWithServer(String host, Int32 port = 3478)
        {
            try
            {
                return QueryWithServer(NetHelper.ParseAddress(host), port);
            }
            catch { return null; }
        }

        /// <summary>在指定服务器上执行查询</summary>
        /// <param name="address"></param>
        /// <param name="port"></param>
        /// <returns></returns>
        public StunResult QueryWithServer(IPAddress address, Int32 port)
        {
            EnsureSocket();
            Socket socket = Socket.Socket;
            var remote = new IPEndPoint(address, port);

            // Test I
            // 测试网络是否畅通
            var msg = new StunMessage();
            msg.Type = StunMessageType.BindingRequest;
            var rs = Query(socket, msg, remote);

            // UDP blocked.
            if (rs == null) return new StunResult(StunNetType.Blocked, null);
            WriteLog("Stun服务器:{0}", rs.ServerName);
            WriteLog("映射地址:{0}", rs.MappedAddress);
            WriteLog("源地址:{0}", rs.SourceAddress);
            WriteLog("新地址:{0}", rs.ChangedAddress);
            var remote2 = rs.ChangedAddress;

            // Test II
            // 要求改变IP和端口
            msg.ChangeIP = true;
            msg.ChangePort = true;
            msg.ResetTransactionID();

            // 如果本地地址就是映射地址,表示没有NAT。这里的本地地址应该有问题,永远都会是0.0.0.0
            //if (client.LocalEndPoint.Equals(test1response.MappedAddress))
            var pub = rs.MappedAddress;
            if (pub != null && (socket.LocalEndPoint as IPEndPoint).Port == pub.Port && NetHelper.GetIPs().Any(e => e.Equals(pub.Address)))
            {
                // 要求STUN服务器从另一个地址和端口向当前映射端口发送消息。如果收到,表明是完全开放网络;如果没收到,可能是防火墙阻止了。
                rs = Query(socket, msg, remote);
                // Open Internet.
                if (rs != null) return new StunResult(StunNetType.OpenInternet, pub);

                // Symmetric UDP firewall.
                return new StunResult(StunNetType.SymmetricUdpFirewall, pub);
            }
            else
            {
                rs = Query(socket, msg, remote);
                if (rs != null && pub == null) pub = rs.MappedAddress;
                // Full cone NAT.
                if (rs != null) return new StunResult(StunNetType.FullCone, pub);

                // Test II
                msg.ChangeIP = false;
                msg.ChangePort = false;
                msg.ResetTransactionID();

                // 如果是Tcp,这里需要准备第二个重用的Socket
                if (socket.ProtocolType == ProtocolType.Tcp)
                {
                    EnsureSocket2();
                    socket = Socket2.Socket;
                }

                rs = Query(socket, msg, remote2);
                // 如果第二服务器没响应,重试
                if (rs == null) rs = Query(socket, msg, remote2);
                if (rs != null && pub == null) pub = rs.MappedAddress;
                if (rs == null) return new StunResult(StunNetType.Blocked, pub);

                // 两次映射地址不一样,对称网络
                if (!rs.MappedAddress.Equals(pub)) return new StunResult(StunNetType.Symmetric, pub);

                // Test III
                msg.ChangeIP = false;
                msg.ChangePort = true;
                msg.ResetTransactionID();

                rs = Query(socket, msg, remote2);
                if (rs != null && pub == null) pub = rs.MappedAddress;
                // 受限
                if (rs != null) return new StunResult(StunNetType.RestrictedCone, pub);

                // 端口受限
                return new StunResult(StunNetType.PortRestrictedCone, pub);
            }
        }
        #endregion

        #region 获取公网地址
        /// <summary>获取公网地址</summary>
        /// <returns></returns>
        public IPEndPoint GetPublic()
        {
            EnsureSocket();
            var socket = Socket.Socket;
            var msg = new StunMessage();
            msg.Type = StunMessageType.BindingRequest;
            IPEndPoint ep = null;
            foreach (var item in Servers)
            {
                ep = NetHelper.ParseEndPoint(item, 3478);
                //Int32 p = item.IndexOf(":");
                //if (p > 0)
                //    ep = new IPEndPoint(NetHelper.ParseAddress(item.Substring(0, p)), Int32.Parse(item.Substring(p + 1)));
                //else
                //    ep = new IPEndPoint(NetHelper.ParseAddress(item), 3478);
                var rs = Query(socket, msg, ep);
                if (rs != null && rs.MappedAddress != null) return rs.MappedAddress;
            }
            return null;
        }
        #endregion

        #region 业务
        /// <summary>查询</summary>
        /// <param name="request"></param>
        /// <param name="remoteEndPoint"></param>
        /// <returns></returns>
        public StunMessage Query(StunMessage request, IPEndPoint remoteEndPoint)
        {
            EnsureSocket();
            return Query(Socket.Socket, request, remoteEndPoint);
        }

        static StunMessage Query(Socket socket, StunMessage request, IPEndPoint remoteEndPoint)
        {
            var ms = new MemoryStream();
            request.Write(ms);
            ms.Position = 0;
            Byte[] buffer = null;
            try
            {
                if (socket.ProtocolType == ProtocolType.Tcp)
                {
                    // Tcp协议不支持更换IP或者端口
                    if (request.ChangeIP || request.ChangePort) return null;

                    if (!socket.Connected) socket.Connect(remoteEndPoint);
                    socket.Send(ms.ToArray());
                }
                else
                    socket.SendTo(ms.ToArray(), remoteEndPoint);

                var data = new Byte[150];
                Int32 count = socket.Receive(data);
                buffer = new Byte[count];
                Buffer.BlockCopy(data, 0, buffer, 0, count);
            }
            catch { return null; }

            var rs = StunMessage.Read(new MemoryStream(buffer));
            //if (rs != null && rs.Type != StunMessageType.BindingResponse) return null;
            if (rs == null) return null;

            // 不是同一个会话不要
            if (rs.TransactionID.CompareTo(request.TransactionID) != 0) return null;
            // 不是期望的响应不要
            if (rs.Type != (StunMessageType)((UInt16)request.Type | 0x0100)) return null;
            return rs;
        }
        #endregion
    }
}