將代碼中的調試信息輸出到日誌文件中

来源:https://www.cnblogs.com/hyacinthLJP/archive/2023/04/05/17291132.html
-Advertisement-
Play Games

一、將調試信息輸出到屏幕中 1.1 一般寫法 我們平常在寫代碼時,肯定會有一些調試信息的輸出: #include <stdio.h> #include <stdlib.h> int main() { char szFileName[] = "test.txt"; FILE *fp = fopen(s ...


一、將調試信息輸出到屏幕中

1.1 一般寫法

我們平常在寫代碼時,肯定會有一些調試信息的輸出:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char szFileName[] = "test.txt";
    FILE *fp = fopen(szFileName, "r");
    if (fp == NULL)
    {
        // 文件打開失敗,提示錯誤並退出
        printf("open file(%s) error.\n", szFileName);
        exit(0);
    }
    else
    {
        // 文件打開成功,進行相應的文件讀/寫操作
    }

    return 0;
}

假設當前目錄下沒有 test.txt 文件。當程式執行到第 7 行時,必然返回 NULL,這時候通過第 11 行的調試信息,我們可以幫助我們精確排查到程式退出的原因:原來是文件打開失敗了。

image-20230402170006527

那如果當前目錄下存在 test.txt 文件,只是不可讀呢?

image-20230402175503739

  • 同樣輸出了 open file(test.txt) error

在這種情況下如何快速定位文件打開失敗的原因呢?我們可以考慮使用 errno

1.2 使用 errno

errno 是記錄系統的最後一次錯誤代碼。錯誤代碼是一個 int 型的值,在 errno.h 中定義。

#include <errno.h>	// errno 頭文件
#include <string.h>	// strerror 頭文件

// 文件打開失敗,提示錯誤並退出
printf("open file(%s) error, errno[%d](%s).\n", szFileName, errno, strerror(errno));

修改後再次運行 main.exe:

image-20230402180115008

如果代碼中包含很多的調試信息呢?我們並不能一下子知道這條信息到底是在哪裡列印出來的,於是,我們又想,能不能把當前調試信息所在的文件名和源碼行位置也列印出來呢,這樣不就一目瞭然了嗎。基於此,便有了 1.3 的內容。

1.3 編譯器內置巨集

ANSI C 標準中有幾個標準預定義巨集:

  • __LINE__:在源代碼中插入當前源代碼行號
  • __FILE__:在源文件中插入當前源文件名
  • __FUNCTION_:在源文件中插入當前函數名
  • __DATE__:在源文件中插入當前的編譯日期
  • __TIME__:在源文件中插入當前編譯時間
  • __STDC__:當要求程式嚴格遵循ANSI C標準時該標識被賦值為 1
  • __cplusplus:當編寫C++程式時該標識符被定義

於是我們這麼修改輸出語句:

// 文件打開失敗,提示錯誤並退出
printf("[%s][%s:%d] open file(%s) error, errno[%d](%s).\n", 
                                                    __FILE__,
                                                    __FUNCTION__,
                                                    __LINE__,
                                                    szFileName, 
                                                    errno, strerror(errno));

image-20230402181519935

  • 從日誌信息中,我們可以精確的獲取到:main.c 文件中的 main 函數的第 16 行報錯了,錯誤原因是 Permission denied

相比於之前,確實是能幫助我們精準的定位問題,但是,總不能每次都要寫這麼長的 printf 吧,有沒有偷懶的辦法呢?

1.4 使用可變巨集輸出調試信息

1.4.1 可變巨集介紹

用可變參數巨集(variadic macros)傳遞可變參數表,你可能很熟悉在函數中使用可變參數表,如:

void printf(const char* format, ...);

在 1999 年版本的 ISO C 標準中,巨集可以像函數一樣,定義時可以帶有可變參數。巨集的語法和函數的語法類似,如下所示:

#define DEBUG(...) printf(__VA_ARGS__)

int main()
{
    int x = 10;
    DEBUG("x = %d\n", x); // 等價於 printf("x = %d\n", x);
    
    return 0;
}
  • 預設號(...)指可變參數
  • __VA_ARGS__巨集用來接收不定數量的參數

這類巨集在被調用時,它(這裡指預設號...)被表示成零個或多個符號(包括裡面的逗號),一直到右括弧結束為止。當被調用時,在巨集體( macro body )中,這些符號序列集合將代替裡面的 _VA_ARGS_ 標識符。當巨集的調用展開時,實際的參數就傳遞給 printf 了。

