C#判断一个端口是不是被占用以及返回一个空闲端口
一.引言
在最近的工作当中,用到了 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());
测试结果图: