12/19/2009 4:29:29 AM

[設計案例] 清除Cache物件 #1. 問題與作法

ASP.NET | C# | CS | Microsoft.NET | MSDN | 小技巧 | 技術隨筆 | 物件導向 Facebook Share

每次心裡有什麼好點子想寫出來時,第一關就卡在想不出個好標題... 想來想去的標題,怎麼看就是既不顯眼又不聳動... 果然是個老實的工程師性格 =_= ...  這次要講的,是 .NET HttpRuntime 裡提供的 Cache 物件的操作心得。這個東西我想不用我多作介紹,大家都用到爛掉了吧? 不過好用歸好用,有個老問題其實一直困擾著我很久了...

" 我該怎麼手動的把某個物件從 cache 裡移除? "

老實說,這問題蠻沒水準的... 老叫別人要翻 MSDN,我自己怎麼沒翻? 不不... 容我花點篇幅先說明一下問題。Cache物件,是個典型的 Dictionary 型態的應用 (雖然它沒有 implement interface: IDictionary… ), 透過 key 就可以拿到 cached item. 要從 cache 裡移除某個 item, 簡單的很,只要用 Remove 這個 method, 一行就搞定了:

從 key 移除指定的 cache item[copy code]
   1:  HttpRuntime.Cache.Remove(“cache-key”);

別小看這一行,實作起來障礙還不少。首先,你得額外去記著 cache key 的值。當你要移除的 cache item 有多個的時後,或是移除的 items 之間的關係有點複雜時,這些 code 就不怎麼漂亮了。下一個問題是:

" 我該如何得知所有存在 Cache 內的 keys 有那些? "

這個問題單純的多,那些把 intelligent sense 當購物網站的人 (平常不看文件,只會按下 . 然後挑個順眼 method 來用的人),可能這次就碰壁了... Cache 物件不像一般的 Dictionary 一樣,有提供 Keys 這樣的 property ... 它藏在 GetEnumerator 這 method 內,它會把所有的 keys 給巡一遍,你需要所有的 keys 的話,可以這樣用:

跑過 cache 裡每一個 key[copy code]
   1:  foreach (string key in HttpRuntime.Cache) { 
   2:      // … 
   3:  }

不過這樣的風險也是蠻高的,誰曉得你拿到 key 後的下一秒,這個 cache item 還在不在 cache 內?

 

 

 

--------------------------------------------------------------

本文正式開始! 哈哈,前面那一段只是廢話 + 碎碎唸,現在才是正題。前面想表達的只是,因為 cache 的不確定性 (資料隨時都會被 remove), 操作起來變的要格外小心, 即使它用起來像一般的 Dictionary 一樣。

我舉個案例,來說明我應用 cache 的情況。假如我想實作一個簡單的 web browser, 透過網路下載資源是很慢的動作,每種 browser 都會有某種程度的 cache 機制。我們就拿 Cache 物件替代 IE 的 "temporary internet files” 目錄吧。這時很簡單,只要用 URL 當作 KEY,下載的 content 就當物件塞進去就好...

不過事情沒那麼簡單。如果程式運作了一陣子,我想提供使用者手動清除 "部份" cache 的功能的話,那該怎麼辦? 我舉幾種情況:

  1. 從 cache 裡刪除所有從某個特定網站 (如: columns.chicken-house.net) 下載的資料
  2. 從 cache 裡刪除所有特定類型的資料 (如: content-type 為 image/jpeg 的圖檔)
  3. 從 cache 裡刪除所有透過特定 protocol (如: https) 下載的資料

這樣的要求應該不算過份吧? 用前面提到的兩種作法,你會想哭吧 XD .. 用這些基礎,你大概只能選這幾種作法 (各位網友有好作法也記得提供一下):

  1. 自己另外管理所有下載過的 URL, 用盡各種適合的資料結構,讓你可以順利的挑出這些 match 的 key, 然後移除它。

    缺點: 都作這麼多,你乾脆自己重寫個 cache 機制好了... 何況時間一久,你管理的 key, 那些對應的資料搞不好老早就通通從 cache 裡清掉了...
  2. 聰明一點,用 regular expression … 從 GetEnumerator( ) 一筆一筆過濾出要移除的 URL, 然後清掉它...

    缺點: 這作法只會檢查還留在 cache 內的 URL,不過這樣的 cache 隨便也有成千上萬個,每次都要 looping 掃一次實在不怎麼好看... 有違處女座有潔癖的個性...

 

這些方法 code 寫起來實在不怎麼漂亮,我就不寫 sample code 了,請各位自行想像一下寫起來的樣子。抱歉,如果你用的正好是上面的作法... 那請多包含... :D   這些都是 workable 的作法,但是看起來就是沒什麼設計感;程式可以動,不過就效能、簡潔、可讀性、美感來看,就是覺的不夠精緻 @@。跟朋友討論到這個問題時,我想到一個爛主意...

" 用蠢方法,這些 cache item 先分好類,每一類去關聯一個檔案,設 CacheDependency … 要清掉時去 touch 一下這個檔案,一整組的物件就會自動被清出 cache 了…。”

老實說,我覺的這是個既聰明又愚蠢的作法。聰明的是它很漂亮的解決我要如何移除某一群 item 的問題...,愚蠢的是這種單純程式內可以解決的事,竟然要繞到外面不必要的 file system I/O 動作... 而這通常是最慢的...

 

--

咳,寫太晚,實際的程式碼明天待續...



9/10/2008 4:44:00 AM

莫明奇妙的錯誤訊息: 找不到 VJSharpCodeProvider ?

Microsoft.NET | 543 | ASP.NET | BlogEngine.NET | 小技巧 | 技術隨筆 Facebook Share

話說前陣子處理了 BlogEngine.NET 升級到 1.4.5.0,另外也寫了 SecurePost.cs 這個 extension, 其時都碰過這個鳥問題,只是一直沒去理它而以。接下來為了要改 PostViewCounter.cs (BE extension, too), 又碰到... 於是就認真的研究了一下...。

過程是這樣,為了建立 BlogEngine 的開發環境,首先我從官方網站下載了 source code, 解開後編譯都沒問題,OK。

接下來 WEB 的部份我把網站上的 source code 搬過來 (不包含 ~/App_Data,太大了),編譯也 OK。

不過我要改 Counter 的 Code 啊,沒有一些 SAMPLE DATA 很難測試,只好把資料檔也搬過來.. 結果 Visual Studio 2008 就冷冷的回了這訊息給我:

(0): Build (web): The CodeDom provider type "Microsoft.VJSharp.VJSharpCodeProvider, VJSharpCodeProvider, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" could not be located.

 

我沒有漏貼前面的訊息... 的確是沒有檔名,也沒有行號(0)。我最不能忍受的就是沒頭沒尾的 ERROR MESSAGE 了。除了告訴你 "掛掉了" 之外,無頭無腦的對於追查問題實在沒什麼幫助。只好靠自己了...。雖然這是個 compile error message,不過我要 RUN 的畢竟是個 web site, 不編譯也是可以跑,除了那個惱人的錯誤訊息之外,要執行倒是沒問題。只不過編譯失敗,我就不能設中斷點,直接 F5 執行測試。雖然可以另外手動 Attach Process 的方式來除錯,不過每次都要這樣搞實在是很煩..

 

仔細想了想,沒錯,我是沒裝 Visual J#。不過我的確沒要用 Visual J# 啊,如果真的用到 J# 的話,出這訊息是應該的。訊息沒有原始檔? 也沒有錯誤行號? 那問題應該是 Global 的範圍,第一個想到的就是 web.config 是不是定義了 CodeDom 或是指定了相關的 CodeProvider ? 無奈查了一遍沒看到,VS2008 的 PROJECT 設定也沒看到引用任何 J# 相關的 LIB...

 

已經到了死馬當活馬醫的地步... 開始亂找一通碰碰運氣。搜尋了一下有沒有 *.java 的檔? OUCH,還真的有... 在 ~/App_Data/files 下找到我古董檔案,研究所時代寫的 Java Applet .... 順手試一下,刪掉後還真的就過了? 這個無頭無腦的問題,就在不知不覺中找到 solution, case closed!

 

怒... 這樣也算? 找到 .java 的程式碼,去找 VJ# 來編譯還說的過去,不過找 "source code" 找到 ~/App_Data 實在是太超過了一點... 好歹也列個要編譯那個檔案,然後找不到對應的 CodeProvider,這樣要排除問題也簡單一點...

 

結論是: 各位別太鐵齒,看來 ~/App_Data 下的檔案也是不能亂塞的...



9/6/2008 3:28:00 AM

BlogEngine Extension: Secure Post v1.0

Microsoft.NET | ASP.NET | BlogEngine.NET | 我的作品 | 技術隨筆 | BlogEngine Extension Facebook Share

因為家裡大人開出條件,除非新的 BLOG 系統 (就是我在用的 BlogEngine 啦) 有特定文章要輸入密碼才能看的功能,否則她就不想換系統了 (原來是用 CommunityServer 2007)。要弄密碼其實很簡單,不過過去試過 IIS 加上整合式驗證... 弄到最後該看的人看不到,也沒擋到該擋的人而作罷...。

 

仔細想了想大人的需求,要的就是簡單的控制機制。不需要先建立帳號,也不需要登入,就是特定幾篇文章要輸入暗號才能看到內容,就這樣而以。無耐 BlogEngine 還算很年輕,替它寫的 Extension 也還不多,官方網站提供了幾個 Extension 列表,找到最接近的是這個: Password Protected Post... 不過它是以登入 BE 為使用者認證的方式,再依照 ROLE 跟 CATEGORY 的配對為授權方式,來控制那些讀者能看到那些文章...。就是不想要替每個人建帳號啊,看來只好自己寫了... Orz。

 

以往都是想要作什麼很簡單,難是難在把它作出來..。現在都反過來了,工具越來越強,系統也越來越完整,難的反而是思考要怎麼作,程式碼沒幾行就搞定了。之前的文章介紹過 BlogEngine 的 Extension 機制,這次就實際來試看看。我要寫的東西很簡單,就一組密碼就好,要有夠簡單的方式讓大人能夠指定那幾篇文章是要保護的,而所有的人 (已登入的除外) 只能看到提示輸入密碼的訊息,密碼打對了才會顯示文章內容。至於密碼要不要加密? 會不會被竊聽? 不重要啦,只要保護不要遜到按右鍵簡示原始碼,密碼跟內容都看光光了就好。

 

順手寫了幾行 CODE,先驗證一下最基本的動作做不做的到 (POC: Prove Of Concept)。第一步是先把顯示內容的動作攔下來,換成制示的輸入密碼訊息... 這個簡單,沒幾行就搞定了:

 

image

直接從 CodePlex 抓下來的 Source Code, 解壓縮完就可以寫了。加上這段 CODE 並不難,整個 Extension 只有這樣而以:

 

