在最近負(fù)責(zé)的項(xiàng)目中,需要實(shí)現(xiàn)這樣一個需求:在客戶端程序中,掃描當(dāng)前機(jī)器所在網(wǎng)段中的所有機(jī)器上是否有某服務(wù)啟動,并把所有已經(jīng)啟動服務(wù)的機(jī)器列出來,供用戶選擇,連接哪個服務(wù)。注意:這里所說的服務(wù)事實(shí)上就是在一個固定的端口監(jiān)聽基于 TCP 協(xié)議的請求的程序或者服務(wù)(如 WCF 服務(wù))。
要實(shí)現(xiàn)這樣的功能,核心的一點(diǎn)就是在得到當(dāng)前機(jī)器同網(wǎng)段的所有機(jī)器的 IP 后,對每一 IP 發(fā)生 TCP 連接請求,如果請求超時或者出現(xiàn)其它異常,則認(rèn)為沒有服務(wù),反之,如果能夠正常連接,則認(rèn)為服務(wù)正常。
經(jīng)過基本功能的實(shí)現(xiàn)以及后續(xù)的重構(gòu)之后,就有了本文以下的代碼:一個接口和具體實(shí)現(xiàn)的類。需要說明的是:在下面的代碼中,先提到接口,再提到具體類;而在開發(fā)過程中,則是首先創(chuàng)建了類,然后才提取了接口。之所以要提取接口,原因有二:一是可以支持 IoC控制反轉(zhuǎn);二是將來如果其它的同類需求,可以其于此接口實(shí)現(xiàn)新功能。
一、接口定義
先看來一下接口:
/// <summary> /// 掃描服務(wù) /// </summary> public interface IServerScanner { /// <summary> /// 掃描完成 /// </summary> event EventHandler<List<ConnectionResult>> OnScanComplete; /// <summary> /// 報告掃描進(jìn)度 /// </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 屬性是控制每次連接請求超時的時長。
二、具體實(shí)現(xiàn)
再來看一下具體實(shí)現(xiàn)類:
/// <summary> /// 掃描結(jié)果 /// </summary> public class ConnectionResult { /// <summary> /// IPAddress 地址 /// </summary> public IPAddress Address { get; set; } /// <summary> /// 是否可連接上 /// </summary> public bool CanConnected { get; set; } } /// <summary> /// 掃描完成事件參數(shù) /// </summary> public class ScanCompleteEventArgs { /// <summary> /// 結(jié)果集合 /// </summary> public List<ConnectionResult> Reslut { get; set; } } /// <summary> /// 掃描進(jìn)度事件參數(shù) /// </summary> public class ScanProgressEventArgs { /// <summary> /// 進(jìn)度百分比 /// </summary> public int Percent { get; set; } } /// <summary> /// 掃描局域網(wǎng)中的服務(wù) /// </summary> public class ServerScanner : IServerScanner { /// <summary> /// 同一網(wǎng)段內(nèi) IP 地址的數(shù)量 /// </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> /// 記錄調(diào)用/完成委托的數(shù)量 /// </summary> private int _totalCount = 0; public ServerScanner() { Timeout = TimeSpan.FromSeconds(2); } /// <summary> /// 當(dāng)掃描完成時,觸發(fā)此事件 /// </summary> public event EventHandler<List<ConnectionResult>> OnScanComplete; /// <summary> /// 當(dāng)掃描進(jìn)度發(fā)生更改時,觸發(fā)此事件 /// </summary> public event EventHandler<ScanProgressEventArgs> OnScanProgressChanged; /// <summary> /// 掃描端口 /// </summary> public int ScanPort { get; set; } /// <summary> /// 單次請求的超時時長,默認(rèn)為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> /// 開始掃描當(dāng)前網(wǎng)段 /// </summary> public void StartScan() { if (ScanPort == 0) { throw new InvalidOperationException("必須指定掃描的端口 ScanPort"); } // 清除可能存在的數(shù)據(jù) _resultList.Clear(); _totalCount = 0; _startTime = DateTimeOffset.Now; // 得到本網(wǎng)段的 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); } // 異步調(diào)用每個委托 for (int i = 0; i < SegmentIpMaxCount; i++) { funcs[i].BeginInvoke(ipList[i], ScanPort, OnComplete, funcs[i]); _totalCount += 1; } } /// <summary> /// 得到本網(wǎng)段的所有 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("當(dāng)前計算機(jī)不存在內(nèi)網(wǎng) IP"); } var localInterIPBytes = localInterIP.GetAddressBytes(); for (int i = 1; i <= SegmentIpMaxCount; i++) { // 對末位進(jìn)行替換 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) { // 添加到結(jié)果中 _resultList.Add(result); // 報告進(jìn)度 _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) { // 通過事件拋出結(jié)果 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
以上代碼中注釋基本上已經(jīng)比較詳細(xì),這里再簡單提幾個點(diǎn):
TestConnection 函數(shù)實(shí)了現(xiàn)核心功能,即請求給定的 IP 和端口,并返回結(jié)果;其中通過調(diào)用 IAsyncResult.AsyncWaitHandle 屬性的 WaitOne 方法來實(shí)現(xiàn)對超時的控制;
StartScan 方法中,在得到 IP 列表后,通過生成委托列表并異步調(diào)用這些委托來實(shí)現(xiàn)整個方法是異步的,不會阻塞 UI,而這些委托指向的方法就是 TestConnection 函數(shù);
使用同步上下文 SynchronizationContext,可以保證調(diào)用方在原來的線程(通常是 UI 線程)上處理進(jìn)度更新事件或掃描完成事件;
對于每個委托異步完成后,會執(zhí)行回調(diào)方法 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中掃描局域網(wǎng)服務(wù)的實(shí)現(xiàn)方法就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持腳本之家。
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com