在.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中扫描局域网服务的实现方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。
上一篇: 详解node.js中的npm和webpack配置方法
下一篇: 浅析正则表达式 元字符和普通字符
推荐阅读