修改 POST 內容,改成提示輸出密碼的畫面[copy code]
[Extension("SecurePost", "1.0", "<a href=\"http://columns.chicken-house.net\">chicken</a>")]public class SecurePost{    static SecurePost()    {        Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);    }    private static void Post_Serving(object sender, ServingEventArgs e)    {        Post post = sender as Post;        StringBuilder bodySB = new StringBuilder();        {           // 略。透過 bodySB 輸出 HTML        }        e.Body = bodySB.ToString();    }}
   1:  [Extension("SecurePost", "1.0", "<a href=\"http://columns.chicken-house.net\">chicken</a>")]
   2:  public class SecurePost
   3:  {
   4:      static SecurePost()
   5:      {
   6:          Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);
   7:      }
   8:   
   9:      private static void Post_Serving(object sender, ServingEventArgs e)
  10:      {
  11:          Post post = sender as Post;
  12:          StringBuilder bodySB = new StringBuilder();
  13:          {
  14:             // 略。透過 bodySB 輸出 HTML
  15:          }
  16:          e.Body = bodySB.ToString();
  17:      }
  18:  }

 

 

看起來 CODE 還不少,不過算一算真正在作事的都是在湊那堆 HTML ... 關鍵只有一開始去攔 Post.Serving 事件,接到自己的事件處理器 Post_Serving( ) 上,之後所有會輸出 Post 內容的地方,都會觸發這個事件。然後只要在事件處理器內去調整 Post 內容就可以了。

 

好,好的開始是成功的一半,已經完成 1/3 了 (什麼???) 第一部份的 CODE 產生的 HTML,會引導使用者輸入密碼,按下 [GO] 之後,就會連到 POST 的網址了。不過除了原本網址之外 (post.AbsoluteLink) 後面還要加上 "?pwd=xxxxxx" 帶上使用者輸入的密碼。前面講過我只要最基本的防護,其它進階的安全問題就不理它了。我只要掌握兩個原則:

  1. 密碼一定要在 SERVER 端確認 (不能讓不知道密碼的人 view source 就找到密碼)
  2. 沒輸入密碼前不能在 CLIENT 端出現 POST 內容 (不能單純的用 DHTML 把文章內容 "藏" 起來)

另外補一件事,我也不要讓全部的文章都用這種機制保護。只要有特別標示的 POST 要密碼就好。看到 BlogEngine 內建的 BreakPost 這個擴充程式,我就仿照它的作法,內文找到特定字串就啟用。我定的規則是整篇 POST 內容開頭一定要是 "[password]" 才會啟用密碼保護機制。

既然這樣,第二步也很簡單。如果密碼對,一切照原狀顯示內容。密碼不對的話就一樣攔下來...。程式碼.... 只是在第一步的程式碼多了... 兩行...

 

加上檢查密碼的 CODE[copy code]
    private static void Post_Serving(object sender, ServingEventArgs e)    {        Post post = sender as Post;        if (HttpContext.Current.Request["pwd"] == Password) return;        if (!e.Body.StartsWith("[password]", StringComparison.CurrentCultureIgnoreCase)) return;        StringBuilder bodySB = new StringBuilder();        {           // 略。透過 bodySB 輸出 HTML        }        e.Body = bodySB.ToString();    }
   1:  private static void Post_Serving(object sender, ServingEventArgs e)
   2:  {
   3:      Post post = sender as Post;
   4:      if (HttpContext.Current.Request["pwd"] == Password) return;
   5:      if (!e.Body.StartsWith("[password]", StringComparison.CurrentCultureIgnoreCase)) return;
   6:      StringBuilder bodySB = new StringBuilder();
   7:      {
   8:         // 略。透過 bodySB 輸出 HTML
   9:      }
  10:      e.Body = bodySB.ToString();
  11:  }

 

啥米? 就是第一部份的 CODE 加上第四及第五行就搞定了? 程式不挑的話,現在已經寫完了... 哈哈! 上面的輸入密碼畫面,輸入正確密碼後就可以看到文章內容了。我特地連網址列一起複製下來,在網址列上會看到密碼明碼。照道理應該是要先 HASH 啦,不過 CLIENT SIDE 跟 SERVER SIDE 都要有同樣的 HASH 機制才行,想用 MD5 / SHA256 之類的來算,無耐 CLIENT 要弄這些也是很煩,就決定不理它了...。明碼就明碼吧,執行後的畫面像這樣:

 

image

 

剩下的部份就沒什麼了,想想加上去好了。就是透過 BlogEngine 的 Extension Manager,讓使用者可以簡單的調整參數。要讓使用者自定的參數只有三個:

  1. 文章內容被保護時,要顯示的訊息
  2. 密碼提示
  3. 真正的密碼

這些東西自己做的話,就還得想要開檔案或寫資料庫,有點小囉唆,不過已經有 Extension Manager 了,只要在原本的 static constructor 再加幾行就搞定:

 

加上 Extension 接受的設定參數,及初始值[copy code]
    static SecurePost()    {        Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);        ExtensionSettings settings = new ExtensionSettings("SecurePost");        settings.AddParameter(            "SecurePostMessage",            "顯示訊息:");        settings.AddParameter(            "PasswordHint",            "密碼提示:");        settings.AddParameter(            "PasswordValue",            "指定密碼:");        settings.AddValues(new string[] {            "本篇文章已受密碼保護,請依照題示輸入密碼。",             "一二三四",            "1234"});        settings.IsScalar = true;        settings.Help = "用密碼保護文章的內容。";        ExtensionManager.ImportSettings(settings);        _settings = ExtensionManager.GetSettings("SecurePost");    }
   1:  static SecurePost()
   2:  {
   3:      Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);
   4:      ExtensionSettings settings = new ExtensionSettings("SecurePost");
   5:      settings.AddParameter(
   6:          "SecurePostMessage",
   7:          "顯示訊息:");
   8:      settings.AddParameter(
   9:          "PasswordHint",
  10:          "密碼提示:");
  11:      settings.AddParameter(
  12:          "PasswordValue",
  13:          "指定密碼:");
  14:      settings.AddValues(new string[] {
  15:          "本篇文章已受密碼保護,請依照題示輸入密碼。", 
  16:          "一二三四",
  17:          "1234"});
  18:      settings.IsScalar = true;
  19:      settings.Help = "用密碼保護文章的內容。";
  20:      ExtensionManager.ImportSettings(settings);
  21:      _settings = ExtensionManager.GetSettings("SecurePost");
  22:  }

 

我已經很努力的多撐幾行了... 不過也只有這廿行,寫完了...。整個 .cs 檔案直接丟到 ~/App_Code/Extension 就算安裝完成。用管理者身份登入 BE 後,在 Extension 那頁可以看到:

 

image

不錯,SecurePost 已經出現在 Extension Manager 裡了。因為有加上 settings 的程式碼,所以右邊有 [編輯] 的字樣出現。點下去之後會到這個畫面:

 

image

嗯,看起來真專業,沒想到從頭到尾所有的 CODE 還不到一百行...。幾十行 CODE 寫出來的 Extension 就可以唬人了.. :D,試看看還真的會動耶 (廢話)。早知道寫起來那麼快,當初就不花那麼多時間去找人家寫好的了...。最後附上整段完整的程式碼,有需要的人就拿去用吧! 用法很簡單,全部複製下來 (可以按 [COPY CODE] 就好),存檔,把檔案放在 ~/App_Code/Extension/SecurePost.cs 下,然後用管理者身份進入 BlogEngine Extension Manager 改一改就好了!

 

大功告成! 這個 Extension 如果對你有用的話就拿去用吧,要散佈也歡迎,不過只有個小要求,請不要把程式碼存到別的地方供人下載,請直接提供我這篇文章的網址就好。覺的好用就留個話給我,要幫我推一下文或讚助就更好了 :D,謝謝收看!

 

 

--

完整的 SecurePost.cs 程式碼[copy code]
using System;using System.Web;using System.Web.UI;using BlogEngine.Core.Web.Controls;using BlogEngine.Core;using System.Text;[Extension("SecurePost", "1.0", "<a href=\"http://columns.chicken-house.net\">chicken</a>")]public class SecurePost{    private static string SecurePostMessage { get { return _settings.GetSingleValue("SecurePostMessage"); } }    private static string Password { get { return _settings.GetSingleValue("PasswordValue"); } }    private static string PasswordHint { get { return _settings.GetSingleValue("PasswordHint"); } }    private static ExtensionSettings _settings = null;    static SecurePost()    {        Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);        ExtensionSettings settings = new ExtensionSettings("SecurePost");        settings.AddParameter(            "SecurePostMessage",            "顯示訊息:");        settings.AddParameter(            "PasswordHint",            "密碼提示:");        settings.AddParameter(            "PasswordValue",            "指定密碼:");        settings.AddValues(new string[] {            "本篇文章已受密碼保護,請依照題示輸入密碼。",             "一二三四",            "1234"});        //settings.ShowAdd = false;        //settings.ShowDelete = false;        //settings.ShowEdit = true;        settings.IsScalar = true;        settings.Help = "用密碼保護文章的內容。";        ExtensionManager.ImportSettings(settings);        _settings = ExtensionManager.GetSettings("SecurePost");    }    private static void Post_Serving(object sender, ServingEventArgs e)    {        Post post = sender as Post;        if (HttpContext.Current.User.Identity.IsAuthenticated == true) return;        if (HttpContext.Current.Request["pwd"] == Password) return;        if (!e.Body.StartsWith("[password]", StringComparison.CurrentCultureIgnoreCase)) return;        StringBuilder bodySB = new StringBuilder();        {            bodySB.AppendFormat(                "<b>{0}</b><p/>",                HtmlEncode(SecurePostMessage));            if (e.Location == ServingLocation.Feed)            {            }            else            {                bodySB.Append("<div>");                bodySB.AppendFormat(                    @"請輸入密碼(提示: <b>{0}</b>): <input id=""postpwd"" type=""password""/><button onclick=""document.location.href='{1}'+'?pwd='+escape(this.parentNode.all.postpwd.value);"">GO</button>",                     PasswordHint,                    post.AbsoluteLink);                bodySB.Append("</div>");            }        }        e.Body = bodySB.ToString();    }    private static string HtmlEncode(string text)    {        return HttpContext.Current.Server.HtmlEncode(text);    }}
   1:  using System;
   2:  using System.Web;
   3:  using System.Web.UI;
   4:  using BlogEngine.Core.Web.Controls;
   5:  using BlogEngine.Core;
   6:  using System.Text;
   7:   
   8:   
   9:   
  10:   
  11:  [Extension("SecurePost", "1.0", "<a href=\"http://columns.chicken-house.net\">chicken</a>")]
  12:  public class SecurePost
  13:  {
  14:      private static string SecurePostMessage { get { return _settings.GetSingleValue("SecurePostMessage"); } }
  15:      private static string Password { get { return _settings.GetSingleValue("PasswordValue"); } }
  16:      private static string PasswordHint { get { return _settings.GetSingleValue("PasswordHint"); } }
  17:   
  18:      private static ExtensionSettings _settings = null;
  19:   
  20:      static SecurePost()
  21:      {
  22:          Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);
  23:   
  24:          ExtensionSettings settings = new ExtensionSettings("SecurePost");
  25:   
  26:          settings.AddParameter(
  27:              "SecurePostMessage",
  28:              "顯示訊息:");
  29:          settings.AddParameter(
  30:              "PasswordHint",
  31:              "密碼提示:");
  32:          settings.AddParameter(
  33:              "PasswordValue",
  34:              "指定密碼:");
  35:   
  36:          settings.AddValues(new string[] {
  37:              "本篇文章已受密碼保護,請依照題示輸入密碼。", 
  38:              "一二三四",
  39:              "1234"});
  40:   
  41:          //settings.ShowAdd = false;
  42:          //settings.ShowDelete = false;
  43:          //settings.ShowEdit = true;
  44:          settings.IsScalar = true;
  45:          settings.Help = "用密碼保護文章的內容。";
  46:   
  47:          ExtensionManager.ImportSettings(settings);
  48:   
  49:          _settings = ExtensionManager.GetSettings("SecurePost");
  50:   
  51:      }
  52:   
  53:      private static void Post_Serving(object sender, ServingEventArgs e)
  54:      {
  55:          Post post = sender as Post;
  56:   
  57:   
  58:          if (HttpContext.Current.User.Identity.IsAuthenticated == true) return;
  59:          if (HttpContext.Current.Request["pwd"] == Password) return;
  60:          if (!e.Body.StartsWith("[password]", StringComparison.CurrentCultureIgnoreCase)) return;
  61:   
  62:   
  63:          StringBuilder bodySB = new StringBuilder();
  64:          {
  65:              bodySB.AppendFormat(
  66:                  "<b>{0}</b><p/>",
  67:                  HtmlEncode(SecurePostMessage));
  68:   
  69:              if (e.Location == ServingLocation.Feed)
  70:              {
  71:              }
  72:              else
  73:              {
  74:                  bodySB.Append("<div>");
  75:                  bodySB.AppendFormat(
  76:                      @"請輸入密碼(提示: <b>{0}</b>): <input id=""postpwd"" type=""password""/><button onclick=""document.location.href='{1}'+'?pwd='+escape(this.parentNode.all.postpwd.value);"">GO</button>", 
  77:                      PasswordHint,
  78:                      post.AbsoluteLink);
  79:                  bodySB.Append("</div>");
  80:              }
  81:          }
  82:          e.Body = bodySB.ToString();
  83:      }
  84:   
  85:      private static string HtmlEncode(string text)
  86:      {
  87:          return HttpContext.Current.Server.HtmlEncode(text);
  88:      }
  89:  }


8/12/2008 10:33:00 PM

原來是 IPv6 搞的鬼...

Microsoft.NET | ASP.NET Facebook Share

以前 (古早以前) 寫過一個簡單的 LIBRARY,就是去抓現在連上網頁的 CLIENT IP,然後簡單的套上 NET MASK,看看是不是在指定的網段內? 是的話就作些特別的處理 blah blah... 原本的 code 有點雜,我精簡之後變這樣,如果是 192.168.2.0 / 24 這範圍內的使用者連到這網頁,就會顯示 "Is Intranet? YES" ... 夠簡單吧? (怎麼連幾篇都這種不入流的 sample code ...)

這段 code 一直都運作的很好,沒碰過什麼大問題,不過就是把 IP address 切成四個 bytes, 然後利用位元運算併成 unsing integer, 方便跟後面的 netmask 作 bits and ...。不過某日興沖沖裝好 vista x64 + IIS7 之後發現,程式竟然不動了!?

 

先來看一下原始碼:

 

ASP.NET 程式範例[copy code]
<%@ Page Language="C#" Trace="true" %><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><script runat="server">    protected void Page_Load(object sender, EventArgs e)    {        this.Trace.Warn(System.Net.IPAddress.Parse(this.Request["REMOTE_HOST"]).AddressFamily.ToString());        this.IPLabel.Text = this.IsInSubNetwork(            "192.168.2.0",            "255.255.255.0",            this.Request.ServerVariables["REMOTE_HOST"]) ? ("YES") : ("NO");    }    private bool IsInSubNetwork(string network, string mask, string address)    {        uint netval = _IP2INT(network);        uint maskval = _IP2INT(mask);        uint addval = _IP2INT(address);        return (netval & maskval) == (addval & maskval);    }        private uint _IP2INT(string address)    {        string[] segments = address.Split('.');        uint ipval = 0;        foreach (string segment in segments)        {            ipval = ipval * 256 + uint.Parse(segment);        }        return ipval;    } </script><html xmlns="http://www.w3.org/1999/xhtml"><head runat="server">    <title>Untitled Page</title></head><body>    <form id="form1" runat="server">    <div>    Is Intranet? <asp:Label ID="IPLabel" runat="server" />    </div>    </form></body></html>
   1:  <%@ Page Language="C#" Trace="true" %>
   2:  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   3:  <script runat="server">
   4:   
   5:      protected void Page_Load(object sender, EventArgs e)
   6:      {
   7:          this.Trace.Warn(System.Net.IPAddress.Parse(this.Request["REMOTE_HOST"]).AddressFamily.ToString());
   8:          this.IPLabel.Text = this.IsInSubNetwork(
   9:              "192.168.2.0",
  10:              "255.255.255.0",
  11:              this.Request.ServerVariables["REMOTE_HOST"]) ? ("YES") : ("NO");
  12:      }
  13:   
  14:   
  15:      private bool IsInSubNetwork(string network, string mask, string address)
  16:      {
  17:          uint netval = _IP2INT(network);
  18:          uint maskval = _IP2INT(mask);
  19:          uint addval = _IP2INT(address);
  20:   
  21:          return (netval & maskval) == (addval & maskval);
  22:      }
  23:      
  24:      private uint _IP2INT(string address)
  25:      {
  26:          string[] segments = address.Split('.');
  27:   
  28:          uint ipval = 0;
  29:          foreach (string segment in segments)
  30:          {
  31:              ipval = ipval * 256 + uint.Parse(segment);
  32:          }
  33:   
  34:          return ipval;
  35:      }
  36:   
  37:   
  38:  </script>
  39:   
  40:  <html xmlns="http://www.w3.org/1999/xhtml">
  41:  <head runat="server">
  42:      <title>Untitled Page</title>
  43:  </head>
  44:  <body>
  45:      <form id="form1" runat="server">
  46:      <div>
  47:      Is Intranet? <asp:Label ID="IPLabel" runat="server" />
  48:      </div>
  49:      </form>
  50:  </body>
  51:  </html>

 

 

後來追了半天才意外發現問題出在這... 打開 ASP.NET Trace, 看一下 REMOTE_ADDR 到底抓到啥子東西?

 

image

 

嘖嘖嘖,搞半天原來是 Vista 預設把 IPv6 給開了起來,IIS7 / DevWeb 都中獎,直接回報 IPv6 格式的 IP Address 回來... 怎麼解? 這種問題說穿了就不值錢,強迫用 IPv4 就好。我試過幾種可行的方式,有:

 

 

  1. 直接用 IPv4 的位址連線: 這簡單,以我來說,URL 從 http://localhost/default.aspx 改成 http://192.168.100.40/default.aspx 就好了。不過這樣對 DevWeb 就沒用了,DevWeb 只接受來自 localhost 的連線...
    image



  2. 改 IIS 設定,直接綁到 IPv4 的位址,不過這招試不出來,似呼沒啥用,localhost 不會連到 192.168.100.40,而我直接打這 IP 的話就會變成範例1...
    image

  3. 改 c:\windows\system32\drivers\etc\hosts
    無意間 PING 看看 localhost, 才發現連 localhost 都被對應到 IPv6 了...
    image

    打開 C:\windows\system32\drivers\etc\hosts 這檔案看一看,果然...
    image


    把 IPv6 那行拿掉後再試試 ping localhost ...
    image 


    耶! 這次 IP 就變成 IPv4 的了... 開 IE, 連 http://localhost/default.aspx 看看,it works!
    image

    因為這招是直接把 localhost 對應到 127.0.0.1,因此對於鎖 localhost 的 WEBDEV 也可以用。

  4. 大絕招: 直接關掉 IPv6 ...
    真是個沒品的傢伙,打不過就來這套...
    image

    image
    這樣也可以...

 

 

 

碰到這種怪問題,一時之間還熊熊不知道是那裡掛掉,還真是麻煩... 特地記一下這篇,讓一樣吃過 IPv6 苦頭的人參考一下。至於怎樣作才對? 當然是用 "正規" 的方式來處理 IP Address...   System.Net.IPAddress 類別包含一個靜態方法: IPAddress Parse(string ipaddress), 用它可以把字串格式的 IP 換成這個類別的 instance, 用它內建的 property: AddressFamily,看看值是 enum 型態的 InterNetwork 還是 InterNetworkV6 就知道了,不要像我當年年少不更事一樣,自己硬去拆字串... Orz



7/26/2008 12:17:30 AM

x64 programming #2: ASP.NET + ODBC (讀取 CSV)

Microsoft.NET | ASP.NET | x64 | 小技巧 | 技術隨筆 Facebook Share

今天的範例超簡單,簡單到很沒水準的地步... 難道本 columns 的水準降低了嘛? 咳咳... 不多說,今天的例子也不需要解釋,直接來看 sample code:

 

Default.aspx.cs 程式碼[copy code]
   1:  using System;
   2:  using System.Data;
   3:  using System.Web.UI.WebControls;
   4:  using System.Data.Odbc;
   5:   
   6:  public partial class _Default : System.Web.UI.Page 
   7:  {
   8:      protected void Page_Load(object sender, EventArgs e)
   9:      {
  10:          DataSet ds = new DataSet();
  11:          OdbcConnection conn = new OdbcConnection("Driver={Microsoft Text Driver (*.txt; *.csv)};DBQ=" + Server.MapPath("~/App_Data"));
  12:          OdbcDataAdapter adpt = new OdbcDataAdapter("select * from [database.txt]", conn);
  13:          adpt.Fill(ds);
  14:   
  15:          this.DataGrid1.DataSource = ds.Tables[0];
  16:          this.DataGrid1.DataBind();
  17:      }
  18:  }

 

真的是沒什麼特別的 code, 連 exception 都沒處理... 難道 .aspx 有什麼特別的嘛? 來看看:

Default.aspx[copy code]
   1:  <%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>
   2:  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   3:  <html xmlns="http://www.w3.org/1999/xhtml">
   4:  <head runat="server">
   5:      <title>Untitled Page</title>
   6:  </head>
   7:  <body>
   8:      <form id="form1" runat="server">
   9:      <div>
  10:      <asp:DataGrid ID="DataGrid1" runat="server" />
  11:      </div>
  12:      </form>
  13:  </body>
  14:  </html>

 

真的沒啥特別的,再來看看 CSV 檔的內容好了,看看有沒有什麼特別的...

~/App_Data/database.txt[copy code]
   1:  name,email
   2:  chicken,chicken@chicken-house.net
   3:  peter,peter@chicken-house.net
   4:  annie,annie@chicken-house.net
   5:  nancy,nancy@chicken-house.net

 

咳,想扁我的忍一下... 這支程式大概只比 "Hello World" 好一點,會看本 BLOG 的大概用看的就知道結果是什麼了吧? 不就把所有的內容套到 DataGrid 裡顯示出來? 像這樣:

image

 

真是沒營養的內容... 現在要開始進入主題了。執行環境是 Vista x64 + Visual Studio 2008,這個 web site 是透過 DevWeb 來執行的,不是透過 IIS... 反正都一樣嘛,測完可以 RUN (這種程式應該不會有什麼 BUG,頂多打錯字編譯錯誤..) 後就收工了,把它 DEPLOY 到 IIS 上面 ( windows 2003 x64, IIS6 ) 跑看看:

 

image

 

掛了... 當然... 不然這篇是要討論什麼? 老實說這是我親身碰到的例子,從這錯誤訊息還真摸不著頭腦,完全搞不懂發生什麼事。該裝的都裝了,也都沒錯,為什麼會這樣?  二話不說,先確定系統的 ODBC 是正常的,最好的方式就是找現成的程式試看看,就可以初步判定是我的問題 OR 系統的問題。到控制台的 ODBC Data Source Administrator 看看,先建個同樣的 ODBC data source...

 

image

真是見鬼了,我的 windows 95 vpc 能用的 odbc driver 都比這裡多... 看來真的是沒有 ODBC driver,那我裝的 ADODB 是裝到那裡去了?

科學的實驗都講求先假設,再控制變因,然後證明假設是正確的... 不過現在一點線索都沒有,只能靠運氣了。會有這篇也真的是運氣好,聯想到是不是 x64 的問題? 用我謹有的知識: x64 / x86 兩種模式的程式不能同時出現在單一 process, 為了證明這件事,就特地在 SERVER 安裝了 excel, 用 excel 來開啟 odbc, 竟然可以?

解這問題,裝 office 是一年多前的事了,現在也沒畫面好抓,就跳過去... 想到 x64 一堆東西都有兩種版本,控制台是不是也有? 真該好好拿箱仙草蜜,拜一拜交大門口的土地公... Bingo! 執行了32位元版的 ODBC Data source Administrator (c:\Windows\SysWOW64\odbccp32.cpl), 結果出現了這畫面:

image

真是好狗運,如果沒矇對的話,不知道還要搞多久... 這時才恍然大悟,原來 x64 要求所有的 driver 都要是 64 位元版,加上單一 process 不能混用 x86 / x64 兩種模式的 code,就是指這個... driver 不只是 "硬體" 的 driver,連各種軟體的都算。廣義一點來說,ODBC driver, OleDB Provider 也都算 driver 的一種,各種 Plug-Ins,甚至是各種 COM 元件 (只要是 In-Process 的都算),到 COM 的延伸... IE ActiveX Control,Media Player 用的 Codec ... 通通都算...

我終於體會到要轉移到 x64 有多麻煩了。在 DOS 年代或是 WIN 3.1 年代,每個軟體都很獨立,換到 WIN32 沒什麼問題。現在的軟體就不一樣,轉到 x64 都可以跑,不過要用到的各種共用元件就不一定了。拿掉 COM 的話,VB / ASP 大概就什麼都不剩了吧...

回題,來看看這問題怎麼解。雖然搞清楚原因,但是我的程式還是不能動。CSV 其實還可以用文字檔硬解,不過我實際工作上碰到的例子是要解讀上傳的 EXCEL 檔的內容... EXCEL 我可沒辦法硬搞... 不過現在方向清楚了,只要有辦法把程式從 64 位元模式改成 32 位元模式執行,就可以抓的到 32 位元模式下的 ODBC Data Source, 程式就正常了。不過該怎麼告訴 IIS6,我的程式需要的執行環境是 32 位元?

 

上網查了一下 x64 版的 IIS 如何執行 x86 模式的程式? 找到這篇:

http://support.microsoft.com/kb/894435/zh-tw

IIS 6.0 同時支援 32 位元模式及 64 位元模式。但是,IIS 6.0 不支援同時在 64 位元版的 Windows 上執行兩種模式。ASP.NET 1.1 只能在 32 位元模式中執行。ASP.NET 2.0 可以在 32 位元模式或 64 位元模式中執行。因此,如果要同時執行 ASP.NET 1.1 和 ASP.NET 2.0,您必須在 32 位元模式中執行 IIS。

實際切換的動作在這篇也有寫...

ASP.NET 2.0 的 32 位元版本
如果要執行 32 位元版的 ASP.NET 2.0,請依照下列步驟執行: 1. 按一下 [開始],再按一下 [執行],輸入 cmd,然後按一下 [確定]。
2. 輸入下列命令以啟用 32 位元模式:
cscript %SYSTEMDRIVE%\inetpub\adminscripts\adsutil.vbs SET W3SVC/AppPools/Enable32bitAppOnWin64 1
3. 輸入下列命令以安裝 ASP.NET 2.0 (32 位元) 的版本,以及在 IIS 根目錄和下列位置底下安裝指令碼對應:
%SYSTEMROOT%\Microsoft.NET\Framework\v2.0.40607\aspnet_regiis.exe -i
4. 請確定在 Internet Information Services Manager 的 Web Service Extension 清單中,將 ASP.NET 2.0.40607 版 (32 位元) 的狀態設定為 Allowed。

 

切換過後,再重新執行一次,一切就正常了:

image

 

雖然在 x64 下執行 x86 的程式,也是有一堆額外的好處,不過看起來就是不大爽... IIS6 只能二選一,兩種模式只能挑一種。這個問題到了 IIS7 就獲得解決了。 IIS7 允許同時存在這兩種模式的 Application ..

其實在 x64 下的問題還很多,不過大都不外乎這模式,x64 / x86 的 dll 不能混用。現今軟體都用一堆元件,你得確保每一個用到的元件都有 x64 版,如果有一個沒有? 乖乖的切回 x86 來執行吧...。類似的小狀況其實還蠻多的,下回多列幾種我碰到的狀況,以免各位跟我一樣碰釘子還試個老半天... 敬請期帶第三集 :D



7/6/2008 4:13:00 AM

[BlogEngine Extension] PostViewCount 1.0

Microsoft.NET | ASP.NET | BlogEngine.NET | 我的作品 | 技術隨筆 | BlogEngine Extension Facebook Share

這篇拖好久了,本來上禮拜要寫,結果正好碰到 BlogEngine 1.4 RELEASE,就一直拖到現在...。之前找到一個給 BlogEngine 用的 Counter Extension,以功能來說還不錯用,不過用久了就開始不滿足了。正好翻到這篇教學文章,算是官方文章了吧 (BlogEngine 作者之一寫的教學文)? 所以就動起自己寫的念頭。舊的其實沒什麼不好,不過缺了這幾項我想要的功能:

  1. 只有計 Total Count (謎: 不然你要 counter 記什麼?)
  2. 資料檔的結構及 I/O 的設計有點 Orz...
    1. 讀寫 XML 的 CODE 寫的很... Orz
    2. 沒有處理同時讀寫的問題 (後面寫的資料可能會蓋掉前面的,我的點閱率不知道少了幾百次 :D)
    3. 要有 CACHE 來加速處理速度

 

既然要重寫,當然要寫個合用的. 底下是我對於新的 COUNTER 期望:

  1. 要能記流水帳。
    流水帳就是不只要記總數,我還要知道每次點閱的時間,來源 IP ... 等等
    ( darkthread 指示: 當你流量大的時後就不會去在意這個了... Orz, 真是一針見血... )
  2. 要處理多執行緒下讀寫資料檔的問題,這部份 Code 必需為 ThreadSafe。
  3. 妥善利用 CACHE,降低 (2) 的複雜度。
  4. COUNTER COMPACT
    配合 (1) 的需求,流水帳記錄太多的話也會造成問題,COUNTER要能適當的刪除舊的 HIT RECORD。

 

決定好後就動工了! 既然問題都圍繞在 data storage 上,先來看看原來的檔案格式:

原有的 ~/App_Data/PostViews.xml 片段:[copy code]
<?xml version="1.0" encoding="utf-8" standalone="yes"?><posts>    <post id="b43ec49e-e9a2-4696-bcc7-2ba1667ecda9">781</post>    <post id="f1411c11-11ed-4f35-b383-0c6c8b2b963a">603</post>    <post id="e7b57492-652b-4247-bcd4-bc3ac2e56318">589</post>    <post id="7e2c2c88-240c-40ea-8477-2c96880adc8e">556</post>    <post id="0fda9c32-d294-4f09-85cd-41dab8e677cb">678</post>    ......</posts>
   1:  <?xml version="1.0" encoding="utf-8" standalone="yes"?>
   2:  <posts>
   3:      <post id="b43ec49e-e9a2-4696-bcc7-2ba1667ecda9">781</post>
   4:      <post id="f1411c11-11ed-4f35-b383-0c6c8b2b963a">603</post>
   5:      <post id="e7b57492-652b-4247-bcd4-bc3ac2e56318">589</post>
   6:      <post id="7e2c2c88-240c-40ea-8477-2c96880adc8e">556</post>
   7:      <post id="0fda9c32-d294-4f09-85cd-41dab8e677cb">678</post>
   8:      ......
   9:  </posts>

 

很普通的格式,配合我的需求,新的檔案結構我打算改成這樣:

新的 ~/App_Code/counter/{post-id}.xml 檔的片段內容:[copy code]
<?xml version="1.0" encoding="utf-8"?><counter base="8828">  <hit time="2008-06-29T12:42:51" referer="" remote-host="66.249.73.185" user-agent="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" />  <hit time="2008-06-29T13:04:15" referer="http://www.google.com.tw/search?complete=1&amp;hl=zh-TW&amp;cr=countryTW&amp;rlz=1B3GGGL_zh-TWTW237TW238&amp;q=%E9%A6%99%E6%B8%AFg9&amp;start=30&amp;sa=N" remote-host="124.10.1.162" user-agent="Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9) Gecko/2008052906 Firefox/3.0" />  <hit time="2008-06-29T13:04:20" referer="" remote-host="66.249.73.185" user-agent="Mediapartners-Google" />  ......</counter>
   1:  <?xml version="1.0" encoding="utf-8"?>
   2:  <counter base="8828">
   3:    <hit time="2008-06-29T12:42:51" referer="" remote-host="66.249.73.185" user-agent="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" />
   4:    <hit time="2008-06-29T13:04:15" referer="http://www.google.com.tw/search?complete=1&amp;hl=zh-TW&amp;cr=countryTW&amp;rlz=1B3GGGL_zh-TWTW237TW238&amp;q=%E9%A6%99%E6%B8%AFg9&amp;start=30&amp;sa=N" remote-host="124.10.1.162" user-agent="Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9) Gecko/2008052906 Firefox/3.0" />
   5:    <hit time="2008-06-29T13:04:20" referer="" remote-host="66.249.73.185" user-agent="Mediapartners-Google" />
   6:    ......
   7:  </counter>

 

其中, /counter/@base 是給你作弊用的,這數字你填多少就是多少,然後再加上底下有幾筆 <hit /> 記錄,總數就出來了。很簡單的格式,而流水帳就是記在 <hit /> 這個 XML ELEMENT 的 ATTRIBUTES 上。目前記的有時間,IP,REFERRER 而以。等到網站人數暴增 (幻想嘛?),COMPACT的動作就免不了了,預留的 /counter/@base 就派上用場了。COUNTER 在必要時就能刪掉多餘的 <hit /> 記錄,再把刪掉的筆數加到 /counter/@base 上,讓最後點擊總數不變,又能控制記錄檔的大小。

 

邏輯確定後就可以開始動工了,不過另外要解決技術問題,就是 ThreadSafe 的部份。這部份讓我煩惱了一下,因為在 File System 就提供了 FILE LOCK 的機制可以用,不過取得 LOCK 失敗是會引發 EXCEPTION,而不是像其它的 THREAD 控制機制會 WAIT。因此最後我決定 FILE LOCK 的機制做第二層保障 ,主要的 LOCK 機制還是自己來,用基本的 Monitor 來實作。要實做 LOCK 的前題,是要有明確的 LOCK "對像"。兩個 thread lock 同一個物件,後面 lock 的 thread 會被 block 暫停執行,直到前一個先 lock 同一個物件的 thread 釋放之後才會被喚醒,因此要先解決兩個同 ID 的 COUNTER 能拿到同一個物件,才能實作 LOCK 機制。這物件就好幾位合適的候選人,第一種方法是 COUNTER 本身,第二種方法則是替每個 counterID 產生一個無用的物件,就單純拿來 LOCK 用。

 

要用 (1) 成本太高,一來就必需實作 Flyweight 這個 design pattern,這個設計模式並不難,只要實作 factory pattern再搭配個 Dictionary<string, Counter> 物件就可以搞定,但是我最後沒有選擇這個作法。因為最糟的情況下有可能會讓整個系統可能會用到的 counter 通通被放到這個 dictionary ,沒有被釋放回收的機會,因為沒有明確的時間點可以把這物件從 dictionary 移掉,移不掉的話永遠就會留著一個 object reference 指向這 COUNTER 物件,reference 只要存在,它就永遠不會被 GC 收掉...。不過這還是有解,只要用 WeakReference 就可以解決了,不過我只是要作個簡單的 Counter,搬出這一堆東西會不會太過頭了?

 

因此我選擇了第二個方法。一樣用 flyweight pattern,只不過我放的是拿來 lock 的物件。我是直接 new object() 就拿來用了,物件很小我就不用耽心建立太多個又不能回收的問題...,而 counter 就讓它回歸最簡單的用法,需要就 new 一個出來,用完就丟著等著被回收。

 

每次都是講的話比 CODE 還多,來看看程式碼:

 

COUNTER 物件的 SYNC 機制[copy code]
        // 所有 COUNTER 用的 SYNC 物件 DICTIONARY        private static Dictionary<string, object> _counter_syncroot = new Dictionary<string, object>();        // 取得這個 COUNTER 用的 SYNC 物件        private object SyncRoot        {            get            {                return Counter._counter_syncroot[this._counterID];            }        }        private Counter(string counterID)        {            this._counterID = counterID;            //            //  建立 SYNC 物件 (如果沒有的話)            //            lock (Counter._counter_syncroot)            {                if (Counter._counter_syncroot.ContainsKey(this._counterID) == false)                {                    Counter._counter_syncroot.Add(this._counterID, new object());                }            }            //  略 ....        }        public void Hit()        {            lock (this.SyncRoot)            {                //                //  LOCK 後再開始更新檔案內容。 程式碼 略...                //            }        }
   1:  // 所有 COUNTER 用的 SYNC 物件 DICTIONARY
   2:  private static Dictionary<string, object> _counter_syncroot = new Dictionary<string, object>();
   3:  // 取得這個 COUNTER 用的 SYNC 物件
   4:  private object SyncRoot
   5:  {
   6:      get
   7:      {
   8:          return Counter._counter_syncroot[this._counterID];
   9:      }
  10:  }
  11:  private Counter(string counterID)
  12:  {
  13:      this._counterID = counterID;
  14:      //
  15:      //  建立 SYNC 物件 (如果沒有的話)
  16:      //
  17:      lock (Counter._counter_syncroot)
  18:      {
  19:          if (Counter._counter_syncroot.ContainsKey(this._counterID) == false)
  20:          {
  21:              Counter._counter_syncroot.Add(this._counterID, new object());
  22:          }
  23:      }
  24:      //  略 ....
  25:  }
  26:  public void Hit()
  27:  {
  28:      lock (this.SyncRoot)
  29:      {
  30:          //
  31:          //  LOCK 後再開始更新檔案內容。 程式碼 略...
  32:          //
  33:      }
  34:  }

 

這問題解決掉後,剩下的就是單純把我要的邏輯實作出來,這部份我相信讀者的程度都不用我多講了,有需要的看 CODE 就看的懂。請直接下載最後面的程式碼就好。

 

另外一個要提一下的是跟 BlogEngine 本身 Extension 相關的,這部份也花了點時間研究該怎麼寫。BlogEngine 的 Extension 寫法比較特別一點,一般這種外掛都是採 Provider 的方式實作 ( factory pattern 加上 abstract class ),先定義好這個 Provider 能作什麼事,然後每個寫 Extension 的人就自己繼承這類別來修改,再靠 Factory 動態的建立正確的 Extension 物件來使用。不過在這部份 BlogEngine 採用完全不同的作法來設計它的架構: Event Handler。

 

Provider 依賴的是事先定義好的 ProviderBase (abstract class) 類別。這個類別定義了多少東西給底下的人覆寫,就決定了寫外掛的人能處理多少事。好處是簡單,架構清楚。缺點是能讓你擴充的功能,在 DESIGN TIME 就決定了,要多一個能 "擴充" 的地方,就得改 ProviderBase 類別定義,這很有可能會讓現有的 Extension 不能跑...。換成 EVENT 的方式就沒有跟程式碼綁的那麼緊了。多了新的功能,多定義一些事件就夠了。BlogEngine 就是採這種方式來實作它的 Extension .. 

 

另外比較特別的是,BlogEngine 替每個 Extension 規劃好存放設定的地方。 1.3 版是所有的 Extension 共用一個設定檔, 1.4 則是有獨立的設定檔可以用。不過這些改變倒是沒有影響到它提供的 API。對於 API 來說,設定檔提供每個 Extension 一個像是 DataTable 那樣的 data storage, 讓你自訂欄位名,型別,然後能讓你一筆一筆的加進去,可以有多筆資料,而 BlogEngine Runtime 會負責幫你管理好這些設定。

 

這部份帶入門就好,用法還是去查官方文件比較快。我簡單貼一下這部份 CODE 跟畫面上提供的設定頁面給大家看看:

準備設定值的 SCHEMA 及載入目前的設定值[copy code]
    public PostViewCounter()	{        Post.Serving += new EventHandler<ServingEventArgs>(OnPostServing);        ExtensionSettings settings = new ExtensionSettings("PostViewCounter");        settings.AddParameter(            "MaxHitRecordCount",             "最多保留筆數:");        settings.AddParameter(            "HitRecordTTL",             "最長保留天數:");        settings.AddValues(new string[] { "500", "90" });        //settings.ShowAdd = false;        //settings.ShowDelete = false;        //settings.ShowEdit = true;        settings.IsScalar = true;        settings.Help = "設定 counter hit records 保留筆數及時間。只有在筆數限制內且沒有超過保留期限的記錄才會被留下來。";                ExtensionManager.ImportSettings(settings);        _settings = ExtensionManager.GetSettings("PostViewCounter");    }
   1:    public PostViewCounter()
   2:  {
   3:        Post.Serving += new EventHandler<ServingEventArgs>(OnPostServing);
   4:        ExtensionSettings settings = new ExtensionSettings("PostViewCounter");
   5:        settings.AddParameter(
   6:            "MaxHitRecordCount", 
   7:            "最多保留筆數:");
   8:        settings.AddParameter(
   9:            "HitRecordTTL", 
  10:            "最長保留天數:");
  11:        settings.AddValues(new string[] { "500", "90" });
  12:        //settings.ShowAdd = false;
  13:        //settings.ShowDelete = false;
  14:        //settings.ShowEdit = true;
  15:        settings.IsScalar = true;
  16:        settings.Help = "設定 counter hit records 保留筆數及時間。只有在筆數限制內且沒有超過保留期限的記錄才會被留下來。";
  17:        ExtensionManager.ImportSettings(settings);
  18:        _settings = ExtensionManager.GetSettings("PostViewCounter");
  19:    }

 

 

對應的設定頁面:

image

 

 

 

最後講了半天,真正想自己動手寫的人應該不多吧 :D,只是想下載回去裝來用的人就不用聽我前面廢話一堆了,只要下載這檔案,放到 ~/App_Code/Extension 下,就安裝完成了... 咳咳,連安裝手冊都省了。檔案 COPY 好後就會在 Extension Manager 裡看到我寫的外掛,就可以開始用了。有任何意件歡迎留話給我 :D

 

檔案下載: http://columns.chicken-house.net/file.axd?file=PostViewCounter.cs



6/29/2008 11:04:00 PM

[BlogEngine.NET] 改造工程 - 整合 FunP 推推王

Microsoft.NET | 543 | ASP.NET | BlogEngine.NET Facebook Share

古早以前,曾替我的 BLOG 加上推推王的小貼紙,不過當時也僅止於把 CODE 加上去而以,成效不大好...。這次搬家搬到 BlogEngine 後,又開始一樣的循環了..,要不要加上這些共用書籤? 要加那一套? 目前台灣用的最多就是黑米推推王了。

原本挑了黑米,只因為它有提供 [黑米卡],正好取代掉 BlogEngine 右邊那塊 [關於作者] .. 不過試用的情況不怎麼理想,除了速度有點慢之外,同一頁放太多 (幾十個) 的速度也很慢,也許跟 BlogEngine 我選用的樣板有點不合,速度太慢時有時整個版面就毀了,下載到一半就掛掉...

相對之下,看了看 FunP 提供的 SCRIPT,看起來 CODING STYLE 比較合我的胃口,速度也快一些,沒碰到會讓我版面掛掉的問題。另外使用上的流程 FunP 也簡單一點,本來想兩家的書籤都放的,到最後就決定支持一下交大的學弟,就全力跟 FunP 推推王整合好了。

動手前先計劃了一下,毫無目的的把一堆 CODE 加上去,我最忌誨這樣弄了,看起來一點主題都沒有。常看到別人的 BLOG 滿滿一堆標籤,從國內的 FunP,黑米,MyShare,到國外的DELICIUS,還有一堆叫不出名字的,一字排開落落長...

BlogEngine 原本也有內建一些,不過被我拿掉了。底下列出我調整的前後差異:

  1. 捨棄內建的 Rating 機制,直接用推文就好。
  2. 版面我希望類似原有 CS 的樣式,正好拿推文按鈕來取代原本的計數器
  3. 就鎖定一個共用書簽就好,把原有左下方的其它都拿掉
  4. Tags 我也決定捨棄不用,以分類為主。因此左下的 Tags 就移掉了
  5. 分類放右下很礙眼,移到右上
  6. 不爽被盜文,加上一段智財權的聲明
  7. 加上自己補的計數器... (說來話長,請見下一篇)
  8. 推文時自動帶出文章的基本資訊,如標題,內文,標簽等等

 

原 CS 的樣式:

image

 

 

修改前:

image

修改後:

image

 

看了一下推推王的工具,不外乎都是插入一段 <SCRIPT> 標簽,然後用 document.write( ) 或是 eval 等等 client side script 的方式產生片 HTML Code, 缺點就是繞了一大圈,出了問題也常讓人搞不清楚問題在那裡。花了點時間追一下,追出最後插在網頁的 HTML CODE 長這樣:

[copy code]
    <IFRAME	width=60 	height=55	marginWidth=0 	marginHeight=0 	frameBorder=0 	scrolling=no 	src="http://funp.com/tools/buttoniframe.php?url=xxxxxxxxxxxxxx&s=1" mce_src="http://funp.com/tools/buttoniframe.php?url=xxxxxxxxxxxxxx&s=1">    </IFRAME>
   1:    <IFRAME
   2:  width=60 
   3:  height=55
   4:  marginWidth=0 
   5:  marginHeight=0 
   6:  frameBorder=0 
   7:  scrolling=no 
   8:  src="http://funp.com/tools/buttoniframe.php?url=xxxxxxxxxxxxxx&s=1">
   9:    </IFRAME>

 

 

 

看起來就是直接產生一個帶著指定參數的 <IFRAME ...>,於是我在 BlogEngine Themes 版面就直接產生 <IFRAME> ...底下是 BlogEngine THEME 目錄下的 PostView.ascx 片段:

在 PostView.ascx 顯示推文按鈕的片段[copy code]
    <%        Regex _stripHTML = new Regex("<[^>]*>", RegexOptions.Compiled);        string PostTextContent = _stripHTML.Replace(Post.Content, "");        int maxLength = 70;                    string EncodedAbsoluteLink = Page.Server.UrlEncode(Post.AbsoluteLink.ToString());        string EncodedPostTitle = Page.Server.UrlEncode(Post.Title);        string EncodedPostBody = Page.Server.UrlEncode((PostTextContent.Length > maxLength) ? (PostTextContent.Substring(0, maxLength) + "...") : (PostTextContent));        string TagsQueryString = "";        foreach (BlogEngine.Core.Category cat in Post.Categories)        {            TagsQueryString += string.Format("&tags[]=" + Page.Server.UrlEncode(cat.Title));        }    %>        <IFRAME	width=60 	height=55	marginWidth=0 	marginHeight=0 	frameBorder=0 	scrolling=no 	src="http://funp.com/tools/buttoniframe.php?url=<%=EncodedAbsoluteLink %>&s=1">    </IFRAME>
   1:    <%
   2:        Regex _stripHTML = new Regex("<[^>]*>", RegexOptions.Compiled);
   3:        string PostTextContent = _stripHTML.Replace(Post.Content, "");
   4:        int maxLength = 70;
   5:        string EncodedAbsoluteLink = Page.Server.UrlEncode(Post.AbsoluteLink.ToString());
   6:        string EncodedPostTitle = Page.Server.UrlEncode(Post.Title);
   7:        string EncodedPostBody = Page.Server.UrlEncode((PostTextContent.Length > maxLength) ? (PostTextContent.Substring(0, maxLength) + "...") : (PostTextContent));
   8:        string TagsQueryString = "";
   9:        foreach (BlogEngine.Core.Category cat in Post.Categories)
  10:        {
  11:            TagsQueryString += string.Format("&tags[]=" + Page.Server.UrlEncode(cat.Title));
  12:        }
  13:    %>
  14:    <IFRAME
  15:  width=60 
  16:  height=55
  17:  marginWidth=0 
  18:  marginHeight=0 
  19:  frameBorder=0 
  20:  scrolling=no 
  21:  src="http://funp.com/tools/buttoniframe.php?url=<%=EncodedAbsoluteLink %>&s=1">
  22:    </IFRAME>

 

這是推文的部份,如果要張貼的話就不一樣了,要放的是把文章的預設資訊都帶過去,免的到時要重新輸入一次... 這部份的 CODE 比較囉唆,不過產生出來的 CODE 比較單純,就是個 <A> LINK 而以,不過因為帶的資訊比較多,所以部份 CODE 是由上面的 CODE 事先產生好,這裡才拿來用的:

產生推文按鈕的部份[copy code]
   1:    <a href="http://funp.com/push/submit/add.php?url=<%=EncodedAbsoluteLink %>&s=<%=EncodedPostTitle %>&t=<%=EncodedPostBody %><%=TagsQueryString %>&via=tools" title="貼到funP">
   2:  <img src="http://funp.com/tools/images/post_03.gif" border="0"/>
   3:    </a>

 

 

 

 

果然效果好多了,也不會再碰到版面掛掉等等鳥問題,只不過載入 [封存] 頁面時,一次四五百個 <IFRAME> 同時在跑,IE也是跑的很吃力....

image

 

 

 

同樣的技巧也拿來修改 ~/archive.aspx 這頁。這頁原本是把所有的文章按照分類一篇一篇列出來,捨棄原有的 RATING 機制不用,直接用推文的機制取代。因此這頁原本顯示 RATING 分數的地方就被我改成推推王的推薦次術了。我的文章有兩百多篇,出現過的地方都列一次,加一加總共會出現近五百個推文按鈕 @_@,自然也不可能用原本官方的作法產生按鈕,直接用上面挖出來的方法,修改 archive.aspx.cs:

~/archive.aspx.cs 顯示推文按鈕的片段程式[copy code]
          if (BlogSettings.Instance.EnableRating)          {              HtmlTableCell rating = new HtmlTableCell();              rating.InnerHtml = string.Format(                @"<IFRAME   marginWidth=0   marginHeight=0   src='http://funp.com/tools/buttoniframe.php?url={0}&amp;s=12'   frameBorder=0   width=80   scrolling=no   height=15></IFRAME>",                 (post.AbsoluteLink.ToString()));              rating.Attributes.Add("class", "rating");              row.Cells.Add(rating);          }
   1:            if (BlogSettings.Instance.EnableRating)
   2:            {
   3:                HtmlTableCell rating = new HtmlTableCell();
   4:                rating.InnerHtml = string.Format(
   5:                  @"
   6:  <IFRAME 
   7:    marginWidth=0 
   8:    marginHeight=0 
   9:    src='http://funp.com/tools/buttoniframe.php?url={0}&amp;s=12' 
  10:    frameBorder=0 
  11:    width=80 
  12:    scrolling=no 
  13:    height=15>
  14:  </IFRAME>", 
  15:                  (post.AbsoluteLink.ToString()));
  16:                rating.Attributes.Add("class", "rating");
  17:                row.Cells.Add(rating);
  18:            }

 

嗯,看起來效果好多了,至少我自己看起來順眼多了 :D

下一篇預告一下,下一篇會推出我自己寫的 PostViewCounter Extension,主要就是拿來作每篇文章的點閱率。BlogEngine 沒內建,找來的現成的又不是很合用,索性就自己寫了一個,請期待續篇 :D



6/25/2008 2:48:00 AM

Bot Checker 回來了!

Microsoft.NET | 543 | ASP.NET | BlogEngine.NET | 我的作品 Facebook Share

哈哈,終於加回來了 :D

 

為什麼原本在 CS 上很簡單就加上去的 Bot Checker, 在 BE 上弄到現在才好? 原因只有一個,就是 BE 在張貼回應時用了不少 AJAX 的機制,變的要插一段 CODE 進去要追半天 @_@...

很諷刺的是,AJAX 其實是 Community Server 用的比較兇,到處都要來一下... 反而 BlogEngine.NET 就中規中舉多了,很多地方就都乖乖的用 PostBack.. 唯讀回應的地方很突兀,感覺好像是特別要現一下回應的那個 TEXT EDITOR 還有 BBCODE 預覽的樣子... 那邊的 CODE 弄的實在是有點亂...

也是之前幾次都沒認真追啦... 追到一半覺的煩就去逛網站了,哈哈... 今天狠下心把它解決掉了。只不過卡著 AJAX,又不想跟它奮鬥,所以有些地方就偷懶混過去了。什麼意思? 意思就是攤開 HTML 原始碼,這個 Bot Checker 也是防不了 Bot 啦,不過我就賭我的站還有我的 Bot Checker 沒大到有人願意寫 Bot 來攻擊我.. 哈哈.. 來看看效果:

image

 

當然,張貼出去的回應也會帶著 Bot Checker 的問題。只不過礙於 AJAX,一堆東西被迫要移到 CLIENT SIDE 來處理,這邊就偷懶,題目產生完就先填到 [評論] 欄了,各位在填回應時,不爽附上 "芭樂雞萬歲" 的話,還是可以把它刪掉...

 

image

 

 

--
題外話,在追 BE 的程式碼的過程中,發現 BE 也有 CAPTCHA ? 不過還真的找不到怎麼把它打開... 有興趣用 BlogEngine.NET 又想用正統的 CAPTCHA 驗證的人可以試看看



6/21/2008 4:10:22 AM

[BlogEngine.NET] 改造工程 - CS2007 資料匯入

Microsoft.NET | 543 | ASP.NET | BlogEngine.NET | CS | 技術隨筆 Facebook Share

自從搬到 BlogEngine.NET 之後,每天晚上都沒閒著,一點一點的慢慢把整個網站改成我要的樣子... 資料庫 / 檔案是我最在意的,當然要優先顧好,光這部份就花了一個多禮拜...

最早找到了 CS2007 --> BlogML --> BlogEngine,大概把我想要的 90% 資料都轉過來了,不過看到那 10% 的資訊沒過來,心裡就很不是滋味...

 

"這是我要一直保留下來的資料耶,現在沒把資料補回來,以後就永遠補不回來了..."

 

就是心裡一直這樣想,所以... 重匯吧! 不然以後後悔也是補不回來的。首先當然是想先從 BlogEngine 匯入 BlogML 的工具下手,看了一下,沒提供 Source Code ? 再看一下,一篇文章一個 Web Service Call,一則回應也是一個 Web Service Call ... 感覺起來有點沒效率,不過不管了,我內容也不多 (兩百多篇文章) ... 既然有 Web Services,先看看它的 WSDL ...

 

image
http://columns.chicken-house.net/api/blogImporter.asmx

 

沒幾個 WebMethod 嘛,不過看一看它的 Interface 就已經漏掉很多資訊沒轉進來了 (像是我要的原 CS PostID,事後作新舊網址對照表用,還有 PageViewCount... etc),要改原程式也是個大工程,不但 CLIENT 要改,WEB METHOD 也要擴充... 我又不是非得遠端用 WEB SERVICES 執行匯入不可,就當下決定另外寫一個匯入程式還比較快...。重寫的話就得研究它的寫法,正好每個 WebMethod 都只作單一的動作,裡面的實作就是現成的範例... 省了不少熟悉 API 的時間 :P

 

事後證明這作法沒錯,BlogEngine 的 LIB 蠻簡單易懂的,另外寫真的比較快... 就一切以原 .ASMX 的程式碼為藍本,寫了一個 .ASHX ,不作什麼事,就是把事先放在 ~/App_Data 下的 BLOGML 讀進來,一篇一篇 POST 處理,一個一個回應處理... 兩層迴圈就搞定原本那 90% 的匯入作業...

 

接下來就是自訂的部份了。先來列一下那些是我要補的資訊:

  1. 每篇 POST 的 COUNTER
  2. 每則回應的詳細資訊
  3. 每篇 POST 的 ID ( CS2007 內的 ID ),及匯入後的新 ID
  4. 所有必要的網址修正 (圖檔,站內文章連結..)
  5. 原程式的一些缺陷修正 (如前文提到,Modified Date錯誤的問題)

 

源頭就有東西要補了。如果一切基於 BlogML 的話,BlogML 沒定義到的資料就沒機會撈出來了。看了一下我還需要額外抓出來的資訊,像匿名的 COMMENTS 作者資訊,IP 等... 不過這些都用某種序列化的方式存在 CSDB,ㄨ!! 真麻煩... 在 CS2007 DB 內的 [cs_posts] 有這兩個欄位: PropertyNames, PropertyValues... 沒錯,全部壓成一個大字串,得自己寫程式去 PARSE ...

CS2007 的作法是這樣,在 PropertyNames 欄位放的值長的像這樣 (包含 Name, 型別, 字串起始及結束位置):

SubmittedUserName:S:0:3:TitleUrl:S:3:35:

 

而在 PropertyValues 對應的值為:

小熊子http://michadel.dyndns.org/netblog/

 

拆起來有點小麻煩,PropertyNames 是 {Name}:{Type}:{StartPos}:{EndPos}: 的組合,有多組的話就一直重複下去。以 Comments 的 SubmittedUserName 來說,它的型別是 S ( String ),它的值就要從後面的 PropertyValues 整個字串的第 0 個字元 ~ 第 3 個字元之間的值 ( 小熊子 ),而 TitleUrl 則是第 3 個字元 ~ 第 35 個字元的值 ( http://michadel.dyndns.org/netblog/ ) ...

看來用 T-SQL 拆或是用 XPath / XSLT 拆都有點辛苦,想想算了... 這段 CODE 跑不掉一定要寫... 就硬解出來吧。解出來後就可以修正 CS2007 匿名張貼的 Comment 沒正確顯示在 BlogML 內的問題。

接下來要把 SQL 這堆資料倒成 XML 檔,偷吃步一下,找工具直接倒出來就好。反正只作一次,可以不要寫 CODE 就不要寫了... SQL2005 要把 QUERY 結果存成 XML 倒是很簡單,打開 SQL2005 Management Studio, 執行這段 QUERY:

取得所有 comments 的額外 PropertyValues 的查詢指令[copy code]
   1:  select 
   2:    PostID, 
   3:    PostAuthor, 
   4:    PropertyNames, 
   5:    PropertyValues 
   6:  from cs_posts 
   7:  where ApplicationPostType = 4 
   8:  for xml auto


 

會看到這樣的畫面:

image

 

點下去就是 XML 了,手到加上頭尾的 Root Element 存檔就搞定了。接下來補段 CODE 把我要的 Property 拆出來存 XML 檔,第一步驟收工!

接下來這堆問題就以 (4) 最麻煩了。原本想的很天真,用文字編輯器把所有 XML 檔替換掉就沒事... 錯! 先從最單純的圖檔路逕來看吧..

 

[替換圖檔及下載檔案的網址]

最基本的就是圖檔網址... 在 CS2007 裡圖檔是這樣放的 (WLW會幫你補上絕對路逕,不是用相對路逕)

http://columns.chicken-house.net/blogs/chicken/xxxxxxx/xxx.jpg

而看了 BlogEngine 的作法,它用 HttpHandler 的方式來提供前端下載圖檔,格式像這樣:

http://columns.chicken-house.net/xxxxx/image.axd?picture=xxx.jpg

而 BlogEngine 的設定檔是這樣寫的:

 

BlogEngine 在 Web.config 內的 HttpHandler 區段[copy code]
   1:  <httpHandlers>
   2:    <add  verb="*" 
   3:      path="file.axd" 
   4:      type="BlogEngine.Core.Web.HttpHandlers.FileHandler, BlogEngine.Core" 
   5:      validate="false"/>
   6:    <add  verb="*" 
   7:      path="image.axd" 
   8:      type="BlogEngine.Core.Web.HttpHandlers.ImageHandler, 
   9:      BlogEngine.Core" 
  10:      validate="false"/>
  11:    .....
  12:  </httpHandlers>

 

看起來不難,理論上我只要找到原網址,把 ~/blog/chicken/ 後的路逕切出來,前段改成 image.axd?picthre= 就可以了。

大致上是這樣沒錯,不過變數還不少... 因為有好幾種網址 @_@

  1. 分家前  ( http://community.chicken-house.net/blogs/chicken/xxxxx )
  2. 分家後  ( http://columns.chicken-house.net/blogs/chicken/xxxxx )
  3. 古早時代手工打的 ( /blogs/chicken/xxxxx )
  4. 有些內建的要避開,像 ( http://columns.chicken-house.net/blogs/chicken/rss.aspx )

代換要嘛一次做完,要嘛一定要照 1 2 3 順序,否則先做 (3) 就會把 (1) (2) 都破壞掉了... 但是怎麼想程式都很不乾脆,雜七雜八的 CODE 一堆... 最後搬出  http://regexlib.com/ 這個網站,它提供大量的 Regular Expression 的資料庫供你使用,也提供了線上版本讓你測試 MATCH 的結果,很好用! 一定要推一下...

最後定案的 Regular Expression 長這樣:

(http\://(columns|community)\.chicken\-house\.net)?/blogs/chicken/([a-zA-Z0-9\-_\./%]*)

一切都很順利,簡單的就精確的抓到要代換的範圍。補一下小插曲,最後轉完才發現漏掉 (4),不過沒幾篇,就手動改掉了 :P,所以這段 REGEXP 是沒處理到 (4) 這種情況的...

 

 

[站內連結的修正]

再來就開始麻煩了... 動手前我先想了一下,每篇文章要匯進去後才會知道它的新 ID,新網址... 在還不知道新網址之前有些內容的聯結就無法替換了,怎麼樣都不可能在 1 PASS 內解決這問題... 只能用 2 PASS 來處理了。

既然這樣,第一次匯入就不修站內聯結了,只要把新舊 ID 記下來就好...

頭痛的來了,CS2007 的網址格式有好幾種 @_@ (難怪它有專門一個 config 檔來設定幾十個 URL Rewrite 的設定...):

  1. /blogs/chicken/archive/2008/06/20/1234.aspx
  2. /blogs/chicken/archive/2008/06/20/MyPostTitleHash.aspx
  3. /blogs/1234.aspx

其中 1234 是 CS2007 內部使用的 PostID .  再看看 BlogEngine 的網址格式:

  1. /post.aspx?id=xxxx-xxxx-xxxxxx-xxxxxxxxxxx
  2. /post/xxxxxxxxxxxxxxxx.aspx (某種 TITLE 的 HASH,它稱作 SLUG)

還好 BlogEngine 單純多了,只要抓到 (1) 的 ID,SLUG 就有 API 抓的到...

記得 CS 網址還有不需要 URL Rewrite 的版本 ( /blogs/xxxxx.aspx?postID=1234 這樣),不過從來沒出現在我文章內容,就不理它了...

這裡的重點不是單純的代換了,還要精確的抓出 1234 這段 PostID .. 最後定案的 Regular Expression 長這樣:

(http\://(columns|community)\.chicken\-house\.net)?/blogs/chicken/archive/\d+/\d+/\d+/(\d+)\.aspx

果然人家說 Regular Expression 是 "WRITE ONLY LANGUAGE" 真有道理,寫完連我自己都看不懂這堆符號是在幹嘛... 最後新舊 ID 都抓到就可以產生出正確的新站內連結了..

 

 

[站外連結修正]

我自己的 LINK 找到就可以修,不過寫在別人那邊的我那修的到啊... 因此就要靠上一篇講的,想辦法要去接有人點到舊的 CS LINK,然後當場做轉址的動作...

好在舊的文章不會再變多了,靠前面做出來的對照表就有足夠的資訊了... 成果看上一篇就知道了。這裡主要是靠 CSUrlMap.cs 這個我自己寫的 HttpHandler 來解決所有連到 /blogs/*.aspx 的連結...

方法差不多,我得從各種可能的網址格式,抓出舊的 CS PostID,然後查表,找出新的網址,把使用者引導到新的頁面... 貼一下主程式的部份:

CSUrlMap, 將 CS2007 的網址重新導向到 BlogEngine 網址的 HttpHandler[copy code]
   1:  public void ProcessRequest(HttpContext context)
   2:  {
   3:      if (matchRss.IsMatch(context.Request.Path) == true)
   4:      {
   5:          //  redir to RSS
   6:          context.Response.ContentType = "text/xml";
   7:          context.Response.TransmitFile("~/blogs/rss.xml");
   8:      }
   9:      else if (matchPostID.IsMatch(context.Request.Path) == true)
  10:      {
  11:          //  redir to post URL
  12:          Match result = matchPostID.Match(context.Request.Path);
  13:          if (result != null && result.Groups.Count > 0)
  14:          {
  15:              string csPostID = result.Groups[result.Groups.Count - 1].Value;
  16:              if (this.MAP.postIDs.ContainsKey(csPostID) == true)
  17:              {
  18:                  context.Items["postID"] = this.MAP.postIDs[csPostID];
  19:                  context.Items["redirDesc"] = "網站系統更新,原文章已經搬移到新的網址。";
  20:              }
  21:              else
  22:              {
  23:                  context.Items["redirDesc"] = "查無此文章代碼,文章不存在或是已被刪除。將返回網站首頁。";
  24:              }
  25:          }
  26:          else
  27:          {
  28:              context.Items["redirDesc"] = "4444";
  29:          }
  30:      }
  31:      else if (matchPostURL.IsMatch(context.Request.Path) == true)
  32:      {
  33:          context.Items["postID"] = this.MAP.postURLs[context.Request.Path];
  34:          context.Items["redirDesc"] = "網站系統更新,原文章已經搬移到新的網址。";
  35:      }
  36:      else
  37:      {
  38:          context.Items["redirDesc"] = "查無此頁。將返回網站首頁。";
  39:      }
  40:      context.Server.Transfer("~/blogs/AutoRedirFromCsUrl.aspx");
  41:  }

 

看程式說故事,大家都會吧 :D,結果就是上一篇各位看到的樣子... 已經沒力一行一行再說明下去了 :P

 

寫到這邊真是大工程 @_@,動作都不困難,但是都是雜七雜八的工作,害我搞了一個禮拜... 不過到此為止搬家要處理的資料部份全部都完成了。下一篇就輕鬆多了,把網站的版面調整成我想要的樣式... 有興趣的人請多等一等吧 :D 主題會放在跟 FunP 推推王的密切整合上... 敬請期待 :D



6/3/2008 11:29:00 PM

[RUN! PC] 2008 六月號

Microsoft.NET | ASP.NET | RUNPC | 技術隨筆 Facebook Share

IMG_8698 (Canon PowerShot G9)

 

運氣不錯,還有續集耶 [:D]

沒想到這些有點冷門的內容,雜誌社還願意刊... 真是太感謝了... 隔了兩個月刊出第二篇了。不過這次稿擠,最後簡介跟網址沒貼出來,讀者大概都看不到這邊吧,沒關係,看的到的就是有緣人...。

一樣,一方面只是留個紀念,另一方面讓讀者 (如果真的有的話) 有個留話的地方。文章內提到的 SAMPLE CODE 在這裡。想要線上試試範例程式的可以到 這裡 執行看看。



6/3/2008 3:04:00 PM

FlickrProxy #4 - 修正同步上傳的問題

Microsoft.NET | ASP.NET | 我的作品 Facebook Share

寫到這,越寫越拖抬了... 這次沒有加上任何 "新功能",有的只是修正使用上的一些問題而以...

 

首先,還是要感謝愛用者 小熊子 的回報,照片初次被下載時會觸發上傳到 Flickr 的動作,上傳完成才重新引導 Request 到 Flickr 存取照片。如果在這一連串動作尚未完成前,就有第二個 Request 跑來的話,那這張照片就會被傳到 Flickr 兩次...。Orz,枉費我還投搞這些並行處理的文章,怎麼可以犯這種錯...

就跟很多 BUG 一樣,難在沒想到,難在沒發現,難在不知道原因...,不然修正 BUG 倒是很簡單的事,感謝回報這個 BUG 的恩公... 找到問題後剩下的 ISSUE 就迎刃而解了,要做的就是把關鍵的程式碼包裝在臨界區 (CRITICAL SECTION) 內,以防這段 CODE 同時間執行多次。這段 CODE 不能太大,鎖定範圍太大會影響效能 (好不容易換了四核CPU,鎖太大就糟踏這顆 CPU 了...),最後找出關鍵應該是 [判定是否需要上傳到 FLICKR] 及 [建立 FLICKR 副本檔] 這兩個動作,一定要包括在內,拆開的話就不能保證結果正確了。

修正過的程式,加上 LOCK 鎖定部份程式碼[copy code]
            string flickrURL = null;            lock (this.GetType())            {                if (File.Exists(this.CacheInfoFile) == false)                {                    //                    //  CACHE INFO 不存在,重新建立                    //                    this.BuildCacheInfoFile(context);                    context.Response.AddHeader("X-FlickrProxy", "Upload");                }            }
   1:  string flickrURL = null;
   2:  lock (this.GetType())
   3:  {
   4:      if (File.Exists(this.CacheInfoFile) == false)
   5:      {
   6:          //
   7:          //  CACHE INFO 不存在,重新建立
   8:          //
   9:          this.BuildCacheInfoFile(context);
  10:          context.Response.AddHeader("X-FlickrProxy", "Upload");
  11:      }
  12:  }

 

 

寫好後,這段 CODE 越看越不順眼,雖然我的網站流量沒那麼大啦 [H],不過怎麼可以這麼短視... 這段 CODE 的問題還是一樣,鎖定的範圍 "太大了" !! 會嚴重影響效能.. (如果流量真的很大的話)

怎麼說? 不過才兩行,那到底是要縮到多小? 不不,問題其實不在於 LOCK 的區段到底有多少 CODE,而是該 LOCK 的只有對同一張照片的 Http Request 才該被阻檔下來,同時間有多個 Http Request 來下載不同張照片,以現在的點閱率來說我應該要高興吧... 幹嘛還去 LOCK 它? 不過上面的 CODE 就是在做這件事,不分青紅皂白只要是有上傳到 FLICKR 的動作就一率 LOCK。更糟的是如果有一張照片正在上傳中,其它照片的 Http Request 也都會被迫停下來等它傳完...

 

該要有個改進的版本了。LOCK過度也是初次碰到 Multi-threading Programming 的人很容易犯的錯誤,接下來看看第二個版本:

修正過的版本,只會LOCK同一個檔案的REQUEST:[copy code]
            lock (this.LockHandle)            {                if (File.Exists(this.CacheInfoFile) == false)                {                    //                    //  CACHE INFO 不存在,重新建立                    //                    this.BuildCacheInfoFile(context);                    context.Response.AddHeader("X-FlickrProxy", "Upload");                }            }
   1:  lock (this.LockHandle)
   2:  {
   3:      if (File.Exists(this.CacheInfoFile) == false)
   4:      {
   5:          //
   6:          //  CACHE INFO 不存在,重新建立
   7:          //
   8:          this.BuildCacheInfoFile(context);
   9:          context.Response.AddHeader("X-FlickrProxy", "Upload");
  10:      }
  11:  }

 

看起來只有第一行 LOCK STATEMENT 裡指定的物件不一樣而以。沒錯,這裡跟第一段 CODE 的差別只在於 LOCK 的標的物不一樣。同一個物件只能被 LOCK 一次,當物件被 LOCK 還沒放開時,第二個人想要 LOCK 同一個物件,很抱歉... 得先等第一個人願意放掉它才可以。LOCK不同物件就各不相干了。沒錯,這就是我要的邏輯。所以這個問題的關鍵在於,我如何讓每張照片有專屬的 "物件" 來 LOCK ?

檔名的字串物件? 不適合... 可能有多個字串值相同,但是是不同物件... FileInfo? 也不行,因為找不到文件會保證同一個檔案拿到的 FileInfo 物件會是同一個... 沒辦法,只好自己弄一個。來看一下 LockHandle 的實作:

LockHandle Property 實作的程式碼[copy code]
        private object LockHandle        {            get            {                string hash = this.GetFileHash();                lock (_locks)                {                    if (_locks.ContainsKey(hash) == false)                    {                        _locks.Add(hash, new object());                    }                }                return _locks[hash];            }        }        private static Dictionary<string, object> _locks = new Dictionary<string, object>();
   1:  private object LockHandle
   2:  {
   3:      get
   4:      {
   5:          string hash = this.GetFileHash();
   6:          lock (_locks)
   7:          {
   8:              if (_locks.ContainsKey(hash) == false)
   9:              {
  10:                  _locks.Add(hash, new object());
  11:              }
  12:          }
  13:          return _locks[hash];
  14:      }
  15:  }
  16:  private static Dictionary<string, object> _locks = new Dictionary<string, object>();

 

其實以值來說,拿檔名就足夠拿來示別了,只不過有大小寫的問題要處理。拿檔案的內容做 MD5 實在有點殺雞用牛刀,不過因為處理照片本來就需要算檔案的 MD5 了,現成的就拿來用一下...。這裡我簡單的做了個 Dictionary,就放沒什麼用的 OBJECT,我只要這個 PROCESS 在有生之年,同一個檔案都會拿到同一個 OBJECT 就足夠了...

 

 

都寫到這還有什麼不滿意的? 還是有... 哈哈。因為我在測試時有過一個頁面,同一頁會放一堆圖檔...。試想一下當我瀏覽這頁面會發生什麼事?

"同時間 IE 發出了數個 HttpRequest 來跟我的程式要圖檔,如果正巧都是第一次,嗯,有限的頻寬要一次上傳多張圖檔到 Flickr,不就更慢了?"

就算我的頻寬沒問題,同一瞬間這麼多 UPLOAD 的動作,引起 Flickr 的 "關切" 就不好了... 我是不是應該要限制一下同時上傳的數量才對? 就像 FlashGet 可以限制同時下載的數量一樣...

哈,不就是之前寫過的文章,用 SEMAPHORE ? 沒錯... 怎麼老覺的這篇像在替我其它文章打廣告用的... 事實上不見得要動用到 SEMAPHORE,如果你要限制的是一次只能一個 UPLOAD 動作,直接用各種的 LOCK 機制就夠了。如果你要限制並行的 UPLOAD 動作是兩個以上,甚至更動態隨著 LOADING 變化等等,才需要動用到 SEMAPHORE ...

既然要 DEMO,就 DEMO 實際一點的 CODE 吧。假設我要限制並行 UPLOAD 的數量不超過 2 個,則需要把 CODE 改成這樣。首先要先準備好 SEMAPHORE 物件:

準備 SEMAPHORE,事先插好兩根旗子[copy code]
        public static Semaphore FlickrUploaderSemaphore = new Semaphore(2, 2);
   1:  public static Semaphore FlickrUploaderSemaphore = new Semaphore(2, 2);

 

真正執行上傳動作的部份要加上 SEMAPHORE 的管控:

用 SEMAPHORE 控制同步執行的數量[copy code]
            FlickrUploaderSemaphore.WaitOne();            photoID = flickr.UploadPicture(this.FileLocation);            FlickrUploaderSemaphore.Release();
   1:  FlickrUploaderSemaphore.WaitOne();
   2:  photoID = flickr.UploadPicture(this.FileLocation);
   3:  FlickrUploaderSemaphore.Release();

 

嗯,真是小題大作,不過這種機會不拿來練習練習,真正碰到怎麼寫的出來? 如果各位對於在 ASP.NET 上怎麼使用 LOCK 及 SEMAPHORE 不大熟的,可以參考一下我投稿的文章... 萬分感謝 [:D]



5/21/2008 1:15:00 AM

FlickrProxy #3 - 終於搞定大圖網址錯誤的問題

Microsoft.NET | ASP.NET | 我的作品 Facebook Share

 

由於在使用 Flickr API 時, 老是碰到上傳成功後, 結果拿到的照片 URL 不能看的問題... 被它整了好久, 不過總算解決了 @_@... 原來 API 拿到的資料是錯的, 嘖...

說來話長, 不過既然花時間解決了就要記錄一下... 問題大概是這樣. 上傳照片完成之後可以拿到 photoId, 代表某一張放在 Flickr 上的照片. 之後透過 PhotosGetInfo(photoId) 這個 API 可以取得這張照片的相關資訊, 當然也有 MediumUrl, LargeUrl... 等等 properties 可以用.

很直覺嘛, 要秀大圖我就直接拿 LargeUrl 就好, 偏偏有時是好的, 有時是壞的... API 傳回來的東西應該不會錯吧? 我心裡是這樣想的. 不過跟 Flickr 網站的 url 對照一下才發現, 竟然有時是不同的... 一路追下去, google 跟作者在 codeplex 網站上的 forums 都找了, 才發現...

PhotosGetInfo( ) 抓到的只是一堆 ID, 然後用 Flickr 公布的網址格式 "湊" 出各種 URL. 然而過去 Flickr 層經有一次改變部份網址的格式, 當你的圖檔不是很大時, Flickr 判定沒有另外存一張大圖的需要了, 就直接跳到原圖 (original size). 而原圖的網址格式又不一樣, 因此當圖檔太小時, API 抓到的 LargeUrl 就會是錯的...

My God,.... 為了這種鳥問題害我多白了好幾根頭髮... 找到原因後找 solution 就簡單了. 因應這個問題, 也多了一組 API: PhotosGetSizes( ), 直接連回 Flickr 明確的查詢可用的 size 有幾種, 連同它的網址及一堆相關資訊一起傳回來... 改用這個 API 傳回的資訊, 結果就正確了, 沒有圖掛掉的問題... Orz

 

不能怪人家 API 寫的不好, 只能怪自己功課沒作足... 不看文件直接拿 API 就用, 看名字猜用法才會這樣.. code 改一改就 ok 了, 少了這個不確定性, 原本畫蛇添足加上去的確認圖檔的動作也不用了.. 貼一下修改前跟修改後的 code:

 

使用 PhotoInfo 物件 (可能會出現 photo not available) [copy code]
            PhotoInfo pi = flickr.PhotosGetInfo(photoID);            string flickrURL = null;            string size = null;            try            {                flickrURL = this.CheckFlickrUrlAvailability(pi.MediumUrl);                size = "medium";                flickrURL = this.CheckFlickrUrlAvailability(pi.LargeUrl);                size = "large";                flickrURL = this.CheckFlickrUrlAvailability(pi.OriginalUrl);                size = "original";            }            catch { }
   1:  PhotoInfo pi = flickr.PhotosGetInfo(photoID);
   2:  string flickrURL = null;
   3:  string size = null;
   4:  try
   5:  {
   6:      flickrURL = this.CheckFlickrUrlAvailability(pi.MediumUrl);
   7:      size = "medium";
   8:      flickrURL = this.CheckFlickrUrlAvailability(pi.LargeUrl);
   9:      size = "large";
  10:      flickrURL = this.CheckFlickrUrlAvailability(pi.OriginalUrl);
  11:      size = "original";
  12:  }
  13:  catch { }

 

 

改用 PhotosGetSizes( ) API[copy code]
            foreach (Size s in flickr.PhotosGetSizes(photoID).SizeCollection)            {                XmlElement elem = null;                elem = cacheInfoDoc.CreateElement("size");                elem.SetAttribute("label", s.Label);                elem.SetAttribute("source", s.Source);                elem.SetAttribute("url", s.Url);                elem.SetAttribute("width", s.Width.ToString());                elem.SetAttribute("height", s.Height.ToString());                cacheInfoDoc.DocumentElement.AppendChild(elem);            }
   1:  foreach (Size s in flickr.PhotosGetSizes(photoID).SizeCollection)
   2:  {
   3:      XmlElement elem = null;
   4:      elem = cacheInfoDoc.CreateElement("size");
   5:      elem.SetAttribute("label", s.Label);
   6:      elem.SetAttribute("source", s.Source);
   7:      elem.SetAttribute("url", s.Url);
   8:      elem.SetAttribute("width", s.Width.ToString());
   9:      elem.SetAttribute("height", s.Height.ToString());
  10:      cacheInfoDoc.DocumentElement.AppendChild(elem);
  11:  }

 

嗯, 終於搞定. FlickrProxy 正式邁入實用的階段... 收工!

 

 

 



5/19/2008 1:42:00 AM

FlickrProxy #2 - 實作

Microsoft.NET | ASP.NET | 我的作品 Facebook Share

整個進度算是很順利,初版已經可以運作了,而且也成功的套用在小熊子的 BLOG 上... 因為過去已經有幾個類似的 HttpHandler 的 code 可以直接拿來改了,反而真正的瓶頸是在瞭解如何操控 FlickrNet 這個 .NET 版的 Flickr API 身上 Orz。FlickrNet 碰到的問題後面再說明,先來看一下整個 Project 的源頭: system design。

 

這個程式的目標很明確,就是我不想改變任何的使用習慣,我要讓 blogger (比如我家大人) 完全不用理會 flickr 這東西的存在,也不需要在寫文章時去傷腦筋該把照片先放到 Flickr 然後再放到網頁上這類瑣事.. 因此我需要的是在 blog server 上有某些自動的機制,能夠自動把照片丟到 flickr 上,也能夠自動的把網頁上要顯示的照片轉到 flickr 那邊。而必要時,這些機制都能夠取消或調整,不會影響到 blog 的資料等等問題。

初步的想法就是從接手這些圖檔的 HttpHandler 著手。如果前端 (BROWSER) 到 BLOG SERVER 上要求下載照片的要求都能經過我的控制,理論上我就能達成這個目的。因為前端的 Http Request 不需要修改,因此我這次的任務不需要像黑暗大哥那樣辛苦的去調整每一頁的 HTML code (雖然我的方法也沒多輕鬆.. Orz),這是我決定採用這個方式的主要優點。HTML不用改,我也不用改變原本上傳圖檔的方式,因此所有的調整都不會影響到最重要的 DATA,所有改變都是可以還原的。

負責處理照片的 Http Handler 只要能依照這流程作事就夠了,因此等等會看到的程式碼也是很簡單:

  1. 接收 Http Request,如果條件符合 (確認這是要轉到 Flickr 的照片,檔案大小超過指定值... 等等) 則進行下一個步驟,否則就像一般的靜態檔案一樣,直接回傳檔案內容。
  2. 先檢查 CACHE 是否已經有對應的資訊 ( ASP.NET 內建的 Cache, 及在暫存目錄建立的 cache file ... 等等 ),有的話直接把 Http Request 重新導向到 Flickr 上同一張照片的網址。
  3. 如果 (2) 不成立的話,就要執行主要的動作 - 上傳到 Flickr 並且建立必要的 CACHE 資訊,然後重複 (2) 的動作。主要的動作包括:
    • 計算 HASH,建立 CACHE 檔案
    • 透過 Flickr API,把檔案丟到 Flickr 服務上,並且取得 URL 等資訊
    • 把取得的資訊放在 CACHE 檔案裡
    • 執行 (2),直接把 Http Request 重新導向到 Flickr URL

這樣就完成了。我把它畫成 UML Sequency Diagram:

 

1

 

 

接下來就是看 Code 了,寫這樣的程式,關鍵有幾個,大部份都是 IIS / ASP.NET 的設定要正確,讓 IIS 能把 REQUEST 轉到你的程式,剩下的就沒什麼特別的了。對 HttpHandler 不熟的人可以先參考一下這幾篇 ( From MSDN ):

How to: Configure an HTTP Handler Extension in IIS
How to: Register HTTP Handlers
HTTP Handlers and HTTP Modules Overview
Walkthrough: Creating a Synchronous HTTP Handler

在 IIS 上寫這些東西,比較麻煩的是 configuration,反而不是程式... 初學者常會卡在這裡,而這部份的行為正好又跟 Visual Studio 內建的 DevWeb 差很多 (真的差很多... 有時連抓到的 Path Info 都會不一樣 @_@),強烈建議開發階段就直接在 IIS 上面開發...

要克服的第一個設定,就是 IIS。預設情況下,IIS 看到圖檔的 request,毫不考慮就會把內容丟回去了,你程式怎麼寫都沒機會攔到,所以要在應用程式對應這邊,先把 *.JPG 的控制權交給 .NET Framework 的 ISAPI filter ..

有兩個選擇,你心藏夠力的話可以把所有的 Request 都指到 .NET Framework,或是只指定 .JPG 就好。我這邊是以 .JPG 為例:

image

仔細看一下可以發現,其實所有 ASP.NET 的附檔名,通通都是指向同一個 ISAPI Filter: aspnet_isapi.dll。至於每一種附擋名會有什麼不同的行為,那是 .NET 自己關起門來解決的事,這邊不用傷腦筋... 直接 COPY 別的設定過來最快..

 

IIS 的部份搞定了,如果現在就透過 IIS 去看網站上的 .JPG,全部都會破圖... 因為所有的 Request 全都被 .NET 接管了。除非你整個 WEB APP 的 .JPG 都要透過你的 HTTP HANDLER,否則請先在 WEB.CONFIG 裡加上這段:

加在 /configuration/system.web/httpHandlers 下 (這是 XPath):

用內建的 StaticFileHandler 來處理 *.JPG 的 Http Request[copy code]
    <httpHandlers>      <add path="*.jpg" verb="*" type="System.Web.StaticFileHandler" />    </httpHandlers>
   1:  <httpHandlers>
   2:    <add path="*.jpg" verb="*" type="System.Web.StaticFileHandler" />
   3:  </httpHandlers>

 

System.Web.StaticFileHandler 是 ASP.NET 內建的,是跟 IIS 預設一樣的行為,就是原封不動的把檔案的 BINARY DATA 照傳回去而以。預設的還有其它幾個,Forbidden 等等的都可以用同樣的方式指定。

這一段加上去等於繞了一圈,IIS把 .JPG 交給 ASP.NET,而 ASP.NET 又原封不動的傳了回去。沒錯,一切都是為了後面作準備... 接下來要把未來會放照片的目錄,重新指定 HttpHandler。我的例子是 ~/storage 下的 *.JPG 通通都要轉到 Flickr,因此我在 Web.config 加上這段 (當然,你直接放在 ~/storage/web.config 也是可以):

重新指定在 ~/storage 目錄下的 HttpHandlers[copy code]
  <location path="storage">    <system.web>      <httpHandlers>        <add path="*.jpg" verb="*" type="ChickenHouse.Web.HttpHandlers.FlickrProxyHttpHandler,App_Code" />      </httpHandlers>    </system.web>  </location>
   1:  <location path="storage">
   2:    <system.web>
   3:      <httpHandlers>
   4:        <add path="*.jpg" verb="*" type="ChickenHouse.Web.HttpHandlers.FlickrProxyHttpHandler,App_Code" />
   5:      </httpHandlers>
   6:    </system.web>
   7:  </location>

 

設定的部份大功告成,剩下的就是程式碼了。看一下主要的部份:

FlickrProxyHttpHandler 主程式片段[copy code]
            //            //  確認 CACHE 目錄已存在            //            if (Directory.Exists(this.CacheFolder) == false)            {                Directory.CreateDirectory(this.CacheFolder);            }            XmlDocument cacheInfoDoc = new XmlDocument();            string flickrURL = null;            if (File.Exists(this.CacheInfoFile) == false)            {                //                //  CACHE INFO 不存在,重新建立                //                flickrURL = this.BuildCacheInfoFile(context);            }            else            {                //                //  CACHE INFO 已經存在。確認 CACHE 的正確性後就可以直接導到 FLICKR URL                //                string cacheKey = "flickr.proxy." + this.GetFileHash();                flickrURL = context.Cache[cacheKey] as string;                if (flickrURL == null)                {                    cacheInfoDoc.Load(this.CacheInfoFile);                    flickrURL = cacheInfoDoc.DocumentElement.GetAttribute("url");                    context.Cache.Insert(                        cacheKey,                        flickrURL,                        new CacheDependency(this.CacheInfoFile));                                    }            }            context.Response.Redirect(flickrURL);
   1:  //
   2:  //  確認 CACHE 目錄已存在
   3:  //
   4:  if (Directory.Exists(this.CacheFolder) == false)
   5:  {
   6:      Directory.CreateDirectory(this.CacheFolder);
   7:  }
   8:  XmlDocument cacheInfoDoc = new XmlDocument();
   9:  string flickrURL = null;
  10:  if (File.Exists(this.CacheInfoFile) == false)
  11:  {
  12:      //
  13:      //  CACHE INFO 不存在,重新建立
  14:      //
  15:      flickrURL = this.BuildCacheInfoFile(context);
  16:  }
  17:  else
  18:  {
  19:      //
  20:      //  CACHE INFO 已經存在。確認 CACHE 的正確性後就可以直接導到 FLICKR URL
  21:      //
  22:      string cacheKey = "flickr.proxy." + this.GetFileHash();
  23:      flickrURL = context.Cache[cacheKey] as string;
  24:      if (flickrURL == null)
  25:      {
  26:          cacheInfoDoc.Load(this.CacheInfoFile);
  27:          flickrURL = cacheInfoDoc.DocumentElement.GetAttribute("url");
  28:          context.Cache.Insert(
  29:              cacheKey,
  30:              flickrURL,
  31:              new CacheDependency(this.CacheInfoFile));
  32:      }
  33:  }
  34:  context.Response.Redirect(flickrURL);

 

刪掉了一些無關緊要的 CODE。主程式很簡單,就上面提到了邏輯而以。想辦法取得照片在 Flickr 那邊的正確網址,Redirect回去就好。如何得知網址? 第一次如何把照片傳上去? 這次來看看主角: BuildCacheInfoFile。

Method: BuildCacheInfoFile( )[copy code]
        private string BuildCacheInfoFile(HttpContext context)        {            Flickr flickr = new Flickr(                ConfigurationManager.AppSettings["flickrProxy.API.key"],                ConfigurationManager.AppSettings["flickrProxy.API.security"]);            flickr.AuthToken = ConfigurationManager.AppSettings["flickrProxy.API.token"];            string photoID = flickr.UploadPicture(this.FileLocation);            PhotoInfo pi = flickr.PhotosGetInfo(photoID);            string flickrURL = null;            try            {                flickrURL = this.CheckFlickrUrlAvailability(pi.MediumUrl);                flickrURL = this.CheckFlickrUrlAvailability(pi.LargeUrl);                flickrURL = this.CheckFlickrUrlAvailability(pi.OriginalUrl);            }            catch { }            XmlDocument cacheInfoDoc = new XmlDocument();            cacheInfoDoc.LoadXml("<proxy />");            cacheInfoDoc.DocumentElement.SetAttribute(                "src",                this.FileLocation);            cacheInfoDoc.DocumentElement.SetAttribute(                "url",                flickrURL);            cacheInfoDoc.DocumentElement.SetAttribute(                "photoID",                photoID);            cacheInfoDoc.Save(this.CacheInfoFile);                        return flickrURL;        }
   1:  private string BuildCacheInfoFile(HttpContext context)
   2:  {
   3:      Flickr flickr = new Flickr(
   4:          ConfigurationManager.AppSettings["flickrProxy.API.key"],
   5:          ConfigurationManager.AppSettings["flickrProxy.API.security"]);
   6:      flickr.AuthToken = ConfigurationManager.AppSettings["flickrProxy.API.token"];
   7:      string photoID = flickr.UploadPicture(this.FileLocation);
   8:      PhotoInfo pi = flickr.PhotosGetInfo(photoID);
   9:      string flickrURL = null;
  10:      try
  11:      {
  12:          flickrURL = this.CheckFlickrUrlAvailability(pi.MediumUrl);
  13:          flickrURL = this.CheckFlickrUrlAvailability(pi.LargeUrl);
  14:          flickrURL = this.CheckFlickrUrlAvailability(pi.OriginalUrl);
  15:      }
  16:      catch { }
  17:      XmlDocument cacheInfoDoc = new XmlDocument();
  18:      cacheInfoDoc.LoadXml("<proxy />");
  19:      cacheInfoDoc.DocumentElement.SetAttribute(
  20:          "src",
  21:          this.FileLocation);
  22:      cacheInfoDoc.DocumentElement.SetAttribute(
  23:          "url",
  24:          flickrURL);
  25:      cacheInfoDoc.DocumentElement.SetAttribute(
  26:          "photoID",
  27:          photoID);
  28:      cacheInfoDoc.Save(this.CacheInfoFile);
  29:      return flickrURL;
  30:  }

 

寫這段,其實時間都花在怎麼用 Flickr API。Flickr API 很重視使用者的安全。認證部份一定要使用者親自到 Flickr 網站登入,同時按下授權後,API才能正常使用。經過這一連串動作,可以拿到三組序號:

  1. API Key
  2. Share Security Key
  3. Token

其中 (1) 及 (2) 是使用者要自己到 Flickr 網站申請的 (http://www.flickr.com/services/api/keys),第三個 TOKEN 就是要程式呼叫過程中會要求使用者連上網啟用後才能得到的。這邊我沒另外寫程式,我是直接用 FlickrNet 的作者提供的 SAMPLE CODE,照著操作就可以拿到 TOKEN 了。

有了這三段序號 API 才能正常運作。之後只要上傳 (第七行) 取得 photoID 回來就算完成。接下來第 12 行取得網址就 OK 了。

 

不過這邊也是卡最久的地方... 有人說 flickr server 忙的時後連到照片網址,有時會出現 "photo not available" 的訊習,有時又正常。有人則說某些照片只會有特定 SIZE,像是 original / large size 的有時也會發生 "photo not available" 的狀況...

試了幾次,實在是抓不出它的規則,也找不出避開的辦法... 只好硬著頭皮,每個取得的網址就都用 HTTP 硬給它試看看... 因為 Flickr API 取得網址的 property 在失敗時會丟出 EXCEPTION,因此這段 code 寫成這樣:

取得 SIZE 最大的可用網址[copy code]
            try            {                flickrURL = this.CheckFlickrUrlAvailability(pi.MediumUrl);                flickrURL = this.CheckFlickrUrlAvailability(pi.LargeUrl);                flickrURL = this.CheckFlickrUrlAvailability(pi.OriginalUrl);            }            catch { }
   1:  try
   2:  {
   3:      flickrURL = this.CheckFlickrUrlAvailability(pi.MediumUrl);
   4:      flickrURL = this.CheckFlickrUrlAvailability(pi.LargeUrl);
   5:      flickrURL = this.CheckFlickrUrlAvailability(pi.OriginalUrl);
   6:  }
   7:  catch { }

 

CheckFlickrUrlAvailability() 是我自己寫的,就是真正連到 Flickr 判定到底照片能不能從這網址下載... 任一行只要發生 EXCEPTION 就會跳出,flickrURL 變數就可以保留最後一個 (最大) 可用的網址...。

 

好,程式碼看完了... 最後來看看測試網站。我放了一個很簡單的網頁,簡單的幾行 HTML,加上一張圖。

測試用的 HTML 檔[copy code]
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head>    <title>Untitled Page</title></head><body><hr /><img src="smile_sunkist.jpg" mce_src="smile_sunkist.jpg" alt="test image" /><hr /></body></html>
   1:  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   2:   
   3:  <html xmlns="http://www.w3.org/1999/xhtml">
   4:  <head>
   5:      <title>Untitled Page</title>
   6:  </head>
   7:  <body>
   8:   
   9:  <hr />
  10:   
  11:  <img src="smile_sunkist.jpg" alt="test image" />
  12:   
  13:  <hr />
  14:   
  15:   
  16:  </body>
  17:  </html>

 

網頁上看到的結果:

image

 

這有啥好看的? 只是證明 USER 看起來完全正常而以... 哈哈,拿出 Fiddler 看一下:

image

 

#0 及 01 都是正常情況下就會有的 HTTP REQUEST,代表 IE 要下載 HTML 跟 JPG。不過在下載 JPG 檔,卻收到了 302 (OBJECT MOVE) 的重新導向的結果,因此 IE 就接著再到 Flickr 去下載照片,最後秀在網頁上。不過照片真的有出現在 Flickr 上嗎? 用我的帳號登入看看...

image

 

哈哈,果然出現了。看來這沒幾行的 CODE 真正發恢它的作用了。網站什麼都不用改,只要加上這 HttpHandler,配合調一些設定,馬上下載圖片的頻寬就省下來了。不過最少還是得花一次頻寬啦。BLOGGER把圖檔傳上來就不說了,圖檔第一次有人來看的時後,程式還是需要把檔案傳出去,放到 Flickr 上。不過一旦放成功了,以後第二次第三次.... 的頻寬就都省下來了。要花的只有 Fiddler 抓到的 #1 那少少的 302 REDIR 回應而以。

 

這個 Project 告一段落,後續還是會繼續改進,不過就都是小地方的調整,一些重構及把舊的 code 整理在一起的動作而以。對程式碼有興趣的人可以再跟我聯絡。



5/16/2008 3:39:00 AM

FlickrProxy #1 - Overview

Microsoft.NET | ASP.NET | 我的作品 Facebook Share

久久沒動手寫 code,手又開始癢了... 前陣子跟小熊子聊的時後想到一個新的 IDEA,就順手記下來。像我們這種自己用ADSL架的小型網站,瓶頸都是卡在頻寬。愛拍照的小熊子最在意的當然就是網站擺照片所需要的頻寬... 解決方式其實有一堆,像是 Live Writer 就有個很有名的 PLUGIN,可以直接插入你放在 Flickr 的照片...

其實這類的 solution 很多,但是用起來就覺的不大喜歡,怎麼說? 倒不是說軟體不好用,而是它的作法。這類 plug-ins 都是幫你一次把事情作好,幫你上傳到 flickr,幫你查出 link,幫你產生HTML片段,貼到 WLW... 這種作法的問題在於你張貼文章時,這些資訊就得確定下來。你沒辦法裝了這個外掛,就把你八百年前貼的照片一起轉到 flickr,你也被這個外掛還有你的 flickr 帳號綁死了,未來換帳號的話,或是你沒有 WLW 可以用時,這些功能都沒有了。

我比較喜歡的是 server side 的 solution,如果是透明的 (像 PROXY) 更好,如果隨時可以不要或是改設定最好,這樣我不但不用綁死在我的某個 flickr 帳號上,甚至不用綁在 flickr 這樣的服務...

過去 (沒想到已經快四年了 -_- ) 其實我作過幾個類似的 HttpHandler,都是用一樣的理念去開發的。這次想的是利用 Flickr 提供的 API,來做一樣的事。

簡單的說,如果我能夠寫個程式,能夠在 Run Time 動態去檢查網站上某張圖檔有沒有傳到 flickr 上? 如果沒有且判定不需要,則像一般網站一樣直接在 Http Response 傳回圖檔的內容。如果需要則自動上傳到 flickr,最後把這個 Http Request 重新導向到 flickr 上的照片網址。

這樣作法最後的結果跟 WLW 搭配 flickr plugins 差不多,差別在一個是靠 client 端,在你張貼文章時幫你處理掉一連串的動作,我的作法是統一在 server 端,在觀看文章內容時才做這件事。效能一定不如 WLW + Flickr plugin 好,不過就是多了彈性。我可以隨時關掉這個功能,隨時換相片服務,另一個更重要的是我自己保有一份完整的網站跟檔案資料。如果我的 BLOG 資料散在各地各個服務,我要備份或是還原等等也都很辛苦...

講了一堆都是廢話,其實這篇主題只有一個,就是想到好點子又要開始動手寫 code 了 [:D] ,有點成果之後會陸續在貼幾篇相關的文章。在之前作了 POC ( Prove Of Concept ),證明這個技巧是可行的,剩下的就是真正動手寫了。另外也有一個目的,就是想把之前寫的另外兩個 HttpHandler 整合起來,弄成統一的 provider 架構來實作。

想的很美,照片就轉到 Flickr,影片就轉到 YouTube ... 不過影片那邊的難度就高的多了,現在的實作只是從 HTTP 自動轉到 RTSP 而以.. ZIP 檔現在是虛擬化成一個資料夾,未來看看能不能自動轉到 Microsoft SkyDrive 之類的服務....

好,先寫到這裡,敬請期待下集 [:D]



4/3/2008 11:23:00 PM

[RUN! PC] 2008 四月號

Microsoft.NET | 543 | ASP.NET | RUNPC | 技術隨筆 Facebook Share

IMG_6727 (Canon PowerShot G9) - 複製

沒錯,我的文章在四月號的 RUN! PC 刊出來了,之前花了些時間在研究執行緒跟 ASP.NET 搭配起來用的技術問題,有點小心得,就整理了一下投稿了,運氣還不錯,雜誌社也願意刊出。初次投稿花了不少時間,花在重新思考 sample code 怎麼寫比較能突顯主題,圖表要怎麼畫才清楚明瞭等等瑣事上面,原來當個專欄作家 (我沒有專欄啦,只是投稿而以) 也不是這麼簡單的...

文章的內容嘛,看雜誌就知道了,這篇是留著作個紀念,同時也是讓看了這篇文章有話要說的讀者們,有個留下 comments 的地方。

文章裡提到的 sample code 可以到 [這裡] 下載,懶的抓回去執行的人,也可以直接到 [這裡] 試 RUN 看看文章裡提到的範例。

在執行這個範例程式之前,請先注意一下,IE預設只會對同一個網站建立兩個Http Connection,因此有可能會看到不一樣的測試結果。如果想要調大這個限制,請修改下列的註冊機碼,或是下載本文的範例程式,匯入[IE.reg]註冊機碼。

調整IE同時連線數的註冊機碼
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings]
"MaxConnectionsPerServer"=dword:00000008
"MaxConnectionsPer1_0Server"=dword:00000008

相關連結: [範例程式下載] [執行範例程式] [下載調整IE連線上限的註冊檔]



4/1/2008 1:54:00 AM

BotCheck 改版...

Microsoft.NET | 543 | ASP.NET | 我的作品 Facebook Share

有鑑於好奇心強的網友,回應時老愛研究 BotCheck 跟內容的關聯性... (Honga 就是你...),一時興起把 BotCheck 的 ASCX 改寫了一下,會在驗証通過時,把 BotCheck 的題目及答案附加在 comment 的後面,就像這樣:

image

免的每次都在那邊貼這次的 BotCheck 是啥.. 哈! 特此留念!



7/24/2007 7:02:00 PM

推!!!

Microsoft.NET | 543 | ASP.NET | 技術隨筆 Facebook Share

鄉民們, 一起來推吧... 上啊...

看到 FunP 人氣開始超越黑米的新聞之後, 原本想很久因為懶都沒動手的念頭又起來了... 放個推文的 button 在 blog 上面看看, 搞不好會因為這樣找到更多同好也說不定 [:D]

放的方式很簡單, 只是照 FunP 網站上面的工具產生出來的 html tag, 包裝成 ASP.NET User Control (單一ASCX), 然後加到樣板裡面, 完全沒有什麼深度可言, 我就不貼 code 了...

每篇都有個推文的 link, 大家就順手幫我推一推吧 [:D]



2/16/2007 12:00:00 AM

Web Site Application + Sandcastle

Microsoft.NET | ASP.NET | 技術隨筆 | 小技巧 Facebook Share

這次要解決的問題, 跟上次想要在 Web Application 裡執行單元測試的問題類似, C# 有跟 Java 類似的 comment help, 可以把寫在註解的文字萃取出來, 製作成一份文件...

不過, 過去的 NDoc, 到現在新掘起的 SandCastle, 都要求兩個東西:

  1. Assembly Files: 可以透過 reflection api, 取得 asm 的 metadata
  2. Compiler 在 compile code 時匯出的 xml document

通常 help 製作工具都需要這兩種檔案才能制作 help, 不過 asp.net 2.0 引進的 web app, 正常情況下跟本拿不到這些東西, 因為 code 只要丟在 App_Code 就可以跑了, 跟本不需要 compile 成 dll, 更不用說 xml document 了.... 想到幾種可能可行的辦法, 今天就抽空試了一下:

 

在 web.config 裡加上 compiler option 輸出 xml

要產生 dll 倒不難, 有一堆方法, 可以用 aspnet_compiler.exe 這個工具, 直接 build web site, 可以輸出 DLL. 不過還缺 xml doc, 翻了翻 MSDN, 找到一條路, 就是在 web.config 裡可以加 compiler options.. 加上去之後, 就會產生 xml document...

<configuration>
    <system.codedom>
        <compilers>
            <compiler ...... compilerOptions="/doc:c:\sample.xml" />
...

簡單寫個 web app 試了一下可以 work, 但是放到 production site 就不行了, compile 的過程中, 你會看到這個 xml 檔案不斷的產生出來, 又被砍掉... 原來 App_Code 目錄下, aspnet engine compile 的方式是以目錄為單位, 每個目錄下的 *.cs 會被當成一個 project compile 一次, 換個目錄再來一次, 所以你指定的 xml document 檔會不斷的被覆蓋...

氣的是這個參數還一定得指定檔名, 不能指定 *.xml 或是不指定之類的... 所以除非你的 App_Code 目錄下只有一層, 不然就放棄吧..

 

Web Deployment Project

Visual Studio 2005 有個附加的 Web Deployment Project, 在 SP1 之後就直接內建了, 它大概的作法就是 aspnet_compiler.exe 先輸出一堆 assembly, 然後再用 asm merge tools 把它併成單一個 assembly dll file. 是可以很簡單的拿到 dll 了, 不過產生 xml document 的部份仍然一樣無解

 

寫 msbuild project file

在 google 查這個問題時, 看到有老美用的作法是自己寫一段 task, 把所有 *.cs 加到 csc task 裡... 不過太麻煩了, 一來我整個 build 程序沒有用到 msbuild project file, 二來我也不熟, 寫起來有點辛苦..

 

手動下 CSC.exe 指令

來硬的, 我自己想辦法生個 dll + xml 總可以了吧, 反正我的目的只是要 help, dll 用完就丟也沒關係, 只要我不要寫兩份 code 就好. 查了一下指令, 可以這樣用:

csc.exe /out:App_Code.dll /doc:App_Code.xml /target:library /recurse:App_Code\*.cs

哇哈哈, 這次就成功了, dll 跟 xml 都成功的產生出來, 之後就丟給 NDoc / Sandcastle 就解決了. 額外抱怨一下, 雖然 SandCastle 能夠處理 .net 2.0 額外的功能, 像 generic 等等, 不過速度真是爆慢... Orz, 以前用 NDoc 約 20 min 能夠 build 好 chm, 現在用 sandcastle 要跑 60 min ...

雖然如此, 不過這個方法還是有幾個缺點.. 列一下我已經確定的:

  1. App_Code 下可以包含 source code 以外的檔案.
    除了 .cs 這種 source code 會被自動編譯之外, 還有其它型態的檔案也會自動被處理. 比如 wsdl 放著就會自動 gen web service proxy, xsd 放著就會自動 gen typed dataset 的 source code... 這些是單用 csc.exe 沒辦法解決的
  2. 另外像 .ascx .aspx 對應的 cs, 也無法用 csc.exe 處理, 因為還有另一半的 partial class 是由 template file 會透過 asp.net engine 動態 parsing 後產生 .cs 再一起 compile, 這部份也沒有辦法單靠 csc.exe 處理掉
  3. 理論上還是有辦法把這些動作加在 batch file 或是 msbuild task 裡, 不過這些通通做下來, 那等於你自己搞一個 aspnet_compiler.exe 了... 工程太大, 不划算...

最後我選擇放棄這些遺漏的部份. 因為 help file 主要就是為了能共用的 class library 能有對應的文件, 上面漏掉的三塊都不大會有 comment help 產出, 唯獨 .ascx 還是很需要 help, 不過在沒有更方便的工具之前就先不管它了... 哈哈



1/18/2007 12:17:00 AM

皮哥皮妹的年齡 user control ...

Microsoft.NET | 543 | ASP.NET | CS | 技術隨筆 | 家人 Facebook Share

每次看 sea 在貼文章都會貼小皮幾歲幾個月, 妹妹幾個月... 就想說直接寫個 user control 就搞定了, 沒想到真的寫下去還有點小麻煩... 哈哈...

曆法的規則還真不少, 難怪每個教寫程式的書都會來一段萬年曆的 sample code.. 每個月天數都不一樣, 還有潤年不潤年的, 四年一潤, 百年不潤, 四百年又潤...

這堆原則弄下來, 單純的幾歲幾個月反而不好算了, 算從出生到現在共幾天, 去除 365 當歲數的誤差還好, 餘數再除 30 當月份的誤差就不小了, 最後的餘數再當天數就完全不對了...

弄了一下, 果然寫元件的爽度就是不一樣... 這個 control 寫好後的用法是這樣:

<CH:Age runat="server" birthday="2000/05/20" pattern="阿扁當總統已經 {0} 年 {1} 個月了" />

會顯示: 阿扁當總統已經 6 年 7 個月了

細節就不多說了, 以後進到 皮哥&皮妹的小天地 的左上角, 就看的到年紀, 純脆自己爽一下, 我們家的 blog 又跟別人的有一點點地方不一樣了 :D



3/19/2005 2:20:00 PM

修改 Community Server 的 blog editor

543 | ASP.NET | CS Facebook Share

好像每次換一套 blog, 我的宿命就是先改 editor, 讓它可以貼圖及貼表情符號... 哈哈

CommunityServer 用的是之前我介紹過的 FreeTextBox, 還不難改, 但是討厭的是 CS 並不是直接內嵌 FTB, 而是
中間多擋了一層 CS 自己的 Editor Wrapper... , 然後 CS 提供的 source code 就是少了這一塊...

沒辦法, 所以改出來的東西就有點格格不入, 多的工具列得排在畫面上方, 沒辦法加到原本 FTB 自己的工具列.
不然 FTB 其實還有很多好用的工具列可以打開... 真是可惜..






精選文章

RUN! PC 文章及範例下載
2008/11. 生產線模式的多執行緒應用
2008/09. 用ThreadPool發揮CPU運算能力
2008/06. SEMAPHORE在ASP.NET的應用
2008/04. 以ASP.NET開發同步WEB應用程式

如何學好 "寫程式" 系列
#1. 該如何學好 "寫程式" ??
#2. 為什麼 programmer 該學資料結構 ??
#3. 進階應用 - 資料結構 + 問題分析
#4. 你的程式夠 "可靠" 嗎?

#5. 善用 TRACE / ASSERT

安德魯是誰?

Andrew Wu | Create Your Badge

我喜歡鑽研物件導向、軟體工程及作業系統等相關技術。我會在這裡發表我的研究心得,也當作我自己的學習筆記。


Recent comments

Comment RSS