Windows結構化異常處理淺析

来源:https://www.cnblogs.com/wqliceman/archive/2018/04/22/8906654.html
-Advertisement-
Play Games

結構化異常處理(**structured exception handling**,下文簡稱:**SEH**),是作為一種系統機制引入到操作系統中的,本身與語言無關。在我們自己的程式中使用**SEH**可以讓我們集中精力開發關鍵功能,而把程式中所可能出現的異常進行統一的處理,使程式顯得更加簡潔且增加... ...


近期一直被一個問題所困擾,就是寫出來的程式老是出現無故崩潰,有的地方自己知道可能有問題,但是有的地方又根本沒辦法知道有什麼問題。更苦逼的事情是,我們的程式是需要7x24服務客戶,雖然不需要實時精準零差錯,但是總不能出現斷線丟失數據狀態。故剛好通過處理該問題,找到了一些解決方案,怎麼捕獲訪問非法記憶體地址或者0除以一個數。從而就遇到了這個結構化異常處理,今就簡單做個介紹認識下,方便大家遇到相關問題後,首先知道問題原因,再就是如何解決。廢話不多說,下麵進入正題。


什麼是結構化異常處理

結構化異常處理(structured exception handling,下文簡稱:SEH),是作為一種系統機制引入到操作系統中的,本身與語言無關。在我們自己的程式中使用SEH可以讓我們集中精力開發關鍵功能,而把程式中所可能出現的異常進行統一的處理,使程式顯得更加簡潔且增加可讀性。

使用SHE,並不意味著可以完全忽略代碼中可能出現的錯誤,但是我們可以將軟體工作流程和軟體異常情況處理進行分開,先集中精力乾重要且緊急的活,再來處理這個可能會遇到各種的錯誤的重要不緊急的問題(不緊急,但絕對重要)

當在程式中使用SEH時,就變成編譯器相關的。其所造成的負擔主要由編譯程式來承擔,例如編譯程式會產生一些表(table)來支持SEH的數據結構,還會提供回調函數。

註:
不要混淆SHE和C++ 異常處理。C++ 異常處理再形式上表現為使用關鍵字catchthrow,這個SHE的形式不一樣,再windows Visual C++中,是通過編譯器和操作系統的SHE進行實現的。

在所有 Win32 操作系統提供的機制中,使用最廣泛的未公開的機制恐怕就要數SHE了。一提到SHE,可能就會令人想起 *__try__finally* 和 *__except* 之類的詞兒。SHE實際上包含兩方面的功能:終止處理(termination handing)異常處理(exception handing)


終止處理

終止處理程式確保不管一個代碼塊(被保護代碼)是如何退出的,另外一個代碼塊(終止處理程式)總是能被調用和執行,其語法如下:

__try
{
    //Guarded body
    //...
}
__finally
{
    //Terimnation handler
    //...
}

**__try__finally** 關鍵字標記了終止處理程式的兩個部分。操作系統和編譯器的協同工作保障了不管保護代碼部分是如何退出的(無論是正常退出、還是異常退出)終止程式都會被調用,即**__finally**代碼塊都能執行。


try塊的正常退出與非正常退出

try塊可能會因為returngoto,異常等非自然退出,也可能會因為成功執行而自然退出。但不論try塊是如何退出的,finally塊的內容都會被執行。

int Func1()
{
    cout << __FUNCTION__ << endl;
    int nTemp = 0;
    __try{
        //正常執行
        nTemp = 22;
        cout << "nTemp = " << nTemp << endl;
    }
    __finally{
        //結束處理
        cout << "finally nTemp = " << nTemp << endl;
    }
    return nTemp;
}

int Func2()
{
    cout << __FUNCTION__ << endl;
    int nTemp = 0;
    __try{
        //非正常執行
        return 0;
        nTemp = 22;
        cout << "nTemp = " << nTemp << endl;
    }
    __finally{
        //結束處理
        cout << "finally nTemp = " << nTemp << endl;
    }
    return nTemp;
}

結果如下:

Func1
nTemp = 22  //正常執行賦值
finally nTemp = 22  //結束處理塊執行

Func2
finally nTemp = 0   //結束處理塊執行

以上實例可以看出,通過使用終止處理程式可以防止過早執行return語句,當return語句視圖退出try塊的時候,編譯器會讓finally代碼塊再它之前執行。對於在多線程編程中通過信號量訪問變數時,出現異常情況,能順利是否信號量,這樣線程就不會一直占用一個信號量。當finally代碼塊執行完後,函數就返回了。