相比於 ISO C 標準,GCC 始終支持複雜的巨集,它使用一種不同的語法從而可以使你可以給可變參數一個名字,如同其它參數一樣。例如下麵的例子:

#define DEBUG(format, args...) printf(format, args)

int main()
{
    int x = 10;
    DEBUG("x = %d\n", x); // 等價於 printf("x = %d\n", x);
    
    return 0;
}
  • 這和上面舉的「ISO C」定義的巨集例子是完全一樣的,但是這麼寫可讀性更強並且更容易進行描述

在標準 C 里,你不能省略可變參數,但是你卻可以給它傳遞一個空的參數。例如,下麵的巨集調用在「ISO C」里是非法的,因為字元串後面沒有逗號:

#define DEBUG(...) printf(__VA_ARGS__)

int main()
{
    DEBUG("hello world.\n"); // 非法調用
}

GCC 在這種情況下可以讓你完全的忽略可變參數。在上面的例子中,編譯是仍然會有問題,因為巨集展開後,裡面的字元串後面會有個多餘的逗號。為瞭解決這個問題, GCC 使用了一個特殊的##操作。書寫格式為:

#define DEBUG(format, args...) printf(format, ##args)
  • 這裡,如果可變參數被忽略或為空,##操作將使預處理器去除掉它前面的那個逗號

  • 如果你在巨集調用時,確實提供了一些可變參數,該巨集定義也會工作正常,它會把這些可變參數放到逗號的後面

1.4.2 使用可變巨集輸出調試信息

有了 1.4.1 的基礎知識,我們可以這麼修改代碼:

#define DEBUG(format, args...) \
            printf("[%s][%s:%d] "format"\n", \
                        		__FILE__, \
                        		__FUNCTION__, \
                        		__LINE__, \
                        		##args)

// 文件打開失敗,提示錯誤並退出
DEBUG("open file(%s) error, errno[%d](%s).", szFileName, errno, strerror(errno));
  • 通過可變巨集,完美解決了調試信息書寫過長的問題

書寫過長的問題解決後,又來新問題了,如果我想知道某一調試信息是何時被列印的呢?

下麵讓我們學習一下 Linux 中與時間相關的內容。

二、Linux 中與時間相關的函數

2.1 表示時間的結構體

通過查看頭文件「/usr/include/time.h」和「/usr/include/bits/time.h」,我們可以找到下列四種表示「時間」的結構體:

/* Returned by `time'. */
typedef __time_t time_t;
/* A time value that is accurate to the nearest
   microsecond but also has a range of years. */
struct timeval
{
    __time_t tv_sec;       /* Seconds. */
    __suseconds_t tv_usec; /* Microseconds. */
};
struct timespec
{
    __time_t tv_sec;  /* Seconds. */
    long int tv_nsec; /* Nanoseconds. */
};
struct tm
{
    int tm_sec;   /* Seconds.		[0-59] (1 leap second) */
    int tm_min;   /* Minutes.		[0-59] */
    int tm_hour;  /* Hours.    		[0-23] */
    int tm_mday;  /* Day.			[1-31] */
    int tm_mon;   /* Month.			[0-11] */
    int tm_year;  /* Year.			自 1900 起的年數 */
    int tm_wday;  /* Day of week.	[0-6] */
    int tm_yday;  /* Days in year.	[0-365] */
    int tm_isdst; /* DST.			夏令時 */

#ifdef __USE_BSD
    long int tm_gmtoff;    /* Seconds east of UTC. */
    __const char *tm_zone; /* Timezone abbreviation. */
#else
    long int __tm_gmtoff;    /* Seconds east of UTC. */
    __const char *__tm_zone; /* Timezone abbreviation. */
#endif
};
  1. time_t 是一個長整型,用來表示「秒數」
  2. struct timeval 結構體用「秒和微秒」來表示時間
  3. struct timespec 結構體用「秒和納秒」來表示時間
  4. struct tm 直接用「秒、分、小時、天、月、年」等來表示時間

2.2 獲取當前時間

// 可以獲取精確到秒的當前距離1970-01-01 00:00:00 +0000 (UTC)的秒數
time_t time(time_t *t); 
// 可以獲取精確到微秒的當前距離1970-01-01 00:00:00 +0000 (UTC)的微秒數
int gettimeofday(struct timeval *tv, struct timezone *tz);
// 可以獲取精確到納秒的當前距離1970-01-01 00:00:00 +0000 (UTC)的納秒數
int clock_gettime(clockid_t clk_id, struct timespec *tp)

使用方式如下所示:

#include <stdio.h>
#include <time.h>
#include <sys/time.h>

int main()
{
    time_t lTime;
    time(&lTime);
    printf("lTime       : %ld\n", lTime);

    struct timeval stTimeVal;
    gettimeofday(&stTimeVal, NULL);
    printf("stTimeVal   : %ld\n", stTimeVal.tv_sec);

    struct timespec stTimeSpec;
    clock_gettime(CLOCK_REALTIME, &stTimeSpec);
    printf("stTimeSpec  : %ld\n", stTimeSpec.tv_sec);

    return 0;
}
  • 我們可以通過上面三個函數獲得三種不同精度的當前時間

Notes:

  1. POSIX.1-2008 marks gettimeofday() as obsolete, recommending the use of clock_gettime(2) instead.
  2. 並且,有人曾經做過測試,連續兩次使用 gettimeofday 時,會以一種小概率出現「時光倒流」的現象,第二次函數調用得到的時間要小於或說早於第一次調用得到的時間。
  3. gettimeofday 函數並不是那麼穩定,沒有 times 或 clock 計時準確,但它們用法相似。
  4. clock有計時限制,據說是 596.5+小時,一般情況足以應付。
  5. ntpd 之類的進程可能會修改系統時間,導致計時出現誤差。
  6. 據網上的討論來看,TSC 和 HPET 中斷之類的東西,可能導致系統的 wall time 回退。這個應該和具體的系統實現有關了,總之 gettimeofday 函數並沒有保證提供怎樣的精度,也不保證得到系統的準確時間,它返回的結果是「the system's best guess at wall time」。
  7. 有可能的話,儘量使用 clock_gettime(CLOCK_MONOTONIC),不過不是所有系統都實現了 posix realtime,例如 mac os x。
  8. 所以現在應該用:int clock_gettime(CLOCK_MONOTONIC, struct timespec *tp);
    CLOCK_MONOTONIC:Clock that cannot be set and represents monotonic time since some unspecified starting point.

2.3 秒、毫秒、微秒、納秒之間的轉換

  • 1 秒 = 1000 毫秒
  • 1 毫秒 = 1000 微秒
  • 1 微秒 = 1000 納秒

so:

  • 1 秒 = 1000,000 微秒(一百萬微秒)
  • 1 秒 = 1000,000,000 納秒(十億納秒)

從秒到毫秒,毫秒到微秒,微秒到納秒都是 1000 的倍關係,也就是多 3 個 0 的關係。

另:個人電腦的微處理器執行一道指令(如將兩數相加)約需 2~4 納秒,所以程式只要精確到納秒就夠了。

2.4 對時間進行格式化輸出

  1. 首先將 struct timevalstruct timespec 轉換成 time_t 表示的秒數:

    struct timeval stTimeVal;
    gettimeofday(&stTimeVal, NULL);
    
    time_t lTime = stTimeVal.tv_sec;
    
  2. 利用系統函數將 time_t 轉換為 struct tm

    struct tm stTime;
    localtime_r(&lTime, &stTime); // 註意,localtime_r 的第二個參數是入參
    
  3. 格式化輸出:

    char buf[128];
    // 自定義輸出格式:YYYY-MM-DD hh:mm:ss
    snprintf(buf, 128, "%.4d-%.2d-%.2d %.2d:%.2d:%.2d", 
                            stTime.tm_year + 1900,
                            stTime.tm_mon + 1,
                            stTime.tm_mday,
                            stTime.tm_hour,
                            stTime.tm_min,
                            stTime.tm_sec);
    puts(buf);
    

將 time_t 轉換成 struct tm 的函數一共有 4 個,分別為:

  1. struct tm *gmtime(const time_t *timep);
  2. struct tm *gmtime_r(const time_t *timep, struct tm *result);
  3. struct tm *localtime(const time_t *timep);
  4. struct tm *localtime_r(const time_t *timep, struct tm *result);

形如 localtime 和形如 localtime_r 函數的區別是:localtime 獲得的返回值存在於一個 static 的 struct tm 型的變數中,可能被後面的 localtime 調用覆蓋掉。如果要防止覆蓋,我們可以自己提供一個 struct tm 型的變數,利用 localtime_r 函數,將我們自己定義的變數的地址傳進去,將結果保存在其中,這樣就可以避免覆蓋。

因此可知,函數 gmtime 和 localtime 是線程不安全的,多線程編程中要慎用!

2.5 獲取毫秒時間

#include <stdio.h>
#include <time.h>
#include <sys/time.h>
#include <stdlib.h>
#include <string.h>

char *GetMsecTime()
{
    static char buf[128];
    time_t lTime = 0;
    struct timeval stTimeVal = {0};
    struct tm stTime = {0};

    gettimeofday(&stTimeVal, NULL);
    lTime = stTimeVal.tv_sec;
    localtime_r(&lTime, &stTime);

    snprintf(buf, 128, "%.4d-%.2d-%.2d %.2d:%.2d:%.2d.%.3d",
             stTime.tm_year + 1900,
             stTime.tm_mon + 1,
             stTime.tm_mday,
             stTime.tm_hour,
             stTime.tm_min,
             stTime.tm_sec,
             stTimeVal.tv_usec / 1000); // 微秒 -> 毫秒
    return buf;
}
int main()
{
    puts(GetMsecTime());

    return 0;
}
  • 註意,該函數所返回的 buf 是通過 static 修飾的,是線程不安全的

2.6 調試信息中新增時間信息

#define DEBUG(format, args...) \
            printf("%s [%s][%s:%d] "format"\n", \
                        		GetMsecTime(), \
                        		__FILE__, \
                        		__FUNCTION__, \
                        		__LINE__, \
                        		##args)

至此,我們已經將調試信息的輸出格式完善了,接下來就要考慮怎麼將調試信息輸出到日誌文件中了。

三、將調試信息輸出到日誌文件中

3.1 日誌等級

Log4J 定義了 8 個級別的 Log(除去 OFF 和 ALL,可以說分為 6 個級別),優先順序從高到低依次為:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。

  • OFF:最高等級的,用於關閉所有日誌記錄

  • FATAL:指出每個嚴重的錯誤事件將會導致應用程式的退出。這個級別比較高了,重大錯誤,這種級別你可以直接停止程式了

  • ERROR:指出雖然發生錯誤事件,但仍然不影響系統的繼續運行。列印錯誤和異常信息,如果不想輸出太多的日誌,可以使用這個級別

  • WARN:表明會出現潛在錯誤的情形,有些信息不是錯誤信息,但是也要給程式員的一些提示

  • INFO:列印一些你感興趣的或者重要的信息,這個可以用於生產環境中輸出程式運行的一些重要信息,但是不能濫用,避免列印過多的日誌

  • DEBUG:主要用於開發過程中列印一些運行信息

  • TRACE: 很低的日誌級別,一般不會使用

  • ALL: 最低等級的,用於打開所有日誌記錄

Log4J 建議只使用四個級別,優先順序從高到低分別是 ERROR、WARN、INFO、DEBUG。我們下麵的程式也將圍繞這四個日誌等級來進行編碼。

先貼上源碼,後續有時間在詳細解釋~

3.2 源碼

3.2.1 log.h

#ifndef __LOG_H__
#define __LOG_H__

#ifdef __cplusplus
extern "C"
{
#endif

// 日誌路徑
#define LOG_PATH       "./Log/"
#define LOG_ERROR             "log.error"
#define LOG_WARN              "log.warn"
#define LOG_INFO              "log.info"
#define LOG_DEBUG             "log.debug"
#define LOG_OVERFLOW_SUFFIX             "00"    // 日誌溢出後的文件尾碼,如 log.error00

#define LOG_FILE_SIZE  (5*1024*1024)            // 單個日誌文件的大小,5M

// 日誌級別
typedef enum tagLogLevel
{
    LOG_LEVEL_ERROR    = 1,                             /* error級別 */
    LOG_LEVEL_WARN     = 2,                             /* warn級別  */
    LOG_LEVEL_INFO     = 3,                             /* info級別  */
    LOG_LEVEL_DEBUG    = 4,                             /* debug級別 */
} LOG_LEVEL_E;

typedef struct tagLogFile
{
    char szCurLog[64];
    char szPreLog[64];
} LOG_FILE_S;

#define PARSE_LOG_ERROR(format, args...)  \
    WriteLog(LOG_LEVEL_ERROR, __FILE__, __FUNCTION__, __LINE__, format, ##args)

#define PARSE_LOG_WARN(format, args...)  \
    WriteLog(LOG_LEVEL_WARN, __FILE__, __FUNCTION__, __LINE__, format, ##args)

#define PARSE_LOG_INFO(format, args...)  \
    WriteLog(LOG_LEVEL_INFO, __FILE__, __FUNCTION__, __LINE__, format, ##args)

#define PARSE_LOG_DEBUG(format, args...)  \
    WriteLog(LOG_LEVEL_DEBUG, __FILE__, __FUNCTION__, __LINE__, format, ##args)

extern void WriteLog
(
    LOG_LEVEL_E enLogLevel,
    const char *pcFileName,
    const char *pcFuncName,
    int iFileLine,
    const char *format, 
    ...
);

#ifdef __cplusplus
}
#endif

#endif

3.2.2 log.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>     // va_stat 頭文件
#include <errno.h>      // errno 頭文件
#include <time.h>       // 時間結構體頭文件
#include <sys/time.h>   // 時間函數頭文件
#include <sys/stat.h>   // stat 頭文件
#include "log.h"

static LOG_FILE_S gstLogFile[5] = 
{
    {"", ""},
    {
        /* error級別 */
        LOG_PATH LOG_ERROR,                     // ./Log/log.error
        LOG_PATH LOG_ERROR LOG_OVERFLOW_SUFFIX  // ./Log/log.error00
    },
    {
        /* warn級別 */
        LOG_PATH LOG_WARN,                      // ./Log/log.warn
        LOG_PATH LOG_WARN LOG_OVERFLOW_SUFFIX   // ./Log/log.warn00
    }, 
    {
        /* info級別 */
        LOG_PATH LOG_INFO,                      // ./Log/log.info
        LOG_PATH LOG_INFO LOG_OVERFLOW_SUFFIX   // ./Log/log/info00
    }, 
    {
        /* debug級別 */
        LOG_PATH LOG_DEBUG,                     // ./Log/log.debug
        LOG_PATH LOG_DEBUG LOG_OVERFLOW_SUFFIX  // ./Log/log.debug00
    }, 
};

static void __Run_Log
(
    LOG_LEVEL_E enLogLevel,
    const char *pcFileName,
    const char *pcFuncName,
    int iFileLine,
    const char *format,
    va_list vargs
)
{
    FILE *logfile = NULL;
    logfile = fopen(gstLogFile[enLogLevel].szCurLog, "a");
    if (logfile == NULL)
    {
        printf("open %s error[%d](%s).\n", gstLogFile[enLogLevel].szCurLog, errno, strerror(errno));
        return;
    }

    /* 獲取時間信息 */
    struct timeval stTimeVal = {0};
    struct tm stTime = {0};
    gettimeofday(&stTimeVal, NULL);
    localtime_r(&stTimeVal.tv_sec, &stTime);

    char buf[768];
    snprintf(buf, 768, "%.2d-%.2d %.2d:%.2d:%.2d.%.3lu [%s][%s:%d] ",
                                            stTime.tm_mon + 1,
                                            stTime.tm_mday,
                                            stTime.tm_hour,
                                            stTime.tm_min,
                                            stTime.tm_sec,
                                            (unsigned long)(stTimeVal.tv_usec / 1000),
                                            pcFileName,
                                            pcFuncName,
                                            iFileLine);

    fprintf(logfile, "%s", buf);
    vfprintf(logfile, format, vargs);
    fprintf(logfile, "%s", "\r\n");
    fflush(logfile);

    fclose(logfile);

    return;
}
static void __LogCoverStrategy(char *pcPreLog) // 日誌滿後的覆蓋策略
{
    int iLen = strlen(pcPreLog);
    int iNum = (pcPreLog[iLen - 2] - '0') * 10 + (pcPreLog[iLen - 1] - '0');
    iNum = (iNum + 1) % 10;

    pcPreLog[iLen - 2] = iNum / 10 + '0';
    pcPreLog[iLen - 1] = iNum % 10 + '0';
}

void WriteLog
(
    LOG_LEVEL_E enLogLevel,
    const char *pcFileName,
    const char *pcFuncName,
    int iFileLine,
    const char *format, 
    ...
)
{
    char szCommand[64]; // system函數中的指令
    struct stat statbuff;
    if (stat(gstLogFile[enLogLevel].szCurLog, &statbuff) >= 0) // 如果存在
    {
        if (statbuff.st_size > LOG_FILE_SIZE) // 如果日誌文件超出限制
        {
            printf("LOGFILE(%s) > 5M, del it.\n", gstLogFile[enLogLevel].szCurLog);
            snprintf(szCommand, 64, "cp -f %s %s", gstLogFile[enLogLevel].szCurLog, gstLogFile[enLogLevel].szPreLog); 
            puts(szCommand);
            system(szCommand);      // 將當前超出限制的日誌保存到 log.error00 中

            snprintf(szCommand, 64, "rm -f %s", gstLogFile[enLogLevel].szCurLog);
            system(szCommand);      // 刪掉 log.error
            printf("%s\n\n", szCommand);
            
            // 如果 log.error 超出 5M 後,將依次保存在 log.error00、log.error01、... 中
            __LogCoverStrategy(gstLogFile[enLogLevel].szPreLog); 
        }
    }
    else // 如果不存在,則創建
    {
        printf("LOGFILE(%s) is not found, create it.\n\n", gstLogFile[enLogLevel].szCurLog);
        snprintf(szCommand, 64, "touch %s", gstLogFile[enLogLevel].szCurLog);
        system(szCommand);
    }

    va_list argument_list;
    va_start(argument_list, format);

    if (format)
    {
        __Run_Log(enLogLevel, pcFileName, pcFuncName, iFileLine, format, argument_list);
    }

    va_end(argument_list);

    return;
}

3.3.3 main.c

#include <stdio.h>
#include <unistd.h> // sleep 頭文件
#include "log.h"

int main()
{
    for (int i = 0; i < 5; i++)
    {
        PARSE_LOG_ERROR("我是第 %d 條日誌", i+1);
    }

    return 0;
}

3.3.4 Tutorial

  1. 將 log.h、log.c、main.c 置於同一個目錄中

  2. 並新建一個 Log 目錄
    image-20230405221056915

  3. 編譯、運行
    image-20230405221126201

參考資料


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

-Advertisement-
Play Games
更多相關文章
  • MySQL explain 和 profiling 詳解 mysql explain MySQL 的 EXPLAIN 是一個用於查詢優化的工具,它可以顯示 MySQL 資料庫如何執行查詢。它返回一組關於查詢執行計劃的信息,包括用到的索引,表的連接順序以及 MySQL 使用的查詢類型。下麵是 EXPL ...
  • 優質博文:IT-BLOG-CN 一、binlog binlog記錄資料庫表結構和表數據變更,比如update/delete/insert/truncate/create,它不會記錄select。存儲著每條變更的SQL語句和XID事務Id等等。binlog日誌文件如下: [[email protected] ...
  • 1. 處理有序集合也並非SQL的直接用途 1.1. SQL語言在處理數據時預設地都不考慮順序 2. 處理數據的方法有兩種 2.1. 第一種是把數據看成忽略了順序的集合 2.2. 第二種是把數據看成有序的集合 2.2.1. 首先用自連接生成起點和終點的組合 2.2.2. 其次在子查詢中描述內部的各個元 ...
  • 已知出生年月日,求到今天為止多少歲 select *, --如果當前月份大於出生月,年齡 = 當前年份 - 出生年 if (month(current_date())-month(substr(id_card,7,8))>0, year(current_date())-year(substr(id_ ...
  • Android Banner - ViewPager 02 現在來給viewpager實現的banenr加上自動輪播 自動輪播的原理,使用handler的延遲消息來實現。 自動輪播實現如下內容 開始輪播&停止輪播 可配置輪播時長、輪播方向 通過自定義屬性來配置輪播時長,方向 感知生命周期,可見時開始 ...
  • 文件中引入JavaScript 嵌入到HTML文件中 在body或者head中添加script標簽 <script> var age = 10; console.log(age); </script> 引入js文件 創建一個js文件 var age = 20; console.log(age); 在 ...
  • 支付永遠是一個公司的核心領域,因為這是一個有交易屬性公司的命脈。那麼,支付系統到底長什麼樣,又是怎麼運行交互的呢?拋開帶有支付牌照的金融公司的支付架構,下述鏈路和系統組成基本上符合絕大多數支付場景。其實整體可以看成是交易核心+支付核心 兩個大系統。交易系統關聯了業務場景和底層支付,而支付系統完成了調 ...
  • Go語言流媒體開源項目 LAL 今天發佈了v0.34.3版本。 LAL 項目地址:https://github.com/q191201771/lal 老規矩,簡單介紹一下: ▦ 一. 音頻G711 新增了對音頻G711A/G711U(也被稱為PCMA/PCMU)的支持。主要表現在: ✒ 1) rtm ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...