C# 基本練習 - 識別 IP 所屬網段 (物件導向寫法)
0 |
專案上的小需求,公司內網依實體網路架構區分了多個網段,系統有網段清單,已知不同 CIDR (Classless Inter-Domain Routing) 格式(例如:192.168.1.0/24、10.0.0.0/8) 對映的代碼及說明。系統在接收到任一 IP 地址時,需識別出其隸屬哪一個網段。有個小眉角是子網段範圍有大小之分,例如同時定義了 10.0.0.0/8 以及 10.10.0.0/16,要能識別除了 10.10.* 以外的 10.0-255.* 是前者,10.10.* 為後者。
這邏輯說來不難,但我想用物件導向一點的方式來寫。
傳統平鋪直述寫法應該會寫一個迴圈,用 IP 跟每個網段 CIDR 相比,檢查套用遮罩後的結果是否相符,然後要注意兩個範圍不同網段重疊時以範圍小的優先... 等細節,程式寫起來類似這樣:
var netZones = new string[] {
"10.0.0.0/8\tSvr-ALL\t10 網段",
"10.10.0.0/16\tSvr-10\t10.10 網段",
"172.16.0.8/12\tOffice-ALL\t172.16-31 網段",
"172.17.0.0/16\tOffice-17\t172.17 網段",
"192.168.0.0/16\tNetDevice\t192.168 網段"
};
Action<string> test = (ip) =>
{
var maxMaskBits = 0;
var matchNetZone = "Undefined";
var ipUint = IPToUint(ip);
foreach (var netZone in netZones)
{
var p = netZone.Split('\t');
var cidr = p[0];
var zoneCode = p[1];
var comment = p[2];
p = cidr.Split('/');
var maskBits = int.Parse(p[1]);
var netUint = IPToUint(p[0]) & (uint.MaxValue << (32 - maskBits));
if ((ipUint & (uint.MaxValue << (32 - maskBits))) == netUint)
{
if (maskBits > maxMaskBits) {
maxMaskBits = maskBits;
matchNetZone = netZone;
}
}
}
Console.WriteLine($"{ip,-15} => {matchNetZone}");
};
test("10.10.123.123");
test("10.123.123.123");
test("192.168.1.1");
test("172.28.1.1");
test("172.16.1.1");
test("172.17.1.1");
test("8.8.8.8");
static uint IPToUint(string ip)
{
var segments = ip.Split('.');
if (segments.Length != 4) return 0;
try
{
return (uint.Parse(segments[0]) << 24)
| (uint.Parse(segments[1]) << 16)
| (uint.Parse(segments[2]) << 8)
| uint.Parse(segments[3]);
}
catch (Exception)
{
return 0;
}
}
如果用物件導向寫法,我們可讓每個網段是一個 NetworkZone 物件,用 bool Match(string ip) 判斷是否屬於該網段;而所有 NetworkZone 集合也寫成物件 - NetworkZoneTable,提供一個 NetworkZone(string ip) 方法,傳入任意 IP 可判斷其屬於何網段。
使用起來會像這樣:
var netZones = new NetworkZoneTable
{
new NetworkZone("10.0.0.0/8", "Svr-ALL", "10 網段"),
new NetworkZone("10.10.0.0/16", "Svr-10", "10.10 網段"),
new NetworkZone("172.16.0.8/12", "Office-ALL", "172.16-31 網段"),
new NetworkZone("172.17.0.0/16", "Office-17", "172.17 網段"),
new NetworkZone("192.168.0.0/16", "NetDevice", "192.168 網段")
};
Action<string> test = (ip) =>
{
var netZone = netZones.Match(ip);
Console.WriteLine($"{ip,-15} => {netZone}");
};
test("10.10.123.123");
test("10.123.123.123");
test("192.168.1.1");
test("172.28.1.1");
test("172.16.1.1");
test("172.17.1.1");
test("8.8.8.8");
NetworkZone 及 NetworkZoneTable 寫法如下:
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
namespace netmask_lab
{
/// <summary>
/// 網段定義
/// </summary>
public class NetworkZone
{
[JsonIgnore]
/// <summary>
/// IP 位址
/// </summary>
public string IP = "0.0.0.0";
[JsonIgnore]
/// <summary>
/// 子網路遮罩位元數
/// </summary>
public int MaskBits = 32;
[JsonIgnore]
/// <summary>
/// IP 位址轉為 uint
/// </summary>
public uint UintVal = 0;
/// <summary>
/// 是否為未定義網段
/// </summary>
public bool IsUndefined => UintVal == 0 && MaskBits == 32;
/// <summary>
/// 子網段識別(CIDR)
/// </summary>
public string Cidr { get; set; } = "0.0.0.0/32";
/// <summary>
/// 網段代碼
/// </summary>
public string ZoneCode { get; set; } = "NA";
/// <summary>
/// 網段說明
/// </summary>
public string Comment { get; set; } = "Undefined";
/// <summary>
/// 將 IP 轉為 uint,方便 Mask 計算
/// </summary>
/// <param name="ip"></param>
/// <returns></returns>
public static uint IPToUint(string ip)
{
var segments = ip.Split('.');
if (segments.Length != 4) return 0;
try
{
return (uint.Parse(segments[0]) << 24)
| (uint.Parse(segments[1]) << 16)
| (uint.Parse(segments[2]) << 8)
| uint.Parse(segments[3]);
}
catch (Exception)
{
return 0;
}
}
void Init()
{
if (!Regex.IsMatch(Cidr, @"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}$"))
throw new Exception("CIDR format error.");
IP = Cidr.Split('/').First();
MaskBits = int.Parse(Cidr.Split('/').Last());
UintVal = IPToUint(IP) & (uint.MaxValue << (32 - MaskBits));
}
public NetworkZone()
{
Init();
}
public NetworkZone(string cidr, string zoneCode, string comment)
{
this.Cidr = cidr;
this.ZoneCode = zoneCode;
this.Comment = comment;
Init();
}
public bool Match(uint ip) => (ip & (uint.MaxValue << (32 - MaskBits))) == UintVal;
public bool Match(string ip) => Match(IPToUint(ip));
override public string ToString()
{
if (IsUndefined) return Comment;
return $"{Cidr} ({ZoneCode} {Comment})";
}
}
public class NetworkZoneTable : List<NetworkZone>
{
public NetworkZone Match(string ip)
{
var ipUint = NetworkZone.IPToUint(ip);
return this.OrderByDescending(o => o.MaskBits)
.ThenBy(o => o.IP).FirstOrDefault(z => z.Match(ipUint)) ?? new NetworkZone();
}
}
}
補充幾個小地方:
- 網段判別主要靠兩個 IP 套用網段遮罩後比對是否一致。我採用的做法是將 IP 的四個 0-255 轉成 Unsigned Integer 的四個 Bytes,用
Uint_Value_of_IP & uint.MaxValue << (32 - MaskBits)
套用遮罩。 - 每個 NetworkZone 有個 public bool Match(uint ip),依序比對遇到 true 時代表該 IP 屬於此網段。由於要先比範圍小的再比範圍大的,故比對順序會依
.OrderByDescending(o => o.MaskBits).ThenBy(o => o.IP)
排序。 - NetworkZoneTable 骨子裡是個 IList<NetworkZone>,只差在多了一個 public NetworkZone Match(string ip)。
public class NetworkZoneTable : List<NetworkZone>
繼承 List<T> 可直接獲得 List<NetworkZone> 的所有屬性、方法,如開始的程式碼,能用 new NetworkZoneTable { new NetworkZone(...), new NetworkZone(...) } 以集合初始設定式指定內容。
A requirement to compare CIDR and find out the subnet it belongs to, implemented using C# object-oriented approach.
Comments
Be the first to post a comment