ブログトップ 記事一覧 ログイン 無料ブログ開設

子持ちししゃもといっしょ RSSフィード

2014-12-19

ずっと勘ちがいしていたstring.Format()の挙動というか仕様

先日、ちょっとしたプログラムを作っていたところ、いままで(少なくとも5年くらい)ずっと勘ちがいしていたことに気付いたことがあったのでメモ代わりに残しておきます。


C#にはstringクラスという文字列を扱うためのプリミティブな型があります*1

このクラスには文字列を操作するための便利なメソッドがたくさん実装されています。その中でもわたしがすごく好きでよく使っているのがFormatメソッドです。これはどういうメソッドかというとCでいうところのprint系の関数と同じでして、文字列を整形して出力するためのメソッドです。


どんな感じで使うのかと言うとこんな感じで使います。


var x = 123;
Console.WriteLine(string.Format("(1) {0}\r\n(2) {0:00000}\r\n(3){0:D5}", x));

// ↓出力例
// 
// (1) 123
// (2) 00123
// (3) 00123

*2


第一引数には文字列を入れるのですが、その中に{n}という部分を入れておくと第二引数以降で渡した値がここに埋め込まれます。

第二引数が0番目になるので{0}、第三引数は1番目なので{1}という感じです。


また、上の例では{0:00000}とか{0:D5}とありますがこれはどちらも同じ意味で「先頭をゼロ詰めにして5桁で表示する」という指示です。


こんな感じですごく簡単なのに便利なので重宝していたのですが、実はstring.Formatの第二引数以降に渡す値が数値ではなく文字列の場合は先頭ゼロ詰めという指示が有効ではないらしいのです。


どういうことかというとこんな感じになります。


var x = 123;
Console.WriteLine(string.Format("(1) {0}\r\n(2) {0:00000}\r\n(3){0:D5}", x.ToString()));

// ↓出力例
// 
// (1) 123
// (2) 123
// (3) 123

最初の例ではintが第二引数に渡っていたのですが、この例ではそれをstringに変換してから渡しています。すると指定した書式はすべて無視されてそのまま表示されます。


もともとD5というのはDecimalのDだろうから数値前提だと言われたら返す言葉がないのですが、でもなあ...。


え?当たり前??

*1:厳密にはstringはSystem.Stringのエイリアスですがここではとりあえず置いておきます

*2:Console.WriteLine()自体がstring.Formatと同じような形式で整形する文字列を受け取れるのですが、ここでは分かりやすくするためにあえてstring.Formatを入れています。

2014-06-26

SQLServerの動的管理ビューについて調べてみた


作りかけのツールで必要になったのでSQLServerの動的管理ビューについて調べてみました。

参考にしたのは以下のサイト。



いま作っているのは「特定のテーブルのデータをオンメモリ(キャッシュ)に配置するためにクエリーを投げるツール」なのですが、その効果を知るためにクエリーを投げる前後のデータキャッシュのサイズを取得する必要があってそれに使える機能がなにかないか調べていて動的管理ビューのことを教えてもらいました。


最終的には「SQL Server オペレーティング システム関連の動的管理ビュー (Transact-SQL)」のsys.dm_os_buffer_descriptorsを使って実現しました。


SELECT (COUNT(*) * 0.008024) AS cached_size ,name
  FROM sys.dm_os_buffer_descriptors AS bd 
    INNER JOIN 
    (
        SELECT object_name(object_id) AS name 
            ,index_id ,allocation_unit_id
        FROM sys.allocation_units AS au
            INNER JOIN sys.partitions AS p 
                ON au.container_id = p.hobt_id 
                    AND (au.type = 1 OR au.type = 3)
        UNION ALL
        SELECT object_name(object_id) AS name   
            ,index_id, allocation_unit_id
        FROM sys.allocation_units AS au
            INNER JOIN sys.partitions AS p 
                ON au.container_id = p.partition_id 
                    AND au.type = 2
    ) AS obj 
        ON bd.allocation_unit_id = obj.allocation_unit_id
 WHERE database_id = DB_ID()
 GROUP BY name, index_id 
 ORDER BY cached_pages_count DESC;

