1. 程式人生 > >在 .NET 中,掃描局域網服務的實現

在 .NET 中,掃描局域網服務的實現

finally from hang nta connect rip 項目 ini slist

在最近負責的項目中,需要實現這樣一個需求:在客戶端程序中,掃描當前機器所在網段中的所有機器上是否有某服務啟動,並把所有已經啟動服務的機器列出來,供用戶選擇,連接哪個服務。註意:這裏所說的服務事實上就是在一個固定的端口監聽基於 TCP 協議的請求的程序或者服務(如 WCF 服務)。

要實現這樣的功能,核心的一點就是在得到當前機器同網段的所有機器的 IP 後,對每一 IP 發生 TCP 連接請求,如果請求超時或者出現其它異常,則認為沒有服務,反之,如果能夠正常連接,則認為服務正常。

經過基本功能的實現以及後續的重構之後,就有了本文以下的代碼:一個接口和具體實現的類。需要說明的是:在下面的代碼中,先提到接口,再提到具體類;而在開發過程中,則是首先創建了類,然後才提取了接口。之所以要提取接口,原因有二:一是可以支持 IoC控制反轉;二是將來如果其它的同類需求,可以其於此接口實現新功能。

一、接口定義

先看來一下接口:

    /// <summary>
    /// 掃描服務
    /// </summary>
    public interface IServerScanner
    {
        /// <summary>
        /// 掃描完成
        /// </summary>
        event EventHandler<List<ConnectionResult>> OnScanComplete;

        /// <summary>
        /// 報告掃描進度
        
/// </summary> event EventHandler<ScanProgressEventArgs> OnScanProgressChanged; /// <summary> /// 掃描端口 /// </summary> int ScanPort { get; set; } /// <summary> /// 單次連接超時時長 /// </summary> TimeSpan Timeout { get; set
; } /// <summary> /// 返回指定的IP與端口是否能夠連接上 /// </summary> /// <param name="ipAddress"></param> /// <param name="port"></param> /// <returns></returns> bool IsConnected(IPAddress ipAddress, int port); /// <summary> /// 返回指定的IP與端口是否能夠連接上 /// </summary> /// <param name="ip"></param> /// <param name="port"></param> /// <returns></returns> bool IsConnected(string ip, int port); /// <summary> /// 開始掃描 /// </summary> void StartScan(); }

其中 Timeout 屬性是控制每次連接請求超時的時長。

二、具體實現