為了讓整個機制運行起來,編譯器必鬚生成一些額外代碼,而系統也必須執行一些額外工作,所以應該在寫代碼的時候避免再try代碼塊中使用return語句,因為對應用程式性能有影響,對於簡單demo問題不大,對於要長時間不間斷運行的程式還是悠著點好,下文會提到一個關鍵字**__leave**關鍵字,它可以幫助我們發現有局部展開開銷的代碼。

一條好的經驗法則:不要再終止處理程式中包含讓try塊提前退出的語句,這意味著從try塊和finally塊中移除return,continue,break,goto等語句,把這些語句放在終止處理程式以外。這樣做的好處就是不用去捕獲哪些try塊中的提前退出,從而時編譯器生成的代碼量最小,提高程式的運行效率和代碼可讀性。


####finally塊的清理功能及對程式結構的影響

在編碼的過程中需要加入需要檢測,檢測功能是否成功執行,若成功的話執行這個,不成功的話需要作一些額外的清理工作,例如釋放記憶體,關閉句柄等。如果檢測不是很多的話,倒沒什麼影響;但若又許多檢測,且軟體中的邏輯關係比較複雜時,往往需要化很大精力來實現繁瑣的檢測判斷。結果就會使程式看起來結構比較複雜,大大降低程式的可讀性,而且程式的體積也不斷增大。

對應這個問題我是深有體會,過去在寫通過COM調用WordVBA的時候,需要層層獲取對象、判斷對象是否獲取成功、執行相關操作、再釋放對象,一個流程下來,本來一兩行的VBA代碼,C++ 寫出來就要好幾十行(這還得看操作的是幾個什麼對象)。

下麵就來一個方法讓大家看看,為什麼有些人喜歡腳本語言而不喜歡C++的原因吧。

為了更有邏輯,更有層次地操作 OfficeMicrosoft 把應用(Application)按邏輯功能劃分為如下的樹形結構

Application(WORD 為例,只列出一部分)
  Documents(所有的文檔)
        Document(一個文檔)
            ......
  Templates(所有模板)
        Template(一個模板)
            ......
  Windows(所有視窗)
        Window
        Selection
        View
        .....
  Selection(編輯對象)
        Font
        Style
        Range
        ......
  ......

只有瞭解了邏輯層次,我們才能正確的操縱 Office。舉例來講,如果給出一個VBA語句是:

Application.ActiveDocument.SaveAs "c:\abc.doc"

那麼,我們就知道了,這個操作的過程是:

  1. 第一步,取得Application
  2. 第二步,從Application中取得ActiveDocument
  3. 第三步,調用 Document 的函數 SaveAs,參數是一個字元串型的文件名。

這隻是一個最簡單的的VBA代碼了。來個稍微複雜點的如下,在選中處,插入一個書簽:

 ActiveDocument.Bookmarks.Add Range:=Selection.Range, Name:="iceman"

此處流程如下:

  1. 獲取Application
  2. 獲取ActiveDocument
  3. 獲取Selection
  4. 獲取Range
  5. 獲取Bookmarks
  6. 調用方法Add

獲取每個對象的時候都需要判斷,還需要給出錯誤處理,對象釋放等。在此就給出偽碼吧,全寫出來篇幅有點長

#define RELEASE_OBJ(obj) if(obj != NULL) \
                        obj->Realse();

BOOL InsertBookmarInWord(const string& bookname)
{
    BOOL ret = FALSE;
    IDispatch* pDispApplication = NULL;
    IDispatch* pDispDocument = NULL;
    IDispatch* pDispSelection = NULL;
    IDispatch* pDispRange = NULL;
    IDispatch* pDispBookmarks = NULL;
    HRESULT hr = S_FALSE;

    hr = GetApplcaiton(..., &pDispApplication);
    if (!(SUCCEEDED(hr) || pDispApplication == NULL))
        return FALSE;

    hr = GetActiveDocument(..., &pDispDocument);
    if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
        RELEASE_OBJ(pDispApplication);
        return FALSE;
    }

    hr = GetActiveDocument(..., &pDispDocument);
    if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
        RELEASE_OBJ(pDispApplication);
        return FALSE;
    }

    hr = GetSelection(..., &pDispSelection);
    if (!(SUCCEEDED(hr) || pDispSelection == NULL)){
        RELEASE_OBJ(pDispApplication);
        RELEASE_OBJ(pDispDocument);
        return FALSE;
    }

    hr = GetRange(..., &pDispRange);
    if (!(SUCCEEDED(hr) || pDispRange == NULL)){
        RELEASE_OBJ(pDispApplication);
        RELEASE_OBJ(pDispDocument);
        RELEASE_OBJ(pDispSelection);
        return FALSE;
    }

    hr = GetBookmarks(..., &pDispBookmarks);
    if (!(SUCCEEDED(hr) || pDispBookmarks == NULL)){
        RELEASE_OBJ(pDispApplication);
        RELEASE_OBJ(pDispDocument);
        RELEASE_OBJ(pDispSelection);
        RELEASE_OBJ(pDispRange);
        return FALSE;
    }

    hr = AddBookmark(...., bookname);
    if (!SUCCEEDED(hr)){
        RELEASE_OBJ(pDispApplication);
        RELEASE_OBJ(pDispDocument);
        RELEASE_OBJ(pDispSelection);
        RELEASE_OBJ(pDispRange);
        RELEASE_OBJ(pDispBookmarks);
        return FALSE;
    }
    ret = TRUE;
    return ret;