ほとんどサンプルのままなのですが、サンプルだと1列目のcached_sizeはページ数になってしまうので8KBをかけて1024で割ることでMB単位に変換しています。あと不要な列を削除したり。


この動的管理ビューはSQLServer2005からの機能だそうでして、SQLServer2014が出ていることを考えればかなり前からあるのに気付いていなかったことになります。もったいない。。。たいへん便利なのでこれからは積極的に使っていきたいです。

2012-06-18

ユニークなファイル名を取得する方法


先日、大きなサイズのXMLファイルを解析して変換し、小さなサイズのXMLファイルを出力するというプログラムをC#で作りました。


XMLの解析や作成はLINQを使えばかなり簡単なのでその部分のロジックはまったく問題なくできたのですが、出力するファイル名の付け方を間違ってしまうというミスをしてしまいました。


もう少し詳しく書くと、A.xmlというXMLファイルを10個のファイルに分割したときに、分割後のファイルの名前をA_yyyyMMddhhmissfff.xmlとしたところ、ファイル名が重複してしまったというミスです。


ここでyyyyMMddhhmissfffというのは、日時を入れるという意味でして、たとえば2012年6月18日12時34分56秒789ミリ秒に処理をした場合にはA_20120618123456789.xmlという名前を付けることになります。


なんでこれがダメだったのかはもう一目瞭然ですが、XMLを作るのに要した時間が1ミリ秒以下だった場合に名前が同じXMLができてしまうというミスです。初歩的すぎる...。


これはこれで手抜きをすると痛い目にあうというよい教訓になったわけですが、せっかくなのでファイル名を自動生成する方法についてまとめたいと思います。


1. Path.GetTempFileName()を使う

一時ファイルを作成する方法を調べた時に一番最初に出てきたのがこのPath.GetTempFileName()というメソッドでした。


(メリット)

    • メソッドを呼び出すだけでファイル名が取れて楽

(デメリット)

    • ファイルの出力先フォルダやファイルの拡張子が選択できない
    • IOExceptionを出す場合がある

試しに使ってみたところ、以下の仕様で動作しているようでした。


出力先%TEMP%
ファイル名tmp????.tmp
その他メソッドを呼び出したタイミングでサイズが0Byteのファイルが作られる

ファイル名の????には0000-FFFFまでの文字列が順番に入るようです。これがFFFFまでいってしまうと、次の呼び出しがIOExceptionになりますので、つまりは65535回しかこのメソッドは呼べないことになります。もちろん使い終わったファイルは消せばよいのですが、そもそも「一意なファイル名が欲しい」だけなのに、使い終わったあとの始末までやることを強制されるのってなんか違う気が...。いや、やることはやるんですが、本末転倒な気がしてなりません。


とか書いてたらちゃんとMSDNに書いてました。


一意な名前を持つ 0 バイトの一時ファイルをディスク上に作成し、そのファイルの完全パスを返します。

Microsoft のテクニカル ドキュメントの以前のバージョン | Microsoft Docs

このメソッドは、.TMP という拡張子を持つ一時ファイルを作成します。

以前の一時ファイルを削除せずに、GetTempFileName メソッドを使用して 65535 個を超える数のファイルを作成した場合、IOException が発生します。

GetTempFileName メソッドは、一意な一時ファイル名が使用できない場合に、IOException を発生させます。このエラーを解決するには、すべての不必要な一時ファイルを削除します。

Microsoft のテクニカル ドキュメントの以前のバージョン | Microsoft Docs

このあたりの不便さ(出力先や拡張子が選べない)は、staticなメソッドを呼び出すだけで使えるという気軽さとトレードオフした結果だと思うのでここはしょうがないのかなと。不便とはいえ、たとえば出力した後に名前を変えて移動すれば解決できる程度の問題ですのクリティカルな問題点ではありません。