再來看一下具體實現的類:

    /// <summary>
    /// 掃描結果
    /// </summary>
    public class ConnectionResult
    {
        /// <summary>
        /// IPAddress 地址
        /// </summary>
        public IPAddress Address { get; set; }

        /// <summary>
        /// 是否可連接上
        /// </summary>
        public bool CanConnected { get; set; }
    }

    /// <summary>
    /// 掃描完成事件參數
    /// </summary>
    public class ScanCompleteEventArgs
    {
        /// <summary>
        /// 結果集合
        /// </summary>
        public List<ConnectionResult> Reslut { get; set; }
    }

    /// <summary>
    /// 掃描進度事件參數
    /// </summary>
    public class ScanProgressEventArgs
    {
        /// <summary>
        /// 進度百分比
        /// </summary>
        public int Percent { get; set; }
    }

    /// <summary>
    /// 掃描局域網中的服務
    /// </summary>
    public class ServerScanner : IServerScanner
    {
        /// <summary>
        /// 同一網段內 IP 地址的數量
        /// </summary>
        private const int SegmentIpMaxCount = 255;

        private DateTimeOffset _endTime;
        private object _locker = new object();
        private SynchronizationContext _originalContext = SynchronizationContext.Current;
        private List<ConnectionResult> _resultList = new List<ConnectionResult>();
        private DateTimeOffset _startTime;

        /// <summary>
        /// 記錄調用/完成委托的數量
        /// </summary>
        private int _totalCount = 0;

        public ServerScanner()
        {
            Timeout = TimeSpan.FromSeconds(2);
        }

        /// <summary>
        /// 當掃描完成時,觸發此事件
        /// </summary>
        public event EventHandler<List<ConnectionResult>> OnScanComplete;

        /// <summary>
        /// 當掃描進度發生更改時,觸發此事件
        /// </summary>
        public event EventHandler<ScanProgressEventArgs> OnScanProgressChanged;

        /// <summary>
        /// 掃描端口
        /// </summary>
        public int ScanPort { get; set; }

        /// <summary>
        /// 單次請求的超時時長,默認為2秒
        /// </summary>
        public TimeSpan Timeout { get; set; }

        /// <summary>
        /// 使用 TcpClient 測試是否可以連上指定的 IP 與 Port
        /// </summary>
        /// <param name="ipAddress"></param>
        /// <param name="port"></param>
        /// <returns></returns>
        public bool IsConnected(IPAddress ipAddress, int port)
        {
            var result = TestConnection(ipAddress, port);
            return result.CanConnected;
        }

        /// <summary>
        /// 使用 TcpClient 測試是否可以連上指定的 IP 與 Port
        /// </summary>
        /// <param name="ip"></param>
        /// <param name="port"></param>
        /// <returns></returns>
        public bool IsConnected(string ip, int port)
        {
            IPAddress ipAddress;
            if (IPAddress.TryParse(ip, out ipAddress))
            {
                return IsConnected(ipAddress, port);
            }
            else
            {
                throw new ArgumentException("IP 地址格式不正確");
            }
        }

        /// <summary>
        /// 開始掃描當前網段
        /// </summary>
        public void StartScan()
        {
            if (ScanPort == 0)
            {
                throw new InvalidOperationException("必須指定掃描的端口 ScanPort");
            }

            // 清除可能存在的數據
            _resultList.Clear();
            _totalCount = 0;
            _startTime = DateTimeOffset.Now;

            // 得到本網段的 IP
            var ipList = GetAllRemoteIPList();

            // 生成委托列表
            List<Func<IPAddress, int, ConnectionResult>> funcs = new List<Func<IPAddress, int, ConnectionResult>>();
            for (int i = 0; i < SegmentIpMaxCount; i++)
            {
                var tmpF = new Func<IPAddress, int, ConnectionResult>(TestConnection);
                funcs.Add(tmpF);
            }

            // 異步調用每個委托
            for (int i = 0; i < SegmentIpMaxCount; i++)
            {
                funcs[i].BeginInvoke(ipList[i], ScanPort, OnComplete, funcs[i]);
                _totalCount += 1;
            }
        }

        /// <summary>
        /// 得到本網段的所有 IP
        /// </summary>
        /// <returns></returns>
        private List<IPAddress> GetAllRemoteIPList()
        {
            var localName = Dns.GetHostName();
            var localIPEntry = Dns.GetHostEntry(localName);

            List<IPAddress> ipList = new List<IPAddress>();

            IPAddress localInterIP = localIPEntry.AddressList.FirstOrDefault(m => m.AddressFamily == AddressFamily.InterNetwork);
            if (localInterIP == null)
            {
                throw new InvalidOperationException("當前計算機不存在內網 IP");
            }

            var localInterIPBytes = localInterIP.GetAddressBytes();
            for (int i = 1; i <= SegmentIpMaxCount; i++)
            {
                // 對末位進行替換
                localInterIPBytes[3] = (byte)i;
                ipList.Add(new IPAddress(localInterIPBytes));
            }

            return ipList;
        }

        private void OnComplete(IAsyncResult ar)
        {
            var state = ar.AsyncState as Func<IPAddress, int, ConnectionResult>;
            var result = state.EndInvoke(ar);

            lock (_locker)
            {
                // 添加到結果中
                _resultList.Add(result);

                // 報告進度
                _totalCount -= 1;
                var percent = (SegmentIpMaxCount - _totalCount) * 100 / SegmentIpMaxCount;

                if (SynchronizationContext.Current == _originalContext)
                {
                    OnScanProgressChanged?.Invoke(this, new ScanProgressEventArgs { Percent = percent });
                }
                else
                {
                    _originalContext.Post(conState =>
                    {
                        OnScanProgressChanged?.Invoke(this, new ScanProgressEventArgs { Percent = percent });
                    }, null);
                }

                if (_totalCount == 0)
                {
                    // 通過事件拋出結果
                    if (SynchronizationContext.Current == _originalContext)
                    {
                        OnScanComplete?.Invoke(this, _resultList);
                    }
                    else
                    {
                        _originalContext.Post(conState =>
                        {
                            OnScanComplete?.Invoke(this, _resultList);
                        }, null);
                    }

                    // 計算耗時
                    Debug.WriteLine("Compete");
                    _endTime = DateTimeOffset.Now;
                    Debug.WriteLine($"Duration: {_endTime - _startTime}");
                }
            }
        }

        /// <summary>
        /// 測試是否可以連接到
        /// </summary>
        /// <param name="address"></param>
        /// <param name="port"></param>
        /// <returns></returns>
        private ConnectionResult TestConnection(IPAddress address, int port)
        {
            TcpClient c = new TcpClient();

            ConnectionResult result = new ConnectionResult();
            result.Address = address;
            using (TcpClient tcp = new TcpClient())
            {
                IAsyncResult ar = tcp.BeginConnect(address, port, null, null);
                WaitHandle wh = ar.AsyncWaitHandle;
                try
                {
                    if (!ar.AsyncWaitHandle.WaitOne(Timeout, false))
                    {
                        tcp.Close();
                    }
                    else
                    {
                        tcp.EndConnect(ar);
                        result.CanConnected = true;
                    }
                }
                catch
                {
                }
                finally
                {
                    wh.Close();
                }
            }

            return result;
        }
    }

代碼中註釋基本上已經比較詳細,這裏再簡單提幾個點:

  1. TestConnection 函數實了現核心功能,即請求給定的 IP 和端口,並返回結果;其中通過調用 IAsyncResult.AsyncWaitHandle 屬性的 WaitOne 方法來實現對超時的控制;
  2. StartScan 方法中,在得到 IP 列表後,通過生成委托列表並異步調用這些委托來實現整體操作是異步的,不會阻塞 UI,而這些委托指向的方法就是 TestConnection 函數;
  3. 使用同步上下文 SynchronizationContext,可以保證調用方在原來的線程(通常是 UI 線程)上處理進度更新事件或掃描完成事件;
  4. 對於每個委托異步完成後,會執行回調方法 OnComplete,在它裏面,對全局變量的操作需要加鎖,以保證線程安全。

三、如何使用

最後來看一下如何使用,非常簡單:

        private void View_Loaded()
        {
            // 在界面 Load 事件中添加以下代碼
            ServerScanner.OnScanComplete += ServerScanner_OnScanComplete;
            ServerScanner.OnScanProgressChanged += ServerScanner_OnScanProgressChanged;

            // 掃描的端口號
            ServerScanner.ScanPort = 7890;
        }

        private void StartScan()
        {
            // 開始掃描
            ServerScanner.StartScan();
        }


        private void ServerScanner_OnScanComplete(object sender, List<ConnectionResult> e)
        {
            ...
        }

        private void ServerScanner_OnScanProgressChanged(object sender, ScanProgressEventArgs e)
        {
            ...
        }

如果你有更好的建議或意見,請留言互相交流。

在 .NET 中,掃描局域網服務的實現