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

来源: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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...