ただ、そういう不便さは許せる一方で、使うためには決まりごとがいくつか生じてしまうことやエラーハンドリングが必要なこと、そして「ファイル名が欲しいだけなのに後始末まで約束させられる」というのは個人的にはいただけないなーと思います。


いまのところ「めんどくさいから使わね」という結論に...。


2. データのユニークキーを流用する

作成するファイルに格納されるデータのキー情報(個人や商品のIDなど)がある場合には、それをファイル名に入れることでユニークなファイル名が作成できます。


(メリット)

    • データの中とファイル名が結びついているので分かりやすい

(デメリット)

    • ファイルの中にキー情報が無いとそもそも使えない
    • 同一キーに対して複数のファイルが必要になった場合など、これ単体では使えないケースがある

アイディアとしては悪くないのですが、基本的にこれだけではユニークなファイル名は生成できません。

それは他のほとんどのアイディアも同じなんですが...。


3. 連番を利用する


ファイル名に処理をした順番に連番を振るという方法です。

ベタといえばベタなんですが、ハンドリングしやすいですしとても確実な方法です。

さらに処理した順番もわかるので個人的にはすごく好きな方法です。


int cnt = 1;
string before = DateTime.Now.ToString("yyyyMMdd");

foraech (xxxxxxx){ // ここのループ条件は適当に
  string now = DateTime.Now.ToString("yyyyMMdd");
  if (before != now) cnt = 1;
  string.Format("A_{0}_{1:000}.xml", DateTime.Now.ToString("yyyyMMdd"), cnt++);
  before = now;
}

ここでは日付ごとに連番を振っていますが、このあたりは好き好きで。


(メリット)

    • ファイルを作成した順番がわかる

(デメリット)

    • 連番だけではユニークであることを保証できない

上の例だと日付と組み合わせて使っていますが、こんな感じで連番を振るにしても他の方法との併用が望ましいです。



4. 乱数/ハッシュを利用する


ここまで引っ張ってしまいましたが、プログラマーが一意なデータを得ようと最初に思いつくのはおそらく乱数orハッシュだと思います。好みはあるでしょうけど。


乱数だとこんな感じ。

foraech (xxxxxxx){ // ここのループ条件は適当に
  string.Format("A_{0:0000000000}.xml", new Random().Next());
}

ハッシュはいいサンプルが思いつかないので、参考サイトを紹介するにとどめておきます。ファイルがそれほど大きくなければ、ファイルの中身全部でハッシュ値を生成するのがよいのかなと。


(参考) MD5やSHA1などでハッシュ値を計算する



(メリット)

    • 扱いが簡単な割に効果は大きい

(デメリット)

    • そもそも一意であることを完全に保証する仕組みではない
    • 名前からファイルの中身を判断するのが難しい

確率的には被る可能性はほぼゼロなんですが、ゼロではないだけで可能性としてはありえます。

なので仕組みとしてそもそも不完全なのと、あとはファイル名が無作為過ぎてファイルの中と結びつかないのは個人的にはあまり好きではないです。なので私自身はあまり使いたい方法ではありません。


5. PIDを利用する

プロセスごとに振られたIDを使えばユニークになるんじゃね?という安直な方法です。


int cnt = 1;
int pid = Process.GetCurrentProcess().Id;
foraech (xxxxxxx){ // ここのループ条件は適当に
  string.Format("A_{0}_{1:000}.xml", pid , cnt++);
}

(メリット)

    • 呼び出すだけで簡単

(デメリット)

    • 常駐型プロセスだとIDは変わらない

とりあえずサービスとして動かしている場合などはプロセスIDは常に変わらないのでこれだけはキー情報になりません。日付や連番と組み合わせて使うのがよいのかなと。というか、それだったら別にプロセスIDはいらないか...。


GUIDを利用する


GUID(Global Unique IDentifier)、日本語だとグローバル一意識別子というそうですが、WikipediaによるとUUID(Universally Unique IDentifier)のMS版実装を指すそうです。ただ、MS版と言いつつも多くのサービス、ソフトウェアで使われているために概ね一般的な言葉ととらえてもよさそうです。


MSDNを読んで気付いたのですが、これクラスじゃなく構造体なんですね。ただC#の場合は構造体もメソッドをもてるので、値型か参照型かとか配置される場所がヒープかスタックかくらいしか違いはなくて扱いは変わんないので


GUID は、一意な識別子が必要とされるコンピュータおよびネットワーク全体で使用できる 128 ビットの整数 (16 バイト) です。このような識別子は、重複する確率がかなり低くなっています。

Guid 構造体 (System)

これも使い方は簡単で、NewGuidメソッドで初期化してからToString()するだけです。

    Guid guidValue = Guid.NewGuid();
    Console.WriteLine(guidValue.ToString()); // -が付いてる
    Console.WriteLine(guidValue.ToString("N")); // -が付いてない
    Console.WriteLine(guidValue.ToString("B")); // -が付いていて、{}で囲まれている
    Console.WriteLine(guidValue.ToString("P")); // -が付いていて、()で囲まれている

で、結果はこんな感じです。

f:id:itotto:20120613132437p:image


新しいGUIDが欲しい場合は再度NewGuidメソッドを呼べばOKです。


(参考) GUID値を生成するには?


(メリット)

    • 呼び出すだけでほぼ一意な文字列が取れて便利

(デメリット)

    • 任意過ぎて名前に規則性がなく扱いにくいケースがある
    • 確実にファイル名がかぶらないわけではない

かなり便利ですし、名前が被る確率もほぼないので悪くないのですが、これも乱数などと同じで規則性が無さすぎてファイル名としてはとても扱いにくいです。

あとこれもここまで紹介したとおり、これだけで完全に重複を防ぐ方法ではないので、他の方法と同じく単体で使うのではなく要素として使うべきかなと。


6. 結論

結局、今回私が選んだのは「日付(yyyyMMdd)」+「ユニークな情報を付与」+「連番」という組み合わせでした。


public string GetUniqueFileName(string id, ref int num){
    while (true){
        string fileName = string.Format("XMLファイル_{0}_{1}_{2:00000}.xml", DateTime.Now.ToString("yyyyMMdd"), id, num++);
        if (File.Exists(fileName)) {
            if (num > 99999) throw new IOException("連番でか過ぎワロタ");
            continue;
        }
        return fileName;
    }
}

連番を5桁(99999まで)にしているのは、現実的に作成される上限とファイル名が極力短くしたいというところで折り合いをつけて決めただけでここはケースバイケースで変えてよいです。


あとソースにもあるとおり、既にその名前のファイルがあった場合には「連番」をインクリメントして再度名前を変えるという対応も合わせて行ったので、これでファイル被ることはなさそうです。本当は抜け番があった場合にも対応しようかと思ったのですが、現実的にそこまでファイルは増えないのでここでは割愛しました。


ちなみに、今回はシングルスレッド前提の処理なのでこの程度でも十分ですが、マルチスレッドな環境だともうちょっと工夫や気配りが必要かもです。

# 面倒なので今回はそこまでは説明しません(詳しくないし)


もっと良い方法があるよ!という人はコメントで教えてくださいー。

2010-04-12

Ephemeralポートとして利用されるポートの範囲を変更する

今日、仕事で調べたらとてもおもしろかったので忘れないうちに記録しておきます。

(要求)Ephemeralポートとして利用されるポートを制限したい

特定のTCPポートで接続を受け付ける自作のサービスアプリケーションを作るときに悩むのが、何番ポートを使うのかということです。

一般的にTCP/UDPで使えるポート番号は1〜65535となっていますが、これらすべてが自由に使っていいというわけではありません。IANAによってポート番号の範囲ごとにその用途は決められているので、それを考慮して利用すべきです。


ポート番号の範囲呼称説明
1 〜 1024Well Known PortIANAが管理しているよく知られたサービスの使うポート
1025 〜 49151Registered PortIANAが利便性を考慮して公開している登録済みサービスの使用ポート
49152 〜 65535Ephemeral Port好きに使っていいポート

上記を見る限りでは49152〜65535が適しているように見えますが、インターネットに公開しない場所ではRegisteredポートもよく使われます。以前イントラ内で限定公開したサービスでは、わたしの誕生日にちなんで5325番を使いました。

ただし注意が必要なのはEphemeralポートというのはその名のとおり一時的な接続に使うためのポートであり、つまりネットワークを経由して外部のサービスへ接続する際のクライアントポートとして利用されるものでもあります。そのため、いざサービスを起動してみたら既に他のセッションで使われていたのでリッスンできなかったなんてこともあるのです。


そんなことにならないように、あらかじめ利用するポートを予約しておいて一時接続のためのポートとしてOSに利用されないようにする方法を以下で紹介します。



(対応方法) OSによって対応方法は異なるのでそれぞれを紹介

Windows 2000 or Windows 2003 Server or Windows XPの場合

Windows 2000, 2003, XPの場合はレジストリに予約したいポートの範囲を記載することで予約可能です。


f:id:itotto:20100412230612p:image


↑こんな感じでxxxx-yyyyとすれば、xxxxからyyyyまでのポート番号を予約します


詳しくはこちらに記載されています。

Windows 2003とXPは同じ方法(Regedit.exeで編集)ですが、Windows 2000はRegEdt32.exeを使う方法になっています。レジストリファイルを作って実行するだけだったら内容は一緒で大丈夫そうです。


Windows Vista or Windows Server 2008以降の場合(Windows Server 2012R2までは動作確認済み)

Kernelが6以上の環境では2003以前のレジストリを使う方法の代わりにnetshコマンドを使うようになっています。


@netsh int ipv4 set dynamicport tcp start=5000 num=1000

↑これはポート番号を5000から1000個分予約する(つまり6000まで予約)という指定


詳しくはこちらに記載されています。


(おまけ)OSが変わって変更になったのは予約方法だけではないらしい

この確認をする過程でわかったのですが、ポート番号の予約をする方法はOSごとに違うのは見てのとおりですが、Ephemeralポートとして使用できる範囲に設定されているポート番号もOSごとに異なるようなのです。

具体的には2003以前は1025〜5000がEphemeralポートとして利用されていたようですが、Vista以降は49152〜65536が利用されるようです。


Vista以降の環境であれば「netsh int ipv4 show dynamicport tcp」というコマンドで範囲を確認することが可能です。


C:\Users\itotto>netsh int ipv4 show dynamicport tcp

プロトコル tcp の動的ポートの範囲
---------------------------------
開始ポート      : 49152
ポート数        : 16384


C:\Users\itotto>

そういうわけでVista以前にRegisteredポートやEphemeralポートを使って作っていたプログラムがVistaになったら突然動かなくなったという一因にはなりそうな気がします。


(関連リンク)

2006-09-16

【MS06-049】 Windows カーネルの脆弱性により、特権が昇格される

http://support.microsoft.com/kb/920958

珍しくまともな話題を。


先月末に大きく話題になったこの障害。

やっとMicorsoftのサイトにも情報が載ったので話題にしてみます。Windows2000(Server or Professional) SP4環境で、KB920958を適用するとNTFS圧縮されたフォルダ内部のファイル(4kb以上のサイズでかつ4096バイトの整数倍よりも少し小さい程度のサイズのファイル)が破損するという現象です。安定した環境が大好きなのでWindows2000を使ってた私にはかなりショッキングな障害だったのですが、よく考えたらNTFS圧縮は使ってなかったのであまり影響はありませんでした。


が、Windows UpdateやWSUSで更新したファイルのアンインストールフォルダがNTFS圧縮されていたようで、いくつかアンインストールできなくなったファイルがありました。オソロシス...。


Windows2000を使っている方はご注意を。