在最近负责的项目中,需要实现这样一个需求:在客户端程序中,扫描当前机器所在网段中的所有机器上是否有某服务启动,并把所有已经启动服务的机器列出来,供用户选择,连接哪个服务。注意:这里所说的服务事实上就是在一个固定的端口监听基于 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; ???????} ???}
代码中注释基本上已经比较详细,这里再简单提几个点:
- 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 中,扫描局域网服务的实现
原文地址:https://www.cnblogs.com/wpinfo/p/serverscan.html