這隻是偽碼,雖然也可以通過goto減少代碼行,但是goto用得不好就出錯了,下麵程式中稍不留神就goto到不該取得地方了。

BOOL InsertBookmarInWord2(const string& bookname)
{
    BOOL ret = FALSE;
    IDispatch* pDispApplication = NULL;
    IDispatch* pDispDocument = NULL;
    IDispatch* pDispSelection = NULL;
    IDispatch* pDispRange = NULL;
    IDispatch* pDispBookmarks = NULL;
    HRESULT hr = S_FALSE;

    hr = GetApplcaiton(..., &pDispApplication);
    if (!(SUCCEEDED(hr) || pDispApplication == NULL))
        goto exit6;

    hr = GetActiveDocument(..., &pDispDocument);
    if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
        goto exit5;
    }

    hr = GetActiveDocument(..., &pDispDocument);
    if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
        goto exit4;
    }

    hr = GetSelection(..., &pDispSelection);
    if (!(SUCCEEDED(hr) || pDispSelection == NULL)){
        goto exit4;
    }

    hr = GetRange(..., &pDispRange);
    if (!(SUCCEEDED(hr) || pDispRange == NULL)){
        goto exit3;
    }

    hr = GetBookmarks(..., &pDispBookmarks);
    if (!(SUCCEEDED(hr) || pDispBookmarks == NULL)){
        got exit2;
    }

    hr = AddBookmark(...., bookname);
    if (!SUCCEEDED(hr)){
        goto exit1;
    }

    ret = TRUE;
exit1:
    RELEASE_OBJ(pDispApplication);
exit2:
    RELEASE_OBJ(pDispDocument);
exit3:
    RELEASE_OBJ(pDispSelection);
exit4:
    RELEASE_OBJ(pDispRange);
exit5:
    RELEASE_OBJ(pDispBookmarks);
exit6:
    return ret;

此處還是通過SEH的終止處理程式來重新該方法,這樣是不是更清晰明瞭。

BOOL InsertBookmarInWord3(const string& bookname)
{
    BOOL ret = FALSE;
    IDispatch* pDispApplication = NULL;
    IDispatch* pDispDocument = NULL;
    IDispatch* pDispSelection = NULL;
    IDispatch* pDispRange = NULL;
    IDispatch* pDispBookmarks = NULL;
    HRESULT hr = S_FALSE;

    __try{
        hr = GetApplcaiton(..., &pDispApplication);
        if (!(SUCCEEDED(hr) || pDispApplication == NULL))
            return FALSE;

        hr = GetActiveDocument(..., &pDispDocument);
        if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
            return FALSE;
        }

        hr = GetActiveDocument(..., &pDispDocument);
        if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
            return FALSE;
        }

        hr = GetSelection(..., &pDispSelection);
        if (!(SUCCEEDED(hr) || pDispSelection == NULL)){
            return FALSE;
        }

        hr = GetRange(..., &pDispRange);
        if (!(SUCCEEDED(hr) || pDispRange == NULL)){
            return FALSE;
        }

        hr = GetBookmarks(..., &pDispBookmarks);
        if (!(SUCCEEDED(hr) || pDispBookmarks == NULL)){
            return FALSE;
        }

        hr = AddBookmark(...., bookname);
        if (!SUCCEEDED(hr)){
            return FALSE;
        }

        ret = TRUE;
    }
    __finally{
        RELEASE_OBJ(pDispApplication);
        RELEASE_OBJ(pDispDocument);
        RELEASE_OBJ(pDispSelection);
        RELEASE_OBJ(pDispRange);
        RELEASE_OBJ(pDispBookmarks);
    }
    return ret;

這幾個函數的功能是一樣的。可以看到在InsertBookmarInWord中的清理函數(RELEASE_OBJ)到處都是,而InsertBookmarInWord3中的清理函數則全部集中在finally塊,如果在閱讀代碼時只需看try塊的內容即可瞭解程式流程。這兩個函數本身都很小,可以細細體會下這兩個函數的區別。


