操作ログに「どのPCから操作したか」を残したい
業務システムの操作ログには、「誰が・いつ・何をしたか」に加えて、「どのPCから操作したか」を残しておくと、あとで調べるときに役立ちます。
Webアプリでは、接続してきたPCのIPアドレスは分かります。ただ、IPアドレスだけでは、どの席のPCなのかが分かりにくいです。
操作ログの「192.168.0.15」が、誰のPCなのか分からない
そこで、IPアドレスからPCの名前(コンピューター名)を調べて、IPアドレスと一緒に記録します。IPアドレスから名前を調べることを逆引きといいます。ふつうはDNS(名前とIPアドレスを結び付ける仕組み)に問い合わせるので、DNSの逆引きともいいます。
本記事では、C#でIPアドレスからPC名を調べる方法を紹介します。自作の立替精算システムで、操作ログの「端末名」を作ったときに調べたことをもとにしています。
立替精算システムで使ったところ

操作ログ照会の画面です。IPの列の右に、端末名(PCの名前)を出しています。
私が自作した立替精算システムでは、サーバと同じPCのブラウザから操作したときは、IPアドレスが「::1」や「127.0.0.1」になります。この場合は逆引きをせずに、サーバ自身のコンピューター名を記録しています。社内のほかのPCからの操作は、IPアドレスから名前を逆引きしています。引けなかったときは空にしています。
考え方
- 「::1」「127.0.0.1」(同じPC)なら、このPCの名前を返す
- 一度引いた結果があれば、それを返す(1時間覚えておく)
- 逆引きする。待つのは短い時間(0.3秒)だけ
- 引けなかったら空にする(推測で埋めない)
- 0.3秒で間に合わなかったときは、裏で続く問い合わせが終わったら、その結果を覚え直す
操作のたびに逆引きの応答を待っていると、画面の動きが遅くなります。PCの名前はめったに変わらないので、「覚えておく」「待つ時間を決める」の2つで、画面を遅くしないようにしています。
C#でIPアドレスからPC名を調べるサンプルコード
動作確認は Windows 11・.NET 10 です(.NET 8 以降で動きます)。Visual Studio でコンソールアプリを作り、Program.cs に貼り付けて実行してください。.NET 10 なら、ファイルに保存して dotnet run ファイル名.cs でも実行できます。
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
// 調べる IP アドレス
var ipList = new List<IPAddress> { IPAddress.IPv6Loopback, IPAddress.Loopback };
// この PC 自身の IP アドレス(IPv4)を 1 つ加える
ipList.AddRange((await Dns.GetHostAddressesAsync(Dns.GetHostName()))
.Where(x => x.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(x))
.Take(1));
// 社内に無い IP アドレス(名前は引けない)
ipList.Add(IPAddress.Parse("192.168.250.250"));
var tanmatsu = new TanmatsuNm();
foreach (var ip in ipList)
{
var sw = Stopwatch.StartNew();
var nm = await tanmatsu.GetAsync(ip);
Console.WriteLine($"{ip,-16} → {nm ?? "(引けない)",-20} {sw.ElapsedMilliseconds,4} ミリ秒");
}
// 2 回目は覚えておいた結果を返すので、すぐに終わる
var sw2 = Stopwatch.StartNew();
await tanmatsu.GetAsync(IPAddress.Parse("192.168.250.250"));
Console.WriteLine($"2 回目(192.168.250.250):{sw2.ElapsedMilliseconds} ミリ秒");
/// <summary>IP アドレスから PC の名前(コンピューター名)を引く。</summary>
public sealed class TanmatsuNm
{
/// <summary>逆引きを待つ時間。これを過ぎたら名前は空にする。</summary>
public static readonly TimeSpan Machijikan = TimeSpan.FromMilliseconds(300);
/// <summary>引いた結果を覚えておく時間。</summary>
public static readonly TimeSpan Oboeru = TimeSpan.FromHours(1);
private readonly ConcurrentDictionary<string, (string? Nm, DateTime Kigen)> kioku = new();
public async Task<string?> GetAsync(IPAddress? ip, CancellationToken cancellationToken = default)
{
if (ip is null)
{
return null;
}
// 同じ PC からの接続(::1・127.0.0.1)は、この PC の名前
if (IPAddress.IsLoopback(ip))
{
return Environment.MachineName;
}
// 覚えている結果があれば、それを返す
var key = ip.ToString();
if (kioku.TryGetValue(key, out var oboe) && oboe.Kigen > DateTime.UtcNow)
{
return oboe.Nm;
}
var toiawase = Dns.GetHostEntryAsync(key);
try
{
// 待つのは短い時間だけ。逆引きは CancellationToken では途中で止まらなかったので、
// WaitAsync で「待つのをやめる」
var nm = Namae(await toiawase.WaitAsync(Machijikan, cancellationToken));
Nokosu(key, nm);
return nm;
}
catch (TimeoutException)
{
// 待ちすぎた。今回は空にするが、問い合わせは裏で続いているので、
// 終わったらその結果を覚え直す(次の操作から名前が入る)
Nokosu(key, null);
_ = toiawase.ContinueWith(
t => Nokosu(key, t.Exception is null ? Namae(t.Result) : null),
TaskScheduler.Default);
return null;
}
catch (OperationCanceledException)
{
// 呼び出し元が取り消した(画面を閉じたなど)。引けなかったわけではないので覚えない
return null;
}
catch (Exception)
{
// 引けない(SocketException など)
Nokosu(key, null);
return null;
}
}
private void Nokosu(string key, string? nm) => kioku[key] = (nm, DateTime.UtcNow + Oboeru);
// 逆引きできないと、IP アドレスがそのまま返ることがある。名前ではないので空にする
private static string? Namae(IPHostEntry entry) =>
string.IsNullOrWhiteSpace(entry.HostName) || IPAddress.TryParse(entry.HostName, out _)
? null
: entry.HostName;
}実行結果
::1 → SAMPLE-PC 3 ミリ秒
127.0.0.1 → SAMPLE-PC 1 ミリ秒
192.168.0.10 → SAMPLE-PC 4 ミリ秒
192.168.250.250 → (引けない) 311 ミリ秒
2 回目(192.168.250.250):0 ミリ秒「SAMPLE-PC」「192.168.0.10」は例です。実行したPCの名前とIPアドレスが出ます。ミリ秒の数字も、実行するたびに少し変わります。
3行目は、実行したPC自身のIPアドレスです。自分のアドレスなので、すぐにこのPCの名前が返ります(ネットワークにつながっていないPCでは、3行目は出ません)。
社内に無いIPアドレス(192.168.250.250)は名前が引けず、0.3秒で待つのをやめています。2回目は覚えておいた結果を返すので、待ち時間はありません。
コードの説明
IPAddress.IsLoopbackで、「::1」「127.0.0.1」かどうかを調べています。同じPCならEnvironment.MachineNameでコンピューター名を返します。Dns.GetHostEntryAsync(IPアドレスの文字列)で逆引きします。結果のHostNameがPCの名前です。「pc01.example.local」のように、後ろにドメイン名などの付いた名前で返ることもあります。- このPC(Windows 11・.NET 10)で試すと、名前が引けないときは
SocketException(そのようなホストは不明です)になりました。ただ、公式ドキュメントには、IPv6のアドレスではHostNameにIPアドレスの文字列がそのまま入ることがある、と書かれています。名前ではないので、IPAddress.TryParseで読めるものは空にしています。 WaitAsync(Machijikan, cancellationToken)は、指定した時間を過ぎるとTimeoutExceptionを出して待つのをやめます。問い合わせそのものは止まらずに裏で続くので、ContinueWithで、終わったときに結果を覚え直しています。次に同じPCから操作したときは、名前が入ります。- 呼び出し元が取り消したとき(
OperationCanceledException)は、名前が引けなかったわけではないので、結果を覚えません。次の操作で、もう一度引きます。 - Webアプリでは同時に何人も操作するので、覚えておく辞書には、複数の処理から同時に使っても壊れない
ConcurrentDictionaryを使っています。
CancellationTokenでは止まらなかった
待つ時間を決める書き方として、CancellationTokenSource の CancelAfter で取り消す方法もよく見かけます。
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromMilliseconds(300));
var entry = await Dns.GetHostEntryAsync(ip.ToString(), cts.Token);ところが、この書き方で計ってみると、0.3秒で止まらず、逆引きが終わるまで待っていました。このPC(Windows 11・.NET 10)で、0.3秒で待つのをやめる WaitAsync の書き方と比べた結果です。
| IPアドレス | CancelAfter(0.3秒) | WaitAsync(0.3秒) |
|---|---|---|
| 192.0.2.1(説明用に決められていて、実際には使われないアドレス) | 約4.6秒待って、引けない | 約0.3秒で空(裏の問い合わせは約4.6秒で終わる) |
| 192.168.0.250(同じネットワークの、使われていないアドレス) | 約9.5秒待って、引けない | 約0.3秒で空(裏の問い合わせは約9.5秒で終わる) |
| 192.168.0.101(DNSに登録の無いNAS) | 約5秒待って、名前が引ける | 約0.3秒で空(裏の問い合わせは約5秒で名前が返る) |
このPCでは、CancelAfter で取り消しても、IPアドレスからの逆引きは最後まで止まりませんでした。
そこでサンプルコードでは、WaitAsync で「待つのをやめる」形にしています。問い合わせ自体は裏で続きますが、画面の処理は0.3秒で先に進めます。
裏で続く問い合わせは、終わるまでスレッド(処理を動かす係)を1つずつ使います。このPCで、名前の引けないIPアドレス20件を同時に問い合わせると、スレッドが21本に増え、約7秒で全部終わりました。サンプルコードは同じIPアドレスの結果を1時間覚えておくので、同じPCへの問い合わせは、ふつうは1時間に1回で済みます。
ASP.NET Coreで使うとき
// Program.cs:覚えた結果を持ち続けるので、1 つを使い回す
builder.Services.AddSingleton<TanmatsuNm>();
// 画面の処理の中:接続してきた PC の IP アドレスから名前を引く
var ip = HttpContext.Connection.RemoteIpAddress;
var tanmatsuNm = await tanmatsu.GetAsync(ip, HttpContext.RequestAborted);接続してきたPCのIPアドレスは、HttpContext.Connection.RemoteIpAddress で取れます。TanmatsuNm は引いた結果を覚えておくので、AddSingleton で登録して1つを使い回します。画面の処理では、コンストラクターで TanmatsuNm を受け取って tanmatsu として使います。
HttpContext.RequestAborted を渡しているので、利用者が画面を閉じるなどして要求が取り消されたときは、待つのをやめて空を返します。
注意点
- 名前が引けるかどうかは、社内のネットワークの作りしだいです。社内のDNSに逆引きの登録(PTRレコード)があれば、すぐに引けます。このPCでは、ルーターのDNSに登録のあるアドレスは0.03秒以内に引けました。
- DNSに登録が無くても、Windowsが同じネットワークの中で名前を尋ねる仕組み(LLMNR・mDNS・NetBIOSなど)で見つけることがあります。このPCでは、DNSに登録の無いNASの名前が、約5秒かかって返ってきました。0.3秒では間に合わないので、サンプルコードでは1回目の操作は空になり、裏の問い合わせが終わったあとの操作から名前が入ります。同じネットワークのほかの3台の機器は、名前が引けませんでした。
- VPNやルーターを通った接続では、PCではなくルーターのIPアドレスに見えることがあります。
- リバースプロキシ(nginxなど)やロードバランサの後ろで動かすと、
RemoteIpAddressはプロキシのIPアドレスになります。プロキシが同じサーバの中にあると「127.0.0.1」になり、すべての操作がサーバ自身の名前で記録されてしまいます。元のIPアドレスはX-Forwarded-Forヘッダーで届くので、UseForwardedHeadersでRemoteIpAddressに入れ直します。ヘッダーは偽れるので、信用するプロキシを決めておきます(既定では、同じPCのプロキシだけを信用します)。IISの後ろで動かすとき(アウトオブプロセス)は、この設定が自動で入ります。立替精算システムは、プロキシを通さずに直接つなぐ作りです。 - DHCP(IPアドレスを自動で割り当てる仕組み)でIPアドレスが変わる社内では、覚えている1時間のうちに同じIPアドレスが別のPCに移ると、前のPCの名前で記録されることがあります。覚えておく時間は、社内のIPアドレスの割り当て方に合わせて決めてください。
- PCの名前は利用者が変えられるので、本人の確認には使えません。あくまで、あとで調べるときの手がかりとして記録してください。
最後に
以上が、C#でIPアドレスからPC名を調べる方法です。
逆引きは Dns.GetHostEntryAsync の1行でできますが、操作ログのように画面の操作のたびに呼ぶ処理では、「待つ時間を決める」「覚えておく」の2つを入れておくと、画面を遅くせずに使えます。待つ時間は、CancellationToken ではなく WaitAsync で決めるのがポイントです。
この部品を使っている立替精算システムは、こちらで紹介しています。
Pythonでホスト名とIPアドレスを調べる方法は、こちらで紹介しています。




コメント