zoukankan      html  css  js  c++  java
  • 在 .NET 中,扫描局域网服务的实现

    在最近负责的项目中,需要实现这样一个需求:在客户端程序中,扫描当前机器所在网段中的所有机器上是否有某服务启动,并把所有已经启动服务的机器列出来,供用户选择,连接哪个服务。注意:这里所说的服务事实上就是在一个固定的端口监听基于 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;
            }
        }
    ServerScanner

    以上代码中注释基本上已经比较详细,这里再简单提几个点:

    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)
            {
                ...
            }

    如果你有更好的建议或意见,请留言互相交流。

  • 相关阅读:
    Vue.js 章6 v-model与表单
    Vue.js 简单购物车开发
    Vue.js实战 章五:内置指令
    Vue stage3
    Vue初接触 stage1
    前端代码的一些恶优化
    websocket介绍 以及 vue websocket使用案例
    回忆一下跨域
    如何使用Flexible这样的一库来完成H5页面的终端适配
    css 输入px单位的数值 直接转换为rem的插件
  • 原文地址:https://www.cnblogs.com/wpinfo/p/serverscan.html
Copyright © 2011-2022 走看看