關鍵字 __leave

try塊中使用**__leave關鍵字會使程式跳轉到try塊的結尾,從而自然的進入finally塊。
對於上例中的InsertBookmarInWord3try塊中的return完全可以用
__leave** 來替換。兩者的區別是用return會引起try過早退出系統會進行局部展開而增加系統開銷,若使用**__leave**就會自然退出try塊,開銷就小的多。

BOOL InsertBookmarInWord4(const string& bookname)
{
    BOOL ret = FALSE;
    IDispatch* pDispApplication = NULL;
    IDispatch* pDispDocument = NULL;
    IDispatch* pDispSelection = NULL;
    IDispatch* pDispRange = NULL;
    IDispatch* pDispBookmarks = NULL;
    HRESULT hr = S_FALSE;

    __try{
        hr = GetApplcaiton(..., &pDispApplication);
        if (!(SUCCEEDED(hr) || pDispApplication == NULL))
            __leave;

        hr = GetActiveDocument(..., &pDispDocument);
        if (!(SUCCEEDED(hr) || pDispDocument == NULL))
            __leave;

        hr = GetActiveDocument(..., &pDispDocument);
        if (!(SUCCEEDED(hr) || pDispDocument == NULL))
            __leave;

        hr = GetSelection(..., &pDispSelection);
        if (!(SUCCEEDED(hr) || pDispSelection == NULL))
            __leave;

        hr = GetRange(..., &pDispRange);
        if (!(SUCCEEDED(hr) || pDispRange == NULL))
            __leave;

        hr = GetBookmarks(..., &pDispBookmarks);
        if (!(SUCCEEDED(hr) || pDispBookmarks == NULL))
            __leave;

        hr = AddBookmark(...., bookname);
        if (!SUCCEEDED(hr))
            __leave;

        ret = TRUE;
    }
    __finally{
        RELEASE_OBJ(pDispApplication);
        RELEASE_OBJ(pDispDocument);
        RELEASE_OBJ(pDispSelection);
        RELEASE_OBJ(pDispRange);
        RELEASE_OBJ(pDispBookmarks);
    }
    return ret;
}


異常處理程式

軟體異常是我們都不願意看到的,但是錯誤還是時常有,比如CPU捕獲類似非法記憶體訪問和除0這樣的問題,一旦偵查到這種錯誤,就拋出相關異常,操作系統會給我們應用程式一個查看異常類型的機會,並且運行程式自己處理這個異常。異常處理程式結構代碼如下

  __try {
      // Guarded body
    }
    __except ( exception filter ) {
      // exception handler
    }

註意關鍵字**__except**,任何try塊,後面必須更一個finally代碼塊或者except代碼塊,但是try後又不能同時有finallyexcept塊,也不能同時有多個finnalyexcept塊,但是可以相互嵌套使用


異常處理基本流程

int Func3()
{
    cout << __FUNCTION__ << endl;
    int nTemp = 0;
    __try{
        nTemp = 22;
        cout << "nTemp = " << nTemp << endl;
    }
    __except (EXCEPTION_EXECUTE_HANDLER){
        cout << "except nTemp = " << nTemp << endl;
    }
    return nTemp;
}

int Func4()
{
    cout << __FUNCTION__ << endl;
    int nTemp = 0;
    __try{
        nTemp = 22/nTemp;
        cout << "nTemp = " << nTemp << endl;
    }
    __except (EXCEPTION_EXECUTE_HANDLER){
        cout << "except nTemp = " << nTemp << endl;
    }
    return nTemp;
}

結果如下:

Func3
nTemp = 22  //正常執行

Func4
except nTemp = 0 //捕獲異常,

Func3try塊只是一個簡單操作,故不會導致異常,所以except塊中代碼不會被執行,Func4try塊視圖用22除0,導致CPU捕獲這個事件,並拋出,系統定位到except塊,對該異常進行處理,該處有個異常過濾表達式,系統中有三該定義(定義在Windows的Excpt.h中):

1. EXCEPTION_EXECUTE_HANDLER:
    我知道這個異常了,我已經寫了代碼來處理它,讓這些代碼執行吧,程式跳轉到except塊中執行並退出
2. EXCEPTION_CONTINUE_SERCH
    繼續上層搜索處理except代碼塊,並調用對應的異常過濾程式
3. EXCEPTION_CONTINUE_EXECUTION
    返回到出現異常的地方重新執行那條CPU指令本身

