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

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

TestConnection 函数实了现核心功能,即请求给定的 IP 和端口,并返回结果;其中通过调用 IAsyncResult.AsyncWaitHandle 属性的 WaitOne 方法来实现对超时的控制;

StartScan 方法中,在得到 IP 列表后,通过生成委托列表并异步调用这些委托来实现整个方法是异步的,不会阻塞 UI,而这些委托指向的方法就是 TestConnection 函数;

使用同步上下文 SynchronizationContext,可以保证调用方在原来的线程(通常是 UI 线程)上处理进度更新事件或扫描完成事件;

对于每个委托异步完成后,会执行回调方法 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中扫描局域网服务的实现方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • asp.net实现访问局域网共享目录下文件的解决方法

    本文以实例讲述了asp.net实现访问局域网共享目录下文件的解决方法,完整代码如下所示: using System; using System.Collections; using System.Configuration; using System.Data; using System.Linq; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.HtmlControls;

  • 在.NET中扫描局域网服务的实现方法

    在最近负责的项目中,需要实现这样一个需求:在客户端程序中,扫描当前机器所在网段中的所有机器上是否有某服务启动,并把所有已经启动服务的机器列出来,供用户选择,连接哪个服务.注意:这里所说的服务事实上就是在一个固定的端口监听基于 TCP 协议的请求的程序或者服务(如 WCF 服务). 要实现这样的功能,核心的一点就是在得到当前机器同网段的所有机器的 IP 后,对每一 IP 发生 TCP 连接请求,如果请求超时或者出现其它异常,则认为没有服务,反之,如果能够正常连接,则认为服务正常. 经过基本功能的实

  • Springboot中集成Swagger2框架的方法

    摘要:在项目开发中,往往期望做到前后端分离,也就是后端开发人员往往需要输出大量的服务接口,接口的提供方无论是是Java还是PHP等语言,往往会要花费一定的精力去写接口文档,比如A接口的地址.需要传递参数情况.返回值的JSON数据格式以及每一个字段说明.当然还要考虑HTTP请求头.请求内容等信息.随着项目的进度快速高速的迭代,后端输出的接口往往会面临修改.修复等问题,那也意味着接口文档也要进行相应的调整.接口文档的维护度以及可读性就大大下降. 既然接口文档需要花费精力去维护,还要适当的进行面对面交

  • Android编程实现wifi扫描及连接的方法

    本文实例讲述了Android编程实现wifi扫描及连接的方法.分享给大家供大家参考,具体如下: 主界面,搜索附近WIFI信息 /** * Search WIFI and show in ListView * */ public class MainActivity extends Activity implements OnClickListener, OnItemClickListener { private Button search_btn; private ListView wifi_l

  • 微信小程序中使用echarts的实现方法

    刚开始学微信小程序,有说的不对的地方大家可以提出! 首先体验示例小程序 在微信中扫描下面的二维码即可体验 ECharts Demo: 下载 为了兼容小程序 Canvas,我们提供了一个小程序的组件,用这种方式可以方便地使用 ECharts. 首先,下载 GitHub 上的 ecomfe/echarts-for-weixin项目. 其中,ec-canvas 是我们提供的组件,其他文件是如何使用该组件的示例. ec-canvas 目录下有一个 echarts.js,默认我们会在每次 echarts-

  • Spring Boot项目中定制拦截器的方法详解

    这篇文章主要介绍了Spring Boot项目中定制拦截器的方法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Servlet 过滤器属于Servlet API,和Spring关系不大.除了使用过滤器包装web请求,Spring MVC还提供HandlerInterceptor(拦截器)工具.根据文档,HandlerInterceptor的功能跟过滤器类似,但拦截器提供更精细的控制能力:在request被响应之前.request被响应之后.视

  • springboot多模块包扫描问题的解决方法

    问题描述: springboot建立多个模块,当一个模块需要使用另一个模块的服务时,需要注入另一个模块的组件,如下面图中例子: memberservice模块中的MemberServiceApiImpl类需要注入common模块中的RedisService组件,该怎么注入呢? 解决: 在memberservice模块的启动类上加上RedisService类所在包的全路径的组件扫描,就像这样: 注意启动类上方的注解@ComponentScan(basePackages={"com.whu.comm

  • 在SpringBoot项目中的使用Swagger的方法示例

    一. 首先Swagger是什么? Swagger 是一个规范和完整的框架,用于生成.描述.调用和可视化 RESTful 风格的 Web 服务.总体目标是使客户端和文件系统作为服务器以同样的速度来更新.文件的方法,参数和模型紧密集成到服务器端的代码,允许API来始终保持同步.Swagger官方API文档:https://swagger.io/ 作用:   1. 接口的文档在线自动生成.   2. 功能测试. Swagger的主见介绍:    Swagger Codegen: 通过Codegen 可

  • Java中使用Filter过滤器的方法

    Filter过滤器 着重记录下 public void doFilter(){} 方法参数: (1) ServletRequest servletRequest 请求reqeust: (2)ServletResponse servletResponse 响应response: (3)FilterChain filterChain 参数 filterChain,有一个doFilter() 方法,调用这个方法,可以使程序继续往下走,执行后续代码,如果有多个过滤器,它会继续执行下一个过滤器逻辑(开发中

  • MyBatis-Plus中如何使用ResultMap的方法示例

    目录 问题说明 解决方法 自定义@AutoResultMap注解 MyBatis-Plus (简称MP)是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发.提高效率而生. MyBatis-Plus对MyBatis基本零侵入,完全可以与MyBatis混合使用,这点很赞. 在涉及到关系型数据库增删查改的业务时,我比较喜欢用MyBatis-Plus,开发效率极高.具体的使用可以参考官网,或者自己上手摸索感受一下. 下面简单总结一下在MyBatis-Plus中如何使用R

  • 教你在Spring Boot微服务中集成gRPC通讯的方法

    一.首先声明gRPC接口 这里引入的是最新的gRpc-core 1.37版本, 采用的grcp-spring-boot-starter封装的版本进行实现,github地址: https://github.com/yidongnan/grpc-spring-boot-starter 要实现gRpc通讯, 先定义接口以及入参出参信息 syntax = "proto3"; option java_multiple_files = true; option java_package = &qu

随机推荐