如何降低网络IP扫描中重复IP绕过Contains判断的概率
项目背景与流程
我正在开发一个网络扫描项目,核心流程如下:
- 选择用于扫描的网络接口卡(NIC)
- 启动数据包捕获,仅提取IP地址和端口信息
- 创建
IP List列表,收集当前子网内发现的所有网络IP - 创建
host info列表,存储主机的详细信息 - 遍历
IP List执行两项判断:- 若主机已在
host info中,则跳过后续处理 - 若应用需要与该主机通信,则记录其对应属性
- 若主机已在
当前问题
我尝试通过Contains判断对IP List去重,但仍有少量重复IP出现,推测是多线程并发访问导致的竞态条件——数据包到达事件device_OnPacketArrival可能由多个线程触发,在Contains检查和Add操作之间,其他线程可能已经添加了同一个IP,导致重复IP绕过判断被加入列表。
以下是数据包到达时的代码片段:
public void device_OnPacketArrival(object sender, PacketCapture e) { var time = e.Header.Timeval.Date; var len = e.Data.Length; var rawPacket = e.GetPacket(); var packet = PacketDotNet.Packet.ParsePacket(rawPacket.LinkLayerType, rawPacket.Data); var tcpPacket = packet.Extract<PacketDotNet.TcpPacket>(); if (tcpPacket != null) { var ipPacket = (PacketDotNet.IPPacket)tcpPacket.ParentPacket; System.Net.IPAddress srcIp = ipPacket.SourceAddress; System.Net.IPAddress dstIp = ipPacket.DestinationAddress; int srcPort = tcpPacket.SourcePort; int dstPort = tcpPacket.DestinationPort; Console.WriteLine("{0}:{1}:{2},{3} Len={4} {5}:{6} -> {7}:{8}", time.Hour, time.Minute, time.Second, time.Millisecond, len, srcIp, srcPort, dstIp, dstPort); if (device_ips.Contains(srcIp.ToString())) { Console.WriteLine("the list contains the current ip"); if (InvokeRequired) { this.Invoke(new MethodInvoker(delegate { dataGridView1.Refresh(); label2.Text = MyProperty.ToString(); })); } } else { string ipinquestion = srcIp.ToString(); device_ips.Add(ipinquestion); MyProperty++; } if (device_ips.Contains(dstIp.ToString())) { Console.WriteLine("the list contains the current ip"); if (InvokeRequired) { this.Invoke(new MethodInvoker(delegate { dataGridView1.Refresh(); label2.Text = MyProperty.ToString(); Console.WriteLine("Stuff & Junk"); })); } } else { string ipinquestion = dstIp.ToString(); device_ips.Add(ipinquestion); MyProperty++; } } var udpPacket = packet.Extract<PacketDotNet.UdpPacket>(); if(udpPacket != null) { var uipPacket = (PacketDotNet.IPPacket)udpPacket.ParentPacket; System.Net.IPAddress sourceIP = uipPacket.SourceAddress; int usrcPort = udpPacket.SourcePort; int udestPort = udpPacket.DestinationPort; string myipaddress = sourceIP.ToString(); if (device_ips.Contains(myipaddress)) { Console.WriteLine("the list contains the current ip"); } else { string ipinquestion = sourceIP.ToString(); device_ips.Add(ipinquestion); if (InvokeRequired) { this.Invoke(new MethodInvoker(delegate { foreach (var i in device_ips) { listBox1.Items.Add(i.ToString()); } })); } MyProperty++; } } }
解决方法
1. 使用线程安全集合(推荐)
将普通的List<string>替换为线程安全的集合,比如.NET Core/.NET 5+内置的ConcurrentHashSet<string>,或者用ConcurrentDictionary<string, bool>(用键存储IP,值可忽略)。这类集合的添加操作是原子性的,能直接避免竞态条件。
示例(用ConcurrentDictionary):
// 初始化集合 private ConcurrentDictionary<string, bool> device_ips = new ConcurrentDictionary<string, bool>(); // 处理TCP源IP的逻辑修改 string srcIpStr = srcIp.ToString(); if (!device_ips.TryAdd(srcIpStr, true)) { // IP已存在,执行原逻辑 Console.WriteLine("the list contains the current ip"); if (InvokeRequired) { this.Invoke(new MethodInvoker(delegate { dataGridView1.Refresh(); label2.Text = MyProperty.ToString(); })); } } else { // IP为新添加,计数加1 MyProperty++; // 界面更新逻辑... }
TryAdd方法会原子性地检查IP是否存在,不存在则添加并返回true,存在则返回false,完全避免了判断和添加之间的间隙。
2. 对普通列表加锁
如果必须使用List<string>,需要在所有访问device_ips的代码块外加锁,确保同一时间只有一个线程操作集合:
// 定义锁对象,全局唯一 private readonly object _ipLock = new object(); // 处理TCP源IP时加锁 lock (_ipLock) { if (device_ips.Contains(srcIp.ToString())) { Console.WriteLine("the list contains the current ip"); if (InvokeRequired) { this.Invoke(new MethodInvoker(delegate { dataGridView1.Refresh(); label2.Text = MyProperty.ToString(); })); } } else { device_ips.Add(srcIp.ToString()); MyProperty++; } }
注意:所有涉及device_ips的Contains和Add操作(包括TCP目的IP、UDP源IP的处理)都要包裹在同一个锁对象的lock块中,否则锁的作用会失效。
3. 优化界面更新逻辑
原代码中UDP部分每次添加新IP都会遍历整个device_ips列表,将所有IP重新添加到listBox1,这不仅低效,还可能引发界面线程的问题。建议只添加新的IP:
// 替换原有的foreach遍历 if (InvokeRequired) { this.Invoke(new MethodInvoker(delegate { listBox1.Items.Add(ipinquestion); })); }
内容的提问来源于stack exchange,提问作者Loren Zimmer

