一.引言
在最近的工作當中,用到了 Socket 通信,然后要給 Socket 服務器端的監聽獲取一個空閑的本地監聽端口。
對於這個獲取方法要滿足如下幾點的要求:
- 這個端口不能是別的程序所使用的端口;
- 這個獲取要支持異步,即多個線程同時獲取不會出現返回多個相同的空閑端口(即線程安全);
- 這端口要有效的遍歷一個區域內的端口,直到返回一個可用的空閑端口;
二.實現方法
網上的實現方法主要有兩種:
1. 使用 .NET 提供的 IPGlobaProperties.GetIPGlobaProperties() 來獲得一個 IPGlobaProperties 對象,然后通過它的成員函數 GetActiveTcpListeners()、GetActiveUdpListeners() 以及 GetActiveTcpConnections() 來獲得被連接或者被監聽所使用了的端口,進而刷選出空閑的端口:
//獲取本地計算機的網絡連接和通信統計數據的信息 IPGlobalProperties ipProperties = IPGlobalProperties.GetIPGlobalProperties(); //返回本地計算機上的所有Tcp監聽程序 IPEndPoint[] ipEndPoints = ipProperties.GetActiveTcpListeners(); //返回本地計算機上的所有UDP監聽程序 IPEndPoint[] ipsUDP = ipProperties.GetActiveUdpListeners(); //返回本地計算機上的Internet協議版本4(IPV4 傳輸控制協議(TCP)連接的信息 TcpConnectionInformation[] tcpConnInfoArray = ipProperties.GetActiveTcpConnections();
2. 使用 Process 創建一個命令行進程,執行命令 " netstat -an " 來獲得所有的已經被使用的端口,我們僅僅通過 cmd 窗體輸入這個命令的輸出如下:
我們通過匹配 " :端口號 " 是不是在上面返回的數據中就可以很容易的知道端口是不是被占用。
經過測試之后發現,使用第一種方法有時候並不能檢索到部分被使用了的端口,所以最后還是使用了第一種和第二種混合的檢測方案。
三.程序代碼
通過第一種和第二種方法各查詢一次並緩存,在本次查詢中使用這個緩存(為了平衡效率與 " 在查找的時候被端口被占用 " 的問題)。於此同時,我們通過 lock 來避免異步問題,並且對於前后兩次獲取,如果前一個端口被獲取到,那么我們之后的端口就從前一個的后面那個開始做查詢。
下面是程序的核心代碼:
public static class IPAndPortHelper { #region 成員字段 /// <summary> /// 同步鎖 /// 用來在獲得端口的時候同步兩個線程 /// </summary> private static object inner_asyncObject = new object(); /// <summary> /// 開始的端口號 /// </summary> private static int inner_startPort = 50001; #endregion #region 獲得本機所使用的端口 /// <summary> /// 使用 IPGlobalProperties 對象獲得本機使用的端口 /// </summary> /// <returns>本機使用的端口列表</returns> private static List<int> GetPortIsInOccupiedState() { List<int> retList = new List<int>(); //遍歷所有使用的端口,是不是與當前的端口有匹配 try { //獲取本地計算機的網絡連接和通信統計數據的信息 IPGlobalProperties ipProperties = IPGlobalProperties.GetIPGlobalProperties(); //返回本地計算機上的所有Tcp監聽程序 IPEndPoint[] ipEndPoints = ipProperties.GetActiveTcpListeners(); //返回本地計算機上的所有UDP監聽程序 IPEndPoint[] ipsUDP = ipProperties.GetActiveUdpListeners(); //返回本地計算機上的Internet協議版本4(IPV4 傳輸控制協議(TCP)連接的信息 TcpConnectionInformation[] tcpConnInfoArray = ipProperties.GetActiveTcpConnections(); //將使用的端口加入 retList.AddRange(ipEndPoints.Select(m => m.Port)); retList.AddRange(ipsUDP.Select(m => m.Port)); retList.AddRange(tcpConnInfoArray.Select(m => m.LocalEndPoint.Port)); retList.Distinct();//去重 } catch(Exception ex)//直接拋出異常 { throw ex; } return retList; } /// <summary> /// 使用 NetStat 命令獲得端口的字符串 /// </summary> /// <returns>端口的字符串</returns> private static string GetPortIsInOccupiedStateByNetStat() { string output = string.Empty; try { using (Process process = new Process()) { process.StartInfo = new ProcessStartInfo("netstat", "-an"); process.StartInfo.CreateNoWindow = true; process.StartInfo.UseShellExecute = false; process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; process.StartInfo.RedirectStandardOutput = true; process.Start(); output = process.StandardOutput.ReadToEnd().ToLower(); } } catch(Exception ex) { throw ex; } return output; } #endregion #region 獲得一個當前沒有被使用過的端口號 /// <summary> /// 獲得一個當前沒有被使用過的端口號 /// </summary> /// <returns>當前沒有被使用過的端口號</returns> public static int GetUnusedPort() { /* * 在端口獲取的時候防止兩個進程同時獲得一個一樣的端口號 * 在一個線程獲得一個端口號的時候,下一個線程獲取會從上一個線程獲取的端口號+1開始查詢 */ lock (inner_asyncObject)//線程安全 { List<int> portList = GetPortIsInOccupiedState(); string portString = GetPortIsInOccupiedStateByNetStat(); for (int i = inner_startPort; i < 60000; i++) { if (portString.IndexOf(":" + inner_startPort) < 0 && !portList.Contains(inner_startPort)) { //記錄一下 下次的端口查詢從 inner_startPort+1 開始 inner_startPort = i + 1; return i; } } //如果獲取不到 return -1; } } #endregion }
測試代碼:
Console.WriteLine(IPAndPortHelper.GetUnusedPort());
Console.WriteLine(IPAndPortHelper.GetUnusedPort());
測試結果圖: