ASP.NET 環境で動作するアクセス カウンターを作りました。自由に使ってください。
ソース コードのダウンロードは ここ をクリック。
カウンターの設置方法は こちら をご覧ください。
MyCounter.ashx
1: <%@ WebHandler Language="C#" Class="MyCounter" %>
2:
3: // 2009-7-25 新規作成
4:
5: using System;
6: using System.Web;
7: using System.IO;
8: using System.Drawing;
9: using System.Drawing.Imaging;
10:
11: public class MyCounter : IHttpHandler
12: {
13: private string _counterName; // カウンターの名前
14: private string _dataFileName; // 設定ファイルの名前
15:
16: private Bitmap[] _png_images = new Bitmap[10]; // 0 ~ 9 の数字画像
17: private Size _size; // 数字画像ひとつの幅と高さ
18:
19: public void ProcessRequest (HttpContext context)
20: {
21: try
22: {
23: if (!Check(context))
24: {
25: return;
26: }
27: Init(context);
28: Shori(context);
29: }
30: catch (Exception ex)
31: {
32: WriteExceptionInfo(context, ex);
33: Error(context, 0); // 何かよくわからないが、例外が出てる!
34: }
35: finally
36: {
37: Term();
38: }
39: }
40:
41: public bool IsReusable
42: {
43: get
44: {
45: return true;
46: }
47: }
48:
49: /// <summary>
50: /// 事前のチェックを行います。
51: /// </summary>
52: /// <param name="context"></param>
53: /// <returns>チェック合格なら true を返します。</returns>
54: private bool Check(HttpContext context)
55: {
56: _counterName = context.Request.QueryString["n"];
57: if (IsValidCounterName(_counterName) == false)
58: {
59: Error(context, 1); // カウンターの名前が不正
60: return false;
61: }
62:
63: _dataFileName = GetDataFileName(context, _counterName);
64: if (File.Exists(_dataFileName) == false)
65: {
66: Error(context, 2); // 設定ファイルが存在しない
67: return false;
68: }
69:
70: return true;
71: }
72:
73: /// <summary>
74: /// 準備
75: /// </summary>
76: /// <param name="context"></param>
77: private void Init(HttpContext context)
78: {
79: // ---------------
80: // 数字画像ファイルを読む
81: // ---------------
82: Read_NumberSymbols(context);
83: }
84:
85: /// <summary>
86: /// 後片付け
87: /// </summary>
88: private void Term()
89: {
90: // ---------------
91: // 数字画像ファイルを開放する
92: // ---------------
93: foreach (Bitmap bmp in _png_images)
94: {
95: if (bmp != null)
96: {
97: bmp.Dispose();
98: }
99: }
100: }
101:
102: /// <summary>
103: /// 処理します。
104: /// </summary>
105: /// <param name="context"></param>
106: private void Shori(HttpContext context)
107: {
108: // ---------------
109: // 設定ファイルを読み書きする
110: // ---------------
111: int counterValue; // カウンターの値。設定ファイルの一行目に記載。
112: int counterWidth; // カウンターの桁数。設定ファイルの二行目に記載。
113: Increment(_dataFileName, out counterValue, out counterWidth);
114:
115: // ---------------
116: //
117: // ---------------
118: string fmt_str = ""; // フォーマット用文字列
119: for (int i = 0; i < counterWidth; i++)
120: {
121: fmt_str += "0";
122: }
123:
124: // ---------------
125: //
126: // ---------------
127: string counterString; // カウンター文字列。例えば 00045 のように先頭が 0 埋め。
128: counterString = counterValue.ToString(fmt_str);
129:
130: // ---------------
131: // カウンター画像を作ってクライアントに返す
132: // ---------------
133: var newBitmap = new Bitmap(
134: _size.Width * counterWidth,
135: _size.Height
136: );
137: using (newBitmap)
138: {
139: var g = Graphics.FromImage(newBitmap);
140: using (g)
141: {
142: for (int i = 0; i < counterString.Length; i++)
143: {
144: char c = counterString[i];
145: int num = int.Parse(c + "");
146: g.DrawImage(_png_images[num], _size.Width * i, 0);
147: }
148: }
149:
150: GeneratePngImage(context, newBitmap);
151: }
152:
153: }
154:
155: /// <summary>
156: /// 数字の画像ファイル (0 ~ 9) を読み込みます。
157: /// </summary>
158: /// <param name="context"></param>
159: private void Read_NumberSymbols(HttpContext context)
160: {
161: string path = context.Server.MapPath("~/image");
162:
163: for (int i = 0; i < 10; i++)
164: {
165: _png_images[i] = new Bitmap(path + "\\" + i + ".png");
166: }
167:
168: _size.Width = _png_images[0].Width;
169: _size.Height = _png_images[0].Height;
170: }
171:
172: /// <summary>
173: /// カウンターの名前が適切かどうかチェックします。
174: /// </summary>
175: /// <param name="counterName">カウンターの名前</param>
176: /// <returns>カウンターの名前が適切なら true を返します。</returns>
177: private bool IsValidCounterName(string counterName)
178: {
179: if (string.IsNullOrEmpty(counterName))
180: {
181: return false;
182: }
183:
184: const string CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
185:
186: foreach (char c in counterName)
187: {
188: if (CHARS.IndexOf(c) == -1)
189: {
190: return false;
191: }
192: }
193:
194: return true;
195: }
196:
197: /// <summary>
198: /// 設定ファイルの完全パスを得ます。
199: /// </summary>
200: /// <param name="context"></param>
201: /// <param name="counterName">カウンターの名前</param>
202: /// <returns>設定ファイルの完全パス</returns>
203: private string GetDataFileName(HttpContext context, string counterName)
204: {
205: return context.Server.MapPath("~/App_Data/" + counterName + ".txt");
206: }
207:
208: /// <summary>
209: /// エラー番号を描いた画像を作ります。
210: /// </summary>
211: /// <param name="context"></param>
212: /// <param name="errorID">エラー番号</param>
213: private void Error(HttpContext context, int errorID)
214: {
215: var bitmap = new Bitmap(100, 20);
216: using (bitmap)
217: {
218: var g = Graphics.FromImage(bitmap);
219: using (g)
220: {
221: var font = new Font("MS UI Gothic", 9);
222: using (font)
223: {
224: g.DrawString("ERROR " + errorID, font, Brushes.Black, 0, 0);
225: }
226: }
227:
228: GeneratePngImage(context, bitmap);
229: }
230: }
231:
232: /// <summary>
233: /// 画像を作ります。
234: /// </summary>
235: /// <param name="context"></param>
236: /// <param name="bitmap"></param>
237: private static void GeneratePngImage(HttpContext context, Bitmap bitmap)
238: {
239:
240: #if true
241: var memStream = new MemoryStream();
242: using (memStream)
243: {
244: bitmap.Save(memStream, ImageFormat.Png);
245:
246: context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
247: context.Response.ContentType = "image/png";
248:
249: //context.Response.BinaryWrite(memStream.ToArray()); // どちらでも OK
250: memStream.WriteTo(context.Response.OutputStream); // どちらでも OK
251:
252: context.Response.Flush();
253: }
254: #else
255: context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
256: context.Response.ContentType = "image/png";
257: bitmap.Save(context.Response.OutputStream, ImageFormat.Png);
258: context.Response.Flush();
259: #endif
260:
261: // ここでわざわざ MemoryStream を使う理由は以下を参照のこと。
262: //
263: // Dynamic PNG images with ASP.NET and GDI+
264: // http://aspalliance.com/319
265: //
266: // Q: Why do I get "A generic error occurred in GDI+. " when
267: // I switch my working GIF to use PNG format instead?
268: // A: You can't use the Bitmap Save() method with a "non-seekable" stream.
269: // Some image formats require that the stream can seek.
270:
271: }
272:
273: /// <summary>
274: /// 例外をテキスト ファイルに記録します。
275: /// </summary>
276: /// <param name="context"></param>
277: /// <param name="ex">例外</param>
278: private void WriteExceptionInfo(HttpContext context, Exception ex)
279: {
280: var dt = DateTime.Now.ToString("yyyyMMdd.HHmmss.fff");
281: string filename = context.Server.MapPath("~/App_Data/" + dt + ".txt");
282: using (var s = new StreamWriter(filename))
283: {
284: s.WriteLine(dt);
285: s.WriteLine(ex.ToString());
286: }
287: }
288:
289: /// <summary>
290: /// 設定ファイルからカウンターの値と幅を読み、カウンターの値をインクリメントします。
291: /// </summary>
292: /// <param name="dataFileName">設定ファイルの完全パス</param>
293: /// <param name="counterValue">[out] インクリメント後のカウンター値</param>
294: /// <param name="width">[out] カウンターの幅</param>
295: private void Increment(string dataFileName, out int counterValue, out int width)
296: {
297: FileStream fs = null;
298:
299: for (int i = 0; i < 10; i++)
300: {
301: try
302: {
303: fs = new FileStream(
304: dataFileName,
305: FileMode.Open, // ファイルを開きます。ファイルが無いなら死亡。
306: FileAccess.ReadWrite, // ファイルを読み書きします。
307: FileShare.None // 共有を拒否します。
308: );
309:
310: break;
311: }
312: catch (IOException)
313: {
314: System.Threading.Thread.Sleep(new Random().Next(1000));
315: }
316: }
317:
318: using (fs)
319: {
320: // -----------------
321: // 読む
322: // -----------------
323: var reader = new StreamReader(fs);
324: string line1 = reader.ReadLine();
325: string line2 = reader.ReadLine();
326: if (int.TryParse(line1, out counterValue) == false)
327: {
328: counterValue = 0; // デフォルトのカウンター値は 0
329: }
330: if (int.TryParse(line2, out width) == false)
331: {
332: width = 5; // デフォルトのカウンター桁数は 5
333: }
334:
335: // -----------------
336: // インクリメント
337: // -----------------
338: counterValue++;
339:
340: // -----------------
341: // 書く
342: // -----------------
343: fs.SetLength(0);
344: var writer = new StreamWriter(fs);
345: writer.WriteLine(counterValue);
346: writer.WriteLine(width);
347: writer.Flush();
348: }
349:
350: }
351: }
299 行目から 316 行目で 10 回繰り返しや Sleep を入れているのは
ab -n 300 -c 10 http://www.example.net/app1/MyCounter.ashx?n=counterName
程度で例外 (*1) が出たからです。
繰り返しと Sleep を入れた後は -c 100 でも耐えられるようになりました。
といっても負荷を生成するクライアント側が一台でしかも非力なので、-c 100 というのは本当にそれだけ出てるのか怪しいですけども。
(*1) System.IO.IOException: The process cannot access the file 'ファイル名' because it is being used by another process.
参考になるページ