在 .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;
        }
    }

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

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

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

原文地址:https://www.cnblogs.com/wpinfo/p/serverscan.html

时间: 2024-11-17 04:45:15

在 .NET 中,扫描局域网服务的实现的相关文章

Kail Linux渗透测试教程之在Metasploit中扫描

Kail Linux渗透测试教程之在Metasploit中扫描 在Metasploit中扫描 在Metasploit中,附带了大量的内置扫描器.使用这些扫描器可以搜索并获得来自一台计算机或一个完整网络的服务信息.本节将介绍使用Metasploit中的辅助模块实现扫描. [实例4-4]在Metasploit中,扫描目标主机.具体操作步骤如下所示: (1)启动MSF终端.执行命令如下所示: root@kali :~# msfconsole msf> (2)搜索所有可用的扫描模块.执行命令如下所示:

微信两个二维码无法在同一视窗中扫描——每周汇总(第二期)

接到一个运营页的需求,一开始设计的是两个二维码横着排列在页面上,切完图后发现在手机上点击图片扫一扫只能扫其中的一个,一开始以为是图片的问题,后来尝试换成上下排列还是不行,查阅网上资料后才发现这是微信导致的. 微信识别二维码的原理大概是,点击屏幕时截取了屏幕,然后扫描了截图中的二维码 解决办法:两个二维码不出现在同一个屏幕里,我采取的是只加一个二维码

iOS中 扫描二维码/生成二维码详解 韩俊强的博客

最近大家总是问我有没有关于二维码的demo,为了满足大家的需求,特此研究了一番,希望能帮到大家! 每日更新关注:http://weibo.com/hanjunqiang  新浪微博 指示根视图: self.window.rootViewController = [[UINavigationController alloc]initWithRootViewController:[SecondViewController new]]; 每日更新关注:http://weibo.com/hanjunqi

从 TWAIN 设备中扫描图像

转自(http://yonsm.net/scan-images-from-a-twain-device/) 一.简介 TWAIN 数据源管理程序 (DSM) 工业标准的软件库,用于从静态图像设备提取图像.绝大部分的扫描仪和数码相机都提供了 TWAIN 驱动程序,利用统一的 TWAIN 接口,应用程序可以非常方便地从这些设备中获取图像. 二.使用步骤 互联网上关于 TWAIN 编程的中文资料很少,代码更是难找到,因为我不得不仔细阅读了 http://www.twain.org/ 提供的 TWAIN

从手机中扫描以com.xx.xxx 为前缀的apk包,使用列表的形式展现

apk 包可以使用PackageManager获取,apk 包中的资源可以获取其对应的Context,再通过Context获取对应的Resouce获取提示2: apk 是指已经安装过的程序,不是存在sdcard. 上的apk安装包. 先上效果图 开始代码部分 首先是布局文件activity_main.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android=&quo

在这个网页中扫描某个字符串

/*获取body中的内容*/ //var content =  document.getElementsByTagName("body")[0].innerHTML; /*如果body中有很多iframe那么就要循环去找了*/ var contents =  document.getElementsByTagName("iframe"); //var content = $(contents[0]).contents().find("body")

在浏览器中输入URL按下回车键后发生了什么

在浏览器中输入URL按下回车键后发生了什么 [1]解析URL[2]DNS查询,解析域名,将域名解析为IP地址[3]ARP广播,根据IP地址来解析MAC地址[4]分别从应用层到传输层.网络层和数据链路层分别加入各个层的头部封装为包[5]进行三次握手后,客户端与服务器建立连接[6]客服务器向客户端返回数据,浏览器接收到数据[7]浏览器开始渲染页面 补充:浏览器渲染页面详解 [1]由从服务器接收到的html形成DOM[2]样式被加载和解析,形成css对象模型CSSOM[3]DOM和CSSOM创建一个渲

iOS开发-二维码扫描和应用跳转

iOS开发-二维码扫描和应用跳转 序言 前面我们已经调到过怎么制作二维码,在我们能够生成二维码之后,如何对二维码进行扫描呢? 在iOS7之前,大部分应用中使用的二维码扫描是第三方的扫描框架,例如ZXing或者ZBar.使用时集成麻烦,出错也不方便调试.在iOS7之后,苹果自身提供了二维码的扫描功能,从效率上来说,原生的二维码远高于这些第三方框架.本文讲解如何使用原生框架实现二维码扫描功能,并且进行扫描后的项目跳转.ps:本期的源代码会在文章结尾给出链接 扫描相关类 二维码扫描需要获取摄像头并读取

Java如何扫描指定package下所有的类

在写一个MVC框架,需要从包中扫描出组件并注册到容器中,而JDK没有提供现成的从方法,只能自己实现. 功能: 给定一个包名,编程得到该包(和其所有子包)下所有的类文件.如,输入包名com.myapp.util, 输出该包下类的全限定名com.myapp.util.StringUtils, com.app.util.ImageUtils等. 思路: 有的web server在部署运行时会解压jar包,因此class文件会在普通的文件目录下.如果web server不解压jar包,则class文件会