面是兩種基本的使用方法:

  • 方式一:直接使用過濾器的三個返回值之一

    __try {
       ……
    }
    __except ( EXCEPTION_EXECUTE_HANDLER ) {
       ……
    }
  • 方式二:自定義過濾器
    ```
    __try {
    ……
    }
    __except ( MyFilter( GetExceptionCode() ) )
    {
    ……
    }

LONG MyFilter ( DWORD dwExceptionCode )
{
if ( dwExceptionCode == EXCEPTION_ACCESS_VIOLATION )
return EXCEPTION_EXECUTE_HANDLER ;
else
return EXCEPTION_CONTINUE_SEARCH ;
}


<br>

##.NET4.0中捕獲SEH異常

在.NET 4.0之後,CLR將會區別出一些異常(都是SEH異常),將這些異常標識為破壞性異常(Corrupted State Exception)。針對這些異常,CLR的catch塊不會捕捉這些異常,一下代碼也沒有辦法捕捉到這些異常。

try{
//....
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}


因為並不是所有人都需要捕獲這個異常,如果你的程式是在4.0下麵編譯並運行,而你又想在.NET程式里捕捉到SEH異常的話,有兩個方案可以嘗試:

 - 在托管程式的.config文件里,啟用legacyCorruptedStateExceptionsPolicy這個屬性,即簡化的.config文件類似下麵的文件:

App.Config

這個設置告訴CLR 4.0,整個.NET程式都要使用老的異常捕捉機制。

-  在需要捕捉破壞性異常的函數外面加一個HandleProcessCorruptedStateExceptions屬性,這個屬性只控制一個函數,對托管程式的其他函數沒有影響,例如:

[HandleProcessCorruptedStateExceptions]
try{
//....
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
```


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 感謝兩位兩位大佬: https://blog.csdn.net/l1028386804/article/details/61417166 https://www.cnblogs.com/runningsmallguo/p/6239215.html 1.首先我們要下載好文件memcached.exe, ...
  • Nginx 場景實踐篇 一、Nginx作為靜態資源Web服務 1、動態資源和靜態資源 客戶端請求的頁面如果是靜態網頁,那麼伺服器會直接把靜態網頁的內容響應給客戶端。如果客戶端請求的是動態網頁,伺服器需要先把動態網頁換成靜態網頁,然後再把轉換後的靜態網頁響應給客戶端 靜態資源的幾種類型 瀏覽器渲染:H ...
  • 1.生成密鑰。 ssh-keygen命令用來生成密鑰對,基本的選項: -t 指定的密鑰類型(rsa,rsa1,dsa,ecdsa)。 -p 指定密語。 -f 指定生成密鑰文件的命名。 -c 添加註釋。 2.將公鑰部署到Linux上和下載私鑰到本地。 下載密鑰可以使用sz命令或scp命令或者filez ...
  • 一、硬碟介面類型 硬碟的介面主要有IDE、SATA、SCSI 、SAS和光纖通道等五種類型。其中IDE和SATA介面硬碟多用於家用產品中,也有部分應用於伺服器,SATA是一種新生的硬碟介面類型,已經取代了大部分IDE介面應用。SCSI 、SAS主要應用於伺服器上,普通家用設備一般不支持SCSI和SA ...
  • 一、淺談id、whoami、su、chage 本篇是續寫上一篇<Linux 用戶篇——用戶管理命令之useradd、passwd、userdel、usermod>。 (1)id命令 命令格式:id username(用戶名) 命令解釋:查看用戶的UID(用戶ID)、GID(組ID)。 (2)whoa ...
  • 我的系統是unbuntu14.04,我先是按照官方教程的安裝,後來也百度了一點別人的教程,算是一個雜燴。 註意,為什麼要使用privoxy? 因為如果不使用的話,就是全局代理,使用全局代理會使所有的連接通過shadowsocks伺服器中轉,一般不建議使用全局代理。另外,gnome桌面的代理設置無法正 ...
  • 7.1 關機&重啟命令 基本介紹: shutdown -h now 立刻進行關機 shutdown -h 1 1分鐘後關機 shutdown -r now 現在重啟電腦 halt 關機,作用和上面一樣 reboot 重啟 sync 把記憶體的數據同步到磁碟 註意細節: 不管是重啟系統還是關閉系統,首 ...
  • Xshell5和Xftp5的安裝包 鏈接:https://pan.baidu.com/s/1q3-ch75TW3lvC3KX25klNQ 密碼:m31n 說明: 公司開發的時候,具體情況是這樣的: 1、linux伺服器是開發小組共用的; 2、正式上線的項目是運行在公網的; 3、因此程式員需要遠程登錄 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...